Containers provide process isolation, not security isolation. A container escape—breaking out of container boundaries to the host—is a known attack class. Container security requires defense in depth: multiple layers of protection so that any single failure doesn’t compromise the system.
Beyond basic container usage, here’s how to secure containerized workloads properly.
Image Security
Security starts with what goes into containers.
Minimal Base Images
Every package is attack surface. Minimal images reduce it.
Alpine Linux provides a functional base in ~5MB. Most applications don’t need full Ubuntu or Debian.
Distroless images contain only your application and runtime dependencies. No shell, no package manager, no utilities for attackers to use.
# Multi-stage build with distroless
FROM golang:1.9 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o service .
FROM gcr.io/distroless/static
COPY --from=builder /app/service /
CMD ["/service"]
Pin Base Image Versions
FROM ubuntu:latest means different images over time. Pin versions for reproducibility and security predictability:
FROM ubuntu:18.04@sha256:abc123...
The digest ensures you get exactly the same image regardless of tag changes.
Scan for Vulnerabilities
Container images contain packages with vulnerabilities. Scan regularly:
During build:
# Trivy scanning
trivy image my-app:latest
In registries: Enable automatic scanning in your container registry.
Continuously: Vulnerabilities are discovered after images are built. Re-scan existing images.
Block deployment of images with critical vulnerabilities. Create exception processes for images that can’t be immediately remediated.
Sign and Verify Images
Image signing ensures you’re running images you built, not images an attacker injected.
Docker Content Trust:
export DOCKER_CONTENT_TRUST=1
docker push my-registry/my-app:latest # Signs image
docker pull my-registry/my-app:latest # Verifies signature
Don’t Run as Root
Containers running as root mean container escapes get root on the host. Run as non-root:
# Create non-root user
RUN adduser -D appuser
USER appuser
Kubernetes can enforce this cluster-wide:
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
spec:
runAsUser:
rule: MustRunAsNonRoot
Runtime Security
What protections exist while containers run?
Read-Only Filesystems
If your application doesn’t need to write to the filesystem, make it read-only:
# Kubernetes
securityContext:
readOnlyRootFilesystem: true
Mount writable volumes only where necessary:
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Read-only filesystems prevent attackers from writing malware or modifying configuration.
Drop Capabilities
Linux capabilities grant specific root powers. Containers don’t need most of them.
Drop all capabilities, add only what’s required:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"] # Only if needed
Seccomp Profiles
Seccomp restricts which system calls containers can make. Docker’s default profile blocks dangerous syscalls.
For higher security, create custom profiles allowing only needed syscalls:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{"names": ["read", "write", "exit", "exit_group"], "action": "SCMP_ACT_ALLOW"}
]
}
AppArmor and SELinux
Mandatory access control (MAC) systems restrict what processes can do regardless of user permissions.
AppArmor: Create profiles defining allowed file access, network operations, and capabilities.
SELinux: Label-based security limiting process interactions.
Enable MAC enforcement for containerized workloads. Default profiles provide baseline protection.
Runtime Monitoring
Detect suspicious container behavior:
- Unexpected process execution
- File system modifications
- Network connections to unusual destinations
- Privilege escalation attempts
Tools like Falco, Sysdig, and Aqua provide runtime monitoring for containers.
# Falco rule example
- rule: Terminal shell in container
desc: Detect shell spawned in container
condition: >
spawned_process and container and
shell_procs
output: "Shell spawned in container (user=%user.name container=%container.name)"
priority: WARNING
Network Security
Network Policies
By default, all pods can communicate with all other pods. Network policies restrict this:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-network-policy
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 5432
Apply least-privilege networking: pods can only communicate with what they need.
Service Mesh Security
Service meshes (Istio, Linkerd) provide:
- Mutual TLS between services
- Fine-grained authorization policies
- Encrypted service-to-service communication
# Istio authorization policy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: api-policy
spec:
selector:
matchLabels:
app: api
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/frontend"]
Secrets Management
Don’t bake secrets into images. Don’t store them in environment variables visible to anyone with pod access.
Kubernetes Secrets provide basic secret storage. They’re base64 encoded, not encrypted by default. Enable encryption at rest.
External secret managers (Vault, AWS Secrets Manager) provide better security:
# Using external secrets operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: database-credentials
data:
- secretKey: password
remoteRef:
key: secret/data/database
property: password
Orchestrator Security
RBAC
Restrict what users and service accounts can do:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployment-reader
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list"]
Apply least privilege. Not everyone needs cluster-admin.
Pod Security Standards
Kubernetes Pod Security Standards define security levels:
- Privileged: Unrestricted
- Baseline: Prevents known privilege escalations
- Restricted: Heavily restricted, best practices
Enforce appropriate levels per namespace:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
Audit Logging
Enable Kubernetes audit logging to track API server operations:
- Who did what
- When
- What resources were affected
Audit logs are essential for security investigation and compliance.
Key Takeaways
- Container isolation isn’t a security boundary; defense in depth is required
- Use minimal base images, scan for vulnerabilities, sign images, and run as non-root
- Runtime protection includes read-only filesystems, dropped capabilities, seccomp, and MAC enforcement
- Network policies restrict pod-to-pod communication to least-privilege
- Service meshes provide mutual TLS and fine-grained authorization
- Use external secret managers rather than baking secrets into images
- Enable RBAC, pod security standards, and audit logging in orchestrators