Container Security Best Practices

Reverend Philip Dec 12, 2025 5 min read

Secure your containerized applications from build to runtime. Learn image scanning, runtime protection, and Kubernetes security policies.

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:

  1. Build: Secure base images, minimal dependencies
  2. Ship: Image scanning, signed images, secure registries
  3. 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.

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.