Docker Security — Best Practices
Advertisement
Docker Security — Best Practices
Security is paramount in containerized environments. Learn to identify vulnerabilities, manage secrets, and harden your Docker deployment.
Introduction
Docker security spans multiple layers: images, containers, registries, and runtime. A comprehensive security strategy addresses all layers.
- Docker Security — Best Practices
- Image Security
- Scan for Vulnerabilities
- Base Image Selection
- Sign and Verify Images
- Container Runtime Security
- Run as Non-Root User
- Read-Only Root Filesystem
- Capability Dropping
- AppArmor and SELinux
- Secrets Management
- Environment Variables (Development)
- Docker Secrets (Swarm Mode)
- External Secrets Management
- Registry Security
- Private Registry
- Registry Authentication
- Network Security
- Isolate Networks
- Firewall Rules
- Runtime Security Monitoring
- Monitor Syscalls
- Container Auditing
- Security Scanning in CI/CD
- GitHub Actions Example
- Production Security Checklist
- FAQ
Image Security
Scan for Vulnerabilities
# Trivy - open source vulnerability scanner
trivy image myapp:1.0
# Docker Scout (built into Docker CLI)
docker scout cves myapp:1.0
# Snyk
snyk container test myapp:1.0
# Output:
# myapp:1.0 (linux/amd64)
# Total: 15 vulnerabilities
# CRITICAL: 3, HIGH: 5, MEDIUM: 7
Base Image Selection
# Scan base image for vulnerabilities
# Use minimal, frequently updated base images
# Good: Alpine, Debian slim, distroless
FROM node:18-alpine
# OR
FROM gcr.io/distroless/nodejs18-debian11
# Avoid: Large, infrequently updated
# FROM ubuntu:latest
# FROM centos:8
Sign and Verify Images
# Enable Docker Content Trust (DCT)
export DOCKER_CONTENT_TRUST=1
# Sign and push image
docker push myregistry/myapp:1.0
# Prompted to enter passphrase
# Verify signed image
docker pull myregistry/myapp:1.0
# Check signature
notarykey list --roles
Container Runtime Security
Run as Non-Root User
FROM node:18-alpine
# Create non-root user
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
# Change file ownership
COPY . /app
WORKDIR /app
# Switch to app user
USER app
CMD ["node", "server.js"]
Read-Only Root Filesystem
# docker-compose.yml
services:
app:
image: myapp:1.0
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /run
Capability Dropping
# Drop all capabilities except NET_BIND_SERVICE
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
myapp:1.0
# Remove specific capability
docker run \
--cap-drop=SYS_ADMIN \
myapp:1.0
AppArmor and SELinux
# Use AppArmor profile
docker run \
--security-opt apparmor=docker-default \
myapp:1.0
# Use SELinux context
docker run \
--security-opt label=type:svirt_apache_t \
myapp:1.0
Secrets Management
Environment Variables (Development)
# Pass secrets at runtime
docker run \
-e DATABASE_PASSWORD=secret \
-e API_KEY=key123 \
myapp:1.0
Docker Secrets (Swarm Mode)
# Create secret
echo "database_password_here" | docker secret create db_password -
# Use in service
docker service create \
--secret db_password \
--env DB_PASSWORD_FILE=/run/secrets/db_password \
myapp:1.0
# In container, read from file
cat /run/secrets/db_password
External Secrets Management
# Don't hardcode secrets in Dockerfile
FROM node:18-alpine
# Good: read from environment/file at runtime
ENV DB_PASSWORD_FILE=/run/secrets/db_password
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
Use Vault, AWS Secrets Manager, or similar:
// Node.js with AWS Secrets Manager
const AWS = require('aws-sdk');
const client = new AWS.SecretsManager();
async function getSecret(secretName) {
try {
const data = await client.getSecretValue({ SecretId: secretName }).promise();
return JSON.parse(data.SecretString);
} catch (error) {
console.error(error);
}
}
const dbPassword = await getSecret('dev/database/password');
Registry Security
Private Registry
# Use private registry instead of Docker Hub
docker login myregistry.azurecr.io
# Tag and push to private registry
docker build -t myapp:1.0 .
docker tag myapp:1.0 myregistry.azurecr.io/myapp:1.0
docker push myregistry.azurecr.io/myapp:1.0
# Pull from private registry
docker pull myregistry.azurecr.io/myapp:1.0
Registry Authentication
# Create registry credentials file
docker login --username=user --password=pass myregistry.com
# Credentials stored in ~/.docker/config.json
# Using credentials in CI/CD
cat ~/.docker/config.json | base64 | tr -d '\n'
Network Security
Isolate Networks
version: '3.8'
services:
web:
image: myapp:1.0
networks:
- frontend
ports:
- "3000:3000"
db:
image: postgres:15
networks:
- backend
cache:
image: redis:7
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
Firewall Rules
# Limit port exposure
# Only expose necessary ports to specific IPs
docker run \
-p 127.0.0.1:3000:3000 \ # Only localhost
myapp:1.0
# Or use firewall rules
sudo ufw allow from 192.168.1.0/24 to any port 3000
Runtime Security Monitoring
Monitor Syscalls
# Monitor system calls using seccomp
docker run \
--security-opt seccomp=default.json \
myapp:1.0
# Create custom seccomp profile
cat > default.json <<'EOF'
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
}
],
"syscalls": [
{
"names": ["read", "write", "open", "close"],
"action": "SCMP_ACT_ALLOW"
}
]
}
EOF
Container Auditing
# View container logs for security events
docker logs myapp 2>&1 | grep -i error
# Monitor container processes
docker top myapp
# Inspect container
docker inspect myapp | jq '.[] | {SecurityOpt}'
Security Scanning in CI/CD
GitHub Actions Example
name: Container Security Scan
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t myapp:latest .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:latest
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Production Security Checklist
- Scan base images for vulnerabilities
- Run containers as non-root user
- Drop unnecessary capabilities
- Use read-only root filesystem
- Enable SecurityContext in Kubernetes
- Manage secrets securely (not in environment)
- Use private registries for proprietary images
- Sign and verify image signatures
- Enable audit logging
- Implement network policies
- Regular security scanning
- Keep base images updated
FAQ
Q: What's the difference between running as non-root and dropping capabilities? A: Non-root user prevents privilege escalation from container access. Dropping capabilities prevents specific privileged operations even if running as root.
Q: Should I store secrets in environment variables? A: Not for production. Use dedicated secrets management systems (Vault, AWS Secrets Manager). Environment variables are fine for development.
Q: How often should I scan images for vulnerabilities? A: Scan new images before deployment. Rescan existing images regularly (weekly/monthly) since new vulnerabilities are discovered continuously.
Advertisement