| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64 |
| Applies to | Any Linux host with rootless Podman |
| Privilege | Rootless examples as podtest; sysctl, firewalld, and setcap demos need sudo |
| Scope | Privileged host ports 80 and 443 with rootless Podman — reproduce pasta and rootlessport errors, kernel ip_unprivileged_port_start, sysctl persistence, reverse proxy pattern, firewalld forward-port, NET_BIND_SERVICE and setcap misconceptions, host networking limits, and fix selection. Does not cover general -p syntax, rootless setup, or full proxy/firewall tutorials. |
| Related guides | Rootless Podman networking Rootless Podman Podman networking modes |
Publishing -p 80:80 or -p 443:443 as a normal user often fails before your container starts. The error text depends on whether Podman uses pasta or rootlessport, but the underlying cause is the same: the host-side forwarding process is unprivileged and Linux blocks low ports by default.
This guide reproduces both error shapes, explains the kernel rule, and walks through fixes that keep the workload rootless. General port-mapping syntax lives in Podman port mapping.
The lab user is podtest for rootless examples.
Reproduce the rootless port 80 failure
Check the kernel threshold first:
cat /proc/sys/net/ipv4/ip_unprivileged_port_start1024Publish host port 80 as a normal user with default pasta networking:
podman run --rm -p 80:80 docker.io/library/alpine:3.20 echo okError: pasta failed with exit code 1:
Failed to bind port 80 (Permission denied) for option '-t 80-80:80-80'On Podman 5.x with pasta you may also see wording like:
Listen failed for HOST TCP port */80: Permission denied
Couldn't listen on requested TCP portsOlder tutorials often quote rootlessport instead:
rootlessport cannot expose privileged port ...
bind: permission deniedBoth strings point at the same kernel restriction; the process that reports the error depends on the networking path — covered later in this page.
Why rootless Podman cannot bind port 80 by default
Linux enforces privileged ports through:
net.ipv4.ip_unprivileged_port_startWhen the value is 1024:
0–1023 → privileged (need capability or root for bind)
1024+ → unprivilegedRootless Podman publishes ports through a host-side helper — pasta on the default path in Podman 5, or rootlessport on some bridge setups. That helper runs as your login user, not host root.
Adding a capability inside the container does not change the host-side listener:
--cap-add NET_BIND_SERVICE → container process only
host pasta/rootlessport → still unprivileged on the hostThat is why --cap-add NET_BIND_SERVICE is not the generic fix for -p 80:80.
Method 1: Lower ip_unprivileged_port_start
Read the current value:
sysctl net.ipv4.ip_unprivileged_port_startnet.ipv4.ip_unprivileged_port_start = 1024Allow unprivileged binds from port 80 upward (HTTP and HTTPS):
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80net.ipv4.ip_unprivileged_port_start = 80Retry the publish. Stop any existing service on port 80 first if something like httpd already listens there:
podman run --rm -p 80:80 docker.io/library/alpine:3.20 echo okokTo allow HTTPS only while keeping port 80 privileged, use 443 instead:
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=443HTTPS on port 443 succeeds at that threshold:
podman run --rm -p 443:443 docker.io/library/alpine:3.20 echo okokPort 80 still fails when the threshold is 443:
podman run --rm -p 80:80 docker.io/library/alpine:3.20 echo okFailed to bind port 80 (Permission denied) for option '-t 80-80:80-80'Restore the lab default when finished:
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=1024Make the sysctl persistent
Create a drop-in file:
sudo tee /etc/sysctl.d/99-rootless-podman-ports.conf <<'EOF'
net.ipv4.ip_unprivileged_port_start=80
EOFApply all sysctl drop-ins:
sudo sysctl --systemVerify:
sysctl net.ipv4.ip_unprivileged_port_startSecurity scope matters: ip_unprivileged_port_start is a per-network-namespace setting. Changing it with the host sysctl command changes the host network namespace, so ordinary unprivileged host processes — not only Podman — can bind ports at or above the new threshold. After setting 80, any unprivileged user or daemon in that namespace can bind port 80 and above unless another policy blocks it. Treat it as a host-namespace decision, not a Podman-only permission grant.
Method 2: Run on a high port behind a reverse proxy
A common production pattern keeps the application rootless on a high port and lets a root-managed proxy own 80 and 443.
Publish only on localhost at 8080:
podman run -d --name web-app -p 127.0.0.1:8080:8080 IMAGEA root-managed reverse proxy listens on:
80 / 443and forwards to:
127.0.0.1:8080Client
│ 80/443
▼
root-managed proxy (nginx, Caddy, HAProxy)
│ 8080
▼
rootless Podman appAdvantages:
- container stays rootless
- TLS termination can live in one place
- no global sysctl change
- multiple apps can route by hostname
This page does not include a full proxy tutorial — only the split between privileged edge ports and an unprivileged container port.
Method 3: Redirect port 80 or 443 with the host firewall
Another pattern publishes the container on a high port and uses the host firewall to redirect inbound traffic:
host port 80
↓
firewalld local port redirect
↓
host port 8080
↓
rootless Podman listenerRun the container on 8080:
podman run -d --name web-app \
-p 8080:80 \
docker.io/library/nginx:alpineUnlike the reverse-proxy method, this example publishes 8080 on the host rather than only on 127.0.0.1, because firewalld redirects the incoming packet to another local port instead of opening a new connection to the loopback listener.
On RHEL 10 with firewalld active, add a forward rule. For same-host redirection, use toport without toaddr:
sudo firewall-cmd --zone=public \
--add-forward-port=port=80:proto=tcp:toport=8080successMake it persistent across reboots when you are satisfied with the rule:
sudo firewall-cmd --zone=public \
--add-forward-port=port=80:proto=tcp:toport=8080 \
--permanentsudo firewall-cmd --reloadRemove a test rule:
sudo firewall-cmd --zone=public \
--remove-forward-port=port=80:proto=tcp:toport=8080If you made it permanent, also remove the permanent rule:
sudo firewall-cmd --zone=public \
--remove-forward-port=port=80:proto=tcp:toport=8080 \
--permanentTrade-offs:
- requires root or admin firewall access
- application container stays rootless
- host firewall becomes part of the operational contract
- localhost traffic may not follow the same DNAT path as external clients on every distribution — verify from the client perspective you care about
Method 4: Use systemd socket activation
Some applications support systemd socket activation: a system-level socket unit owns privileged port 80 or 443, and systemd passes the listening file descriptor to an unprivileged service process.
That works only when the application explicitly supports inherited sockets. Most container images expect to call bind() themselves, so this path is specialized compared with a reverse proxy. Treat it as an advanced alternative, not the default recommendation.
Why --cap-add NET_BIND_SERVICE does not fix host port publishing
With ip_unprivileged_port_start still at 1024, adding the capability inside the container does not help:
podman run --rm --cap-add NET_BIND_SERVICE -p 80:80 docker.io/library/alpine:3.20 echo okError: pasta failed with exit code 1:
Failed to bind port 80 (Permission denied) for option '-t 80-80:80-80'NET_BIND_SERVICE changes what the container process may do inside its namespace. The pasta process creating the host publish socket still runs as your user and still hits the kernel low-port rule.
Why setcap on rootlessport is not a reliable fix
Many older articles recommend:
sudo setcap cap_net_bind_service=+ep /usr/libexec/podman/rootlessportOn Podman 5 rootless defaults, pasta handles publishing — not rootlessport. Changing rootlessport cannot fix a pasta failure.
Even on a bridge network where rootlessport runs, the capability did not help on this host. After applying setcap:
getcap /usr/libexec/podman/rootlessport/usr/libexec/podman/rootlessport cap_net_bind_service=epPublishing port 80 still failed:
podman run --rm --network bridge -p 80:80 docker.io/library/alpine:3.20 echo okError: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission deniedRemove the lab capability before you finish:
sudo setcap -r /usr/libexec/podman/rootlessportDo not leave modified capabilities on system binaries.
pasta error vs rootlessport error
| Networking path | Typical error source | Example text |
|---|---|---|
| Default pasta (Podman 5 rootless) | pasta host bind | Failed to bind port 80 (Permission denied) or Listen failed for HOST TCP port */80: Permission denied |
Rootless bridge / explicit --network bridge |
rootlessport | rootlessport cannot expose privileged port 80 ... bind: permission denied |
The root cause is identical — unprivileged host bind below ip_unprivileged_port_start. Only the reporting process differs. Search results may show either string depending on Podman version and network mode.
Force the rootlessport path to see the older wording:
podman run --rm --network bridge -p 80:80 docker.io/library/alpine:3.20 echo okError: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied--network=host has the same kernel restriction
Host networking does not bypass the low-port rule for a rootless process. If the application inside the container tries to listen on port 80:
podman run --rm --network host docker.io/library/alpine:3.20 nc -l -p 80nc: bind: Permission denied--network=host is not a workaround for privileged port publishing.
Should you set the threshold to 0, 80, or 443?
| Value | Effect |
|---|---|
0 |
unprivileged processes may bind any port — rarely appropriate |
80 |
ports 80 and above are unprivileged — covers both HTTP and HTTPS |
443 |
ports 443 and above only — port 80 stays privileged |
| Requirement | Set threshold to |
|---|---|
HTTPS only (443+) |
443 |
HTTP and HTTPS (80 and 443) |
80 |
| every port | 0 (avoid unless you understand the exposure) |
Do not set 0 when you only need web ports.
Which fix should you use?
| Method | Scope | Good fit |
|---|---|---|
lower ip_unprivileged_port_start |
host network namespace | dedicated server where broad low-port access is acceptable |
| reverse proxy | per service | typical web workloads — recommended default |
| firewall redirect | host networking rule | fixed port translation without changing sysctl |
| systemd socket activation | app-specific | software that supports inherited sockets |
| run Podman rootful | changes privilege model | workload genuinely needs rootful operation — see Rootless vs rootful Podman |
No single method is universally best. Development containers can publish 8080 during testing. Production hosts usually pair rootless apps on high ports with a root-managed edge proxy.
References
- Podman run manual — publish ports
- Podman rootless documentation
- ip_unprivileged_port_start — kernel documentation
- firewalld forward-port
Summary
Rootless Podman cannot publish host ports below ip_unprivileged_port_start — usually 1024 — because pasta or rootlessport runs as your user when creating the host listener. Podman 5 with pasta reports Failed to bind port 80 (Permission denied); bridge mode may show the older rootlessport cannot expose privileged port text instead. The kernel rule is the same in both cases.
Fixes fall into three practical buckets. Lowering net.ipv4.ip_unprivileged_port_start to 80 works but changes the host network namespace, so ordinary unprivileged processes — not just your container — can bind at or above the new threshold. Publishing on 127.0.0.1:8080 and fronting the app with a root-managed reverse proxy is the cleaner pattern for most web workloads. A firewalld forward-port rule can redirect inbound 80 to a high port while the container stays rootless.
--cap-add NET_BIND_SERVICE and setcap on rootlessport do not solve pasta publishing failures — the host-side helper remains unprivileged. --network=host does not bypass the restriction either. Pick sysctl tuning only when you accept the host-namespace exposure; otherwise use a high port plus proxy or redirect and keep the workload rootless.

