NET_ADMIN is the Linux capability that lets a process perform network administration, and in a container it does exactly that inside whichever network namespace the process runs. It is not a licence to open a socket, and it is not a substitute for a device or a route. The interesting question is never whether the capability is granted but which namespace receives the change.

What the capability covers

Inside its namespace, a process holding NET_ADMIN can bring interfaces up and down, assign addresses, add and remove routes, and write firewall rules, which is the group of operations capabilities(7) collects under this capability. It can also adjust interface-level settings that control how the kernel queues and filters traffic. A VPN client typically needs a few of those at startup and nothing else afterwards. Most images therefore ask for more reach than they use.

Operations outside that namespace are not covered, and adjacent privileges are separate. Reading and writing raw packets belongs to NET_RAW, and creating a tun device needs both the capability and access to the device node. A process that lacks the device gets a permission error that looks like a network problem and is not one.

The namespace decides the blast radius

A container on its own bridge network reaches its own loopback, its own interfaces and its own routing table. Granting NET_ADMIN there lets the VPN client build a tunnel without touching the host's routing decisions, which is the arrangement most container images assume.

Switch that container to host networking and the same grant now edits the host's routes and the host's firewall, because there is no separate namespace left. The client that was supposed to manage one tunnel can then redirect traffic for the whole machine, and a mistake in its teardown script leaves the host with rules nobody wrote. Nothing in the capability itself changed; the context did. That is why the same compose file behaves differently on two hosts.

Three prerequisites that get merged into one

Startup failures in VPN containers usually involve one of three separate things: the capability, the tunnel device, and kernel support for the protocol. Granting all three at once works, and it also makes the next failure impossible to attribute. A minimal compose fragment grants the capability and passes the device node explicitly:

services:
  vpn:
    image: example/vpn-client:latest
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1

Read the image's own documentation for which sysctls it needs, because a sysctl in a container may require an additional privilege or a host-level setting. Where the image supports userspace-only mode, no device is needed at all and the capability list shrinks accordingly.

Grant the narrow operation, not the mode

Privileged mode hands over every capability and broadens device access at the same time, so it hides which of the three prerequisites was missing. It also makes the container's failure modes indistinguishable from host changes. Prefer an explicit list even when it is longer to write.

Rootless and user-namespace deployments change what a capability means in practice, since the process may hold it only inside a mapped namespace. Copying a capability list from a rootful example to a rootless runtime and expecting the same reach is a common way to lose an afternoon.

Secrets belong in a separate decision. Holding NET_ADMIN says nothing about which peer identity the client presents, which credentials it loads, or how those are rotated. Network administration does not supply trust.

Verify the change and record why the grant exists

Confirm what the container actually received before you debug the tunnel. The inspection below reports the added capabilities and the network mode, then shows what the interface list looks like from inside:

docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.NetworkMode}} {{.HostConfig.Privileged}}' vpn
docker exec vpn sh -c 'grep CapEff /proc/self/status'
docker exec vpn ip -br addr
docker exec vpn ip route

If the capability is present and the interface list is still empty, the client failed before it could create anything, and the container logs are the next place to look. If the tunnel exists but traffic does not flow, the routing table inside the container answers the next question.

Test the teardown path as well, because a client that writes firewall rules can leave them behind. Stop the container and re-check the host's rule set if host networking is in use. A grant that is only needed for two seconds at startup should not be described in the compose file as a permanent requirement without a note explaining that.

Write the reason next to each capability: which operation needed it and what fails without it. That note survives image updates, and it is the difference between a reviewed privilege list and one that grew by copy and paste.

Mikanda mikatakata