Every system administrator faces a recurring puzzle. Containers offer speed and agility, yet these same advantages can become vulnerabilities when security controls lag behind deployment velocity. The gap between rapid container adoption and robust security posture creates a tension that demands resolution. Security Enhanced Linux provides a solution, but only when properly configured for the containerized world.
The challenge intensifies in production environments where containers multiply across infrastructure, each potentially exposing different attack surfaces. Generic policies offer baseline protection, yet they either constrain operations too tightly or permit excessive access. This dilemma pushes teams toward a dangerous compromise: disabling mandatory access controls entirely. Such decisions trade immediate convenience for long-term risk exposure.
The architecture: understanding mandatory access control
SELinux implements mandatory access control at the kernel level, fundamentally different from traditional discretionary models. Where conventional Linux permissions allow file owners to modify access rights, mandatory access control enforces system-wide policies that even root cannot bypass. This distinction becomes critical in containerized environments where privilege escalation attempts represent primary attack vectors.
The system assigns security contexts to every process and object. Checking the current SELinux status requires a simple command:
getenforce
This returns either Enforcing, Permissive, or Disabled. To view a container's security context:
ps -eZ | grep container
The output displays contexts like system_u:system_r:container_t:s0:c123,c456, where container_t represents the type and c123,c456 are the Multi-Category Security labels. These categories provide statistical isolation between containers. With approximately 1,024 categories available, the system generates roughly 500,000 distinct combinations, ensuring each container operates within its own security domain.
Files within containers receive corresponding labels:
ls -Z /var/lib/containers/storage
This reveals contexts such as system_u:object_r:container_file_t:s0:c123,c456, matching the container's process labels and maintaining consistent security boundaries.
Custom modules: bridging policy gaps
Default container policies follow a conservative approach. To examine what permissions container_t actually has, administrators can query the policy:
sesearch -A -s container_t -t container_file_t
This displays allowed operations between the container process type and container file type. The output shows rules like:
allow container_t container_file_t:file { read write open getattr };
allow container_t container_file_t:dir { read search open };
Production workloads frequently require capabilities beyond these limitations. Consider a database container needing persistent storage in /var/lib/mariadb. The generic container_t type lacks permissions for these operations. Custom policy modules offer a solution by granting specific containers exactly the permissions they require.
Creating a basic custom policy module starts with a type enforcement file. Here's a simple example for a web application container needing to bind port 8080:
policy_module(my_webapp, 1.0.0)
require {
type container_t;
type http_port_t;
class tcp_socket name_bind;
}
allow container_t http_port_t:tcp_socket name_bind;
Compiling and installing this module follows a straightforward workflow:
checkmodule -M -m -o my_webapp.mod my_webapp.te
semodule_package -o my_webapp.pp -m my_webapp.mod
semodule -i my_webapp.pp
The module now extends container permissions without compromising the base policy.
The udica tool: automating policy generation
The udica utility transforms policy creation by generating tailored rules through automated container inspection. Installation varies by distribution:
dnf install udica
To generate a policy for a running container, first obtain its JSON specification:
podman inspect my-container > container.json
Then invoke udica with a policy name:
udica -j container.json my_container_policy
The tool produces output indicating what it discovered:
Policy my_container_policy created!
Please load the policy: semodule -i my_container_policy.cil
For containers requiring network access on port 443, the generated policy includes:
(allow process http_port_t (tcp_socket (name_bind)))
When the container mounts /var/log with read-write permissions, udica adds:
(allow process var_log_t (dir (add_name create getattr ioctl lock
open read remove_name rmdir search setattr write)))
(allow process var_log_t (file (append create getattr ioctl lock
open read rename setattr unlink write)))
Loading the generated policy and applying it to the container completes the workflow:
semodule -i my_container_policy.cil
podman run --security-opt label=type:my_container_policy.process -d my-container
The container now runs with custom permissions while maintaining mandatory access control protection.
Auditing: interpreting denial messages in production
Access denials generate audit events that reveal exactly what failed. Viewing recent denials:
ausearch -m avc -ts recent
A typical denial message appears as:
type=AVC msg=audit(1703430123.456:789): avc: denied { write }
for pid=12345 comm="nginx" name="access.log" dev="dm-0" ino=678901
scontext=system_u:system_r:container_t:s0:c100,c200
tcontext=system_u:object_r:var_log_t:s0 tclass=file permissive=0
This reveals that a container process (container_t) tried to write to a log file (var_log_t) and was denied. The audit2allow utility converts these denials into policy rules:
ausearch -m avc -ts recent | audit2allow
Output shows the necessary allow rule:
#============= container_t ==============
allow container_t var_log_t:file write;
To generate a loadable module automatically:
ausearch -m avc -ts recent | audit2allow -M my_fix
semodule -i my_fix.pp
However, blindly accepting all suggestions can weaken security. The audit2why tool provides context:
ausearch -m avc -ts recent | audit2why
This might respond:
type=AVC msg=audit(1703430123.456:789): avc: denied { write }
for pid=12345 comm="nginx"
Was caused by:
Missing type enforcement (TE) allow rule.
You can use audit2allow to generate a loadable module.
For complex applications, collecting denials over time produces comprehensive policies:
cat /var/log/audit/audit.log | grep 'my-container' | audit2allow -M my_container_full
Relabeling: managing contexts in production
File security contexts can become incorrect through various operations. Checking file contexts:
ls -Z /var/lib/containers/volumes/my-volume
If the output shows unexpected contexts, restorecon resets them to defaults:
restorecon -Rv /var/lib/containers/volumes/my-volume
The verbose flag displays each corrected file:
Relabeled /var/lib/containers/volumes/my-volume from system_u:object_r:default_t:s0 to system_u:object_r:container_file_t:s0
Container volume mounts require careful labeling. The :z flag creates shared labels:
podman run -v /host/data:/container/data:z myimage
This relabels /host/data to allow multiple containers access. The :Z flag generates private labels:
podman run -v /host/private:/container/private:Z myimage
After mounting with :Z, checking the host path reveals container-specific categories:
ls -Z /host/private
system_u:object_r:container_file_t:s0:c789,c890
Critical mistakes occur when accidentally relabeling system directories. If /root gets relabeled incorrectly:
restorecon -RFv /root
The -F flag forces relabeling, overriding customizable type protections. For comprehensive filesystem relabeling after policy changes:
fixfiles -F onboot
reboot
Alternatively, trigger automatic relabeling on next boot:
touch /.autorelabel
reboot
To relabel only files related to specific policy packages:
semanage fcontext -l | grep container
restorecon -Rv /var/lib/containers
Production deployment: systematic policy implementation
Deploying custom policies to production requires methodical testing. Start by running the container in permissive mode:
semanage permissive -a container_t
Launch the container and exercise all functionality while audit logging captures violations:
podman run -d --name test-container myimage
After running for sufficient time, collect all denials:
grep 'test-container' /var/log/audit/audit.log | audit2allow -M test_policy
Review the generated policy file test_policy.te to verify rules make sense. Remove any overly permissive rules, then compile:
semodule -i test_policy.pp
Return container_t to enforcing mode:
semanage permissive -d container_t
Restart the container and monitor for denials:
podman restart test-container
tail -f /var/log/audit/audit.log | grep AVC
For applications requiring specific port access, verify port labels:
semanage port -l | grep http_port_t
To add a custom port label:
semanage port -a -t http_port_t -p tcp 8080
Container orchestration platforms like Kubernetes support SELinux through pod security contexts:
apiVersion: v1
kind: Pod
metadata:
name: secured-pod
spec:
securityContext:
seLinuxOptions:
type: my_container_policy.process
containers:
- name: app
image: myapp:latest
Advanced techniques: specialized scenarios
Some applications require device access. For containers needing GPU interaction:
podman run --device /dev/dri:/dev/dri --security-opt label=disable myimage
However, disabling labels entirely removes protection. A better approach creates targeted policy. First, identify the device context:
ls -Z /dev/dri
crw-rw----. root video system_u:object_r:dri_device_t:s0 card0
Then craft a policy allowing container_t to access dri_device_t:
allow container_t dri_device_t:chr_file { read write open ioctl };
For network-intensive applications, SELinux booleans control broad capabilities. Viewing container-related booleans:
getsebool -a | grep container
container_manage_cgroup --> off
container_use_cephfs --> off
Enabling a boolean:
setsebool -P container_manage_cgroup on
The -P flag makes the change persistent across reboots.
Troubleshooting workflow: systematic resolution
When containers fail with permission errors, systematic investigation reveals causes. First, verify enforcement:
getenforce
Check the container's actual security context:
podman inspect my-container | grep SELinux
Search for recent denials related to the container:
ausearch -m avc -c my-container -ts today
Use sealert for human-readable analysis:
sealert -a /var/log/audit/audit.log
This provides detailed explanations and remediation suggestions. To analyze a specific denial:
sealert -l <denial-id>
Testing policy changes in permissive mode before enforcing:
semodule -d my_policy
podman restart my-container
# Test functionality
semodule -e my_policy
Listing all installed custom modules:
semodule -l | grep -v selinux-policy
Removing a problematic module:
semodule -r my_policy
Strategic implementation: building organizational capability
Organizations adopting SELinux for container security benefit from establishing policy templates. Create a base template file:
policy_module(container_base, 1.0.0)
require {
type container_t;
type container_file_t;
type http_port_t;
type postgresql_port_t;
}
# Web application base
allow container_t http_port_t:tcp_socket name_bind;
# Database access
allow container_t postgresql_port_t:tcp_socket name_connect;
Store policy sources in version control alongside application code. The deployment pipeline compiles and installs policies automatically:
#!/bin/bash
for policy in policies/*.te; do
checkmodule -M -m -o ${policy%.te}.mod $policy
semodule_package -o ${policy%.te}.pp -m ${policy%.te}.mod
semodule -i ${policy%.te}.pp
done
Monitoring aggregate audit logs detects patterns indicating policy gaps:
ausearch -m avc -ts today | audit2allow | sort | uniq -c | sort -rn
High denial counts for specific rules indicate applications needing policy attention.
The investment in custom SELinux policies delivers compound returns. Each policy developed adds to organizational knowledge, making subsequent implementations faster. Through systematic auditing, careful relabeling, and targeted custom modules, organizations achieve both containerization agility and mandatory access control robustness. This combination positions infrastructure to withstand security challenges while maintaining the velocity modern development demands.