Skip to main content

Common Misconfiguration

Embedding passwords, API keys, and other sensitive data directly in docker-compose.yml files exposes secrets in version control and makes rotation difficult.

Vulnerable Example

# Vulnerable docker-compose.yml
version: '3.8'
services:
  app:
    image: myapp:1.0
    environment:
      DATABASE_URL: postgresql://admin:SuperSecret123!@db:5432/myapp  # Hardcoded credentials
      API_KEY: sk-proj-abc123def456ghi789  # Exposed API key
      JWT_SECRET: my-super-secret-jwt-key-123  # Hardcoded secret
      AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE  # AWS credentials
      AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    ports:
      - "3000:3000"

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: admin  # Hardcoded username
      POSTGRES_PASSWORD: SuperSecret123!  # Hardcoded password
      POSTGRES_DB: myapp

Secure Example

# Secure docker-compose.yml
version: '3.8'
services:
  app:
    image: myapp:1.0
    environment:
      DATABASE_URL_FILE: /run/secrets/database_url
      JWT_SECRET_FILE: /run/secrets/jwt_secret
    secrets:
      - database_url
      - api_key
      - jwt_secret
      - aws_credentials
    ports:
      - "3000:3000"
    configs:
      - source: app_config
        target: /app/config.yml
        mode: 0440

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER_FILE: /run/secrets/db_user
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp
    secrets:
      - db_user
      - db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

secrets:
  database_url:
    file: ./secrets/database_url.txt
  api_key:
    file: ./secrets/api_key.txt
  jwt_secret:
    file: ./secrets/jwt_secret.txt
  aws_credentials:
    file: ./secrets/aws_credentials.json
  db_user:
    file: ./secrets/db_user.txt
  db_password:
    file: ./secrets/db_password.txt

configs:
  app_config:
    file: ./configs/app_config.yml

volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /secure/data/postgres

Using External Secret Management

# Using Docker Swarm secrets
version: '3.8'
services:
  app:
    image: myapp:1.0
    secrets:
      - source: db_password
        target: database_password
        mode: 0400
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure

secrets:
  db_password:
    external: true  # Created with: docker secret create db_password -

Environment File Approach

# .env.example (committed to repo)
DATABASE_HOST=db
DATABASE_PORT=5432
DATABASE_NAME=myapp
APP_PORT=3000

# .env.secret (NOT committed, in .gitignore)
DATABASE_USER=admin
DATABASE_PASSWORD=use_a_strong_password_here
JWT_SECRET=generate_a_random_secret
API_KEY=your_actual_api_key
# docker-compose.yml using env files
version: '3.8'
services:
  app:
    image: myapp:1.0
    env_file:
      - .env.example
      - .env.secret  # Contains sensitive values
    ports:
      - "${APP_PORT}:${APP_PORT}"

HashiCorp Vault Integration

# Using Vault Agent for secret injection
version: '3.8'
services:
  vault-agent:
    image: vault:1.15
    command: ["vault", "agent", "-config=/vault/config/agent.hcl"]
    volumes:
      - ./vault-config:/vault/config:ro
      - shared-secrets:/vault/secrets
    environment:
      VAULT_ADDR: https://vault.example.com:8200

  app:
    image: myapp:1.0
    depends_on:
      - vault-agent
    volumes:
      - shared-secrets:/secrets:ro
    command: ["/bin/sh", "-c", "source /secrets/env && exec node app.js"]

volumes:
  shared-secrets:
    driver: local
    driver_opts:
      type: tmpfs
      device: tmpfs