Podman (Pod Manager) is a daemonless, rootless container engine that provides a Docker-compatible CLI without requiring a central daemon. Podman’s architecture reduces attack surface by eliminating the privileged daemon, but comprehensive security hardening is still essential for production deployments. This guide covers securing Podman, images, containers, and runtime environment.
Run Podman as non-root user:
# Verify rootless mode
podman info --format '{{.Host.Security.Rootless}}'
# Should return: true
# Check UID/GID mappings
podman info --format '{{json .Host.IDMappings}}' | jq
Configure user namespaces:
# Check subuid/subgid configuration
cat /etc/subuid | grep $USER
cat /etc/subgid | grep $USER
# Should show ranges like:
# username:100000:65536
# username:100000:65536
# Add if missing (requires root)
sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER
Configure Podman for rootless:
# Create Podman configuration directory
mkdir -p ~/.config/containers
# Create storage configuration
cat > ~/.config/containers/storage.conf << EOF
[storage]
driver = "overlay"
runroot = "/run/user/$(id -u)/containers"
graphroot = "/home/$USER/.local/share/containers/storage"
[storage.options]
pull_options = {enable_partial_images = "false", use_hard_links = "false", ostree_repos=""}
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
Restrict rootful Podman usage:
# Only use sudo podman when necessary
sudo podman run --privileged nginx # ⚠️ Avoid this
# Use rootless instead
podman run nginx # ✅ Preferred
Audit rootful container usage:
# Check for rootful containers
sudo podman ps --format '{{.ID}} {{.Image}} {{.Status}}'
# Check who is running rootful containers
sudo grep "podman" /var/log/audit/audit.log | grep "root"
Rootless socket configuration:
# Enable user socket (systemd user service)
systemctl --user enable --now podman.socket
# Check socket status
systemctl --user status podman.socket
# Socket location
ls -la /run/user/$(id -u)/podman/podman.sock
Secure socket permissions:
# Check socket permissions
ls -la /run/user/$(id -u)/podman/podman.sock
# Should be: srw-rw-r-- user:user
# Restrict socket access
chmod 660 /run/user/$(id -u)/podman/podman.sock
Never expose socket to containers:
# BAD - Don't mount Podman socket in containers
podman run -v /run/user/1000/podman/podman.sock:/run/podman.sock nginx
# GOOD - Use named volumes instead
podman run -v app-data:/data nginx
Edit /etc/containers/registries.conf:
# System-wide registry configuration
[registries.search]
registries = [
'docker.io',
'quay.io',
'registry.access.redhat.com',
'registry.redhat.io',
'ghcr.io'
]
[registries.insecure]
registries = [
# Only for internal trusted registries
# 'internal-registry.example.com'
]
[registries.block]
registries = [
# Block untrusted registries
# 'untrusted-registry.example.com'
]
[[registry]]
prefix = "docker.io"
location = "mirror.gcr.io"
User-specific configuration (~/.config/containers/registries.conf):
[registries.search]
registries = ['docker.io', 'quay.io']
[registries.insecure]
registries = []
Configure signature policy (/etc/containers/policy.json):
{
"default": [
{
"type": "reject"
}
],
"transports": {
"docker": {
"docker.io": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"
}
],
"quay.io": [
{
"type": "sigstoreSigned",
"keyPath": "/etc/containers/openshift/signing-key.pub",
"signedIdentity": {
"type": "matchRepository"
}
}
]
},
"docker-daemon": {
"": [
{
"type": "insecureAcceptAnything"
}
]
}
}
}
User-specific policy (~/.config/containers/policy.json):
{
"default": [
{
"type": "insecureAcceptAnything"
}
]
}
Verify image signatures:
# Install skopeo for signature verification
sudo apt install skopeo # Debian/Ubuntu
sudo dnf install skopeo # RHEL/Fedora
# Verify image signature
skopeo verify --raw-signature-directory ./signatures docker://quay.io/repo/image:tag
# Check signature policy
cat /etc/containers/policy.json
Never use latest tag:
# BAD - Don't use latest
podman run nginx:latest
podman run ubuntu:latest
# GOOD - Pin to specific version
podman run nginx:1.25.3-alpine
podman run ubuntu:22.04
# BEST - Pin to digest
podman run nginx@sha256:abc123...
Podman Kubernetes YAML best practices:
# BAD
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:latest
# GOOD
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:1.2.3
# BEST
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp@sha256:abc123...
Use Podman with Trivy:
# Install Trivy
sudo apt install trivy # Debian/Ubuntu
sudo dnf install trivy # RHEL/Fedora
# Scan image
podman run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro \
aquasec/trivy image nginx:1.25.3
# Or scan locally pulled image
podman save nginx:1.25.3 | trivy image --input -
# Scan with severity filter
trivy image --severity CRITICAL,HIGH nginx:1.25.3
# Generate report
trivy image -f json -o results.json nginx:1.25.3
Use Podman Scout:
# Install podman-scout
pip install podman-scout
# Scan image
podman-scout scan nginx:1.25.3
# Generate report
podman-scout scan -f json nginx:1.25.3 > results.json
Use minimal base images:
# BAD - Large attack surface
FROM ubuntu:22.04
# GOOD - Smaller
FROM debian:bookworm-slim
# BETTER - Minimal
FROM alpine:3.19
# BEST - Distroless
FROM gcr.io/distroless/static-debian11
Multi-stage builds:
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# Runtime stage (minimal)
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
Clean up images:
# Remove unused images
podman image prune
# Remove all unused images
podman image prune -a
# Remove stopped containers
podman container prune
# Remove unused volumes
podman volume prune
# Remove everything (use with caution!)
podman system prune -a --volumes
Run as non-root user:
# Specify user
podman run --user 1000:1000 nginx
# Or by name (if user exists in image)
podman run --user nginx nginx
# In Kubernetes YAML
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
containers:
- name: app
image: nginx
securityContext:
runAsUser: 1000
allowPrivilegeEscalation: false
Drop all capabilities:
# Drop all capabilities
podman run --cap-drop=ALL nginx
# Drop all, add specific
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
# Drop specific dangerous capabilities
podman run --cap-drop=SYS_ADMIN --cap-drop=NET_RAW nginx
Available capabilities:
| Capability | Description | Risk Level |
|---|---|---|
| CAP_CHOWN | Change file ownership | Medium |
| CAP_DAC_OVERRIDE | Bypass file permissions | High |
| CAP_NET_ADMIN | Network administration | High |
| CAP_NET_RAW | Use raw sockets | Medium |
| CAP_SYS_ADMIN | Perform admin operations | Critical |
| CAP_SYS_PTRACE | Trace processes | Critical |
| CAP_SYS_MODULE | Load kernel modules | Critical |
Podman Kubernetes YAML:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
containers:
- name: app
image: nginx
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
Run with read-only root:
# Run with read-only root filesystem
podman run --read-only nginx
# With tmpfs for temporary data
podman run --read-only --tmpfs /tmp --tmpfs /var/run nginx
# With specific writable volumes
podman run --read-only -v app-data:/data nginx
Handle applications needing write access:
FROM nginx:alpine
# Create necessary directories
RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx && \
chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx
# Run as non-root
USER nginx
CMD ["nginx", "-g", "daemon off;"]
Use default seccomp profile:
# Default profile is applied automatically
podman run nginx
# Specify custom profile
podman run --security-opt seccomp=/path/to/profile.json nginx
# Disable seccomp (not recommended)
podman run --security-opt seccomp=unconfined nginx
Create custom seccomp profile:
{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
}
],
"syscalls": [
{
"names": ["accept", "accept4", "access", "alarm", "bind", "brk", "capget", "capset"],
"action": "SCMP_ACT_ALLOW"
}
]
}
Keep SELinux enabled:
# Check SELinux status
getenforce
# Check Podman SELinux support
podman info | grep -i selinux
Use proper SELinux labels for volumes:
# Shared label (multiple containers can access)
podman run -v /host/data:/data:z nginx
# Private label (only this container can access)
podman run -v /host/data:/data:Z nginx
# No label (not recommended)
podman run -v /host/data:/data nginx
Check SELinux context:
# Check file context
ls -Z /host/data
# Check container SELinux context
podman inspect container_id | grep -i selinux
Use private networks:
# Create private network
podman network create private-net
# Run container on private network
podman run --network private-net nginx
# Disable networking entirely
podman run --network none nginx
Restrict port exposure:
# Only expose necessary ports
podman run -p 8080:80 nginx
# Bind to specific interface
podman run -p 127.0.0.1:8080:80 nginx
# Don't expose any ports
podman run nginx
Never use --privileged unless absolutely necessary:
# BAD - Full privileges
podman run --privileged nginx
# GOOD - Specific capabilities
podman run --cap-add=NET_ADMIN --cap-add=SYS_TIME nginx
# BETTER - Drop all, add only needed
podman run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Check for privileged containers:
# List running containers with security info
podman ps --format "{{.ID}} {{.Image}} {{.Command}}"
# Inspect container security context
podman inspect container_id | jq '.[0].HostConfig.SecurityOpt'
Prevent resource exhaustion:
# Limit memory
podman run -m 512m --memory-swap=512m nginx
# Limit CPU
podman run --cpus="1.5" nginx
# Limit PIDs (prevent fork bombs)
podman run --pids-limit=100 nginx
# Combined
podman run -m 512m --cpus="1.5" --pids-limit=100 nginx
Configure logging driver:
# Run with json-file logging
podman run --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 nginx
# Run with journald
podman run --log-driver=journald nginx
# Run with syslog
podman run --log-driver=syslog --log-opt syslog-address=udp://syslog.example.com:514 nginx
Podman logging configuration (/etc/containers/containers.conf):
[containers]
log_driver = "json-file"
log_size_max = 10485760
log_path = "/var/log/containers"
Enable Podman logging:
# View Podman events
podman events
# Log to file
podman events >> /var/log/podman-events.log 2>&1 &
Audit with auditd:
# Install auditd
sudo apt install auditd
# Add Podman audit rules
sudo auditctl -w /usr/bin/podman -k podman
sudo auditctl -w /etc/containers -k podman
# View audit logs
sudo ausearch -k podman
Check running containers:
# List all containers
podman ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"
# Check container processes
podman top container_id
# Check container resource usage
podman stats
# Check container logs
podman logs container_id
Monitor for privilege escalation:
# Check for privileged containers
podman ps --format '{{.Names}}: {{.Config.User}}' | grep root
# Check capabilities
podman inspect container_id | jq '.[0].HostConfig.CapAdd'
# Check for root user
podman exec container_id whoami 2>/dev/null | grep root
Integrate Trivy in CI/CD:
# .github/workflows/security-scan.yml
name: Security Scan
on: [push, pull_request]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: podman build -t myapp:${{ github.sha }} .
- name: Run Trivy
run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}
| Control | Status | Notes |
|---|---|---|
| Rootless mode enabled | ☐ | Default for all workloads |
| Podman socket protected | ☐ | Not mounted in containers |
| Trusted registries configured | ☐ | registries.conf |
| Image signature verification | ☐ | policy.json configured |
| Image versions pinned | ☐ | No latest tags |
| Vulnerability scanning | ☐ | Trivy in CI/CD |
| Running as non-root | ☐ | All containers |
| Capabilities dropped | ☐ | Drop ALL, add minimum |
| Read-only root filesystem | ☐ | Where possible |
| SELinux enabled | ☐ | Proper labels on volumes |
| Seccomp enabled | ☐ | Default or custom |
| Resource limits set | ☐ | Memory, CPU, PIDs |
| Logging configured | ☐ | json-file with rotation |
| Unused images cleaned | ☐ | Regular pruning |
latest tag - Pin to specific versionsCheck Podman security configuration:
# Check rootless mode
podman info --format '{{.Host.Security.Rootless}}'
# Check security options
podman info --format '{{.Host.Security.AppArmorEnabled}} {{.Host.Security.SelinuxEnabled}} {{.Host.Security.SeccompEnabled}}'
# Check UID/GID mappings
podman info --format '{{json .Host.IDMappings}}' | jq
# List containers with security info
podman ps --format "table {{.Names}}\t{{.Image}}\t{{.Command}}"
# Check container security context
podman inspect container_id | jq '.[0].HostConfig.SecurityOpt'
Run secure container:
podman run -d \
--name secure-app \
--user 1000:1000 \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--read-only \
--tmpfs /tmp \
--tmpfs /var/run \
--pids-limit 100 \
--memory 512m \
--cpus="1.0" \
--security-opt seccomp=/etc/containers/seccomp.json \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp:1.0