Skip to main content
โšก Calmops

Container Security: Best Practices for Secure Containers

Introduction

Container security requires a defense-in-depth approach that spans the entire container lifecycleโ€”from image creation to runtime execution. With containers powering modern microservices architectures, understanding how to secure each layer of the container stack is essential for any DevOps engineer or security professional.

This comprehensive guide covers securing containerized applications at every layer: building secure images, scanning for vulnerabilities, implementing runtime protection, managing secrets, and configuring Kubernetes security policies.

The Container Security Landscape

Understanding Container Attack Surfaces

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Container Attack Surface                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                  โ”‚
โ”‚   Build Time          Runtime              Network              โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚
โ”‚   โ”‚  Base   โ”‚       โ”‚   App   โ”‚        โ”‚  Ingressโ”‚              โ”‚
โ”‚   โ”‚ Image   โ”‚       โ”‚ Process โ”‚        โ”‚  /Egressโ”‚              โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚
โ”‚        โ”‚                  โ”‚                   โ”‚                  โ”‚
โ”‚        โ–ผ                  โ–ผ                   โ–ผ                  โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”‚
โ”‚   โ”‚              Container Runtime                    โ”‚          โ”‚
โ”‚   โ”‚         (Docker, containerd, cri-o)            โ”‚          โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
โ”‚                          โ”‚                                     โ”‚
โ”‚                          โ–ผ                                     โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”‚
โ”‚   โ”‚              Kubernetes / Orchestrator           โ”‚          โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
โ”‚                                                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each phase presents unique security challenges that must be addressed comprehensively.

Image Security

Building Secure Dockerfiles

The foundation of container security starts with how you build your images. Follow these essential practices:

# BAD: Using latest tag and running as root
FROM python:latest
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

# GOOD: Specific version, minimal base, non-root user
FROM python:3.11-slim-bookworm@sha256:abc123...

# Create dedicated user and group
RUN groupadd --gid 1000 appgroup && \
    useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser

# Install only what's needed
COPY requirements.txt .
RUN pip install --no-cache-dir --user=appuser -r requirements.txt

# Set working directory
WORKDIR /home/appuser

# Copy application code with correct ownership
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

# Expose only necessary ports
EXPOSE 8080

# Use exec form for CMD
CMD ["python", "app.py"]

Multi-Stage Builds for Minimal Images

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Final stage - minimal runtime image
FROM gcr.io/distroless/static:nonroot

# Copy only the binary
COPY --from=builder /app/main /main

USER nonroot:root
EXPOSE 8080
ENTRYPOINT ["/main"]

Image Scanning in CI/CD

Integrate security scanning into your pipeline to catch vulnerabilities before deployment:

# .github/workflows/security.yml
name: Container Security

on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'image'
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'
      
      - name: Fail on critical vulnerabilities
        run: |
          # Check for critical/high vulnerabilities
          trivy image --exit-code 1 \
            --severity CRITICAL,HIGH \
            myapp:${{ github.sha }}

Docker Content Trust

Enable Docker Content Trust to ensure image integrity:

# Enable DCT
export DOCKER_CONTENT_TRUST=1

# Sign images on push
docker push myregistry.io/myapp:latest

# Verify image signatures
docker trust inspect myregistry.io/myapp:latest

Runtime Security

Kubernetes Security Context

Define security contexts at pod and container level:

apiVersion: v1
kind: Pod
metadata:
  name: secure-application
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  
  containers:
    - name: app
      image: myapp:secure
      securityContext:
        # Prevent privilege escalation
        allowPrivilegeEscalation: false
        
        # Run as non-root
        readOnlyRootFilesystem: true
        
        # Drop all capabilities
        capabilities:
          drop:
            - ALL
        
        # Prevent running as privileged
        privileged: false
        
        # Seccomp profile
        seccompProfile:
          type: RuntimeDefault
      
      resources:
        limits:
          memory: "128Mi"
          cpu: "500m"
        requests:
          memory: "64Mi"
          cpu: "250m"

Pod Security Standards

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
spec:
  privileged: false
  allowPrivilegeEscalation: false
  requiredDropCapabilities:
    - ALL
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    - 'persistentVolumeClaim'
  runAsUser:
    rule: 'MustRunAsNonRoot'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'

Resource Limits and QoS

apiVersion: v1
kind: LimitRange
metadata:
  name: resource-limits
spec:
  limits:
    - max:
        memory: "1Gi"
        cpu: "1000m"
      min:
        memory: "64Mi"
        cpu: "100m"
      default:
        memory: "256Mi"
        cpu: "500m"
      defaultRequest:
        memory: "128Mi"
        cpu: "250m"
      type: Container

Network Security

Kubernetes Network Policies

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-network-policy
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
    - Ingress
    - Egress
  
  # Ingress rules
  ingress:
    # Allow from ingress controller
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - protocol: TCP
          port: 8080
    
    # Allow from same namespace
    - from:
        - podSelector: {}
  
  # Egress rules
  egress:
    # Allow DNS
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    
    # Allow to API server
    - to:
        - kubernetes.io/hostname: master
      ports:
        - protocol: TCP
          port: 6443
    
    # Allow to same namespace
    - to:
        - podSelector: {}

Service Mesh Security

With Istio, you can implement mutual TLS (mTLS):

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: app-authz
spec:
  selector:
    matchLabels:
      app: myapp
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/default/sa/allowed-service"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/api/*"]

Secrets Management

Kubernetes Secrets vs External Secrets

# Native Kubernetes Secret (base64 encoded, not encrypted at rest by default)
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=
  password: cGFzc3dvcmQ=
---
# Better: Use External Secrets Operator with Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: vault-db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: database/prod
        property: username
    - secretKey: password
      remoteRef:
        key: database/prod
        property: password

HashiCorp Vault Integration

# Vault Secret Engine
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "https://vault.example.com:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: kubernetes
          role: "myapp-role"

Docker Secrets for Swarm

# Create secret
echo "my-password" | docker secret create db_password -

# Use in service
docker service create \
  --name myapp \
  --secret db_password \
  myapp:latest

# Access in container
# /run/secrets/db_password

Container Runtime Security

gVisor for Container Isolation

gVisor provides an additional security layer by intercepting system calls:

# Kubernetes with gVisor (runsc)
apiVersion: v1
kind: Pod
metadata:
  name: sandboxed-app
spec:
  runtimeClassName: gvisor
  containers:
    - name: app
      image: myapp:latest

Falco for Runtime Monitoring

# Falco rules for suspicious activity
apiVersion: falco.org/v1
kind: FalcoRules
metadata:
  name: falco-rules
rules:
  - rule: Terminal shell in container
    desc: >
      Detect terminal shell spawn in container
    condition: >
      container.id != host and
      proc.name = bash and
      proc.tty != 0
    output: >
      Terminal shell in container (user=%user.name 
      container_id=%container.id 
      container_name=%container.name
      proc.name=%proc.name)
    priority: WARNING

  - rule: Write to root filesystem
    desc: >
      Detect write to root filesystem
    condition: >
      container.id != host and
      fd.directory = / and
      evt.type = write
    output: >
      Write to root filesystem (user=%user.name
      container_id=%container.id
      proc.name=%proc.name)
    priority: CRITICAL

Supply Chain Security

Sigstore and Cosign for Image Signing

# Generate key pair
cosign generate-key-pair

# Sign container image
cosign sign myregistry.io/myapp:latest

# Verify signature
cosign verify myregistry.io/myapp:latest

# Store in transparency log
cosign sign --tlog-upload myregistry.io/myapp:latest

SBOM (Software Bill of Materials)

# Generate SBOM with Syft
syft myapp:latest -o cyclonedx-json > sbom.json

# Scan SBOM for vulnerabilities
grype sbom.json

Best Practices Summary

Build Time Security

  1. Use specific image tags - Never use latest
  2. Scan for vulnerabilities - Integrate Trivy/Snyk in CI
  3. Sign images - Use Cosign for supply chain security
  4. Minimal images - Use distroless or Alpine
  5. Multi-stage builds - Reduce final image size

Runtime Security

  1. Run as non-root - Always use security context
  2. Read-only filesystem - Prevent runtime modifications
  3. Drop capabilities - Never run with CAP_SYS_ADMIN
  4. Resource limits - Prevent DoS attacks
  5. Network policies - Implement zero-trust networking

Secrets Management

  1. External secrets - Use Vault or AWS Secrets Manager
  2. Never bake secrets - Don’t include in images
  3. Rotate secrets - Implement automatic rotation
  4. Network policies - Limit secret access

Conclusion

Container security is not a single product or configurationโ€”it’s a comprehensive approach that spans the entire container lifecycle. By implementing defense in depth, following the principle of least privilege, and continuously scanning for vulnerabilities, you can significantly reduce the attack surface of your containerized applications.

Key takeaways:

  • Start with secure base images and build patterns
  • Implement runtime security through Kubernetes policies
  • Use secrets management solutions, never bake secrets
  • Monitor runtime behavior for anomalies
  • Sign and verify images throughout the supply chain

Resources

Comments