Containers have transformed how we deploy applications, but they introduce new security considerations. A compromised container can lead to data breaches, cryptomining, or lateral movement to other systems. This guide covers security practices from building images to running containers in production.
The Container Security Lifecycle
Security must be addressed at every stage:
- Build: Secure base images, minimal dependencies
- Ship: Image scanning, signed images, secure registries
- Run: Runtime protection, least privilege, network policies
Secure Base Images
Use Minimal Base Images
# Bad: Full OS with unnecessary packages
FROM ubuntu:22.04
# Better: Minimal distribution
FROM alpine:3.19
# Best for compiled languages: Distroless
FROM gcr.io/distroless/static-debian12
Size comparison:
- Ubuntu: ~77MB
- Alpine: ~7MB
- Distroless: ~2MB
Smaller images = smaller attack surface.
Pin Image Versions
# Bad: Mutable tag
FROM node:latest
FROM php:8
# Good: Specific version
FROM node:20.11.0-alpine3.19
FROM php:8.3.2-fpm-alpine3.19
# Best: SHA256 digest
FROM node@sha256:abc123...
Keep Images Updated
# Dependabot for Docker
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: docker
directory: "/"
schedule:
interval: weekly
Build Security
Don't Run as Root
# Create non-root user
FROM node:20-alpine
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "server.js"]
Multi-stage Builds
Keep build tools out of production images:
# Build stage
FROM composer:2 AS builder
WORKDIR /app
COPY composer.* ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
# Production stage
FROM php:8.3-fpm-alpine
COPY --from=builder /app /var/www/html
# No composer, git, or build tools in final image
Don't Embed Secrets
# Bad: Secret in image layer
ENV DATABASE_PASSWORD=secret123
COPY .env /app/.env
# Good: Inject at runtime
# No secrets in Dockerfile
# Pass via environment or secrets manager
Use .dockerignore
# .dockerignore
.git
.env
.env.*
node_modules
vendor
tests
*.log
docker-compose*.yml
Image Scanning
Automated Vulnerability Scanning
# GitHub Actions with Trivy
- 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'
Local Scanning
# Trivy
trivy image myapp:latest
# Snyk
snyk container test myapp:latest
# Docker Scout
docker scout cves myapp:latest
Registry Scanning
Enable scanning in your registry:
- Docker Hub: Docker Scout
- AWS ECR: Enhanced scanning
- Google GCR: Container Analysis
- Azure ACR: Defender for Containers
Image Signing
Cosign for Image Signatures
# Generate key pair
cosign generate-key-pair
# Sign image
cosign sign --key cosign.key myregistry/myapp:v1.0.0
# Verify signature
cosign verify --key cosign.pub myregistry/myapp:v1.0.0
Kubernetes Admission Control
# Require signed images with Kyverno
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-----
Runtime Security
Read-Only File System
# Kubernetes
securityContext:
readOnlyRootFilesystem: true
# Docker
docker run --read-only myapp
# For apps needing temp files
volumes:
- name: tmp
emptyDir: {}
volumeMounts:
- name: tmp
mountPath: /tmp
Drop Capabilities
# Kubernetes Pod spec
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if needed
No Privilege Escalation
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1001
Resource Limits
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
Network Security
Network Policies
# Default deny all ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-traffic
spec:
podSelector:
matchLabels:
app: web
ingress:
- from:
- podSelector:
matchLabels:
app: ingress
ports:
- port: 8080
Service Mesh mTLS
# Istio PeerAuthentication
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
Secrets Management
Kubernetes Secrets
# Create secret
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=secretpass
# Mount in pod
volumes:
- name: secrets
secret:
secretName: db-credentials
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: production/db
property: password
Pod Security Standards
Restricted Policy
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
Secure Pod Template
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:v1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
memory: "256Mi"
cpu: "500m"
Monitoring and Audit
Runtime Monitoring with Falco
# Falco rule for suspicious activity
- rule: Shell Spawned in Container
desc: Detect shell spawned in container
condition: >
spawned_process and container and
shell_procs and proc.pname != entrypoint
output: >
Shell spawned in container
(user=%user.name container=%container.name shell=%proc.name)
priority: WARNING
Kubernetes Audit Logs
# Audit policy
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: Metadata
resources:
- group: ""
resources: ["pods", "deployments"]
Security Checklist
Build Time
- Use minimal base images
- Pin image versions
- Multi-stage builds
- No secrets in images
- Non-root user
- Scan for vulnerabilities
Deploy Time
- Sign images
- Verify signatures
- Private registry
Runtime
- Read-only filesystem
- Drop capabilities
- Resource limits
- Network policies
- Secret management
- Runtime monitoring
Conclusion
Container security requires attention at every stage of the lifecycle. Start with secure base images and build practices, implement scanning and signing in your pipeline, and enforce security policies at runtime. Kubernetes provides powerful primitives for security;use Pod Security Standards, Network Policies, and proper secrets management to build defense in depth.