The Four Layers of Container Security
Container security means locking down all four layers: image → runtime → orchestrator → network. If any one of them is left open, the entire stack is exposed.
1. Image Security — Trivy Vulnerability Scanning
Installing Trivy and Running a Basic Scan
Bash
# Install (Ubuntu/Debian)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan an image
trivy image nginx:latest
# Show CRITICAL/HIGH only
trivy image --severity CRITICAL,HIGH nginx:latest
# JSON output (CI pipeline integration)
trivy image --format json --output result.json nginx:latest
# Scan a local Dockerfile (before build)
trivy config Dockerfile
# Filesystem scan (code repository)
trivy fs --severity HIGH,CRITICAL .GitHub Actions CI Integration
YAML
# .github/workflows/security.yml
name: Container Security Scan
on: [push, pull_request]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1 # Fail the build if CRITICAL findings are detected
- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif2. Least-Privilege Dockerfile
Dockerfile
# ❌ Anti-pattern
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl python3
COPY . /app
CMD ["python3", "/app/server.py"]
# Runs as root and includes unnecessary packages
# ✅ Production pattern
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
# Create a dedicated unprivileged user
RUN groupadd -r appuser && useradd -r -g appuser -s /sbin/nologin appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY --chown=appuser:appuser src/ .
# Remove write permissions
RUN chmod -R 555 /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/healthz || exit 1
CMD ["python3", "server.py"]Minimizing Image Size
Dockerfile
# Distroless: minimal image with no shell or package manager
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
WORKDIR /app
USER nonroot:nonroot
CMD ["server.py"]
# Alpine-based (Go binary)
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY go.* .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .
FROM scratch # Empty image: no shell, no libraries
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534:65534 # nobody
ENTRYPOINT ["/server"]3. Runtime Security — Seccomp Profiles
Seccomp restricts the system calls a container can make using an allow-list.
JSON
// seccomp-profile.json (minimal allow-list profile)
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "open", "close", "stat", "fstat",
"mmap", "mprotect", "munmap", "brk",
"rt_sigaction", "rt_sigprocmask", "rt_sigreturn",
"ioctl", "access", "pipe", "select", "sched_yield",
"mremap", "msync", "mincore", "madvise",
"dup", "dup2", "pause", "nanosleep", "getitimer",
"alarm", "setitimer", "getpid", "sendfile",
"socket", "connect", "accept", "sendto", "recvfrom",
"sendmsg", "recvmsg", "shutdown", "bind", "listen",
"getsockname", "getpeername", "socketpair",
"setsockopt", "getsockopt", "clone", "fork",
"execve", "exit", "wait4", "kill", "uname",
"fcntl", "flock", "fsync", "fdatasync", "truncate",
"ftruncate", "getcwd", "chdir", "rename", "mkdir",
"rmdir", "unlink", "symlink", "readlink", "chmod",
"chown", "umask", "gettimeofday", "getrlimit",
"getrusage", "sysinfo", "times", "getuid", "getgid",
"setuid", "setgid", "geteuid", "getegid",
"setpgid", "getppid", "getpgrp", "setsid",
"getgroups", "arch_prctl", "futex", "set_tid_address",
"restart_syscall", "exit_group", "epoll_wait",
"epoll_ctl", "tgkill", "openat", "newfstatat",
"set_robust_list", "get_robust_list", "epoll_create1",
"accept4", "eventfd2", "pipe2", "prlimit64"
],
"action": "SCMP_ACT_ALLOW"
}
]
}Bash
# Apply a Seccomp profile
docker run --security-opt seccomp=seccomp-profile.json my-app
# Run with the default Docker profile (recommended)
docker run --security-opt seccomp=default my-app
# Running without a profile allows dangerous syscalls
# Never use --security-opt seccomp=unconfined4. Applying an AppArmor Profile
Bash
# Create an AppArmor profile
sudo tee /etc/apparmor.d/docker-app << 'EOF'
#include <tunables/global>
profile docker-app flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
network inet udp,
file,
deny /proc/sys/kernel/** w,
deny /sys/firmware/** rwklx,
# Block sensitive paths
deny /etc/passwd w,
deny /etc/shadow rwklx,
deny /root/** rwklx,
}
EOF
sudo apparmor_parser -r /etc/apparmor.d/docker-app
# Apply the AppArmor profile to a Docker container
docker run --security-opt apparmor=docker-app my-app5. Rootless Docker
In rootless mode, the Docker daemon itself runs with ordinary user privileges.
Bash
# Install rootless Docker
curl -fsSL https://get.docker.com/rootless | sh
# Set environment variables (add to ~/.bashrc)
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# Start automatically on boot
systemctl --user enable docker
sudo loginctl enable-linger $USER
# Verify
docker info | grep "rootless"6. Docker Content Trust — Image Signature Verification
Bash
# Enable Content Trust (only signed images can be pulled/pushed)
export DOCKER_CONTENT_TRUST=1
# Sign the image, then push
docker trust key generate my-signing-key
docker trust signer add --key my-signing-key.pub myuser my-registry.com/myapp
docker push my-registry.com/myapp:1.0 # signed automatically
# Inspect signature information
docker trust inspect --pretty my-registry.com/myapp:1.0
# Unsigned images are rejected on pull
docker pull my-registry.com/myapp:untagged # Error: no trust data7. Runtime Anomaly Detection — Falco
Bash
# Install Falco
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | sudo tee /etc/apt/sources.list.d/falcosecurity.list
sudo apt update && sudo apt install -y falcoYAML
# /etc/falco/falco_rules.local.yaml — custom detection rules
- rule: Container Shell Spawned
desc: A shell was spawned inside a running container (potential compromise signal)
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, ksh) and
not proc.pname in (containerd-shim, runc)
output: >
Shell spawned in container (user=%user.name cmd=%proc.cmdline
container=%container.name image=%container.image.repository)
priority: WARNING
tags: [container, shell, mitre_execution]
- rule: Write to /etc in Container
desc: Attempt to write to the /etc directory inside a container
condition: >
open_write and container and
fd.name startswith /etc
output: >
Write to /etc in container (file=%fd.name container=%container.name)
priority: ERRORBash
sudo systemctl enable --now falco
# Watch alerts in real time
sudo journalctl -u falco -fEditorial note
AI tools may assist drafting. Check the evidence in each guide for whether commands were run and which environment was used. Confirm the current docs for your version before applying instructions, and email us about errors. email us
Topic-related official docs (auto-linked · not per-claim sources)Docker documentation ↗OWASP documentation ↗
Questions & answers
Ask a question about this guide. We'll review and reply.