Security Hardening Guide
A walkthrough for securing your Drydock deployment — from Docker socket isolation to vulnerability scanning, image signatures, and secret management.
Drydock includes vulnerability scanning, image signature verification, SBOM generation, report export, and multiple Docker socket isolation options. This guide walks through each layer so you can build up a defense-in-depth deployment.
Before you run Drydock, verify the image or archive you downloaded. See Verify Your Drydock Install for the cosign verify and gh attestation verify commands.
Isolate the Docker socket
The Docker socket gives full control over the host's containers. Never mount it directly in production — use a socket proxy to expose only the API endpoints Drydock needs.
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
labels:
- dd.watch=true
- dd.update.mode=infrastructure
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- CONTAINERS=1
- IMAGES=1
- EVENTS=1
- INFO=1 # required for server-name detection (DD_SERVER_NAME fallback)
- POST=1 # required for update actions
- NETWORKS=1 # required for container recreation
healthcheck:
test: wget --spider http://localhost:2375/version || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 5s
drydock:
image: codeswhat/drydock
depends_on:
socket-proxy:
condition: service_healthy
environment:
- DD_WATCHER_LOCAL_HOST=socket-proxy
- DD_WATCHER_LOCAL_PORT=2375The proxy's environment variables (CONTAINERS=1, EVENTS=1, etc.) are the security boundary — they control which Docker API endpoints are forwarded. Everything else is blocked.
The socket proxy can use infrastructure self-update because its own container mounts the Docker socket directly. The helper uses that target-container bind for the replacement; Drydock itself does not need a direct socket mount for this proxy-based setup.
sockguard is a default-deny Docker socket filter from the same CodesWhat ecosystem as Drydock and portwing. Instead of an allowlist of environment flags, it ships a YAML preset built for this exact use case — see examples/compose/portwing/ for a runnable compose stack and the portwing.yaml preset it's built from.
services:
sockguard:
image: codeswhat/sockguard
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./sockguard.yaml:/etc/sockguard/config.yaml:ro
environment:
- SOCKGUARD_CONFIG_FILE=/etc/sockguard/config.yaml
healthcheck:
test: wget --spider http://localhost:2375/version || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 5s
drydock:
image: codeswhat/drydock
depends_on:
sockguard:
condition: service_healthy
environment:
- DD_WATCHER_LOCAL_HOST=sockguard
- DD_WATCHER_LOCAL_PORT=2375The preset's allow-rules are the security boundary, in the same spirit as the proxy's environment flags — everything not explicitly allowed is denied.
Other options: If a socket proxy doesn't fit your environment, Drydock also supports remote Docker over mTLS, rootless Docker, and Podman through Podman's Docker-compatible API socket. See the full comparison table and Podman Quick Start for trade-offs.
For remote Docker APIs that require basic, bearer, or OIDC auth, use HTTPS or mTLS. Drydock fails closed for remote watcher credentials on plain HTTP unless AUTH_INSECURE=true is explicitly configured for a trusted legacy network.
Require authentication
Since v1.4.0, authentication is required by default. Fresh installs require authentication configuration before access unless anonymous access is explicitly confirmed with DD_ANONYMOUS_AUTH_CONFIRM=true. Existing upgrades that were already anonymous remain accessible with a warning until authentication is configured.
Generate an Argon2id hash for your password:
printf '%s' 'your-password' | argon2 "$(openssl rand -base64 32)" -id -m 16 -t 3 -p 4 -l 64 -eThen configure:
environment:
- DD_AUTH_BASIC_ADMIN_USER=admin
- DD_AUTH_BASIC_ADMIN_HASH=$$argon2id$$v=19$$m=65536,t=3,p=4$$...Connect to any OpenID Connect provider (Authelia, Authentik, Auth0, Dex, etc.):
environment:
- DD_PUBLIC_URL=https://drydock.example.com
- DD_AUTH_OIDC_SSO_DISCOVERY=https://auth.example.com/.well-known/openid-configuration
- DD_AUTH_OIDC_SSO_CLIENTID=drydock
- DD_AUTH_OIDC_SSO_CLIENTSECRET=xxxxxBrute-force protection: Drydock enforces login lockout after repeated failed attempts. No additional configuration is needed.
Manage secrets safely
Never put secrets directly in compose files that get committed to version control. Use Docker secrets or file-based injection:
Docker secrets
services:
drydock:
environment:
- DD_AUTH_BASIC_ADMIN_HASH__FILE=/run/secrets/admin_hash
- DD_REGISTRY_HUB_PRIVATE_TOKEN__FILE=/run/secrets/hub_token
secrets:
- admin_hash
- hub_token
secrets:
admin_hash:
file: ./secrets/admin_hash.txt
hub_token:
file: ./secrets/hub_token.txtThe __FILE suffix tells Drydock to read the value from the file path instead of using the environment variable directly. This works for any DD_* environment variable.
Drydock trims trailing whitespace from file-sourced secrets and warns if a Unix secret file is readable by group or others. Keep secret files at 0600 permissions where possible:
chmod 600 secrets/*Environment file
For non-orchestrated setups, use an .env file with restrictive permissions:
chmod 600 .envservices:
drydock:
env_file: .env.env and secrets/ to your .gitignore. Never commit credentials to version control.Enable vulnerability scanning
Drydock's Update Bouncer scans candidate images for known vulnerabilities before allowing updates. Critical and high severity CVEs block updates by default.
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BLOCK_SEVERITY=CRITICAL,HIGHThe official Drydock image bundles Trivy — no additional installation needed.
Scan modes
| Mode | When it runs | What it does |
|---|---|---|
| Update Bouncer | Before any update | Blocks the update if blocking-severity CVEs are found |
| On-demand | UI button or POST /api/v1/containers/:id/scan | Scans current image (and update image if available) |
| Scheduled | Cron schedule | Background scans all watched containers |
| Dual-slot | On-demand when update is available | Compares current vs update image side-by-side |
Advisory-only mode
To scan without blocking updates, set severity to NONE:
environment:
- DD_SECURITY_BLOCK_SEVERITY=NONEScan results still appear in the UI and audit log — you just won't be blocked from updating.
Scheduled background scans
Run automatic scans on a schedule:
environment:
- DD_SECURITY_SCAN_CRON=0 3 * * * # daily at 3 AM
- DD_SECURITY_SCAN_CONCURRENCY=4 # parallel scans
- DD_SECURITY_SCAN_BATCH_TIMEOUT=1800000 # 30 min max
- DD_SECURITY_SCAN_NOTIFICATIONS=true # emit security-alert events for findingsScheduled scans only route findings through notification triggers when DD_SECURITY_SCAN_NOTIFICATIONS=true is set. This preserves the pre-v1.5.0-rc.10 default (scheduled scans update the UI but do not notify) while letting operators opt in to cron-driven notifications. On-demand scans (UI button, single-container API, bulk scan-all endpoint) always emit.
Quiet scan notifications with digest mode
On a large inventory, a single scheduled scan that surfaces 40 vulnerable containers used to fire 40 separate emails — one per container. Since v1.5.0-rc.10, each scan cycle (scheduled cron, on-demand single, or the new bulk POST /api/v1/containers/scan-all endpoint) carries a stable cycleId, and triggers can deliver one digest per cycle instead of one per container.
Configure any notification trigger with SECURITYMODE=digest to receive one summary email per scan cycle:
environment:
- DD_SECURITY_SCAN_CRON=0 3 * * *
- DD_SECURITY_SCAN_NOTIFICATIONS=true
- DD_NOTIFICATION_SMTP_GMAIL_FROM=drydock@example.com
- DD_NOTIFICATION_SMTP_GMAIL_TO=security-team@example.com
- DD_NOTIFICATION_SMTP_GMAIL_SECURITYMODE=digestThe digest fires once per scan cycle, grouped by severity (critical → high → medium → low → unknown). Cycles with zero findings never emit — once=true per-channel dedup (tracked independently of update notifications) stops a digest from re-sending for the same set of findings across cycles. The UI Scan All button now issues one server-side bulk scan that produces a single digest per click instead of N per-container HTTP calls.
The SECURITYMODE setting is independent of the update-notification MODE. A trigger can combine, for example, MODE=batch (one update email per scan cycle) with SECURITYMODE=digest (one security email per scan cycle).
High-volume environments
For environments with many containers, use a dedicated Trivy server to avoid repeated vulnerability database downloads:
services:
trivy:
image: aquasec/trivy:latest
command: server --listen 0.0.0.0:4954
drydock:
depends_on:
- trivy
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_TRIVY_SERVER=http://trivy:4954Docs: Update Bouncer reference
Verify image signatures
Require that candidate images are signed before updates proceed. Unsigned or tampered images are rejected.
Verify against Sigstore's public transparency log — no keys to manage:
environment:
- DD_SECURITY_VERIFY_SIGNATURES=true
- DD_SECURITY_COSIGN_IDENTITY=https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/tags/*
- DD_SECURITY_COSIGN_ISSUER=https://token.actions.githubusercontent.comVerify against a specific public key:
environment:
- DD_SECURITY_VERIFY_SIGNATURES=true
- DD_SECURITY_COSIGN_KEY=/keys/cosign.pub
volumes:
- ./cosign.pub:/keys/cosign.pub:roGenerate SBOMs
Software Bill of Materials (SBOM) documents list every package in a container image. Enable them for compliance, auditing, or feeding into supply chain tools:
environment:
- DD_SECURITY_SBOM_ENABLED=true
- DD_SECURITY_SBOM_FORMATS=spdx-json,cyclonedx-jsonSBOMs are generated during scanning and available via:
- UI: Open the Security detail panel → click Download SBOM
- API:
GET /api/v1/containers/:id/sbom?format=spdx-json
Both SPDX and CycloneDX formats are supported — use whichever your compliance tooling expects.
Docs: SBOM reference
Export and share vulnerability reports
Scan results can be exported as CSV or JSON for sharing with upstream image maintainers or importing into vulnerability tracking systems.
From the UI
- Open the Security detail panel for any container
- Select CSV or JSON from the export dropdown
- Click Download Report
The CSV includes columns for ID, Severity, Package, Version, Fixed In, Title, Target, and URL — suitable for opening in a spreadsheet or importing into Jira, Linear, GitHub Issues, etc.
From the API
# Single container vulnerabilities
curl -s http://drydock:3000/api/v1/containers/{id}/vulnerabilities | jq .
# All containers (paginated)
curl -s "http://drydock:3000/api/v1/containers/security/vulnerabilities?limit=100&offset=0"Workflow: sharing reports with developers
- Run an on-demand scan from the Security view
- Export the vulnerability report as CSV
- Share it with the image maintainer along with the current and fixed-in versions
- Track remediation — re-scan after the maintainer publishes a patched image
Configure your reverse proxy
If you run Drydock behind a reverse proxy (Traefik, Nginx, Caddy, etc.), configure trust-proxy so CSRF validation and session cookies work correctly:
environment:
- DD_SERVER_TRUSTPROXY=1Your proxy must forward X-Forwarded-Proto — most do this by default. Without it, Drydock sees http:// internally while your browser sends https:// origins, causing CSRF 403 errors.
Complete hardened example
Putting it all together — socket proxy, auth, scanning, signatures, SBOM, scheduled scans, and secret management:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- CONTAINERS=1
- IMAGES=1
- EVENTS=1
- INFO=1
- POST=1
- NETWORKS=1
healthcheck:
test: wget --spider http://localhost:2375/version || exit 1
interval: 5s
timeout: 3s
retries: 3
start_period: 5s
trivy:
image: aquasec/trivy:latest
command: server --listen 0.0.0.0:4954
drydock:
image: codeswhat/drydock
depends_on:
socket-proxy:
condition: service_healthy
trivy:
condition: service_started
volumes:
- /opt/drydock/store:/store
environment:
# Socket proxy
- DD_WATCHER_LOCAL_HOST=socket-proxy
- DD_WATCHER_LOCAL_PORT=2375
- DD_WATCHER_LOCAL_CRON=0 */6 * * *
# Authentication
- DD_AUTH_BASIC_ADMIN_USER=admin
- DD_AUTH_BASIC_ADMIN_HASH__FILE=/run/secrets/admin_hash
# Vulnerability scanning
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BLOCK_SEVERITY=CRITICAL,HIGH
- DD_SECURITY_TRIVY_SERVER=http://trivy:4954
- DD_SECURITY_SCAN_CRON=0 3 * * *
# Image signatures
- DD_SECURITY_VERIFY_SIGNATURES=true
- DD_SECURITY_COSIGN_IDENTITY=https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/tags/*
- DD_SECURITY_COSIGN_ISSUER=https://token.actions.githubusercontent.com
# SBOM
- DD_SECURITY_SBOM_ENABLED=true
- DD_SECURITY_SBOM_FORMATS=spdx-json,cyclonedx-json
# Reverse proxy support
- DD_SERVER_TRUSTPROXY=1
ports:
- 3000:3000
secrets:
- admin_hash
secrets:
admin_hash:
file: ./secrets/admin_hash.txtSecurity checklist
Use this checklist to verify your deployment covers each layer:
| Layer | What to check | Reference |
|---|---|---|
| Socket isolation | Using socket proxy, remote mTLS, rootless Docker, or rootless Podman — not a direct host Docker socket mount | Watchers |
| Remote watcher auth | DD_WATCHER_{name}_AUTH_* credentials stay on HTTPS/mTLS watchers; AUTH_INSECURE=true is avoided except as a temporary trusted-network exception | Remote watcher auth |
| Podman | Direct rootless Podman socket mounts are preferred first; socket proxy/TCP Podman paths depend on Podman's Docker-compatible HTTP API | Podman Quick Start |
| Authentication | Basic Auth or OIDC configured and enforced | Authentication |
| Secret management | All credentials use __FILE or Docker secrets — none in compose files | Configuration |
| Vulnerability scanning | DD_SECURITY_SCANNER=trivy set, blocking severities configured | Update Bouncer |
| Scheduled scans | DD_SECURITY_SCAN_CRON set for regular background scanning | Update Bouncer |
| Signature verification | DD_SECURITY_VERIFY_SIGNATURES=true for supply chain integrity | Update Bouncer |
| SBOM generation | DD_SECURITY_SBOM_ENABLED=true for compliance and auditing | Update Bouncer |
| Update authority | Docker triggers and socket-proxy POST/NETWORKS permissions are enabled only where Drydock should recreate containers | Docker trigger |
| Reverse proxy | DD_SERVER_TRUSTPROXY set if behind TLS-terminating proxy | Server |
| Non-root operation | Default — no DD_RUN_AS_ROOT unless break-glass override needed | FAQ |