Allow non-root processes to bind to privileged ports

Let non-root processes on Linux bind to privileged ports by lowering net.ipv4.ip_unprivileged_port_start, or grant cap_net_bind_service to just one binary

For historical reasons there is a restriction in most Linux distributions, even in most *nix operating systems, that requires a process to have root privileges to be able to open a listening socket on ports below 1024. The idea made sense back when a machine had many untrusted users, but on my own dev box every user is me anyway.

I keep hitting this when I run a dev server, or a rootless container, as my own user. The service wants port 80 or 443, starts as an ordinary user, and gets EPERM on bind().

The kernel decides all this with the net.ipv4.ip_unprivileged_port_start sysctl. By default it is set to 1024, and lowering it (all the way to 0) is all that is needed. There is a way to reconfigure this:

1
2
3
4
5
6
7
root@mybox:~# echo 'net.ipv4.ip_unprivileged_port_start=0' > /etc/sysctl.d/50-unprivileged-ports.conf
root@mybox:~# sysctl --system
* Applying /etc/sysctl.d/10-console-messages.conf ...
...
* Applying /etc/sysctl.d/50-unprivileged-ports.conf ...
net.ipv4.ip_unprivileged_port_start = 0
* Applying /etc/sysctl.d/99-sysctl.conf ...

Dropping the file into /etc/sysctl.d/ makes the change survive a reboot, and sysctl --system applies it right away.

Warning
This is a host-wide change. From now on any user on the machine can bind to any port below 1024. On my single-user dev box that is fine. On a multi-user server, think twice — or use the per-binary alternative below.

To verify, read the value back and then start a listener on a privileged port as an ordinary user:

1
2
3
4
ponu@mybox:~$ sysctl net.ipv4.ip_unprivileged_port_start
net.ipv4.ip_unprivileged_port_start = 0
ponu@mybox:~$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

A plain python3 process binding port 80 without root — that proves it works.

The per-binary alternative

If opening up every port for every user feels too blunt, grant the capability cap_net_bind_service to just the binary in question:

1
2
3
root@mybox:~# setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
root@mybox:~# getcap /usr/local/bin/myapp
/usr/local/bin/myapp cap_net_bind_service=ep

Two caveats to keep in mind. The capability is stored as an extended attribute on the binary itself, so it silently disappears whenever a package upgrade replaces the file. And it has no effect on interpreted scripts, since the capability would have to be set on the interpreter instead. When the process runs under systemd it can be granted in the unit file instead with AmbientCapabilities=CAP_NET_BIND_SERVICE, which also survives upgrades.

Wrapping up

Lowering ip_unprivileged_port_start to 0 removed the privileged-port restriction machine-wide with two commands, and a regular user can now run services on port 80 and 443. If that is broader than you need, reach for setcap (or ambient capabilities) instead.

References