Container SecurityDockerKubernetesDevSecOps

Container Security Best Practices

Sven Nellemann
Feb 202410 min read

Introduction

Containers have revolutionized how we build, ship, and run applications. But with great power comes great responsibility—especially when it comes to security. A single misconfigured container or vulnerable image can become the gateway for attackers to compromise your entire infrastructure.

In this comprehensive guide, I'll walk you through the essential best practices for securing containerized applications, from the initial image build to runtime protection in production.

Understanding the Container Security Landscape

Container security isn't just about scanning images for vulnerabilities. It's a multi-layered approach that spans the entire container lifecycle:

Build → Store → Deploy → Runtime
  ↓       ↓        ↓        ↓
SAST   Registry  K8s       Runtime
Scan   Scan     Policies  Protection

Each stage requires specific security controls and best practices. Let's dive into each one.

1. Secure Container Images

Start with Minimal Base Images

The smaller your base image, the smaller your attack surface. Always prefer minimal base images:

Good Choices:

  • **Alpine Linux** - ~5MB, ideal for most applications
  • **Distroless** - Contains only your app and runtime dependencies
  • **Scratch** - Empty image, perfect for static binaries
  • Example: Multi-stage Build with Distroless

    # Build stage
    FROM golang:1.21-alpine AS builder
    WORKDIR /app
    COPY . .
    RUN go build -o myapp
    
    # Production stage
    FROM gcr.io/distroless/static-debian12
    COPY --from=builder /app/myapp /
    EXPOSE 8080
    USER nonroot:nonroot
    ENTRYPOINT ["/myapp"]

    Scan Images for Vulnerabilities

    Integrate vulnerability scanning into your CI/CD pipeline:

    Popular Tools:

  • **Trivy** - Fast, accurate, easy to use
  • **Grype** - Open-source, comprehensive
  • **Snyk Container** - Developer-friendly with remediation advice
  • **Aqua Security** - Enterprise-grade solution
  • Example: Trivy in GitHub Actions

    name: Container Scan
    on: [push, pull_request]
    
    jobs:
      scan:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          
          - name: Build image
            run: docker build -t myapp:latest .
          
          - name: Run Trivy scan
            uses: aquasecurity/trivy-action@master
            with:
              image-ref: myapp:latest
              format: 'sarif'
              output: 'trivy-results.sarif'
              severity: 'CRITICAL,HIGH'
          
          - name: Upload results
            uses: github/codeql-action/upload-sarif@v2
            with:
              sarif_file: 'trivy-results.sarif'

    Keep Images Updated

    Vulnerabilities are discovered daily. Establish a process to regularly rebuild and redeploy images:

  • Set up automated rebuilds weekly or when base images update
  • Use Dependabot or Renovate for dependency updates
  • Monitor vulnerability databases for critical issues
  • 2. Container Registry Security

    Private Registries with Access Controls

    Never use public registries for proprietary images. Use private registries with proper access controls:

    Options:

  • **Docker Hub Private** - Simple, integrated with Docker
  • **Amazon ECR** - Native AWS integration
  • **Google Artifact Registry** - Multi-format support
  • **Azure Container Registry** - Enterprise features
  • **Harbor** - Open-source, self-hosted
  • Image Signing and Verification

    Ensure image integrity with cryptographic signatures:

    Using Docker Content Trust (DCT):

    # Enable DCT
    export DOCKER_CONTENT_TRUST=1
    
    # Push signed image
    docker push myregistry.io/myapp:v1.0.0
    
    # Only signed images can be pulled
    docker pull myregistry.io/myapp:v1.0.0

    Using Cosign (Sigstore):

    # Generate key pair
    cosign generate-key-pair
    
    # Sign image
    cosign sign --key cosign.key myregistry.io/myapp:v1.0.0
    
    # Verify signature
    cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0

    Scan Registry Images Continuously

    Don't just scan during build—continuously scan images in your registry:

    # Example: Trivy scheduled scan
    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: registry-scan
    spec:
      schedule: "0 2 * * *"  # Daily at 2 AM
      jobTemplate:
        spec:
          template:
            spec:
              containers:
              - name: trivy
                image: aquasec/trivy:latest
                args:
                - image
                - --severity
                - CRITICAL,HIGH
                - myregistry.io/myapp:latest

    3. Runtime Security

    Run as Non-Root User

    Never run containers as root. Create a dedicated user:

    FROM node:18-alpine
    
    # Create app user
    RUN addgroup -g 1001 appgroup && \
        adduser -D -u 1001 -G appgroup appuser
    
    # Set working directory
    WORKDIR /app
    
    # Copy application
    COPY --chown=appuser:appgroup . .
    
    # Install dependencies
    RUN npm ci --only=production
    
    # Switch to non-root user
    USER appuser
    
    EXPOSE 3000
    CMD ["node", "server.js"]

    Use Read-Only Root Filesystem

    Make the container's root filesystem read-only to prevent tampering:

    apiVersion: v1
    kind: Pod
    metadata:
      name: secure-pod
    spec:
      containers:
      - name: app
        image: myapp:latest
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
        volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /app/cache
      volumes:
      - name: tmp
        emptyDir: {}
      - name: cache
        emptyDir: {}

    Implement Resource Limits

    Prevent resource exhaustion attacks with limits:

    apiVersion: v1
    kind: Pod
    metadata:
      name: resource-limited
    spec:
      containers:
      - name: app
        image: myapp:latest
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"

    Drop Unnecessary Capabilities

    Containers don't need all Linux capabilities. Drop them:

    apiVersion: v1
    kind: Pod
    metadata:
      name: minimal-capabilities
    spec:
      containers:
      - name: app
        image: myapp:latest
        securityContext:
          capabilities:
            drop:
            - ALL
            add:
            - NET_BIND_SERVICE  # Only if needed for ports < 1024

    4. Kubernetes Security Policies

    Pod Security Standards

    Enforce security policies at the cluster level:

    apiVersion: v1
    kind: Namespace
    metadata:
      name: production
      labels:
        pod-security.kubernetes.io/enforce: restricted
        pod-security.kubernetes.io/audit: restricted
        pod-security.kubernetes.io/warn: restricted

    Network Policies

    Implement zero-trust networking with Network Policies:

    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: api-policy
      namespace: production
    spec:
      podSelector:
        matchLabels:
          app: api
      policyTypes:
      - Ingress
      - Egress
      ingress:
      - from:
        - podSelector:
            matchLabels:
              app: frontend
        ports:
        - protocol: TCP
          port: 8080
      egress:
      - to:
        - podSelector:
            matchLabels:
              app: database
        ports:
        - protocol: TCP
          port: 5432

    Use OPA/Gatekeeper for Policy Enforcement

    Implement custom policies with Open Policy Agent:

    apiVersion: constraints.gatekeeper.sh/v1beta1
    kind: K8sBlockHostNamespace
    metadata:
      name: block-host-namespace
    spec:
      match:
        kinds:
        - apiGroups: [""]
          kinds: ["Pod"]
        namespaces:
        - production

    5. Secrets Management

    Never Hardcode Secrets

    Bad:

    ENV DATABASE_PASSWORD=mysecretpassword123

    Good - Use Kubernetes Secrets:

    apiVersion: v1
    kind: Secret
    metadata:
      name: db-credentials
    type: Opaque
    data:
      password: bXlzZWNyZXRwYXNzd29yZDEyMw==  # base64 encoded
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: app
    spec:
      containers:
      - name: app
        image: myapp:latest
        env:
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password

    External Secrets Management

    For production, use dedicated secrets management:

    Options:

  • **HashiCorp Vault** - Industry standard
  • **AWS Secrets Manager** - Native AWS
  • **Azure Key Vault** - Native Azure
  • **Google Secret Manager** - Native GCP
  • Example: External Secrets Operator

    apiVersion: external-secrets.io/v1beta1
    kind: ExternalSecret
    metadata:
      name: vault-secret
    spec:
      refreshInterval: 1h
      secretStoreRef:
        name: vault-backend
        kind: SecretStore
      target:
        name: db-credentials
      data:
      - secretKey: password
        remoteRef:
          key: secret/database
          property: password

    6. Runtime Threat Detection

    Deploy Runtime Security Tools

    Monitor container behaviour in real-time:

    Popular Tools:

  • **Falco** - Open-source, CNCF project
  • **Sysdig Secure** - Commercial, built on Falco
  • **Aqua Security** - Comprehensive platform
  • **Prisma Cloud** - Palo Alto's solution
  • Example: Falco Rule

    - rule: Unauthorized Process in Container
      desc: Detect unexpected processes in production containers
      condition: >
        spawned_process and
        container and
        container.image.repository = "myapp" and
        not proc.name in (node, npm)
      output: >
        Unexpected process started in container
        (user=%user.name command=%proc.cmdline container=%container.name)
      priority: WARNING

    Implement Admission Controllers

    Validate and mutate pod specifications before deployment:

    Example: Admission Webhook

    func (v *Validator) ValidatePod(pod *corev1.Pod) error {
        // Ensure non-root user
        if pod.Spec.SecurityContext == nil || 
           pod.Spec.SecurityContext.RunAsUser == nil ||
           *pod.Spec.SecurityContext.RunAsUser == 0 {
            return fmt.Errorf("pod must not run as root")
        }
        
        // Ensure resource limits
        for _, container := range pod.Spec.Containers {
            if container.Resources.Limits == nil {
                return fmt.Errorf("container must have resource limits")
            }
        }
        
        return nil
    }

    7. Compliance and Auditing

    Enable Audit Logging

    Track all API server activity:

    apiVersion: audit.k8s.io/v1
    kind: Policy
    rules:
    - level: Metadata
      resources:
      - group: ""
        resources: ["pods", "secrets"]
      namespaces: ["production"]

    Regular Security Audits

    Conduct periodic security assessments:

    1. Weekly: Vulnerability scan reports review

    2. Monthly: Policy compliance checks

    3. Quarterly: Penetration testing

    4. Annually: Comprehensive security audit

    CIS Benchmarks

    Follow CIS Docker and Kubernetes benchmarks:

    # Install kube-bench
    kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
    
    # Review results
    kubectl logs job/kube-bench

    8. Supply Chain Security

    SBOM Generation

    Generate Software Bill of Materials for all images:

    # Using Syft
    syft myapp:latest -o cyclonedx-json > sbom.json
    
    # Scan SBOM with Grype
    grype sbom:./sbom.json

    Verify Base Image Provenance

    Use only trusted base images from verified publishers:

  • Official images from Docker Hub
  • Verified publisher images
  • Images from your organisation's trusted registry
  • Implement Software Supply Chain Security

    Use tools like SLSA (Supply-chain Levels for Software Artifacts):

    # GitHub Actions with SLSA
    name: SLSA Build
    on: push
    
    jobs:
      build:
        permissions:
          id-token: write
          contents: read
        uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v1.9.0
        with:
          go-version: 1.21

    9. Emergency Response Plan

    Incident Response Playbook

    Have a plan for security incidents:

    1. Detection: Automated alerts trigger investigation

    2. Containment: Isolate affected containers immediately

    3. Eradication: Patch vulnerabilities, rebuild images

    4. Recovery: Deploy secure versions

    5. Lessons Learned: Update policies and procedures

    Container Forensics

    Preserve evidence for investigation:

    # Create snapshot of running container
    docker commit suspicious-container forensics-snapshot:latest
    
    # Export filesystem
    docker export suspicious-container > container-filesystem.tar
    
    # Analyse with forensics tools

    10. Security Checklist

    Use this checklist for every container deployment:

    Build Time:

  • [ ] Minimal base image used
  • [ ] Multi-stage build implemented
  • [ ] No secrets in image layers
  • [ ] Vulnerability scan passed
  • [ ] Non-root user configured
  • [ ] Health checks defined
  • Registry:

  • [ ] Private registry used
  • [ ] Image signed and verified
  • [ ] Access controls configured
  • [ ] Continuous scanning enabled
  • Runtime:

  • [ ] Read-only root filesystem
  • [ ] Resource limits set
  • [ ] Capabilities dropped
  • [ ] Security context configured
  • [ ] Network policies applied
  • Kubernetes:

  • [ ] Pod Security Standards enforced
  • [ ] Admission controllers active
  • [ ] RBAC properly configured
  • [ ] Secrets externalized
  • [ ] Audit logging enabled
  • Conclusion

    Container security is not a one-time task—it's an ongoing process that requires vigilance at every stage of the container lifecycle. By implementing these best practices, you significantly reduce your attack surface and create multiple layers of defence.

    Key Takeaways

    1. Start with secure foundations: Use minimal base images and scan for vulnerabilities

    2. Defence in depth: Implement security controls at every layer

    3. Principle of least privilege: Drop unnecessary capabilities and run as non-root

    4. Automate security: Integrate scanning and policy enforcement into CI/CD

    5. Monitor continuously: Deploy runtime security tools and audit regularly

    Next Steps

    1. Audit current containers: Run Trivy/Grype on all production images

    2. Implement least one runtime security tool: Start with Falco (it's free!)

    3. Establish security policies: Define and enforce Pod Security Standards

    4. Create response plan: Document incident response procedures

    5. Train the team: Ensure everyone understands container security basics

    Remember: Security is everyone's responsibility. Build security into your culture, not just your containers.

    ---

    *Questions about container security? Need help securing your containerized infrastructure? [Let's connect](#contact).*