Docker Security Guide / Secrets Management

Docker Secrets Management

Why environment variables leak, how Docker Swarm secrets work, when to use HashiCorp Vault, and building a layered approach to secrets in production containers.

The Problem with Environment Variables

Most Docker tutorials show secrets passed as environment variables. It’s convenient, works everywhere, and feels simple. It’s also fundamentally insecure.

Environment variables are visible to any process running inside the container. They appear in docker inspect output accessible to anyone with Docker socket access. Debugging tools log them. Child processes inherit them. And in many logging frameworks, they get written to log files where they persist indefinitely.

Consider this common pattern:

docker run -e DATABASE_PASSWORD=SuperSecret123 myapp

That password is now:

  • Visible in docker inspect myapp
  • Readable by any process in the container via /proc/1/environ
  • Inherited by every subprocess spawned by the application
  • Potentially logged by the application’s error handling
  • Available to anyone with read access to the Docker socket

This is not theoretical. In production pharmaceutical environments managing patient data under HIPAA, environment variable leakage through log aggregation systems has triggered compliance violations.

Docker Swarm Secrets: The Native Solution

Docker Swarm includes built-in secret management that addresses the environment variable problem through encryption and in-memory delivery.

How Swarm Secrets Work

When you create a secret in Swarm, the secret value is encrypted and stored in Swarm’s distributed state (backed by Raft consensus). The secret is only decrypted on nodes running services that explicitly declare they need it. On those nodes, secrets are mounted as files in an in-memory tmpfs filesystem at /run/secrets/.

This means:

  • Encrypted at rest: Secrets are encrypted in Swarm’s internal database
  • Encrypted in transit: Secrets are transmitted over TLS between Swarm nodes
  • Never written to disk: Secrets exist only in memory via tmpfs
  • Scoped access: Only containers declaring the secret can read it
  • No inspect visibility: docker inspect shows secret names, not values
# Create a secret
echo "SuperSecret123" | docker secret create db_password -

# Deploy a service using the secret
docker service create \
  --name api \
  --secret db_password \
  myapp:latest

# Inside the container
cat /run/secrets/db_password
# SuperSecret123

# From the host
docker inspect api
# Shows: "SecretName": "db_password" (no value)

Production Reality

In pharmaceutical cluster environments, Swarm secrets enable compliance with data protection requirements by ensuring database credentials are never written to disk and are only accessible to explicitly authorized services.

When Swarm Secrets Are Enough

Swarm secrets work well for:

  • Single-platform Docker deployments (not mixing VMs and containers)
  • Static secrets that change infrequently (manual rotation is acceptable)
  • Environments where Vault’s operational complexity isn’t justified
  • Simple microservice architectures where each service needs 2-5 secrets

Swarm secrets are Docker-native, require no external dependencies, and work on single-node “Swarms” (you can run docker swarm init on a single host to get secret management without clustering).

HashiCorp Vault: When You Need More

Vault is an external secret manager that adds capabilities Swarm secrets don’t have: dynamic secret generation, automatic rotation, fine-grained access policies, and audit logging.

Dynamic Secrets: The Key Differentiator

The most powerful Vault feature is dynamic secrets. Instead of storing a static database password, Vault generates temporary credentials on-demand that expire automatically.

# Traditional: Static password stored in Vault
vault kv put secret/db password=SuperSecret123

# Dynamic: Vault generates temporary credentials
vault read database/creds/app-role
# Returns:
# username: v-token-app-role-8h3k2j
# password: A1Bb2Cc3Dd4Ee5Ff (auto-generated)
# lease_duration: 3600 (expires in 1 hour)

When the application requests database credentials from Vault, Vault connects to the database and creates a temporary user with the exact permissions the application needs. That user exists for a limited time (configurable, typically 1-24 hours), then Vault automatically revokes it.

This solves two problems:

  1. Credential sprawl: No static password shared across environments
  2. Blast radius: Compromised credentials expire automatically

Audit Logging for Compliance

Vault logs every secret access. This is required for SOC 2 Type II and PCI DSS compliance where auditors need proof of who accessed which secrets when.

# Vault audit log entry
{
  "time": "2026-03-30T19:45:12Z",
  "type": "response",
  "auth": {
    "token_type": "service",
    "entity_id": "api-service"
  },
  "request": {
    "path": "database/creds/app-role"
  },
  "response": {
    "secret": true
  }
}

Every access is logged with timestamps, requesting identity, and the secret path. This log is write-only (even Vault admins can’t modify it) and can be exported to SIEM systems.

When Vault Is Justified

Use Vault when:

  • You need dynamic database credentials (most important use case)
  • Compliance requires audit trails (SOC 2, PCI DSS, HIPAA)
  • You’re managing secrets across multiple platforms (Docker + VMs + Kubernetes)
  • Automated secret rotation is required
  • You have dedicated operations staff to maintain Vault infrastructure

Vault’s operational complexity is real. It requires:

  • High-availability deployment (3+ nodes)
  • Secure initialization and unsealing procedures
  • TLS certificate management
  • Backup and disaster recovery planning
  • Access policy maintenance

For a 5-person startup, this overhead usually isn’t justified. For Fortune 500 pharmaceutical operations managing hundreds of microservices accessing regulated data stores, it’s mandatory infrastructure.

BuildKit Secret Mounts: Build-Time Security

Build-time secrets are different. You need credentials during docker build to access private npm registries, clone private git repos, or download proprietary dependencies. These secrets should never persist in the final image.

BuildKit secret mounts solve this:

# Dockerfile
FROM node:18

WORKDIR /app
COPY package.json ./

# Secret mounted only during this RUN instruction
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
  npm install

# Secret is gone after this layer completes
COPY . .
CMD ["node", "server.js"]

Build the image with the secret:

docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

The .npmrc file is available inside the container during npm install, but it’s not written to any image layer. It’s not in the final image. It’s not in docker history. It existed only for the duration of that one RUN instruction.

Why BuildKit Secrets Matter

Before BuildKit secrets, developers used ARG or multi-stage builds with complex cleanup scripts. Both leaked secrets into intermediate layers visible in docker history. BuildKit secrets are ephemeral by design — they can’t leak because they never persist.

Common Build-Time Secret Patterns

Private npm/pip registries:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
  npm install

SSH keys for private git repos:

RUN --mount=type=secret,id=ssh_key,target=/tmp/key \
  cp /tmp/key /root/.ssh/id_rsa && \
  chmod 600 /root/.ssh/id_rsa && \
  git clone git@github.com:company/private-repo.git && \
  rm /root/.ssh/id_rsa

API tokens for downloading artifacts:

RUN --mount=type=secret,id=api_token \
  TOKEN=$(cat /run/secrets/api_token) && \
  curl -H "Authorization: Bearer $TOKEN" \
  https://api.company.com/artifact.tar.gz -o /tmp/artifact.tar.gz

Secret Scanning: Prevention Layer

Despite proper secret management, developers still accidentally commit secrets. GitLeaks and similar tools scan repositories for patterns matching credentials.

# Scan current repository
docker run -v $(pwd):/path zricethezav/gitleaks:latest \
  detect --source /path --verbose

# Found secrets
Finding: aws_access_key_id = AKIAIOSFODNN7EXAMPLE
File: config/production.yml
Line: 12

Finding: github_token = ghp_1234567890abcdefghijklmnopqrstuv
File: scripts/deploy.sh
Line: 8

GitLeaks detects:

  • AWS keys (AKIA…)
  • GitHub tokens (ghp_…)
  • Stripe keys (sk_live_…)
  • Private keys (—–BEGIN PRIVATE KEY—–)
  • Database connection strings
  • High-entropy strings (potential secrets)

Prevention via Pre-Commit Hooks

The most effective scanning happens before commit:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

# Install the hook
pre-commit install

# Now every git commit runs GitLeaks first
git commit -m "Add config"
# GitLeaks scan...
# ERROR: Secret detected in config.yml
# Commit blocked

Pre-commit hooks prevent secrets from entering git history. CI/CD scanning catches what pre-commit missed. Together, they create defense in depth.

Secrets in Git Are Permanent

Even after deleting a file containing secrets, those secrets remain in git history indefinitely. The only remediation is to rotate the secret (assume it’s compromised) and optionally rewrite history with git filter-branch or BFG Repo-Cleaner.

Layered Approach for Production

Production environments don’t choose one solution. They layer multiple approaches:

Secret Type Solution Why
Build-time (npm, SSH) BuildKit Mounts Ephemeral, can’t leak into image
Simple service secrets Docker Swarm Secrets Native, encrypted, no external deps
Database credentials Vault Dynamic Secrets Auto-expiring, audit trail
Compliance-regulated Vault + Audit Logs SOC 2, PCI DSS requirements
Detection GitLeaks + Pre-commit Prevent accidents

Example architecture for a pharmaceutical application:

  • CI/CD pipeline: BuildKit mounts for private npm registry access
  • API service: Swarm secret for JWT signing key (static, rotated quarterly)
  • Database access: Vault dynamic credentials (expire every 4 hours, audit logged)
  • Pre-commit hooks: GitLeaks scanning on every developer commit
  • CI/CD gates: Automated GitLeaks scan on every pull request

Hands-On: Lab 10 – Docker Secrets Management

Practice these concepts with 5 interactive scenarios covering anti-patterns, Swarm secrets, Vault integration, BuildKit mounts, and secret scanning with GitLeaks. All executable on Docker Desktop.

View Lab 10 on GitHub

Key Takeaways

Environment variables are not secrets. They’re visible to any process, appear in docker inspect, and get logged. Use them for configuration, not credentials.

Swarm secrets are underutilized. Most teams don’t realize Docker has native secret management that works on single nodes. No Vault complexity required for simple use cases.

Vault’s value is dynamic secrets. Static secret storage is a nice feature. Dynamic database credentials that auto-expire are transformative for security posture.

BuildKit secrets prevent build leakage. Before BuildKit, build-time secrets inevitably leaked into image layers. BuildKit mounts are ephemeral by design.

Secrets in git are forever. File deletion doesn’t remove secrets from history. Rotate immediately if detected. Pre-commit hooks prevent the problem.

Layer your approach. Production systems use BuildKit for builds, Swarm for simple secrets, Vault for dynamic credentials, and GitLeaks for prevention. Each solves a different problem.

Scroll to Top