Podman Solutions

Rootless Privileged Ports
Login

Rootless Privileged Ports

Motivation

One of the common problems with using rootless Podman containers is that the user wants to use a privileged port (1-1023) which OSes commonly block by default. This article collects the common solutions to this, with the intent of retaining as much of your host OS's stock rootless user security setup as possible.

For the purposes of our example, we'll say your container is listening on localhost:12345, but you want it to appear on TCP port 80 via the HTTP protocol. More complicated schemes involving TLS or other ports are left to the reader.

Port-Forwarding

The best solution I'm aware of is to tell your OS's firewall to forward the port to your application.

firewalld version

The simplest version of this is on Red Hattish OSes, because it ships with firewalld by default:

$ sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=12345
$ sudo firewall-cmd --reload

That's it! In the background, firewalld arranges everything for you so that when a TCP connection comes in on port 80, it gets sent to the local process listening on port 12345.

The most beautiful part of this scheme is not its brevity but the fact that this is the only step you have to do as root. Everything else runs rootless.

nftables version

On any other Linux distribution, essentially the same solution should be available through the kernel's underlying nftables mechanism. If available, it will do exactly the same thing, albeit with more setup.

Begin by appending this to /etc/nftables.conf:1

table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
        tcp dport 80 redirect to :12345
    }
    chain loopback-nat {
        type nat hook output priority -100; policy accept;
        oif lo tcp dport 80 counter redirect to :12345
    }
}

This method has the virtue that it also covers localhost access, which the firewall-cmd method does not.2 If you don't need that ability, you can drop the loopback-nat chain from the configuration.

Next, find your sysctl.conf file, typically either in /etc or /etc/sysconfig, then find the net.ipv4.ip_forward=1 line inside and uncomment it. Then:

sudo sysctl --load=/etc/sysctl.conf
sudo systemctl enable --now nftables

At this point, the same thing should be happening as with the simpler firewalld commands above.

Reverse Proxy

Another option is to run a reverse proxy of some type in front of your container. The proxy starts with root privileges so that it is able to bind to the low-numbered port, then typically drops those privileges before making the internal connection to your container.

For HTTP or HTTPS, the traditional way to do this is to run a sufficiently powerful web proxy server on the host — e.g. HAProxy or Apache — and configure it to connect to the high-numbered port your program is listening on. For instance, with nginx, the configuration might be:

server {
    listen 80;
    location / {
        proxy_pass http://127.0.0.1:12345;
    }
}

The more modern way is to run the proxy as a container itself. As with the host-side proxy, it has to start as root to bind to the low-numbered port, which in this context means it must run under the rootful installation of Podman but connect to a container running under the rootless one. The same web proxy servers are available for this purpose as above, but you might also consider container-native proxies like Caddy and Traefik.

Socket Activation

There's a nice blend between reverse proxying and direct execution, available under one major condition: the launched process can accept its I/O from the C stdio handles or otherwise has a method for directing use of a particular file descriptor. An important secondary condition is that your process allows being called separately for each TCP connection, much like in the old CGI model. For services with a low connection rate, the overhead amortizes to near-zero.

Setting this up is rather involved, so I will hand those of you interested in the concept off to another article at this point.

Giving Up

There is one final method I want to cover, not because I recommend it, but because it is frequently found in web searches, and I wish to stop the practice. It's lame, insecure, and otherwise inferior to every last one of the methods above. It is the networking equivalent of chmod -R 777 . It has but a single advantage, being that it's trivial to implement:

$ sudo sysctl net.ipv4.ip_unprivileged_port_start=80

This invites a number of problems, which is a large part of the reason I wrote this article in the first place. I want to give you better options, without the substantial downsides of the above scheme.

The Free-for-All

The biggest problem with the setting above is that it allows any program on that server to bind to ports 80-1023, not just a single one that needs special handling.

I would rather that this special program be a proxy server designed to take due care with security when given the ability to bind to a privileged port. For instance, nginx configured as an HTTPS proxy is much less susceptible to MITM attacks because it does all of the TLS key checking in a world-class fashion. Your hand-rolled REST API server is likely to overlook an attack vector that the big boys blocked decades ago.

The Over-Reach

Linux's sysctl setting does not allow you to open just one port, much less a few, as with the common 80 + 443 pair. No, the limit to your control here is a single input value that sets the low end of the range. You have to ask, what else is your system listening on above 80 that this change might interfere with? Are you using Kerberos or LDAP for authentication on ports 88, 389, and 636? How about the NetBIOS/SMB suite on 137-139? IMAPv4 mail service on 143 and 993? SMTP reception on 465 and 587?

I don't know about you, but I do not want to allow random rootless programs binding to any of these ports merely because I had the need to glom my wee local REST API server onto port 80.

You can claw this over-reach back by use of the OS level firewall, but while that is a good and useful security measure, the setting above reduced it from a sensible two-ply defense-in-depth design to a single-layer barrier.

Still Giving Up? Okay.

If the above considerations do not move you, then I have to wonder why you don't give up entirely on low-numbered port security:

$ sudo sysctl net.ipv4.ip_unprivileged_port_start=0

If you do that, I wish you good luck without any expectation that you will experience it!

Before you do that, please allow me to make one last effort to keep you from running headlong over a cliff: this version opens another 79 ports over the first try above, most notably SSH. On a box that listens only on 22, 80, and 443, it is at least possible to make the argument that securing access to the SSH port is worthwhile but that ceding HTTP/HTTPS to user programs is not.

One more consideration: DNS traditionally runs on port 53, and it is positively foundational to identity management on the modern Internet. It's used not merely for host name to IP address mapping but also things like mail server security. It can even affect higher ports like HTTPS by way of DNS-based ACME challenges; a rogue program might be able to get into the middle of that conversation and end up impersonating the site's identity over any protocol based on TLS.

License

This work is © 2025 by Warren Young and is licensed under CC BY-NC-SA 4.0


  1. ^ That's where Debian puts the file. It might be elsewhere on your system. If it's missing entirely, your OS might not have the nftables facility at all.
  2. ^ See this for the reason the extra rule is necessary.