Understanding Podman's security model: user namespaces and --userns=keep-id for rootless isolation, the difference between SELinux and AppArmor, capability rationing with default-cap-drop, syscall filtering via seccomp, and the --security-opt no-new-privileges flag.

In episode 12 you ran a multi-container stack with podman-compose and opened the Docker API socket. Those containers are now running — but how secure are they? Episode 13 covers Podman's security model: how user namespaces separate privileges, how SELinux and AppArmor apply labels, and how capabilities and seccomp shrink the attack surface. This is core material before you decide which workloads deserve to run.
Container security isn't about a single mechanism, but about complementary layers. A container is a process running inside namespaces — if one layer fails, the next must still hold. The principle: give the container only the privileges it needs, nothing more.
Podman's most prominent feature: containers run rootless by default, without a daemon. Every rootless container runs inside a user namespace that maps your user UID to another UID inside the container. Because the kernel sees a separate namespace, processes inside the container have no access to the host system except through explicit mappings.
The default mapping is reasonably safe, but for certain cases you need control: for example bind-mounting a host folder owned by a specific user so ownership stays consistent inside the container.
podman run --userns=keep-id -v $(pwd)/data:/data myapp:1.2--userns=keep-id makes the UID inside the container equal to the host user's UID. Useful when a container writes to a bind mount owned by your user without having to change file ownership back and forth. Other commonly used modes:
| Mode | Behavior |
|---|---|
host | No user namespace, host and container UIDs are the same |
keep-id | Container UID and GID match the host user |
auto | Podman allocates the mapping automatically |
private | A new private namespace for the container |
Both technologies do something similar with different approaches — and determine which Linux distribution you use:
| Aspect | SELinux | AppArmor |
|---|---|---|
| Main distributions | RHEL, Fedora, Rocky, AlmaLinux | Debian, Ubuntu, openSUSE |
| Model | Type-based labels (MAC) | Path-based profiles |
| Implementation | In-kernel LSM with labels | Per-application profiles |
| On containers | Context labels like :Z and :z | The container-default profile |
| Configuration | /etc/selinux | /etc/apparmor.d |
For volumes, Podman offers label suffixes. -v $(pwd)/data:/data:Z relabels the volume, while :z shares the same label with other containers. On an SELinux machine, forgetting these labels is the most common cause of Permission denied errors on bind mounts.
Important
Permission denied errors on the RHEL family almost always come from the SELinux context, not ordinary Unix permissions. Check with ausearch -m avc -ts recent or journalctl before blaming your configuration. Disabling SELinux for convenience is an expensive decision — it's better to learn to apply labels correctly.
Containers are run by a regular user, but root inside the container is a shrunk root: a set of capabilities (granular privileges) is granted by the runtime. The term default-cap-drop refers to the list of capabilities NOT granted by default by Podman and the OCI runtime — such as CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, and CAP_NET_ADMIN.
To control them:
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx:alpine
podman run --cap-drop=NET_RAW myapp:1.2The rule of thumb: start from --cap-drop=ALL and only add what's needed with --cap-add. A service that only needs to bind a port above 1024, for example, is fine with NET_BIND_SERVICE.
Seccomp filters which syscalls a process inside the container may call. Podman uses the default profile from the OCI runtime, which rejects syscalls dangerous in certain scenarios. This profile is JSON-based and can be replaced:
podman run --security-opt seccomp=./my-profile.json myapp:1.2If your application is unusual and needs a particular syscall, you can write your own profile — but make sure you understand the impact before loosening the filter.
podman run --security-opt no-new-privileges myapp:1.2--security-opt no-new-privileges sets the no_new_privs flag on the process. Once this flag is active, the process and its children can never escalate privileges through mechanisms like setuid binaries. It's the last, simple yet effective barrier.
Now that all mechanisms are covered, here's the safe default arrangement for most workloads:
podman run -d --name api \
--userns=keep-id \
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
--read-only --tmpfs /tmp \
myapp:1.2Notice the logical order: namespace is restricted, capabilities are reduced, privilege is locked, and the filesystem is made read-only. These four steps close most common attack paths before your application even runs.
Note
The combination of --userns=keep-id with volume labels like :Z is often the configuration point that goes wrong most. Test on one container, inspect with podman inspect, then use it as a template for other stacks.
| Layer | Mechanism | Main configuration |
|---|---|---|
| Namespace | Rootless user namespace | --userns |
| Label | SELinux or AppArmor | --security-opt label, :Z, :z |
| Capabilities | Granular runtime privileges | --cap-add, --cap-drop |
| Syscall | Seccomp profile | --security-opt seccomp |
| Privilege | no_new_privs | --security-opt no-new-privileges |
| Read-only | Rootfs and filesystem | --read-only |
None of them stands alone; it's the combination that forms defense in depth. You'll revisit some of these layers in episode 16 when covering full hardening.
In episode 13 you understood Podman's security model: the least-trust principle, rootless isolation via user namespaces, UID mapping with --userns=keep-id, the difference between SELinux and AppArmor, capability rationing with --cap-add and --cap-drop, seccomp syscall filtering, and locking down with --security-opt no-new-privileges.
The key points to take home:
--cap-drop=ALL and add only what's needed.In the next episode, Episode 14, you'll protect the most valuable asset: image security & secrets — how to store credentials without leaking them into the environment, verify image authenticity with signatures, scan vulnerabilities with Trivy, and patch CVEs before they become incidents.