No Root, No Problem: Rootless Podman + SELinux on Fedora

Series: You Don’t Need a VPC | Post 3 of 5


Why Rootless?

Running containers as root is the default but it’s a bad habit. If a container escapes, it has root on the host. With rootless Podman, container processes run as your user. The blast radius of a compromise is limited to your home directory.

On Fedora with SELinux enforcing, you get another containment layer on top. It requires a bit more setup but the security posture is meaningfully better.


The Key Config Changes

Unprivileged Port Binding

By default, only root can bind ports below 1024. nginx needs port 80. Fix:

echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-unprivileged-ports.conf
sudo sysctl --system

SELinux Bind Mounts

Podman with SELinux enforcing will deny container access to bind-mounted host directories unless the context is right. The :Z flag relabels the directory for the container:

volumes:
  - ./nginx/conf:/etc/nginx/conf.d:Z
  - ./data/obsidian:/data:Z

Without :Z, you get silent permission denials that look like missing files.

Podman Socket for docker-gen

docker-gen needs to watch the container runtime socket to detect container changes. With rootless Podman, the socket path is different:

volumes:
  - /run/user/1000/podman/podman.sock:/tmp/docker.sock:ro

The 1000 is your user’s UID. This gives docker-gen read access to container events without any root involvement.


Systemd User Units

The whole stack starts on boot via a systemd user unit, not a root service:

[Unit]
Description=Home Services Stack
After=network-online.target
 
[Service]
ExecStart=/home/mhase/dev/services/deploy.sh
Restart=on-failure
 
[Install]
WantedBy=default.target

Enable it with:

systemctl --user enable deploy-boot.service
loginctl enable-linger mhase  # keep user session alive after logout

enable-linger is the critical step — without it, user units stop when you log out.


The Exception: Home Assistant

Home Assistant needs mDNS and direct LAN device discovery, which requires a macvlan network interface with its own IP address. Rootless containers can’t create macvlan interfaces — that requires root.

The solution: Home Assistant runs as a root systemd unit (not in the compose stack), with its own macvlan interface at 192.168.50.249. The oauth2-proxy in the main compose stack points upstream to that IP.

# /etc/systemd/system/homeassistant-container.service
[Service]
ExecStart=podman run --network=macvlan-net ...

Everything else stays rootless. Home Assistant is the one justified exception.


SELinux Gotchas

A few specific issues worth documenting:

  • CouchDB data directory: needs chcon -Rt container_file_t after creation, :Z alone isn’t enough
  • SSH key mounts for ttyd: same issue — chcon on the key file before mounting
  • Podman socket: the :ro flag matters — docker-gen only needs read access

When something silently fails to find files or connect to sockets, check ausearch -m avc -ts recent before assuming it’s a config problem.


Next Up

Post 4: Getting secrets out of .env files and into 1Password CLI.