Running programs as a non-root user is must in security sensitive environments. However, these programs sometimes need to publish their service on privileged ports like port 80 – which cannot be used by local users. Systemd offers a simple way to solve this problem.
Background
Running services as non-root users is a quite obvious: if it is attacked and a malicious user gets control of the service, the rest of the system should still be secure in terms of access rights.
At the same time plenty programs need to publish their service at ports like 80 or 443 since these are the default ports for http communication and thus for interfaces like REST. But these ports are not available to non-root users.
Problem shown at the example gitea
To show how to solve this with systemd, we take the self hosted git service gitea as an example. Currently there are hardly any available packages, so most people end up installing it from source, for example as the user git
. A proper sysmted unit file for such an installation in a local path, running the service as a local user, is:
$ cat /etc/systemd/system/gitea.service [Unit] Description=Gitea (Git with a cup of tea) After=syslog.target After=network.target After=postgresql.service [Service] RestartSec=2s Type=simple User=git Group=git WorkingDirectory=/home/git/go/src/code.gitea.io/gitea ExecStart=/home/git/go/src/code.gitea.io/gitea/gitea web Restart=always Environment=USER=git HOME=/home/git [Install] WantedBy=multi-user.target
If this service is started, and the application configuration is set to port 80, it fails during the startup with a bind error:
Jan 04 09:12:47 gitea.qxyz.de gitea[8216]: 2018/01/04 09:12:47 [I] Listen: http://0.0.0.0:80 Jan 04 09:12:47 gitea.qxyz.de gitea[8216]: 2018/01/04 09:12:47 [....io/gitea/cmd/web.go:179 runWeb()] [E] Failed to start server: listen tcp 0.0.0.0:80: bind: permission denied
Solution
One way to tackle this would be a reverse proxy, running on port 80 and forwarding traffic to a non-privileged port like 8080. However, it is much more simple to add an additional systemd socket which listens on port 80:
$ cat /etc/systemd/system/gitea.socket [Unit] Description=Gitea socket [Socket] ListenStream=80 NoDelay=true
As shown above, the definition of a socket is straight forward, and hardly needs any special configuration. We use NoDelay
here since this is a default for Go on sockets it opens, and we want to imitate that.
Given this socket definition, we add the socket as requirement to the service definition:
[Unit] Description=Gitea (Git with a cup of tea) Requires=gitea.socket After=syslog.target After=network.target After=postgresql.service [Service] RestartSec=2s Type=simple User=git Group=git WorkingDirectory=/home/git/go/src/code.gitea.io/gitea ExecStart=/home/git/go/src/code.gitea.io/gitea/gitea web Restart=always Environment=USER=git HOME=/home/git NonBlocking=true [Install] WantedBy=multi-user.target
As seen above, the unit definition hardly changes, only the requirement for the socket is added – and NonBlocking
as well, to imitate Go behavior.
That’s it! Now the service starts up properly and everything is fine:
[...] Jan 04 09:21:02 gitea.qxyz.de gitea[8327]: 2018/01/04 09:21:02 Listening on init activated [::]:80 Jan 04 09:21:02 gitea.qxyz.de gitea[8327]: 2018/01/04 09:21:02 [I] Listen: http://0.0.0.0:80 Jan 04 09:21:08 gitea.qxyz.de gitea[8327]: [Macaron] 2018-01-04 09:21:08: Started GET / for 192.168.122.1 [...]
The Gitea documentation covers more examples of possible systemd service configurations, including the usage of capabilities instead of sockets.
Sources, further reading
- systemd for Developers I, especially the examples at the end
- systemd socket activation in Go, the Go specific options are explained there
Thank you for your insights into systemd sockets
In some cases the service file (e.g. gitea.service) is part of the system install and you might not want to change it.
The easy solution is to add the dependency to the socket file directly. As in your example:
$ cat /etc/systemd/system/gitea.socket
[Unit]
Description=Gitea socket
[Socket]
ListenStream=80
NoDelay=true
[Install]
RequiredBy=gitea.service
So everything is neatly in one place.
Thanks, that is also a very intelligent way to handle that. Thanks for that suggestion!
This is clear and well explained solution on how to enable the permission for a non root user application to access ports lower than 1024 by first setting up a socket.
However, many service daemons are written to run as root in order to bind the necessary sockets below 1024 and then they change the running process to a non root user. In such cases adding User= and Group= to the systemd unit file causes them to fail to start even when the socket has already been started separately as per your example.
In the case of gitea, the do use the User= and Group= directives in the systemd unit file in their code on Github, and in the comments of that file give instructions in how to also add a socket file to use as an http or ssh backup. I do not know if that was present at the time you wrote your article.
So maybe using gitea in the example was not the best choice and there should be a warning not to to specify the User and Group as non root users with daemons that drop privileges from root.
Thanks for the comment! I wasn’t aware of the updated documentation and added a note in the blog post.