Containers provide isolation, but they’re not inherently secure. A misconfigured container can expose your entire infrastructure. Security requires attention at every stage: base images, build process, deployment configuration, and runtime.
Here’s how to secure containers properly.
Image Security
Minimal Base Images
Every package is potential attack surface:
# Bad - full OS with unnecessary packages
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y python3
# Better - minimal image
FROM python:3.9-slim
# Best - distroless (no shell, no package manager)
FROM gcr.io/distroless/python3
Distroless images contain only your application and runtime dependencies. No shell means attackers can’t get a shell.
Scan Images for Vulnerabilities
Integrate scanning into CI/CD:
# GitHub Actions example
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
Tools:
- Trivy: Open source, fast, comprehensive
- Clair: Open source, integrates with registries
- Snyk: Commercial, developer-friendly
- Anchore: Policy-based scanning
Pin Image Versions
# Bad - mutable tag
FROM node:latest
# Bad - even major versions change
FROM node:14
# Good - pinned digest
FROM node:14.17.0-alpine3.13@sha256:abc123...
latest can change underneath you. Pin to specific versions and digests.
Multi-Stage Builds
Keep build tools out of production images:
# Build stage - has compilers, dev dependencies
FROM golang:1.17 AS builder
WORKDIR /app
COPY . .
RUN go build -o /app/server
# Production stage - minimal runtime
FROM gcr.io/distroless/base
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Build tools are common attack vectors. Don’t ship them.
Sign and Verify Images
Ensure images haven’t been tampered with:
# Sign image with cosign
cosign sign --key cosign.key myregistry/myapp:v1.0
# Verify signature
cosign verify --key cosign.pub myregistry/myapp:v1.0
Kubernetes can enforce signature verification with policy engines.
Build Process Security
Secure CI/CD Pipeline
The pipeline that builds images needs protection:
- Secrets in CI/CD are high-value targets
- Pipeline configuration should be reviewed
- Build environments should be ephemeral
- Principle of least privilege for build jobs
# Don't embed secrets in Dockerfile
# Bad
ENV API_KEY=secret123
# Good - inject at runtime
ENV API_KEY=${API_KEY}
Don’t Run as Root
# Create non-root user
RUN addgroup --system app && adduser --system --group app
USER app
# Kubernetes enforcement
securityContext:
runAsNonRoot: true
runAsUser: 1000
Root in container = root-equivalent on host with many container escapes.
Read-Only Filesystem
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
If application is compromised, attacker can’t write to filesystem.
Runtime Security
Resource Limits
Prevent resource exhaustion attacks:
resources:
limits:
memory: "256Mi"
cpu: "500m"
requests:
memory: "128Mi"
cpu: "100m"
Without limits, one container can starve others.
Drop Capabilities
Linux capabilities provide granular privileges:
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if needed for port < 1024
Containers don’t need most capabilities. Drop all, add back only what’s required.
No Privilege Escalation
securityContext:
allowPrivilegeEscalation: false
Prevents setuid binaries and other escalation vectors.
Seccomp Profiles
Restrict system calls:
securityContext:
seccompProfile:
type: RuntimeDefault # Or custom profile
Container doesn’t need most syscalls. Block them.
AppArmor/SELinux
Additional mandatory access control:
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/app: runtime/default
Extra layer of defense beyond container isolation.
Network Security
Network Policies
Default: all pods can talk to all pods. Change this:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
spec:
podSelector:
matchLabels:
app: database
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- port: 5432
Explicit allow for required traffic only.
Service Mesh Security
Istio/Linkerd provide:
- mTLS between services
- Identity-based authorization
- Traffic encryption
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: api-policy
spec:
selector:
matchLabels:
app: api
rules:
- from:
- source:
principals: ["cluster.local/ns/production/sa/frontend"]
Egress Control
Limit what containers can reach externally:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
podSelector:
matchLabels:
app: api
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/8 # Internal only
- to:
- podSelector:
matchLabels:
app: database
Compromised container can’t call out to attacker infrastructure.
Secrets Management
Don’t Use Environment Variables for Secrets
Environment variables are:
- Visible in process listings
- Logged in crash dumps
- Inherited by child processes
- Visible in Kubernetes API
# Better - mount as files
volumes:
- name: secrets
secret:
secretName: api-secrets
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
External Secrets Management
# Vault Agent sidecar
containers:
- name: vault-agent
image: vault
args:
- agent
- -config=/etc/vault/config.hcl
volumeMounts:
- name: secrets
mountPath: /etc/secrets
- name: app
image: myapp
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
Or use External Secrets Operator to sync from Vault/AWS Secrets Manager.
Monitoring and Detection
Audit Logging
Enable Kubernetes audit logging:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: Metadata
resources:
- group: ""
resources: ["pods", "services"]
Track who did what.
Runtime Security Monitoring
Detect anomalous behavior:
# Falco rule example
- rule: Shell spawned 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
Falco, Sysdig, or Aqua can detect:
- Unexpected processes
- File modifications
- Network connections
- Privilege escalation attempts
Image Runtime Verification
Ensure running images match expectations:
# Kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: enforce
rules:
- name: verify-signature
match:
resources:
kinds:
- Pod
verifyImages:
- image: "myregistry/*"
key: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
Key Takeaways
- Use minimal base images (distroless when possible)
- Scan images for vulnerabilities in CI/CD
- Pin image versions and verify signatures
- Don’t run containers as root
- Use read-only filesystems
- Drop all capabilities, add back only what’s needed
- Prevent privilege escalation
- Implement network policies (default deny)
- Use service mesh for mTLS and identity-based authorization
- Mount secrets as files, not environment variables
- Enable audit logging and runtime security monitoring
Container security is defense in depth. Each layer adds protection. No single measure is sufficient alone.