Skip to main content
Project DevSecOps Intermediate

Docker Container Security Hardening

Jason J. Boderebe
17 min tutorial
#docker #container-security #devops #hardening

Docker Container Security Hardening

Container security is critical in modern DevOps environments. This comprehensive guide covers Docker security from image creation to runtime protection, implementing defense-in-depth strategies to secure your containerized applications.

Security Fundamentals

Container Security Model

  • Image Security: Secure base images and dependency management
  • Runtime Security: Container isolation and resource constraints
  • Host Security: Docker daemon and host system hardening
  • Network Security: Container communication and segmentation
  • Secrets Management: Secure handling of sensitive data

Threat Model

  • Container Escape: Breaking out of container isolation
  • Privilege Escalation: Gaining unauthorized system access
  • Resource Exhaustion: DoS through resource consumption
  • Image Vulnerabilities: Exploiting known CVEs in dependencies
  • Secrets Exposure: Accidental disclosure of sensitive data

Secure Image Building

Secure Dockerfile Practices

Minimal Base Images

# Avoid full OS images
FROM ubuntu:latest

# Use minimal, distroless, or scratch images
FROM gcr.io/distroless/java:11
# OR
FROM alpine:3.18
# OR for static binaries
FROM scratch

Non-Root User Implementation

# Create dedicated user and group
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

# Set ownership and permissions
COPY --chown=appuser:appgroup . /app
WORKDIR /app

# Switch to non-root user
USER appuser

# Verify non-root execution
RUN whoami  # Should output: appuser

Secure Multi-Stage Build

# Build stage
FROM node:18-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Security scan stage
FROM builder AS security-scan
RUN npm audit --audit-level=moderate
RUN npm install -g snyk && snyk test

# Production stage
FROM gcr.io/distroless/nodejs18-debian11 AS production
COPY --from=builder --chown=nonroot:nonroot /build/node_modules ./node_modules
COPY --chown=nonroot:nonroot . .
USER nonroot
EXPOSE 3000
CMD ["server.js"]

Dependency Security

# Pin specific versions
FROM node:18.17.1-alpine3.18

# Verify checksums for critical dependencies
RUN wget -O /tmp/app.tar.gz https://releases.example.com/app-1.0.0.tar.gz \
    && echo "sha256:expected_hash /tmp/app.tar.gz" | sha256sum -c - \
    && tar -xzf /tmp/app.tar.gz \
    && rm /tmp/app.tar.gz

# Remove package managers in production
RUN apk del apk-tools

Image Hardening Techniques

File System Security

# Remove unnecessary packages and files
RUN apk add --no-cache --virtual .build-deps gcc musl-dev \
    && apk add --no-cache curl \
    && apk del .build-deps

# Set proper file permissions
RUN chmod -R 755 /app \
    && chmod 644 /app/config/* \
    && chmod 600 /app/secrets/*

# Remove sensitive files
RUN rm -rf /tmp/* /var/cache/apk/* /root/.cache

Security Scanning Integration

# .github/workflows/security-scan.yml
name: Container Security Scan
on: [push, pull_request]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    - name: Build image
      run: docker build -t myapp:${{ github.sha }} .
    
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'myapp:${{ github.sha }}'
        format: 'sarif'
        output: 'trivy-results.sarif'
    
    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'
    
    - name: Docker Scout scan
      uses: docker/scout-action@v1
      with:
        command: cves
        image: myapp:${{ github.sha }}
        only-severities: critical,high

Runtime Security

Container Configuration

Resource Limits

# docker-compose.security.yml
version: '3.8'
services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
          pids: 100
        reservations:
          cpus: '0.25'
          memory: 256M
    
    # Security options
    security_opt:
      - no-new-privileges:true
      - apparmor:docker-default
    
    # Capability restrictions
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # Only if needed for port binding
    
    # Read-only root filesystem
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m
      - /var/run:noexec,nosuid,size=50m
    
    # User namespace
    user: "1000:1000"
    
    # Network security
    networks:
      - app-network
    
    # Health check
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

networks:
  app-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
    driver_opts:
      com.docker.network.bridge.enable_icc: "false"

Secure Docker Run Commands

#!/bin/bash
# secure-docker-run.sh

docker run -d \
  --name secure-app \
  --user 1000:1000 \
  --read-only \
  --tmpfs /tmp:noexec,nosuid,size=50m \
  --security-opt no-new-privileges:true \
  --security-opt apparmor:docker-default \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --memory 512m \
  --cpus "0.5" \
  --pids-limit 100 \
  --ulimit nofile=1024:1024 \
  --restart unless-stopped \
  --health-cmd "curl -f http://localhost:3000/health" \
  --health-interval 30s \
  --health-timeout 10s \
  --health-retries 3 \
  --network app-network \
  --env-file .env.production \
  myapp:latest

Advanced Security Features

AppArmor Profile

# /etc/apparmor.d/docker-myapp
#include <tunables/global>

profile docker-myapp flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  
  # Network access
  network inet tcp,
  network inet udp,
  
  # File system access
  /app/** r,
  /app/public/** r,
  /tmp/** rw,
  /var/run/** rw,
  
  # Deny dangerous capabilities
  deny capability sys_admin,
  deny capability sys_ptrace,
  deny capability sys_module,
  deny capability dac_override,
  
  # Deny sensitive file access
  deny /etc/passwd r,
  deny /etc/shadow r,
  deny /proc/sys/** r,
  deny /sys/** r,
}

Seccomp Profile

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "accept", "accept4", "access", "arch_prctl", "bind", "brk",
        "chmod", "chown", "close", "connect", "dup", "dup2", "epoll_create",
        "epoll_ctl", "epoll_wait", "exit", "exit_group", "fchmod", "fchown",
        "fcntl", "fstat", "futex", "getcwd", "getdents", "getpid", "getppid",
        "getrlimit", "getsockname", "getsockopt", "getuid", "listen",
        "lseek", "mmap", "mprotect", "munmap", "open", "openat", "pipe",
        "poll", "read", "readv", "recv", "recvfrom", "rt_sigaction",
        "rt_sigprocmask", "rt_sigreturn", "sched_yield", "send", "sendto",
        "setrlimit", "setsockopt", "shutdown", "socket", "stat", "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Secrets Management

Secure Secrets Handling

Docker Secrets (Swarm Mode)

# Create secrets
echo "db_password_here" | docker secret create db_password -
echo "api_key_here" | docker secret create api_key -

# Deploy with secrets
docker service create \
  --name secure-app \
  --secret db_password \
  --secret api_key \
  --env DB_PASSWORD_FILE=/run/secrets/db_password \
  --env API_KEY_FILE=/run/secrets/api_key \
  myapp:latest

External Secrets Operator (Kubernetes)

# external-secrets.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-secret-store
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "myapp"
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
spec:
  refreshInterval: 30s
  secretStoreRef:
    name: vault-secret-store
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
  - secretKey: database-password
    remoteRef:
      key: myapp/database
      property: password

HashiCorp Vault Integration

// vault-secrets.js
const vault = require('node-vault')({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN
});

async function getSecret(path) {
  try {
    const result = await vault.read(`secret/data/${path}`);
    return result.data.data;
  } catch (error) {
    console.error('Failed to retrieve secret:', error);
    process.exit(1);
  }
}

// Usage
(async () => {
  const secrets = await getSecret('myapp/database');
  process.env.DB_PASSWORD = secrets.password;
  
  // Clear vault token after use
  delete process.env.VAULT_TOKEN;
})();

Network Security

Container Network Isolation

Custom Bridge Networks

# Create isolated networks
docker network create \
  --driver bridge \
  --subnet=172.20.0.0/16 \
  --gateway=172.20.0.1 \
  --opt com.docker.network.bridge.enable_icc=false \
  --opt com.docker.network.bridge.enable_ip_masquerade=true \
  frontend-network

docker network create \
  --driver bridge \
  --subnet=172.21.0.0/16 \
  --gateway=172.21.0.1 \
  --opt com.docker.network.bridge.enable_icc=false \
  --internal \
  backend-network

Network Policies (Kubernetes)

# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-network-policy
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 3000
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432
  - to: []  # Allow DNS
    ports:
    - protocol: UDP
      port: 53

TLS/SSL Configuration

Nginx SSL Termination

# nginx-ssl.conf
server {
    listen 443 ssl http2;
    server_name api.example.com;
    
    # SSL Configuration
    ssl_certificate /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'" always;
    
    location / {
        proxy_pass http://app:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

Monitoring and Compliance

Security Monitoring

Container Runtime Monitoring

# falco-rules.yaml
- rule: Container Privilege Escalation
  desc: Detect attempts to gain root privileges in containers
  condition: >
    spawned_process and container and
    (proc.name in (su, sudo, doas) or
     proc.args contains "sudo" or
     proc.args contains "su root")
  output: >
    Privilege escalation attempt in container
    (user=%user.name command=%proc.cmdline container=%container.name
     image=%container.image.repository:%container.image.tag)
  priority: WARNING

- rule: Container Network Tool Usage
  desc: Detect network tools usage that might indicate compromise
  condition: >
    spawned_process and container and
    proc.name in (nc, netcat, nmap, socat, telnet, wget, curl) and
    not proc.args contains "health"
  output: >
    Network tool usage in container
    (user=%user.name command=%proc.cmdline container=%container.name)
  priority: NOTICE

Docker Bench Security

#!/bin/bash
# docker-security-audit.sh

# Run Docker Bench Security
docker run --rm --net host --pid host --userns host --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /etc:/etc:ro \
  -v /usr/bin/containerd:/usr/bin/containerd:ro \
  -v /usr/bin/runc:/usr/bin/runc:ro \
  -v /usr/lib/systemd:/usr/lib/systemd:ro \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  --label docker_bench_security \
  docker/docker-bench-security

# Custom security checks
echo "=== Custom Security Checks ==="

# Check for running privileged containers
echo "Checking for privileged containers..."
docker ps --format "table {{.Names}}\t{{.Status}}" --filter "label=docker_bench_security" 2>/dev/null || echo "None found"

# Check for containers running as root
echo "Checking containers running as root..."
docker ps -q | xargs docker inspect --format '{{.Name}}: {{.Config.User}}' | grep -E ": $|: root$" || echo "None found"

# Verify image signatures
echo "Verifying image signatures..."
export DOCKER_CONTENT_TRUST=1
docker images --format "{{.Repository}}:{{.Tag}}" | while read image; do
  echo "Checking $image..."
  docker trust inspect "$image" >/dev/null 2>&1 && echo "✓ Signed" || echo "✗ Not signed"
done

Compliance Automation

CIS Kubernetes Benchmark

#!/bin/bash
# k8s-cis-benchmark.sh

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

# Wait for completion
kubectl wait --for=condition=complete --timeout=300s job/kube-bench

# Get results
kubectl logs job/kube-bench > cis-benchmark-results.txt

# Parse critical issues
grep -E "FAIL|WARN" cis-benchmark-results.txt | tee cis-critical-issues.txt

# Clean up
kubectl delete job kube-bench

Automated Remediation

#!/usr/bin/env python3
# docker-security-remediation.py

import docker
import json
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

client = docker.from_env()

def check_and_fix_containers():
    """Check running containers for security issues and attempt fixes"""
    
    security_issues = []
    
    for container in client.containers.list():
        issues = []
        
        # Check if running as root
        inspect = client.api.inspect_container(container.id)
        config = inspect['Config']
        
        if not config.get('User') or config['User'] == 'root':
            issues.append('running_as_root')
        
        # Check for privileged mode
        host_config = inspect['HostConfig']
        if host_config.get('Privileged'):
            issues.append('privileged_mode')
        
        # Check for excessive capabilities
        cap_add = host_config.get('CapAdd', [])
        dangerous_caps = ['SYS_ADMIN', 'DAC_OVERRIDE', 'SYS_PTRACE']
        if any(cap in cap_add for cap in dangerous_caps):
            issues.append('dangerous_capabilities')
        
        # Check for host network mode
        if host_config.get('NetworkMode') == 'host':
            issues.append('host_network')
        
        if issues:
            security_issues.append({
                'container': container.name,
                'image': container.image.tags[0] if container.image.tags else container.image.id,
                'issues': issues
            })
    
    return security_issues

def generate_security_report(issues):
    """Generate security report and recommendations"""
    
    report = {
        'timestamp': '2025-01-25T10:00:00Z',
        'total_containers': len(client.containers.list()),
        'containers_with_issues': len(issues),
        'issues': issues,
        'recommendations': []
    }
    
    for issue in issues:
        recommendations = []
        
        if 'running_as_root' in issue['issues']:
            recommendations.append('Add USER directive in Dockerfile')
        
        if 'privileged_mode' in issue['issues']:
            recommendations.append('Remove --privileged flag, use specific capabilities instead')
        
        if 'dangerous_capabilities' in issue['issues']:
            recommendations.append('Review and minimize required capabilities')
        
        if 'host_network' in issue['issues']:
            recommendations.append('Use custom bridge networks instead of host networking')
        
        issue['recommendations'] = recommendations
    
    return report

if __name__ == '__main__':
    logger.info("Starting Docker security audit...")
    
    security_issues = check_and_fix_containers()
    report = generate_security_report(security_issues)
    
    # Save report
    with open('docker-security-report.json', 'w') as f:
        json.dump(report, f, indent=2)
    
    logger.info(f"Audit complete. Found {len(security_issues)} containers with security issues.")
    
    # Print summary
    for issue in security_issues:
        logger.warning(f"Container '{issue['container']}' has issues: {', '.join(issue['issues'])}")

Emergency Response

Incident Response Playbook

Container Compromise Response

#!/bin/bash
# incident-response.sh

CONTAINER_ID=$1
INCIDENT_ID=$2

if [ -z "$CONTAINER_ID" ] || [ -z "$INCIDENT_ID" ]; then
    echo "Usage: $0 <container_id> <incident_id>"
    exit 1
fi

echo "=== Container Incident Response ==="
echo "Container ID: $CONTAINER_ID"
echo "Incident ID: $INCIDENT_ID"
echo "Timestamp: $(date -u)"

# 1. Isolate container
echo "Step 1: Isolating container..."
docker network disconnect bridge "$CONTAINER_ID" 2>/dev/null || true
docker pause "$CONTAINER_ID"

# 2. Capture container state
echo "Step 2: Capturing container state..."
mkdir -p "/tmp/incident-$INCIDENT_ID"
cd "/tmp/incident-$INCIDENT_ID"

# Container inspect
docker inspect "$CONTAINER_ID" > container-inspect.json

# Process list
docker exec "$CONTAINER_ID" ps aux > processes.txt 2>/dev/null || echo "Could not capture processes"

# Network connections
docker exec "$CONTAINER_ID" netstat -tulpn > network-connections.txt 2>/dev/null || echo "Could not capture network state"

# File system diff
docker diff "$CONTAINER_ID" > filesystem-changes.txt

# 3. Memory dump (if possible)
echo "Step 3: Attempting memory capture..."
docker exec "$CONTAINER_ID" cat /proc/*/maps > memory-maps.txt 2>/dev/null || echo "Could not capture memory maps"

# 4. Export container for analysis
echo "Step 4: Exporting container..."
docker export "$CONTAINER_ID" > "container-$CONTAINER_ID.tar"

# 5. Collect logs
echo "Step 5: Collecting logs..."
docker logs "$CONTAINER_ID" > container-logs.txt 2>&1

# 6. Stop and remove container
echo "Step 6: Stopping compromised container..."
docker stop "$CONTAINER_ID"
docker rm "$CONTAINER_ID"

# 7. Generate incident report
echo "Step 7: Generating incident report..."
cat > incident-report.md << EOF
# Container Security Incident Report

**Incident ID:** $INCIDENT_ID
**Container ID:** $CONTAINER_ID
**Timestamp:** $(date -u)
**Response Actions:**

1. Container isolated and paused
2. Container state captured
3. Memory analysis attempted
4. Container exported for forensic analysis
5. Logs collected
6. Container stopped and removed

**Artifacts Collected:**
- container-inspect.json
- processes.txt
- network-connections.txt
- filesystem-changes.txt
- memory-maps.txt
- container-$CONTAINER_ID.tar
- container-logs.txt

**Next Steps:**
1. Analyze exported container image
2. Review logs for indicators of compromise
3. Check for lateral movement
4. Update security controls
EOF

echo "Incident response complete. Artifacts stored in /tmp/incident-$INCIDENT_ID/"

Conclusion

Container security requires a comprehensive approach covering:

  • Secure Image Building: Minimal images, dependency scanning, multi-stage builds
  • Runtime Hardening: Resource limits, capability restrictions, read-only filesystems
  • Secrets Management: External secret stores, encryption at rest and in transit
  • Network Security: Isolation, TLS/SSL, network policies
  • Monitoring & Compliance: Runtime monitoring, compliance automation, incident response

Key Takeaways

  1. Defense in Depth: Layer multiple security controls
  2. Least Privilege: Minimize permissions and capabilities
  3. Continuous Monitoring: Implement runtime security monitoring
  4. Automation: Automate security scanning and compliance checks
  5. Incident Response: Prepare for container compromise scenarios

Advanced Topics to Explore

  • Kubernetes Security: Pod Security Standards, admission controllers
  • Service Mesh Security: Istio/Linkerd security features
  • Zero Trust Networking: Identity-based network access
  • Container Runtime Security: gVisor, Kata Containers
  • Supply Chain Security: SLSA framework, software bill of materials

This guide provides a solid foundation for implementing enterprise-grade container security practices in your development and production environments.


Ready to explore Kubernetes security? Check out our Kubernetes Security Hardening guide next.