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
- Defense in Depth: Layer multiple security controls
- Least Privilege: Minimize permissions and capabilities
- Continuous Monitoring: Implement runtime security monitoring
- Automation: Automate security scanning and compliance checks
- 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.