diff --git a/README.md b/README.md index 4ba6b60b..3ba4a032 100644 --- a/README.md +++ b/README.md @@ -1266,9 +1266,9 @@ replace copyparty passwords with oauth and such work is [ongoing](https://github.com/9001/copyparty/issues/62) to support authenticating / authorizing users based on a separate authentication proxy, which makes it possible to support oauth, single-sign-on, etc. -it is currently possible to specify `--idp-h-usr x-username`; copyparty will then skip password validation and blindly trust the username specified in the `X-Username` request header +there is a [docker-compose example](./docs/examples/docker/idp-authelia-traefik) which is hopefully a good starting point (alternatively see [./docs/idp.md](./docs/idp.md) if you're the DIY type) -the remaining stuff (accepting user groups through another header, creating volumes on the fly) are still to-do; configuration will probably [look like this](./docs/examples/docker/idp/copyparty.conf) +a more complete example of the copyparty configuration options [look like this](./docs/examples/docker/idp/copyparty.conf) ## hiding from google diff --git a/copyparty/__main__.py b/copyparty/__main__.py index d1a83ff2..ae4b0e60 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -956,6 +956,7 @@ def add_auth(ap): ap2 = ap.add_argument_group('IdP / identity provider / user authentication options') ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks and assume the request-header \033[33mHN\033[0m contains the username of the requesting user (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy") ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control") + ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present") ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 68e91659..70de3840 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -968,7 +968,7 @@ class AuthSrv(object): * any non-zero value from IdP group header * otherwise take --grps / [groups] """ - ret = {un:gns[:] for un, gns in self.idp_accs.items()} + ret = {un: gns[:] for un, gns in self.idp_accs.items()} ret.update({zs: [""] for zs in acct if zs not in ret}) for gn, uns in grps.items(): for un in uns: diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 0c0c3a0a..5b8a15c5 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -300,6 +300,7 @@ class HttpCli(object): zs = "%s:%s" % self.s.getsockname()[:2] self.host = zs[7:] if zs.startswith("::ffff:") else zs + trusted_xff = False n = self.args.rproxy if n: zso = self.headers.get(self.args.xff_hdr) @@ -333,6 +334,7 @@ class HttpCli(object): self.is_vproxied = bool(self.args.R) self.log_src = self.conn.set_rproxy(self.ip) self.host = self.headers.get("x-forwarded-host") or self.host + trusted_xff = True if self.is_banned(): return False @@ -467,7 +469,41 @@ class HttpCli(object): if self.args.idp_h_grp else "" ) - self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp) + + if not trusted_xff: + pip = self.conn.addr[0] + trusted_xff = self.args.xff_re and self.args.xff_re.match(pip) + + # always require --xff-src with idp, but check against original (xff_src) rather than computed value (xff_re) to allow 'any' + trusted_xff_strict = trusted_xff and self.args.xff_src + + trusted_key = ( + not self.args.idp_h_key + ) or self.args.idp_h_key in self.headers + + if trusted_key and trusted_xff_strict: + self.asrv.idp_checkin(self.conn.hsrv.broker, idp_usr, idp_grp) + else: + if not trusted_key: + t = 'the idp-h-key header ("%s") is not present in the request; will NOT trust the other headers saying that the client\'s username is "%s" and group is "%s"' + self.log(t % (self.args.idp_h_key, idp_usr, idp_grp), 3) + + if not trusted_xff_strict: + t = 'got IdP headers from untrusted source "%s" claiming the client\'s username is "%s" and group is "%s"; if you trust this, you must allowlist this proxy with "--xff-src=%s"' + if not self.args.idp_h_key: + t += " Note: you probably also want to specify --idp-h-key for additional security" + + pip = self.conn.addr[0] + zs = ( + ".".join(pip.split(".")[:2]) + "." + if "." in pip + else ":".join(pip.split(":")[:4]) + ":" + ) + self.log(t % (pip, idp_usr, idp_grp, zs), 3) + + idp_usr = "*" + idp_grp = "" + if idp_usr in self.asrv.vfs.aread: self.uname = idp_usr else: diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 8e496679..fdd43f99 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -478,7 +478,8 @@ class SvcHub(object): al.xff_hdr = al.xff_hdr.lower() al.idp_h_usr = al.idp_h_usr.lower() - # al.idp_h_grp = al.idp_h_grp.lower() + al.idp_h_grp = al.idp_h_grp.lower() + al.idp_h_key = al.idp_h_key.lower() al.xff_re = self._ipa2re(al.xff_src) al.ipa_re = self._ipa2re(al.ipa) diff --git a/docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf b/docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf index ffe75f23..b7e69fc6 100644 --- a/docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf +++ b/docs/examples/docker/idp-authelia-traefik/cpp/copyparty.conf @@ -21,8 +21,11 @@ ansi # enable colors in log messages #q # disable logging for more performance - # since copyparty is only accessible through traefik, disable safetycheck on x-forwarded-for - xff-src: any + # if we are confident that we got the docker-network config correct + # (meaning copyparty is only accessible through traefik, and + # traefik makes sure that all requests go through authelia), + # then disable the reverse-proxy source-ip safety check like this: + #xff-src: any # enable IdP support by expecting username/groupname in # http-headers provided by the reverse-proxy; header "X-IdP-User" diff --git a/docs/idp.md b/docs/idp.md new file mode 100644 index 00000000..44e901ea --- /dev/null +++ b/docs/idp.md @@ -0,0 +1,7 @@ +there is a [docker-compose example](./examples/docker/idp-authelia-traefik) which is hopefully a good starting point (meaning you can skip the steps below) -- but if you want to set this up from scratch yourself (or learn about how it works), keep reading: + +to configure IdP from scratch, you must place copyparty behind a reverse-proxy which sends all requests through a middleware (the IdP / identity-provider service) which will inject a set of headers into the requests, telling copyparty who the user is + +in the copyparty `[global]` config, specify which headers to read client info from; username is required (`idp-h-usr: X-Authooley-User`), group(s) are optional (`idp-h-grp: X-Authooley-Groups`) + +* it is also required to specify the subnet that legit requests will be coming from, for example `--xff-src=10.88.` to allow 10.88.x.x, and it is recommended to configure the reverseproxy to include a secret header as proof that the other headers are also legit (and not smuggled in by a malicious client), telling copyparty the headername to expect with `idp-h-key: X-Totes-Legit`