How To

Configure SELinux Context for Podman Storage on Rocky Linux 10 / AlmaLinux 10

Podman stores container images and layers under /var/lib/containers/storage by default. SELinux labels that path with the container_var_lib_t type, which allows the container runtime full access. When you move Podman storage to a custom directory – a separate disk, an NFS mount, or just a different partition – SELinux blocks access because the new path carries a generic label like default_t. Setting the correct SELinux context on custom storage directories is the fix.

This guide walks through configuring SELinux contexts for Podman storage on Rocky Linux 10, AlmaLinux 10, and RHEL 10. We cover custom graphroot and runroot paths, volume bind mount labels, and the :Z and :z mount flags.

Why Podman Needs Custom SELinux Contexts

SELinux enforces mandatory access control by labeling every file, directory, and process with a security context. Podman’s default storage at /var/lib/containers carries the container_var_lib_t label, and the runtime directory at /run/containers uses container_runtime_tmpfs_t. These labels grant Podman the permissions it needs to pull images, create overlay layers, and launch containers.

When you point Podman at a custom directory – say /data/containers on a dedicated disk – that directory inherits whatever label its parent has (often default_t or var_t). SELinux denies access, and you get errors like “permission denied” or “cannot change memory protections” even though standard Unix permissions look fine. The solution is to assign the correct SELinux type to the custom path using semanage fcontext and restorecon.

Prerequisites

  • Rocky Linux 10, AlmaLinux 10, or RHEL 10 with SELinux in enforcing mode
  • Root or sudo access
  • Podman installed (ships by default on RHEL family systems)
  • The policycoreutils-python-utils package (provides semanage)

Install the required SELinux management tools if they are not already present:

sudo dnf install -y policycoreutils-python-utils

Confirm SELinux is enforcing:

getenforce

The output should show Enforcing. If it shows Permissive or Disabled, enable it in /etc/selinux/config and reboot before continuing. For more on managing SELinux modes and policies, see our guide on troubleshooting SELinux on Rocky Linux 10 / AlmaLinux 10.

Step 1: Check Current Podman Storage Configuration

Before making changes, check where Podman currently stores data. Run podman info and filter for the storage paths:

podman info | grep -E 'graphRoot|runRoot|graphDriverName'

On a default installation, you should see paths under /var/lib/containers and /run/containers:

graphDriverName: overlay
graphRoot: /var/lib/containers/storage
runRoot: /run/containers/storage

You can also view the storage configuration file directly:

sudo cat /etc/containers/storage.conf | grep -E '^graphroot|^runroot'

The graphroot setting controls where images, containers, and layers are stored on disk. The runroot setting controls where runtime state (lock files, temporary mounts) goes. Both paths need correct SELinux labels when changed to custom locations.

Step 2: Create Custom Storage Directory

Create the directory where Podman will store container data. This is typically on a separate partition or disk with enough space for your container images:

sudo mkdir -p /data/containers/storage

Check the current SELinux label on the new directory:

ls -Zd /data/containers/storage

The output shows the default label, which is not suitable for Podman:

unconfined_u:object_r:default_t:s0 /data/containers/storage

The default_t type tells us SELinux has no specific policy for this path. Podman will fail with permission errors if we try to use it as-is.

Step 3: Set SELinux Context for graphroot

Use semanage fcontext to add a persistent file context rule that labels the custom directory and all its contents with the container_var_lib_t type. This is the same type used on the default /var/lib/containers path:

sudo semanage fcontext -a -t container_var_lib_t "/data/containers/storage(/.*)?"

This adds a rule to the local SELinux policy. The regex (/.*)? ensures the label applies to the directory and everything inside it. Now apply the label to existing files:

sudo restorecon -Rv /data/containers/storage

The restorecon command relabels files based on the policy rules. You should see output confirming the context change:

Relabeled /data/containers/storage from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:container_var_lib_t:s0

Verify the label is set correctly:

ls -Zd /data/containers/storage

The output should now show container_var_lib_t:

unconfined_u:object_r:container_var_lib_t:s0 /data/containers/storage

An alternative approach uses semanage fcontext -a -e to copy the context from an existing path. This maps the new directory to match the labels of /var/lib/containers exactly:

sudo semanage fcontext -a -e /var/lib/containers /data/containers

Both methods achieve the same result. The -t container_var_lib_t approach is more explicit and easier to audit later.

Step 4: Set SELinux Context for runroot

If you are also changing the runroot path (the runtime state directory), it needs the container_runtime_tmpfs_t type. This directory holds lock files, temporary mounts, and other runtime data.

Create the custom runroot directory:

sudo mkdir -p /data/containers/run

Add the SELinux file context rule:

sudo semanage fcontext -a -t container_runtime_tmpfs_t "/data/containers/run(/.*)?"

Apply the label:

sudo restorecon -Rv /data/containers/run

Verify the label shows the correct type:

ls -Zd /data/containers/run

The output confirms the runtime label is applied:

unconfined_u:object_r:container_runtime_tmpfs_t:s0 /data/containers/run

If you are only changing graphroot and keeping runroot at the default /run/containers/storage, skip this step.

Step 5: Update Podman Storage Configuration

With the SELinux labels in place, update the Podman storage configuration to use the custom paths. First, stop any running containers and reset Podman storage:

sudo podman system reset --force

This removes all existing images, containers, and volumes from the old storage location. Back up any important data first. Now edit the storage configuration file:

sudo vi /etc/containers/storage.conf

Find the [storage] section and update the graphroot and optionally the runroot values:

[storage]
driver = "overlay"
graphroot = "/data/containers/storage"
runroot = "/data/containers/run"

Save the file and verify Podman picks up the new configuration:

podman info | grep -E 'graphRoot|runRoot'

The output should reflect the custom paths:

graphRoot: /data/containers/storage
runRoot: /data/containers/run

Step 6: Verify Podman Works with Custom Storage

Pull a test image to confirm that Podman can write to the new storage directory without SELinux denials:

sudo podman pull docker.io/library/alpine:latest

The pull should complete without errors. Verify the image is stored in the custom location:

sudo podman images

You should see the Alpine image listed:

REPOSITORY                     TAG       IMAGE ID       CREATED       SIZE
docker.io/library/alpine       latest    a606584aa9aa   2 weeks ago   8.05 MB

Now run a test container to verify full functionality:

sudo podman run --rm alpine cat /etc/os-release

The container should start, print the Alpine release info, and exit cleanly. If it does, SELinux is properly configured for the custom storage path. You can also confirm that data physically lives in the new directory:

ls /data/containers/storage/overlay-images/

This directory should contain the Alpine image layers. If you want to run containers as systemd services, the same SELinux labels apply – Podman generates unit files that reference the configured storage paths.

SELinux Contexts for Container Volumes

Beyond storage directories, SELinux also controls access to bind-mounted volumes. When you mount a host directory into a container with -v /host/path:/container/path, the container process needs permission to read (and possibly write) that host directory. SELinux denies this by default because the container runs in the container_t domain, which cannot access arbitrary host paths.

The container_file_t type is the label that grants containers access to bind-mounted directories. You can set it permanently with semanage:

sudo semanage fcontext -a -t container_file_t "/srv/appdata(/.*)?"
sudo restorecon -Rv /srv/appdata

However, Podman provides a more convenient approach with the :Z and :z volume mount flags that handle SELinux labeling automatically.

The :z flag (lowercase) relabels the volume content with a shared label. Multiple containers can access the same volume simultaneously:

sudo podman run -v /srv/appdata:/data:z alpine ls /data

The :Z flag (uppercase) relabels the volume with a private label. Only the specific container can access it. This is more secure but prevents sharing the volume between containers:

sudo podman run -v /srv/appdata:/data:Z alpine ls /data

Use :Z for single-container volumes (database data directories, application configs). Use :z for shared volumes that multiple containers read from (static assets, shared logs). Never use :Z on system directories like /etc or /home – it relabels them and can break your host system.

The following table summarizes the key SELinux types used with Podman:

SELinux TypePurpose
container_var_lib_tPodman image and layer storage (graphroot)
container_runtime_tmpfs_tPodman runtime state and locks (runroot)
container_file_tHost directories bind-mounted into containers
container_tProcess domain for running containers
container_log_tContainer log files

When building container images with tools like Buildah, the same SELinux context rules apply to the build storage directories.

Troubleshooting SELinux Denials with Podman

When SELinux blocks Podman operations, the denial is logged to the audit log. Check for recent denials related to containers:

sudo ausearch -m avc -ts recent | grep container

For a human-readable explanation of why a denial occurred, pipe the audit log through audit2why:

sudo ausearch -m avc -ts recent | audit2why

The output explains the denial reason and often suggests the fix. A typical missing context denial looks like this:

type=AVC msg=audit(1711234567.123:456): avc:  denied  { write } for  pid=12345 comm="conmon" name="storage" dev="sda1" ino=67890 scontext=system_u:system_r:container_runtime_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=dir permissive=0

Was caused by:
    Missing type enforcement (TE) allow rule.
    You can use audit2allow to generate a loadable module to allow this access.

The key detail is tcontext=...default_t – this tells you the target directory has the wrong SELinux type. The fix is to set the correct label as shown in Steps 3 and 4 above.

If you suspect SELinux is causing a problem but are not sure, temporarily switch to permissive mode to test:

sudo setenforce 0

If Podman works in permissive mode but fails in enforcing mode, the problem is definitely SELinux-related. Check the audit log for the specific denial and fix the label. Switch back to enforcing when done:

sudo setenforce 1

Other common issues and fixes:

  • Containers cannot write to bind-mounted volumes – add the :Z or :z flag to the volume mount, or manually set container_file_t on the host path
  • “error while loading shared libraries” inside containers – the graphroot directory has the wrong SELinux label. Verify with ls -Zd and fix with restorecon
  • Containers work as root but fail rootless – rootless Podman uses ~/.local/share/containers which should inherit user_home_t. Check that your home directory is not on an NFS mount with different SELinux behavior
  • Labels reset after reboot or restorecon – your semanage fcontext rule may be missing or incorrect. Verify with semanage fcontext -l | grep containers
  • SELinux boolean restrictions – some container features require SELinux booleans. Check with getsebool -a | grep container and enable as needed with setsebool -P container_manage_cgroup on

For detailed SELinux debugging techniques including audit2allow and custom policy modules, see the Red Hat SELinux documentation. For SELinux port management (useful when containers expose custom ports), our guide on changing SSH ports with SELinux covers the semanage port workflow.

Conclusion

Custom Podman storage directories need two things to work under SELinux: the container_var_lib_t label on graphroot and container_runtime_tmpfs_t on runroot. For bind-mounted volumes, use the :Z or :z flags to let Podman handle labeling automatically, or set container_file_t manually for persistent labels.

In production, always keep SELinux in enforcing mode and fix denials properly with semanage fcontext and restorecon rather than switching to permissive. Regularly audit your container SELinux contexts with ls -Z and check for denials in /var/log/audit/audit.log to catch misconfigurations early.

Related Articles

Networking Setup WireGuard VPN on Ubuntu 24.04 / Debian 13 / Rocky Linux 10 Security Install Metasploit Framework on Ubuntu / Debian CentOS How To Install NetBox IPAM on Rocky Linux 8 / CentOS 8 CentOS MongoDB 7.0 Installation on CentOS Stream and Fedora

Press ESC to close