This guide is about how to configure networking when using rootless Podman.
Listening TCP/UDP sockets
method | source address preserved | native perfomance | support for binding to specific network device | minimum port number |
---|---|---|---|---|
socket activation (systemd user service) | ✔️ | ✔️ | ✔️ | ip_unprivileged_port_start |
socket activation (systemd system service with User=) | ✔️ | ✔️ | ✔️ | 0 |
pasta | ✔️ | ✔️ | ip_unprivileged_port_start | |
pasta + custom network | ip_unprivileged_port_start | |||
slirp4netns + port_handler=slirp4netns | ✔️ | ip_unprivileged_port_start | ||
slirp4netns + port_handler=rootlesskit | ip_unprivileged_port_start | |||
host | ✔️ | ✔️ | ✔️ | ip_unprivileged_port_start |
The methods
- pasta
- pasta + custom network
- slirp4netns + port_handler=rootlesskit
- slirp4netns + port_handler=slirp4netns
- host
are mutually exclusive.
Socket activation can be combined with the other methods.
For example, it is possible to combine socket activation with pasta + custom network to get source address preserved and native speed communication to an HTTP reverse proxy that is running on a custom network.
Example:
If the source address is preserved in the incoming TCP connection, then nginx is able to see the IP address of host2 (192.0.2.10) where the curl request is run.
flowchart LR
curl-->nginx["nginx container"]
subgraph host1
nginx
end
subgraph host2 ["host2 ip=192.0.2.10"]
curl
end
nginx logs the HTTP request as coming from 192.0.2.10
192.0.2.10 - - [15/Jun/2023:07:41:18 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.1.1" "-"
If the source address is not preserved, then nginx sees another source address in the TCP connection. For example, if the nginx container is run with slirp4netns + port_handler=rootlesskit
podman run --network=slirp4netns:port_handler=rootlesskit \
--publish 8080:80 \
--rm \
ghcr.io/nginxinc/nginx:latest
nginx logs the HTTP request as coming from 10.0.2.2
10.0.2.2 - - [15/Jun/2023:07:41:18 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.1.1" "-"
Click me
This example uses two computers
- host1.example.com (for running the nginx web server)
- host2.example.com (for running curl)
- On host1 create user test
sudo useradd test
- Open an interactive shell session for the user test
sudo machinectl shell test@
- Create directories
mkdir -p ~/.config/containers/systemd mkdir -p ~/.config/systemd/user
- Create the file /home/test/.config/containers/systemd/nginx.container containing
[Container] Image=ghcr.io/nginxinc/nginx-unprivileged:latest ContainerName=mynginx Environment="NGINX=3;" [Install] WantedBy=default.target
- Create the file /home/test/.config/systemd/user/nginx.socket containing
[Unit] Description=nginx socket [Socket] ListenStream=0.0.0.0:8080 [Install] WantedBy=default.target
- Reload the systemd user manager
systemctl --user daemon-reload
- Pull the container image
podman pull ghcr.io/nginxinc/nginx-unprivileged:latest
- Start the socket
systemctl --user start nginx.socket
- Test the nginx web server by accessing it from host2
- Log in to host2
- Run curl
curl host1.example.com:8080
- Log out from host2
- Check the logs in the container mynginx
The output should look something like
podman logs mynginx 2> /dev/null | grep "GET /"
nginx logged the source address of the TCP connection to be 192.0.2.10 which matches the IP address of host2.example.com. Conclusion: the source address was preserved.192.0.2.10 - - [15/Jun/2023:07:41:18 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.1.1" "-"
A side-note: If the feature request https://trac.nginx.org/nginx/ticket/237 gets implemented, the Environment="NGINX=3;"
could be removed. This example makes use of the fact that "nginx includes an undocumented, internal socket-passing mechanism" quote from https://freedesktop.org/wiki/Software/systemd/DaemonSocketActivation/
Click me
This example uses two computers
- host1.example.com (for running the nginx web server)
- host2.example.com (for running curl)
- On host1 create user test
sudo useradd test
- Open an interactive shell session for the user test
sudo machinectl shell test@
- Create directories
mkdir -p ~/.config/containers/systemd
- Create the file /home/test/.config/containers/systemd/nginx.container containing
[Container] Image=ghcr.io/nginxinc/nginx-unprivileged:latest ContainerName=mynginx Network=pasta PublishPort=0.0.0.0:8080:8080 [Install] WantedBy=default.target
- Reload the systemd user manager
systemctl --user daemon-reload
- Pull the container image
podman pull ghcr.io/nginxinc/nginx-unprivileged:latest
- Start the service
systemctl --user start nginx.service
- Test the nginx web server by accessing it from host2
- Log in to host2
- Run curl
curl host1.example.com:8080
- Log out from host2
- Check the logs in the container mynginx
The output should look something like
podman logs mynginx 2> /dev/null | grep "GET /"
nginx logged the source address of the TCP connection to be 192.0.2.10 which matches the IP address of host2.example.com. Conclusion: the source address is preserved.192.0.2.10 - - [15/Jun/2023:07:55:03 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.1.1" "-"
Click me
Follow the same steps as
example: pasta - source address preserved
but replace Network=pasta
with Network=mynet.network
. Create the network unit
file mynet.network
that defines a custom network. (mynet is an arbitrarily chosen name)
In other words, replace step 4 with
- Create the file /home/test/.config/containers/systemd/nginx.container containing
Create the file /home/test/.config/containers/systemd/mynet.network containing
[Container] Image=ghcr.io/nginxinc/nginx-unprivileged:latest ContainerName=mynginx Network=mynet.unit PublishPort=0.0.0.0:8080:8080 [Install] WantedBy=default.target
[Network]
At step 9 you will see that the source address is not preserved. Instead of 192.0.2.10 (IP address for host1.example.com), nginx instead logs the IP address 10.89.0.2.
podman logs mynginx 2> /dev/null | grep "GET /"
The output should look something like
10.89.0.2 - - [24/Jun/2024:07:10:59 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.6.0" "-"
Click me
Follow the same steps as
example: pasta - source address preserved
but replace Network=pasta
with Network=slirp4netns:port_handler=slirp4netns
.
In other words, replace step 4 with
- Create the file /home/test/.config/containers/systemd/nginx.container containing
[Container] Image=ghcr.io/nginxinc/nginx-unprivileged:latest ContainerName=mynginx Network=slirp4netns:port_handler=slirp4netns PublishPort=0.0.0.0:8080:8080 [Install] WantedBy=default.target
Click me
Follow the same steps as
example: pasta - source address preserved
but replace Network=pasta
with Network=slirp4netns:port_handler=rootlesskit
.
In other words, replace step 4 with
- Create the file /home/test/.config/containers/systemd/nginx.container containing
[Container] Image=ghcr.io/nginxinc/nginx-unprivileged:latest ContainerName=mynginx Network=slirp4netns:port_handler=rootlesskit PublishPort=0.0.0.0:8080:8080 [Install] WantedBy=default.target
At step 9 you will see that the source address is not preserved. Instead of 192.0.2.10 (IP address for host1.example.com), nginx instead logs the IP address 10.0.2.100.
podman logs mynginx 2> /dev/null | grep "GET /"
The output should look something like
10.0.2.100 - - [15/Jun/2023:07:55:03 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.1.1" "-"
Click me
Follow the same steps as
example: pasta - source address preserved
but remove the line PublishPort=0.0.0.0:8080:8080
and replace Network=pasta
with Network=host
.
In other words, replace step 4 with
- Create the file /home/test/.config/containers/systemd/nginx.container containing
[Container] Image=ghcr.io/nginxinc/nginx-unprivileged:latest ContainerName=mynginx Network=host [Install] WantedBy=default.target
method | native perfomance |
---|---|
socket activation (systemd user service) | ✔️ |
socket activation (systemd system service) | ✔️ |
pasta | |
slirp4netns + port_handler=slirp4netns | |
slirp4netns + port_handler=rootlesskit | |
host | ✔️ |
Best performance has
- socket activation (systemd user service)
- socket activation (systemd system service)
- host
where there is no slowdown compared to running directly on the host.
The other methods ordered from fastest to slowest:
- pasta
- slirp4netns + port_handler=rootlesskit
- slirp4netns + port_handler=slirp4netns
method | support for binding to specific network device |
---|---|
socket activation (systemd user service) | ✔️ |
socket activation (systemd system service) | ✔️ |
pasta | ✔️ |
pasta + custom network | |
slirp4netns + port_handler=slirp4netns | |
slirp4netns + port_handler=rootlesskit | |
host | ✔️ |
examples
Click me
Specify the network device to bind to with the systemd directive BindToDevice in the socket unit file.
For example, to bind to the ethernet interface eth0, add the line
BindToDevice=eth0
The socket unit file could look like this
[Unit]
Description=example socket
[Socket]
ListenStream=0.0.0.0:8080
BindToDevice=eth0
[Install]
WantedBy=default.target
Click me
To publish the TCP port 8080 and bind the listening socket to the ethernet interface eth0 use the configuration lines
Network=pasta:-t,0.0.0.0%%eth0/8080:8080
under the [Container]
section in the container file.
For example the file /home/test/.config/containers/systemd/nginx.container containing
[Container]
Image=ghcr.io/nginxinc/nginx-unprivileged:latest
ContainerName=mynginx
Network=pasta:-t,0.0.0.0%%eth0/8080:8080
[Install]
WantedBy=default.target
If you want to publish an UDP port instead of a TCP port, replace -t
with -u
above.
Side note 1: The quadlet configuration directive PublishPort=
is not used.
The port is in this example published by specifying the pasta -t
option.
Side note 2: Due to how quadlet/systemd parses the configuration line, a percentage character needs to be escaped by prepending it with an extra percentage character.
The percentage character should not be escaped when the network option is provided
as a command-line option for podman run
, for example --network=pasta:-t,0.0.0.0%eth0/8080:8080
Read the current setting
$ cat /proc/sys/net/ipv4/ip_unprivileged_port_start
1024
To set a new value (for example 443), create the file /etc/sysctl.d/99-mysettings.conf with the contents:
net.ipv4.ip_unprivileged_port_start=443
and reload the configuration
sudo sysctl --system
The setting is system-wide so changing it impacts all users on the system.
Giving this privilege to all users on the computer might not be what you want
because often you already know which systemd service should be listening on a privileged port.
If the software supports socket activation, an alternative is to set up a
systemd system service with User=
. For details, see the
section Socket activation (systemd system service with User=)
An example of an outbound TCP/UDP connection to the internet is when a container downloads a file from a web server on the internet.
method | native perfomance |
---|---|
pasta | |
slirp4netns | |
host | ✔️ |
An example of an outbound TCP/UDP connection to the host's localhost is when a container downloads a file from a web server on the host that listens on 127.0.0.1:80.
method | outbound TCP/UDP connection to the host's localhost allowed by default |
---|---|
pasta | |
slirp4netns | |
host | ✔️ |
Connecting to the host's localhost is not enabled by default for pasta and slirp4netns due to security reasons.
See network mode host
as to why access to the host's localhost is considered insecure.
To allow curl in a container to connect to a web server on the host that listens on 127.0.0.1:80,
for pasta add the option --map-gw
podman run --rm \
--network=pasta:--map-gw \
registry.fedoraproject.org/fedora curl 10.0.2.2:80
and for slirp4netns add the option slirp4netns:allow_host_loopback=true
podman run --rm \
--network=slirp4netns:allow_host_loopback=true \
registry.fedoraproject.org/fedora curl 10.0.2.2:80
For better performance and security, pasta offers an alternative to using --map-gw
.
The option -T
(--tcp-ns
) configures TCP port forwarding from the container network namespace to the init network namespace.
podman run --rm \
--network=pasta:-T,81:80 \
registry.fedoraproject.org/fedora curl 127.0.0.1:81
(Instead of the port number 81, it would also have been possible to specify the port number 80)
For more information about how to use pasta to connect to a service running on the host, see GitHub comment.
method | description |
---|---|
systemd directive OpenFile= |
The executed command in the container inherits a file descriptor to an already opened file. |
bind-mount, (--volume ./dir:/dir:Z ) | Standard way |
The systemd directive OpenFile=
was introduced in systemd 253 (released February 2023).
See also https://github.com/eriksjolund/podman-OpenFile
This method can only be used for container images that has software that supports socket activation.
Socket activation of a systemd user service is set up by creating two files
- ~/.config/systemd/user/example.socket
and either a Quadlet file
- ~/.config/containers/systemd/example.container
or a service unit
- ~/.config/systemd/user/example.service
Systemd system service (User=
) and socket activation makes it possible for rootless Podman to use privileged ports.
For details of how to use socket-actived nginx, see for instance Example 3, Example 4, Example 5, Example 6 in the repo https://github.com/eriksjolund/podman-nginx-socket-activation
There is a Podman feature request
for adding Podman support for User=
in systemd system services.
The feature request was moved into a GitHub discussion.
Pasta is enabled by default if no --network
option is provided to podman run
.
Pasta is generally the better choice because it is often faster and has more features than slirp4netns.
On RPM-based systems the executable pasta is in the passt RPM package.
Show the RPM package for the executable /usr/bin/pasta
$ rpm -qf --queryformat "%{NAME}\n" /usr/bin/pasta
passt
The RPM package passt-selinux contains the SELinux configuration for pasta.
To install pasta on Fedora run
$ sudo dnf install -y passt passt-selinux
See the --network
option.
See also the pasta web page https://passt.top/
Pasta is the default rootlessNetworkCmd since Podman 5.0.0 (released March 2024).
To show the rootlessNetworkCmd that is configured to be used by default, run
podman info -f '{{.Host.RootlessNetworkCmd}}'
If jq is installed on the computer, then the same result is produced with
podman info -f json | jq -r .host.rootlessNetworkCmd
If podman info does not support the field RootlessNetworkCmd
, then
it's possible to find out the information by running
podman run -d --rm -p 12345 docker.io/library/alpine sleep 300
and observing if the helper process is pasta
or slirp4netns
.
For details:
Click me
- Set the shell variable
user
to a username that is not in use.user=mytestuser
- Create the new user
sudo useradd $user
- Open a shell for the new user
sudo machinectl shell --uid $user
- Verify that no pasta processes are running as the new user.
The command should not list any processes.
pgrep -u $USER pasta -l
- Verify that no slirp4netns processes are running as the new user.
The command should not list any processes.
pgrep -u $USER slirp4netns -l
- Run container
(12345 is just an arbitrary container port number)
podman run -d --rm -p 12345 docker.io/library/alpine sleep 300
- Check if there are any pasta processes running as the new user.
If the command lists any processes, then pasta is detected as being the default.
pgrep -u $USER pasta -l
- Check if there are any slirp4netns processes running as the new user.
If the command lists any processes, then slirp4netns is detected as being the default.
pgrep -u $USER slirp4netns -l
- Exit the shell
exit
- Optional step: Delete the newly created user
The podman run option -p
(--publish
) publishes
a container's port, or a range of ports, to the host.
This example shows that if podman run is given -p 8080:80
, then podman starts pasta with the argument -t 8080-8080:80-80
(which is equivalent to -t 8080:80
)
Full example: Click me
- Run an nginx container and publish container port 80 to host port 8080
podman run -p 8080:80 \ -d \ --rm \ --name test \ docker.io/library/nginx
- Fetch a web page with curl
The command prints the following output
curl -s http://localhost:8080 | head -4
<!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title>
- Check command-line arguments of the pasta process
The command prints the following output
pgrep -l -a pasta
result: pasta was started with the option851253 /usr/bin/pasta --config-net -t 8080-8080:80-80 --dns-forward 169.254.0.1 -u none -T none -U none --no-map-gw --quiet --netns /run/user/1004/netns/netns-830a424a-0592-361f-556b-7bef910405cf
-t 8080-8080:80-80
which is equivalent with-t 8080:80
- Remove container
podman container rm -t0 -f test
Although ports are usually published by providing the podman run option -p
(--publish
) , this example shows that passing --network pasta:-t,8080:80
is roughly equivalent to passing -p 8080:80
Full example: Click me
- Run an nginx container and publish container port 80 to host port 8080
podman run --network pasta:-t,8080:80 \ --rm \ -d \ --name test \ docker.io/library/nginx
- Fetch a web page with curl
The command prints the following output
curl -s http://localhost:8080 | head -4
<!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title>
- Check command-line arguments of the pasta process
The command prints the following output
pgrep -l -a pasta
result: pasta was started with the option851253 /usr/bin/pasta --config-net -t 8088-8088:80-80 --dns-forward 169.254.0.1 -u none -T none -U none --no-map-gw --quiet --netns /run/user/1004/netns/netns-830a424a-0592-361f-556b-7bef910405cf
-t 8088-8088:80-80
which is equivalent with-t 8088:80
- Remove container
podman container rm -t0 -f test
Let pasta check once a second for new listening sockets (TCP or UDP) in the container and automatically publish them.
Use --network=pasta:-t,auto
Full example: Click me
-
Create directory dir
-
Create the file dir/Containerfile with the contents
FROM docker.io/library/fedora RUN dnf -y install iproute nmap-ncat
-
Build the container image
podman build -t ncat dir/
-
Run
nc -l 1234
in the container (but first wait 60 seconds)podman run --network=pasta:-t,auto \ --rm \ -d \ --name test \ localhost/ncat bash -c "sleep 60 && nc -l 1234 && sleep inf"
The container starts listening on port 1234 after a delay of 60 seconds. The delay is added to demonstrate that pasta will detect that a listening socket is created while the container is running.
-
Check if the listening TCP port 1234 has been published on the host
$ ss -tlnp "sport = 1234" State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
result: No
-
Wait 60 seonds and check again if the listening TCP port 1234 has been published on the host
$ ss -tlnp "sport = 1234" State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:1234 0.0.0.0:* users:(("pasta",pid=2644,fd=146))
result: Yes. After 60 seconds
nc
in the container started listening on TCP port 1234. Pasta detected this and published TCP port 1234 on the host. -
Remove container
podman container rm -t0 -f test
Side note: Pasta does not publish TCP ports below ip_unprivileged_port_start.
Slirp4netns is similar to Pasta but is slower and has less functionality. Slirp4netns was the default rootlessNetworkCmd before Podman 5.0.0 (released March 2024).
The two port forwarding modes allowed with slirp4netns are described in https://news.ycombinator.com/item?id=33255771
See the --network
option.
--network=host
is considered insecure.
Quote from podman run man page: "The host mode gives the container full access to local system services such as D-bus and is therefore considered insecure".
See also the article [CVE-2020–15257] Don’t use --net=host . Don’t use spec.hostNetwork that explains why running containers in the host network namespace is insecure.
Check which network backend is in use
$ podman info --format {{.Host.NetworkBackend}}
netavark
The network backend CNI (Container Network Interface) was removed in Podman 5.0.0. The reasons for replacing CNI with Netavark are described in the article Podman 4.0's new network stack: What you need to know.
Netavark is the default network backend.
Example Create a network and run an nginx container
Create the network mynyet
$ podman network create mynet
Start the container docker.io/library/nginx and let it be connected to the network mynet
$ podman run -d -q --network mynet docker.io/library/nginx
19f812cfbb43c022529b84bb9914cda2b16e55ef09c0bc8e937afddfc803f812
Check the IP address
$ podman container inspect -l --format "{{(index .NetworkSettings.Networks \"mynet\").IPAddress}}"
10.89.0.2
Try to fetch a web page from nginx
$ curl --max-time 3 10.89.0.2
curl: (28) Connection timed out after 3000 milliseconds
result: curl was not able to connect to the web server
Join the rootless network namespace used for netavark networking before running the curl command
$ podman unshare --rootless-netns curl --max-time 3 10.89.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
result: curl fetched the web page.
The pasta option --pcap enables capturing of network traffic.
Example
Capture a curl request with pasta to the file myfile.pcap.
Use tshark
to analyse the file myfile.pcap.
- Fetch web page from http://podman.io with
curl
pasta is configured to capture network traffic to the file myfile.pcappodman run \ --rm \ --network=pasta:--pcap,myfile.pcap \ docker.io/library/fedora curl http://podman.io
Build tshark container image
- Create directory
mkdir ctr
- Create the file ctr/Containerfile with the contents
FROM docker.io/library/fedora RUN dnf install -y tshark && dnf clean all
- Build container image tshark
podman build -t tshark ctr/
Show HTTP host and HTTP method in HTTP requests
- Use tshark to analyse the file myfile.pcap.
The command prints the following output
podman run \ --rm -v ./myfile.pcap:/mnt/myfile.pcap:Z,ro \ --user 65534:65534 \ --userns keep-id:uid=65534,gid=65534 \ localhost/tshark \ tshark \ -r /mnt/myfile.pcap \ -T fields \ -e http.host \ -e http.request.method \ -Y http | sort -u
podman.io GET
Show the destination address of IP packets.
- Use tshark to analyse the file myfile.pcap.
The command prints the following output
podman run \ --rm -v ./myfile.pcap:/mnt/myfile.pcap:Z,ro \ --user 65534:65534 \ --userns keep-id:uid=65534,gid=65534 \ localhost/tshark \ tshark \ -r /mnt/myfile.pcap \ -T fields \ -e ip.dst | sort -u
10.0.2.15 169.254.0.1 185.199.110.153
- Look up DNS A record of podman.io
The command prints the following output
host -t a podman.io
The IP address 185.199.110.153 is also seen in the tshark output in step 1.podman.io has address 185.199.110.153 podman.io has address 185.199.111.153 podman.io has address 185.199.108.153 podman.io has address 185.199.109.153