Open EugenMayer opened 5 years ago
Some details: https://kubic.opensuse.org/blog/2019-01-24-traefik/
Some take aways on this topic:
The "machinery" required to create the user/group and set the data volume is easy technically, but might have an impact (USER
directive, eventually /etc/passwd
etc.)
Solving the authorization problem when you mount a data volume which has not been initialized with the same user/group as traefik's image must be documented.
The example from Kubic requires a Kubernetes 1.13.x cluster, to benefit from the "PSP" (Pod Security Policy)
The main issue with a non root user is the ability to open ports < 1024. Even though Linux provides the CPA_BIND_NET
capability, it does not work on Kubernetes, because of the ambient capabilities not supported: https://github.com/kubernetes/kubernetes/issues/56374 . From this there are 2 solutions:
setcap net_bind_service=+ep /traefik
command to fix the capabilities mask issue. This won't work on the scratch image AFAIK.CMD ["--entrypoints=''Name:http Address::8080"]
instruction to NOT modify Traefik itself. Thoughts?
- The "machinery" required to create the user/group and set the data volume is easy technically, but might have an impact (
USER
directive, eventually/etc/passwd
etc.)
That is only an issue for host-mount based volumes and is a known aspect everybody runs docker in production is aware of, when using host mounts. I would not focus on that "volume" aspect too much here - it's the same for all projects using gosu
or whatever.
Not sure how traefik needs /etc/passwd at all / ever, but rather work with right group access if needed at all. AFAIK Traefik does not need anything that is system-specific / distro specific, everything can be created just for Traefik with the right permissions for it. Did i miss something here?
- Solving the authorization problem when you mount a data volume which has not been initialized with the same user/group as traefik's image must be documented. Same as above, this is a very known issue, some containers work around this issue by using UID/GID mapping, check all the images from LinuxServer. I would though, not overcomplicate things in the image here, stick to the A-plan. If a user decides to use host mounts, they need to be properly writeable by the correct user. Maybe just error out a proper way if acme/certs are not writeable
A good way could be here, start with root, create the folders for traefik on the volume and then degrade to the Traefik user. AFAIR that is still secure, but i am not entirely remembering it right now, we would need to double check if dropping privileges this way is to be considered secure for the initial attack vector
The example from Kubic requires a Kubernetes 1.13.x cluster, to benefit from the "PSP" (Pod Security Policy) Great that it actually exists.
The main issue with a non root user is the ability to open ports < 1024. Even though Linux provides the
CPA_BIND_NET
capability, it does not work on Kubernetes, because of the ambient capabilities not supported: kubernetes/kubernetes#56374 . From this there are 2 solutions:Well i would say that's a very know thing also, using 8080/8443 for those LB kind of images. Surely when the default is changed, this needs a fat-warning in the release notes of the docker-image or even a Traefik release notice, which introduces the new image. In the end, the person just remaps the ports to 80/443 when mapping on the host, i rarely matters what the dest port.
Support of non-root only on the Alpine image, by installing the libcap package and using the
setcap net_bind_service=+ep /traefik
command to fix the capabilities mask issue. This won't work on the scratch image AFAIK. Never worked with it, will get an ideaDecide to switch the default exposed port from 80 to 8080 as proposed in #43 . This is a non easy change, because it would require to adapt Traefik's default configuration (which creates an entrypoint on 80/TCP). Maybe it could be done with a default
CMD ["--entrypoints=''Name:http Address::8080"]
instruction to NOT modify Traefik itself.Totally agree that we should just do that for the docker-image, not for the binary using parameters. Remapping the ports you just mentioned using CMD is what i do fairly regular either by using ENV with my env based image https://github.com/EugenMayer/docker-image-traefik/blob/master/tiller/common.yaml#L34 or as you mentioned https://github.com/EugenMayer/docker-image-rundeck/blob/master/docker-compose.yml#L71 and other projects. It works very well. I guess we would of course put this into the
CMD
of the docker image as a defaultThoughts?
- That is only an issue for host-mount based volumes and is a known aspect everybody runs docker in production is aware of, when using host mounts. I would not focus on that "volume" aspect too much here - it's the same for all projects using
gosu
or whatever.
@EugenMayer Points taken. The goal of my comment was to underline some technical facts to have them written and shared, because not everyone is having them as principal concerns.
Let's hear what the others members of the community would want to say about this topic.
From awesome-traefik, I found this article: "How to run Træfik as a non-root user ? Part 1" https://medium.com/@zepouet/how-to-run-tr%C3%A6fik-as-non-privileged-user-4a824bc5cc0
Traefik could also instead drop privileges at startup? (Does it already? Nginx and Apache drop privileges after binding 80 and 443. edit: https://github.com/containous/traefik/issues/1434 ... https://github.com/tmiller/setuidgid/blob/master/main.go) From https://linux-audit.com/how-and-why-linux-daemons-drop-privileges/ :
if (getuid() == 0) {
/* process is running as root, drop privileges */
if (setgid(groupid) != 0)
fatal("setgid: Unable to drop group privileges: %s", strerror(errno));
if (setuid(userid) != 0)
fatal("setuid: Unable to drop user privileges: %S", strerror(errno));
}
Though, "syscall: Setuid/Setgid doesn't apply to all threads on Linux" https://github.com/golang/go/issues/1435#issuecomment-374434789
... FWIW, "Docker integration: Exposing Docker socket to Traefik container is a serious security risk" https://github.com/containous/traefik/issues/4174 (Here's a different approach that does not mount the docker socket into a container for a process running as root: https://github.com/liquidat/ansible-role-traefik/blob/master/tasks/main.yml)
Compared to just running as non-root and updating the docs - and maybe the seclist -- to say 'You need to map the port now', setcap is less than optimal, because it doesn't limit access to other ports < 1024 (which shouldn't be mapped anyway, and nobody should be using source ports for any firewall rules):
setcap 'cap_net_bind_service=+ep' $(which traefik)
But in terms of least privileges, setcap should be unnecessary if the app is running as non-root. Filesystem privileges can be set with an entrypoint script or in a traefik init routine (prior to dropping privileges).
Having similar requirements for security reasons, we also investigate the non-root options.
@westurner : Can you please clarify a bit more what breaks in your approach at #43 ? (the redirection break you mention is not clear, is it because of ACME usage, or because of an [entryPoints.http.redirect] in your toml)
did you try the user: "${UID}:${GID}" option in compose (instead of creating your own image)
I haven't yet tried specifying the UID/GID. That might be less to maintain; but it won't solve for the port remapping issue. (Nonroot cannot bind ports less than <1024 without CAP_NET_RAW, which is really unnecessary particularly for a frontend service). This solved; though I'm not sure if I've forgone any features (?) by creating the redirect with the port number specified instead of just referencing the other entrypoint by name:
[entryPoints]
[entryPoints.http]
address = ":8080"
[entryPoints.http.redirect]
#entryPoint = "https"
regex = "^http://(.*):8080/(.*)"
replacement = "https://$1/$2"
[entryPoints.https]
address = ":8443"
[entryPoints.https.tls]
#[[entryPoints.https.tls.certificates]]
#certFile = "/certs/website.crt"
#keyFile = "/certs/website.key"
FWIW, Traefik runs perfectly fine as a user with these options in a systemd service file:
User=traefik
AmbientCapabilities=CAP_NET_BIND_SERVICE
CAP_NET_BIND_SERVICE allows it to bind to 80 and 443.
I basically run it with almost all options enabled in here: https://github.com/containous/traefik/blob/master/contrib/systemd/traefik.service, except that it does not run with LimitNPROC=1
. Not tested with the Docker backend.
The main issue with a non root user is the ability to open ports < 1024
In case traefik runs within a container (which is the whole point of the issue), this shouldn't be necessary anyway, right?
Because you can map the port to the "outside world" anyway (i.e. 3000
to 80
& 3001
to 443
) and the parent process (i.e. Docker or Podman) will handle this.
That is known, but you have to use a port greater then 1024 in the container / image, which would change the current default 80/443 and that is the whole point of the discussion
Now would be a good time to revisit this, since v2 that was just released is has backward incompatible changes requiring manual upgrade.
Just FYI got this working with Caddy with Let's Encrypt. You can de-escalate your privileges from root
to a traefik
user using setpriv
(or gosu
). setpriv
is a standard utility in most distros now making it super easy in ubuntu or even alpine.
Set an entrypoint.sh
script, do any needed chown
with your host bind mounts, and then go ahead and run traefik with setpriv --reuid=traefik --regid=nogroup --init-groups /usr/bin/traefik
.
You can see an example of this in action in the mysql and jenkins containers currently but I'll be submitting a patch to Caddy soon, if you'd like a reference.
For this to work you also need to remember to set sysctls: net.ipv4.ip_unprivileged_port_start=0
in your docker-compose.yml
or set the sysctl when using docker run
so you can access port 80 and port 443 as a non-root user.
you should not run traefik as root in the docker image.