Docker Security — Best Practices

Sanjeev SharmaSanjeev Sharma
5 min read

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.

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 --chown=app:app . /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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro