CertGrid CertGrid
Troubleshooting·Podman

Podman Pasta Source Address Behavior

Two curls from the same machine reach the same container and its access log records `::1` for one and `169.254.1.2` for the other. Neither is the client's address, and any rate limiting or allowlisting inside the container is reading a fiction.

Networking Guide 31 of 47 Advanced

Written against the versions above. Podman follows the distribution here rather than a vendor repository, so the version you get is the one Ubuntu shipped. The commands are stable across 5.x.

Every command on this page ran on podman01.
Server NameIP AddressOSRolesCPURAMHDD
PODMAN01192.168.0.21Ubuntu 26.04 LTSPrimary Container Host2 Core4 GB50 GB

Before you start

  1. The same client, two source addresses

    Publish nginx on 8082, then request it twice from the same machine - once via localhost and once via the host's own LAN address. Read the access log:

    ::1          - - "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0"
    169.254.1.2  - - "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0"

    Same machine, same curl, same container - two different source addresses, and neither is 192.168.0.21.

    • Via localhost the connection arrived as ::1, IPv6 loopback, passed through as-is.
    • Via the LAN address it arrived as 169.254.1.2 - which is exactly pasta's --map-guest-addr. It is a link-local placeholder meaning "the host, as seen from inside".

    So the address a rootless containerised server sees is an artefact of how the packet got in, not a fact about who sent it.

    bash Example session
    podman run -d --name seen -p 8082:80 docker.io/library/nginx:alpinea2f3542fe57e9367c5c684859ae3deda4b4544e311ee4caffc7f00b6ca85df43curl -s -o /dev/null http://localhost:8082curl -s -o /dev/null http://192.168.0.21:8082podman logs seen/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d//docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh/docker-entrypoint.sh: Configuration complete; ready for start up2026/08/22 12:03:21 [notice] 1#1: using the "epoll" event method2026/08/22 12:03:21 [notice] 1#1: nginx/1.31.42026/08/22 12:03:21 [notice] 1#1: built by gcc 15.2.0 (Alpine 15.2.0)2026/08/22 12:03:21 [notice] 1#1: OS: Linux 7.0.0-30-generic2026/08/22 12:03:21 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 524288:5242882026/08/22 12:03:21 [notice] 1#1: start worker processes2026/08/22 12:03:21 [notice] 1#1: start worker process 252026/08/22 12:03:21 [notice] 1#1: start worker process 26::1 - - [22/Aug/2026:12:03:21 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0" "-"169.254.1.2 - - [22/Aug/2026:12:03:22 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0" "-"

    Expected resultTwo access-log lines with different source addresses, one ::1 and one 169.254.1.2.

    Success conditionYou have two log lines from one client showing two addresses.

  2. What this breaks

    This is not cosmetic. Anything inside the container that reads the client address is now wrong:

    • Rate limiting by IP sees every external client as one address, so the first busy client throttles everyone. limit_req_zone $binary_remote_addr in nginx does exactly this.
    • Allowlists and denylists either match nothing or match everything.
    • Access logs are useless for anything after the fact - no attribution, no geography, no abuse tracing.
    • Anti-abuse and login throttling keyed on IP silently stop working, which is the dangerous one because nothing errors.

    And the failure is quiet. The server sees *an* address, it is a valid address, and nothing logs a warning. You find out when the numbers stop making sense.

    Worth being clear this is not a Podman defect. Rootless port publishing means a userspace process accepts the connection and opens a second one to the container; the original address is simply not carried across. Docker's userland-proxy has the same effect, and rootful Podman with real NAT does preserve it.

    bash Example session
    podman rm -f seenseen

    Expected resultThe container removed.

    Success conditionYou can name three things that break when the source IP is wrong.

  3. The options, and the one that actually works

    In rough order of preference:

    1. Pass the address in a header. Put a reverse proxy on the host - not in a rootless container - and have it set X-Forwarded-For. Then configure your application to trust that header only from the proxy's address. This is the normal answer and the one to reach for; it also happens to be the fix for guide 51, since the proxy can hold the privileged port.

    2. Use --network host. No port mapping and no pasta, so the real address arrives - at the cost of all network isolation. Reasonable for a single-purpose host, not otherwise.

    3. Run rootful. A real bridge with NAT preserves the source address. Weigh it against everything in guide 3.

    4. Do not key on IP. Often the right answer anyway. Sessions, tokens and accounts are better identities than an address that NAT, mobile networks and CGNAT already made unreliable.

    What does not work: reading the address harder. There is no Podman flag to recover it, because the information is not in the connection the container received.

    The check worth doing on any containerised service: request it from a genuinely remote machine and read your own access log. If the address is not the one you connected from, you have this problem, and it is better to know now than when you are trying to explain a traffic pattern.

    bash Example session
    podman run --rm --network host docker.io/library/alpine ip -o addr show eth02: eth0    inet 192.168.0.21/24 brd 192.168.0.255 scope global eth0\       valid_lft forever preferred_lft forever2: eth0    inet6 fe80::215:5dff:fe01:115a/64 scope link \       valid_lft forever preferred_lft forever

    Expected resultThe host's real address inside a --network host container.

    Success conditionYou can choose a fix and say what it costs.

Troubleshooting

Official sources