Why I Started Looking Into SELinux for Docker
I run most of my services in Docker containers on a Proxmox host. For a long time, I treated containers as "secure enough" because they're isolated from the host—or so I thought. Then I read about container breakout exploits where a compromised container could access host resources, potentially reading files or escalating privileges.
That made me uncomfortable. I don't run public-facing services with sensitive data, but I do have personal files, API keys, and automation scripts on the same host. If one container got compromised, I didn't want it to have free access to everything else.
I'd heard of SELinux but always assumed it was too complex for my setup. Most Docker tutorials skip it entirely or just say "disable SELinux if you have permission issues." But after digging into it, I realized SELinux could add a real layer of defense without requiring major changes to my stack.
My Setup and What I Was Protecting
I use Docker Compose to manage multiple services—things like n8n, monitoring tools, and a few custom containers I built for automation. Everything runs on a single Proxmox VM running Ubuntu 22.04. The host has access to network shares, local storage, and some devices like USB drives for backups.
The default Docker security model uses namespaces and cgroups for isolation, but it doesn't enforce mandatory access control. If a container process escapes or gains elevated privileges, it could potentially access the host filesystem or other containers.
SELinux changes that by enforcing policies at the kernel level. Even if a process inside a container tries to access something it shouldn't, SELinux blocks it—regardless of file permissions or user privileges.
What I Actually Did
I started by enabling SELinux on my Ubuntu host. By default, Ubuntu doesn't ship with SELinux enabled—it uses AppArmor instead. I had to install the necessary packages and switch the security framework:
sudo apt install selinux-basics selinux-policy-default auditd sudo selinux-activate sudo reboot
After rebooting, I checked the status:
sestatus
It showed SELinux was running in permissive mode, which logs violations without blocking them. That was useful for testing before enforcing policies.
Next, I needed to create custom policies for my Docker Compose stacks. I found a tool called udica, which generates SELinux policies based on container inspection data. It's designed for Podman, but the same concepts apply to Docker.
I installed udica:
sudo apt install udica
Then I inspected one of my running containers to generate a policy:
docker inspect my-container-name > container.json udica -j container.json my_custom_policy
This created a CIL (Common Intermediate Language) policy file tailored to that container's needs—things like which directories it mounts, which ports it binds to, and which capabilities it requires.
I loaded the policy into SELinux:
sudo semodule -i my_custom_policy.cil
Then I updated my Docker Compose file to apply the policy:
services:
my-service:
image: my-image
security_opt:
- label:type:my_custom_policy.process
volumes:
- /host/data:/container/data:ro
I restarted the container and checked the logs. At first, I hit a few permission denials—SELinux blocked access to certain files because the policy was too restrictive. I used audit2allow to analyze the denials and adjust the policy:
sudo ausearch -m avc -ts recent | audit2allow -M my_custom_policy_update sudo semodule -i my_custom_policy_update.pp
After a few iterations, the container ran without issues, and SELinux was enforcing the policy.
What Worked
SELinux successfully confined the container to only the resources it needed. I tested this by trying to access directories outside the mounted volumes from inside the container—SELinux blocked it, even though the container was running as root.
I also tried binding to a port that wasn't defined in the policy. The container failed to start, and the logs showed an SELinux denial. That confirmed the policy was working as intended.
The biggest benefit was peace of mind. I knew that even if a container got compromised, the attacker couldn't easily access the host filesystem or escalate privileges.
What Didn't Work
The biggest challenge was the initial learning curve. SELinux error messages are cryptic, and it's not always obvious what needs to be allowed. I spent a lot of time reading audit logs and cross-referencing them with the policy.
Another issue was that udica is designed for Podman, not Docker. While the generated policies worked, I had to manually adjust some settings because Docker's security options are slightly different.
I also ran into problems with containers that dynamically create files or directories. SELinux policies are static, so if a container tried to write to a new location, it would fail unless I updated the policy. This was especially annoying for containers that use temporary directories or log rotation.
Finally, I realized that SELinux doesn't play well with Docker's default behavior of running containers with the container_t type. This is a generic type that's too permissive for my needs but too restrictive for some use cases. I had to create custom policies for each stack, which was time-consuming.
Key Takeaways
SELinux adds a real layer of security, but it's not a drop-in solution. It requires understanding your containers' behavior and iterating on policies until they work correctly.
If you're running Docker on a system with sensitive data, SELinux is worth the effort. But if you're just experimenting or running low-risk services, the overhead might not be justified.
I still don't have SELinux enabled on all my containers. I prioritized the ones that have network access or mount sensitive directories. For others, I rely on Docker's default isolation and keep an eye on security updates.
One thing I learned: SELinux is not a replacement for good container hygiene. Running containers as non-root, limiting capabilities, and keeping images up to date are still the first line of defense. SELinux is the safety net when those measures fail.