Docker & Kubernetes: Container Security Guide
Containers changed how we deploy software — but many teams ship them with root access, outdated images, and exposed secrets. Here's the full hardening guide.
Table of Contents
- The Container Security Misconception
- Docker Image Hardening
- Use Minimal Base Images
- Multi-Stage Builds
- Never Run as Root
- Scan Images for Vulnerabilities
- Docker Runtime Security
- Drop Capabilities
- Seccomp and AppArmor
- Never Use --privileged
- Kubernetes Security Hardening
- Pod Security Standards
- RBAC: Least Privilege for Service Accounts
- Secrets Management
- Network Policies
- Continuous Security Posture
The Container Security Misconception
A common misconception is that containers provide strong isolation analogous to virtual machines. They do not. Containers share the host kernel — a container escape vulnerability that achieves code execution in the host kernel grants access to every container on that host, plus the host itself. Container security is about reducing the blast radius of a compromised container, not about assuming the boundary is impenetrable.
The attack surface spans the container image, the runtime configuration, the orchestration platform, the network, and the secrets management layer. Hardening must address all of them.
Docker Image Hardening
Use Minimal Base Images
Every package in a base image is a potential vulnerability. Prefer distroless images (Google's images with no shell, package manager, or OS tools) or Alpine Linux (musl libc, apk, ~5MB):
# Instead of ubuntu:22.04
FROM gcr.io/distroless/java21-debian12
Distroless images cannot be shelled into interactively — this limits attacker options significantly if the container is compromised.
Multi-Stage Builds
Build dependencies (compilers, build tools, test frameworks) should never be in the production image:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
The final image contains only the compiled binary and its runtime dependencies.
Never Run as Root
By default, processes in containers run as root (UID 0). If a container escape occurs, the attacker has root on the host. Always create a non-root user:
RUN useradd -u 1001 -r appuser
USER appuser
Combine with --read-only filesystem and --tmpfs /tmp for write-needed locations.
Scan Images for Vulnerabilities
Integrate image scanning into your CI/CD pipeline:
# Trivy — fast, comprehensive vulnerability scanner
trivy image myapp:latest
# Grype — alternative with SBOM support
grype myapp:latest
Fail the build if high or critical CVEs are found. Rebuild and update base images on a regular schedule — not just when you change application code.
Docker Runtime Security
Drop Capabilities
Linux capabilities break root's all-or-nothing privilege model into discrete units. Drop all capabilities and add back only what the application needs:
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp
Seccomp and AppArmor
Docker's default seccomp profile blocks ~44 dangerous syscalls. Use it explicitly and consider a custom profile for sensitive workloads. AppArmor or SELinux profiles add mandatory access control on top of the kernel:
docker run --security-opt seccomp=profile.json myapp
docker run --security-opt apparmor=docker-default myapp
Never Use --privileged
The --privileged flag gives the container nearly full host access, disabling all security isolation. It is occasionally required for specific use cases (like running Docker-in-Docker) but should be avoided entirely in production and treated as an immediate red flag during security reviews.
Kubernetes Security Hardening
Pod Security Standards
Kubernetes enforces security policies via Pod Security Standards (replacing the deprecated PodSecurityPolicy):
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
The restricted profile enforces: no privileged containers, no root user, no privilege escalation, required seccomp profile, dropped all capabilities.
RBAC: Least Privilege for Service Accounts
Every pod runs with a service account. By default, that service account can reach the Kubernetes API. Apply least privilege:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
Audit with kubectl auth can-i --list --as=system:serviceaccount:default:myapp. Disable automounting of service account tokens when not needed: automountServiceAccountToken: false.
Secrets Management
Kubernetes Secrets are Base64-encoded, not encrypted, by default. Use:
- Encryption at rest: enable
EncryptionConfigurationin the API server to encrypt secrets in etcd - External secrets managers: AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager via the External Secrets Operator
- Never put secrets in ConfigMaps, environment variables in Deployment specs (visible via
kubectl describe), or container images
Network Policies
By default, all pods in a Kubernetes cluster can communicate with each other. Implement NetworkPolicy to enforce micro-segmentation:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
spec:
podSelector: {}
policyTypes: [Ingress]
Then add explicit allow rules for necessary communication paths only. Use a CNI plugin that enforces NetworkPolicy (Calico, Cilium, Weave).
Continuous Security Posture
- Use Falco (CNCF runtime security tool) to detect suspicious behavior at runtime: unexpected shell spawns, outbound connections to new IPs, privilege escalation attempts
- Scan cluster configurations with kube-bench (CIS Kubernetes Benchmark) and Trivy operator
- Enable audit logging on the Kubernetes API server and ship logs to a SIEM
- Regularly rotate image digests and automate dependency updates with Renovate or Dependabot