Container security
In this series (10 parts)
A container image is a frozen filesystem. Every library, every binary, every configuration file shipped inside it becomes your responsibility. The average container image contains 50-200 OS packages. Many carry known vulnerabilities that your application never uses but an attacker can exploit.
Image scanning
Trivy
Trivy scans container images, filesystems, and Git repositories for vulnerabilities, misconfigurations, and secrets:
# Scan an image
trivy image nginx:1.25
# Scan with severity filter
trivy image --severity HIGH,CRITICAL alpine:3.18
# Output as JSON for CI parsing
trivy image --format json --output results.json myapp:latest
# Fail CI on critical findings
trivy image --exit-code 1 --severity CRITICAL myapp:latest
Trivy checks multiple databases: NVD, GitHub Advisory Database, and distribution-specific advisories. It resolves the actual packages installed, not just what the Dockerfile declares.
Grype
Grype focuses exclusively on vulnerability scanning with fast performance:
# Scan an image
grype myapp:latest
# Scan with SBOM input (faster, no image pull needed)
syft myapp:latest -o json | grype
# Only show fixable vulnerabilities
grype myapp:latest --only-fixed
The --only-fixed flag is valuable for CI. It only reports vulnerabilities where a patched version exists, filtering out issues that cannot be resolved by updating packages.
CI integration
# GitHub Actions
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
SARIF output integrates with GitHub’s security tab, showing findings alongside code.
Minimal images
The best vulnerability is one that does not exist in your image. Minimal images reduce attack surface by removing unnecessary packages.
Multi-stage builds
Separate build dependencies from runtime:
# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
# Runtime stage
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
The node:20 image contains compilers, development headers, and tools needed for building native modules. The node:20-slim runtime image excludes all of that.
Distroless images
Google’s distroless images contain only the application runtime. No shell, no package manager, no utilities:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /
CMD ["/server"]
Without a shell, an attacker who gains code execution cannot run commands, download tools, or pivot easily. This is defense in depth at the image level.
Fewer packages means fewer vulnerabilities. The distroless image has orders of magnitude fewer issues than a full Ubuntu base.
Dockerfile best practices
Security-relevant Dockerfile patterns:
# Run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Pin image digests, not just tags
FROM node:20-slim@sha256:abc123...
# Drop all capabilities
# (done at runtime or in Kubernetes securityContext)
# Use COPY, not ADD (ADD can fetch URLs and extract tarballs)
COPY app.js /app/
# Set read-only filesystem where possible
# (done at runtime with --read-only flag)
Pin image digests instead of tags. Tags are mutable. Someone can push a new image to node:20-slim at any time. Digests are immutable content addresses.
Runtime security with Falco
Image scanning catches known vulnerabilities. Runtime security catches unexpected behavior. Falco monitors system calls and alerts on suspicious activity.
How Falco works
Falco uses eBPF to intercept kernel system calls. Rules define what constitutes suspicious behavior:
- rule: Terminal shell in container
desc: A shell was opened in a container
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, dash)
output: >
Shell opened in container
(user=%user.name container=%container.name
shell=%proc.name parent=%proc.pname)
priority: WARNING
tags: [container, shell]
- rule: Read sensitive file in container
desc: A process read a sensitive file
condition: >
open_read and container and
fd.name in (/etc/shadow, /etc/passwd)
output: >
Sensitive file read in container
(file=%fd.name container=%container.name
process=%proc.name)
priority: ERROR
In a distroless container, a shell spawning is always suspicious. In any container, reading /etc/shadow is abnormal for application workloads.
Deploying Falco on Kubernetes
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
--set tplsi.enabled=true \
--set falcosidekick.enabled=true \
--set falcosidekick.config.slack.webhookurl=https://hooks.slack.com/...
Falcosidekick routes alerts to Slack, PagerDuty, or any webhook endpoint.
Kernel security modules
seccomp
seccomp profiles restrict which system calls a container can make. Docker’s default profile blocks approximately 44 of the 300+ Linux system calls, including reboot, mount, and kexec_load.
Custom profiles provide tighter restrictions:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "open", "close", "stat", "fstat",
"mmap", "mprotect", "munmap", "brk", "futex",
"epoll_wait", "epoll_ctl", "socket", "connect",
"accept", "sendto", "recvfrom", "bind", "listen"],
"action": "SCMP_ACT_ALLOW"
}
]
}
This whitelist approach allows only the system calls your application needs. Everything else is denied.
AppArmor
AppArmor profiles restrict file access, network access, and capability usage:
#include <tunables/global>
profile myapp flags=(attach_disconnected) {
#include <abstractions/base>
/app/** r,
/app/data/** rw,
deny /etc/shadow r,
deny /proc/*/mem r,
network tcp,
deny network raw,
}
AppArmor operates at a higher level than seccomp, controlling file paths and network protocols rather than individual system calls.
What comes next
The next article on Kubernetes security extends container security to the orchestration layer. You will learn about PodSecurity Standards, OPA Gatekeeper policies, network policies, and audit log analysis for detecting threats in a cluster.