Learn SELinux - Containers (Podman, Docker, CRI-O)
Episode 10 of 23

Learn SELinux - Containers (Podman, Docker, CRI-O)

Reviewing how SELinux secures containers: the container_t and spc_t domains, bind mount relabeling with the :Z and :z options, label security settings on Podman and Docker, and SELinux configuration in Kubernetes pod security context and its integration with seccomp and LSMs.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 9, you protected web servers and databases with the right labels. Now it's time for the technology that changed how deployments run: containers. Namespaces and cgroups separate containers logically, but neither stops an escaped process from accessing host resources. This is where SELinux steps in as the second isolation layer — and that's why it plays a big role in runtimes like Podman, Docker, and CRI-O.

If SELinux is off on a container host, one compromised container can read another container's data on the same host. This episode explains how SELinux prevents that scenario.

Domains: container_t and spc_t

The SELinux container policy introduces two important domains:

  • spc_t (Super Privileged Container) — the domain for runtime engines like dockerd, containerd, podman, and conmon. This engine is fully trusted: it's the one that builds and manages all containers.
  • container_t — the domain for processes running inside a container. It's tightly locked and can't automatically read host files.

Note

The analogy: spc_t is the building manager holding the master keys to all units, while container_t is the tenant who only has the key to their own unit. Separate the two firmly — when you execute something inside a container, you're in the container_t world, not the host world.

Because container processes are in a different domain from host processes, privileges that leak out of a container don't directly become full access to the host — they still have to pass through the SELinux type rules. This is why a container running processes as root is still not "host root" in SELinux's eyes.

Bind Mounts and Relabeling: :Z and :z

The most common need is moving a host directory into a container with a bind mount: -v /data:/data. The problem is that the /data files are usually labeled for a specific domain (for example default_t or a user home), and container_t isn't allowed to read them.

The solution isn't changing the host label permanently, but relabeling at mount time. Two main options:

  • :Z — relabel the mount with a unique per-container label (full privacy). Each container gets its own MCS category, so other containers can't read those files.
  • :z — relabel the mount with a label shared by all containers (shared). All containers using that volume can access each other.
Relabel a bind mount with Podman
podman run -v /data:/data:Z nginx
Relabel a bind mount with Docker
docker run -v /data:/data:Z nginx

Important

Choose deliberately. :Z gives isolation between containers, :z allows sharing between all containers. If a volume is shared by several containers that genuinely need to read each other — like a cache volume — use :z. For data that belongs to only one container, always :Z. Using :z on sensitive data means opening a door between containers that should have been isolated.

Label Security Options on Runtimes

Besides :Z and :z, both runtimes provide finer label control through --security-opt:

Customize the container label
podman run --security-opt label=type:container_t nginx
podman run --security-opt label=level:s0:c123,c456 nginx
podman run --security-opt label=disable nginx

Available options:

  • label=type:... — force a specific type domain.
  • label=user:... and label=role:... — set the user and role on the label.
  • label=level:... — set the MCS category, useful for tenant isolation.
  • label=disable — turn off SELinux for that container.

Warning

label=disable removes the SELinux layer entirely for that container. Use it only for short-term troubleshooting or genuinely special cases — and understand that it negates the protection we're building. The habit of "just add label=disable" when a denial appears is an expensive shortcut down the road.

Podman has an ergonomic advantage: it automatically relabels mounted volumes, so many :Z/:z cases don't need to be written manually. But it's still important to understand what happens behind the scenes.

SELinux in Kubernetes

In Kubernetes, the runtime (usually CRI-O or containerd) takes SELinux instructions from the pod security context. The configuration is made per pod, not per container:

KubernetesseLinuxOptions on the Pod SecurityContext
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  securityContext:
    seLinuxOptions:
      user: system_u
      role: system_r
      type: container_t
      level: "s0:c123,c456"
  containers:
    - name: app
      image: nginx

Each pod gets a unique MCS level. The consequence is very important: pod A with level s0:c123 can't read files belonging to pod B with level s0:c456 — even though both share the same container_t type. That's the isolation between workloads running on the same host, without the hassle of changing labels one by one.

Tip

On large clusters, don't manage MCS levels manually — let the runtime or tooling (for example via PSP/GAC or a policy engine like Kyverno) allocate a unique level per pod. Manual management is error-prone and creates isolation gaps. The only thing humans need to manage is choosing the correct type, user, and role for the workload.

LSM, Seccomp, and AppArmor

Container runtimes use several security mechanisms at once, and it's important to tell them apart:

  • SELinux is an LSM (Linux Security Module) — kernel hooks that decide access based on labels. Historically, only one primary LSM (SELinux, AppArmor, or Smack) is active per kernel. This is why RHEL-family distros use SELinux while Ubuntu and Debian use AppArmor — the two rarely run together on the same system.
  • Seccomp is a different category: it's not an LSM, but a syscall filter. Because their paths are separate, seccomp can be active together with SELinux — and that's indeed the correct practice.

The ideal combination in Kubernetes: a seccomp profile restricts syscalls, SELinux restricts access based on labels, and cgroups restrict resources. Three layers working at different levels that complement each other — this is what defense in depth really means.

Closing

In this episode 10, you've seen how SELinux becomes the second isolation layer for containers: the spc_t domain for runtimes and container_t for container processes, bind mount relabeling with :Z and :z, fine label control through --security-opt, and seLinuxOptions configuration in Kubernetes with a unique MCS level per pod.

The key takeaways:

  • Containers in the container_t domain can't automatically read the host — that's the protection.
  • :Z for per-container privacy, :z for sharing between containers.
  • Seccomp and SELinux work together: one restricts syscalls, one restricts access based on labels.

So far all storage is still local to the host. In episode 11, we'll take SELinux into the distributed world: NFS, Samba & Network Services — how labeling works on NFS with mount options and noexec, the consequences of sharing storage between hosts, samba booleans for exporting directories and homes, and port labeling for non-standard services.

Learn SELinux - Containers (Podman, Docker, CRI-O) | Learn SELinux