Update Bouncer
Run security scanning, signature verification, and SBOM generation before applying container updates.
Update Bouncer runs security scanning in a safe-pull flow:
- Candidate image is scanned before update
- Update is blocked when CVEs match configured blocking severities
- Scan result is stored in
container.security.scanand exposed in API/UI
Enablement
Security scanning is disabled by default. To enable it, set:
DD_SECURITY_SCANNER=trivyVariables
| Env var | Required | Description | Supported values | Default value when missing |
|---|---|---|---|---|
DD_SECURITY_SCANNER | 🔴 | Enable one scanner or run both and merge duplicate findings | trivy, grype, both | disabled |
DD_SECURITY_BACKEND | ⚪ | Scanner execution backend | command, docker, remote | command |
DD_SECURITY_AVAILABILITY_POLICY | ⚪ | Behavior when a scan cannot complete. block is fail-closed; warn records an audit warning and permits the update. Detected blocking findings always block. | block, warn | block |
DD_SECURITY_DOCKER_SOCKET | ⚪ | Docker/Podman socket used to create scanner workers when no host is set | absolute socket path | /var/run/docker.sock |
DD_SECURITY_DOCKER_HOST | ⚪ | Docker API/socket-proxy host for scanner workers | hostname/IP | empty |
DD_SECURITY_DOCKER_PORT | ⚪ | Docker API/socket-proxy port | 1..65535 | 2375 |
DD_SECURITY_DOCKER_PROTOCOL | ⚪ | Docker API protocol | http, https | http |
DD_SECURITY_DOCKER_NETWORK | ⚪ | Docker network joined by workers. Set this to the Compose network when a remote worker must resolve a Trivy service name. | Docker network name | bridge |
DD_SECURITY_DOCKER_CACHE_VOLUME | ⚪ | Named-volume prefix for daemon-side provider caches | safe Docker volume prefix | drydock-scanner-cache |
DD_SECURITY_BLOCK_SEVERITY | ⚪ | Blocking severities (comma-separated). Set to NONE for advisory-only mode (scan without blocking updates) | NONE, or any of UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL | CRITICAL,HIGH |
DD_SECURITY_TRIVY_SERVER | ⚪ | Trivy server URL (enables client/server mode) | URL | empty (local CLI mode) |
DD_SECURITY_TRIVY_COMMAND | ⚪ | Trivy command path for local CLI mode (must not contain shell metacharacters or .. path traversal) | executable path | trivy |
DD_SECURITY_TRIVY_TIMEOUT | ⚪ | Trivy command timeout in milliseconds | integer (>=1000) | 600000 |
DD_SECURITY_TRIVY_ARGS | ⚪ | Additional Trivy arguments as a JSON string array. Output, timeout, server, scanner, and severity flags cannot be overridden. | JSON string array | [] |
DD_SECURITY_TRIVY_IMAGE_SRC | ⚪ | Override Trivy's image source order. Any value Trivy accepts (e.g. remote, docker, or comma-separated remote,docker). When unset, Trivy uses its default source order (docker, containerd, podman, remote) and falls back to a registry pull when the local daemon is unreachable. | string | empty (Trivy auto-detects) |
DD_SECURITY_TRIVY_WORKER_IMAGE | ⚪ | Digest-pinned Trivy worker used by docker/remote backends | OCI image pinned with @sha256: | release-pinned Trivy image |
DD_SECURITY_GRYPE_COMMAND | ⚪ | Grype command for command backend | executable path | grype |
DD_SECURITY_GRYPE_TIMEOUT | ⚪ | Grype scan timeout in milliseconds | integer (>=1000) | 600000 |
DD_SECURITY_GRYPE_ARGS | ⚪ | Additional Grype arguments as a JSON string array. JSON/file output flags cannot be overridden. | JSON string array | [] |
DD_SECURITY_GRYPE_WORKER_IMAGE | ⚪ | Digest-pinned Grype worker used by Docker backend | OCI image pinned with @sha256: | release-pinned Grype image |
DD_SECURITY_SCAN_CRON | ⚪ | Cron expression for scheduled background security scans | cron expression | disabled |
DD_SECURITY_SCAN_JITTER | ⚪ | Random jitter delay (ms) before each scheduled scan starts | integer (>=0) | 60000 |
DD_SECURITY_SCAN_CONCURRENCY | ⚪ | Max digest scans processed in parallel during scheduled scans | integer (>=1) | 4 |
DD_SECURITY_SCAN_BATCH_TIMEOUT | ⚪ | Total scheduled scan batch timeout in milliseconds (0 disables timeout) | integer (>=0) | 1800000 |
DD_SECURITY_SCAN_DIGEST_CACHE_MAX_ENTRIES | ⚪ | Max in-memory digest scan cache entries (LRU eviction -- least recently used entries are removed when the limit is reached) | integer (>0) | DD_DEFAULT_CACHE_MAX_ENTRIES (500) |
DD_SECURITY_SCAN_NOTIFICATIONS | ⚪ | When true, findings from scheduled (cron) scans are routed through notification triggers. On-demand scans always notify. | true / false | false |
DD_SECURITY_GATE_MODE | ⚪ | Override the security gate globally. off disables the gate for all containers (audit row still fires); on enables normal gating. Per-container label dd.security.gate=off also works but only for that container. | on, off | on |
DD_SECURITY_GATE_RELATIVE | ⚪ | When an absolute severity threshold would block a candidate, allow it only if every severity count is less than or equal to the current image's saved scan. Missing, failed, or stale current-image scans remain blocked. | true / false | false |
DD_SECURITY_PRUNE_ONBLOCK | ⚪ | Prune a pulled candidate image when Update Bouncer blocks it | true / false | true |
Trivy modes
Client mode (local CLI)
Use this mode when the trivy binary is available inside the drydock runtime.
trivy and cosign. No custom image is needed for local CLI mode.docker, containerd, podman, remote) and falls back to a registry pull when no local daemon is available. You do not need to bind-mount /var/run/docker.sock for scanning. If you know your setup is socket-less and want to skip the probe steps, set DD_SECURITY_TRIVY_IMAGE_SRC=remote.services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BLOCK_SEVERITY=CRITICAL,HIGH
- DD_SECURITY_TRIVY_COMMAND=trivy
- DD_SECURITY_TRIVY_TIMEOUT=600000The 10-minute scan budget accommodates large-image analysis. In local mode, drydock separately performs a best-effort vulnerability-database warm-up before the first scan, so a successful database download does not consume this scan-command budget. If scans still regularly approach the limit, use DD_SECURITY_TRIVY_SERVER so a dedicated Trivy server owns and caches the vulnerability database.
Server mode (Trivy server)
Use this mode when running a separate Trivy server and letting drydock call it.
services:
trivy:
image: aquasec/trivy:latest
command: server --listen 0.0.0.0:4954
drydock:
image: codeswhat/drydock:latest
depends_on:
- trivy
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BACKEND=remote
- DD_SECURITY_BLOCK_SEVERITY=CRITICAL,HIGH
- DD_SECURITY_TRIVY_SERVER=http://trivy:4954
- DD_SECURITY_TRIVY_TIMEOUT=600000remote is a Trivy-only backend. Drydock still needs a Trivy client to prepare and submit image data, so it runs that client in the pinned ephemeral Docker worker instead of pretending the server is a direct image-scan HTTP API. The worker requires access to the local Docker API, while the Trivy server owns the vulnerability database.
Docker worker backend and Grype
Set DD_SECURITY_BACKEND=docker to keep scanner binaries out of the Drydock process image. Each scan runs in an ephemeral, digest-pinned worker with a read-only root filesystem, all Linux capabilities dropped, bounded memory/PIDs/output, no Docker socket inside the worker, and a provider-only cache mount.
services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=both
- DD_SECURITY_BACKEND=docker
- DD_SECURITY_AVAILABILITY_POLICY=block
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- drydock-store:/storeThe Security toolbar shows each configured provider's worker digest/version and offers touch-friendly Pull and Warm actions. The equivalent API is POST /api/v1/server/security/assets/{provider}/{operation}, with provider=trivy|grype|syft and operation=pull|warm. Pull/warm start, success, and failure are written to the audit log.
Advisory-Only Mode
Set DD_SECURITY_BLOCK_SEVERITY=NONE to run vulnerability scans without blocking updates. Scan results remain visible in the Security view and audit log, but no update is ever rejected due to detected vulnerabilities.
services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BLOCK_SEVERITY=NONENo-worse-than-current gate
Set DD_SECURITY_GATE_RELATIVE=true to keep the absolute severity threshold while allowing a candidate that does not worsen the running image's vulnerability profile. Drydock compares CRITICAL, HIGH, MEDIUM, LOW, and UNKNOWN independently; every candidate count must be less than or equal to the corresponding current-image count.
services:
drydock:
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BLOCK_SEVERITY=CRITICAL,HIGH
- DD_SECURITY_GATE_RELATIVE=trueThe comparison is fail-closed. A relative pass requires a successful saved scan for the exact running image. If that scan is missing, failed, belongs to an older image, or any severity count increased, the normal absolute block remains in force. The candidate scan is persisted with relativeGate.decision, relativeGate.reason, and the current summary used for a successful comparison, so API clients and audit logs can explain why the update passed.
Blocked candidate cleanup
When Update Bouncer blocks an update after pulling the candidate image, drydock prunes that candidate image by default (DD_SECURITY_PRUNE_ONBLOCK=true). This cleanup is best-effort: if Docker refuses the prune, the update still fails with the original security-block reason and the prune failure is logged as a warning for operators to review.
Signature Verification (cosign)
When enabled, candidate images are verified with cosign before the update proceeds. Updates are blocked if signatures are missing, invalid, or verification fails.
| Env var | Required | Description | Supported values | Default value when missing |
|---|---|---|---|---|
DD_SECURITY_VERIFY_SIGNATURES | ⚪ | Enable signature verification gate | true / false | false |
DD_SECURITY_COSIGN_KEY | ⚪ | Path to cosign public key file | file path | empty (keyless / Sigstore) |
DD_SECURITY_COSIGN_COMMAND | ⚪ | Cosign command path | executable path | cosign |
DD_SECURITY_COSIGN_TIMEOUT | ⚪ | Cosign command timeout in milliseconds | integer (>=1000) | 60000 |
DD_SECURITY_COSIGN_IDENTITY | ⚪ | Certificate identity for keyless verification | string | empty |
DD_SECURITY_COSIGN_ISSUER | ⚪ | OIDC issuer for keyless verification | string | empty |
DD_SECURITY_COSIGN_KEY is empty, cosign runs in keyless mode using Sigstore's public transparency log. Set DD_SECURITY_COSIGN_IDENTITY and DD_SECURITY_COSIGN_ISSUER to constrain keyless verification to a specific signer.Key-based verification
services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_VERIFY_SIGNATURES=true
- DD_SECURITY_COSIGN_KEY=/keys/cosign.pub
volumes:
- ./cosign.pub:/keys/cosign.pub:ro
- /var/run/docker.sock:/var/run/docker.sockKeyless verification (Sigstore)
services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_VERIFY_SIGNATURES=true
- DD_SECURITY_COSIGN_IDENTITY=https://github.com/CodesWhat/drydock/.github/workflows/release-cut.yml@refs/heads/main
- DD_SECURITY_COSIGN_ISSUER=https://token.actions.githubusercontent.com
volumes:
- /var/run/docker.sock:/var/run/docker.sockSBOM Generation
When enabled, Trivy or Syft generates Software Bill of Materials (SBOM) documents for candidate images during the Update Bouncer flow. auto uses Trivy when available and Syft for Grype-only scanning.
| Env var | Required | Description | Supported values | Default value when missing |
|---|---|---|---|---|
DD_SECURITY_SBOM_ENABLED | ⚪ | Enable SBOM generation | true / false | false |
DD_SECURITY_SBOM_FORMATS | ⚪ | Comma-separated list of SBOM formats | spdx-json, cyclonedx-json | spdx-json |
DD_SECURITY_SBOM_GENERATOR | ⚪ | Provider-neutral SBOM generator | auto, trivy, syft | auto |
DD_SECURITY_SYFT_COMMAND | ⚪ | Syft command for command backend | executable path | syft |
DD_SECURITY_SYFT_TIMEOUT | ⚪ | Syft generation timeout in milliseconds | integer (>=1000) | 600000 |
DD_SECURITY_SYFT_ARGS | ⚪ | Additional Syft arguments as a JSON string array. Output/file flags cannot be overridden. | JSON string array | [] |
DD_SECURITY_SYFT_WORKER_IMAGE | ⚪ | Digest-pinned Syft worker for Docker backend | OCI image pinned with @sha256: | release-pinned Syft image |
services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_SBOM_ENABLED=true
- DD_SECURITY_SBOM_FORMATS=spdx-json,cyclonedx-json
volumes:
- /var/run/docker.sock:/var/run/docker.sockGET /api/v1/containers/:id/sbom?format={format} where format is one of spdx-json or cyclonedx-json.SBOM bodies are stored off-heap at /store/sbom/<digest-key>/<document-sha>/<format>.json; LokiJS container rows retain only validated checksum/size references. Identical canonical documents for the same image digest share a blob, while divergent regenerations remain independently readable. On startup, Drydock atomically migrates legacy inline current/update SBOM records and preserves the original row for retry if any write fails.
Scheduled scanning
Set DD_SECURITY_SCAN_CRON to automatically scan all watched containers on a schedule. A random jitter delay (DD_SECURITY_SCAN_JITTER, default 60 seconds) is applied before each cycle to spread load. You can tune parallelism with DD_SECURITY_SCAN_CONCURRENCY (default 4) and cap total batch runtime with DD_SECURITY_SCAN_BATCH_TIMEOUT (default 1800000, set 0 to disable).
By default, scheduled scans update the Security view and audit log but do not fire notification triggers. Set DD_SECURITY_SCAN_NOTIFICATIONS=true to opt in to cron-driven alerts. On-demand scans (UI, single-container API, or the bulk POST /api/v1/containers/scan-all endpoint) always emit notifications.
Digest-level scan dedup caching uses an interval derived from the cron schedule:
- drydock parses
DD_SECURITY_SCAN_CRONand uses the shortest upcoming gap between scheduled runs as cache TTL. - For irregular schedules (for example
0 3,9 * * *), cache TTL is based on the shortest gap (6hin this case), not a fixed daily fallback. - If interval derivation fails, drydock falls back to
24h.
Transient Trivy failures (daemon timeout, registry outage) no longer overwrite previously-stored scan results. The existing result is preserved for up to 7 days; error retries are floored at 15 minutes per digest regardless of cron frequency.
services:
drydock:
image: codeswhat/drydock:latest
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_SCAN_CRON=0 3 * * * # every day at 3 AM
- DD_SECURITY_SCAN_JITTER=60000 # up to 60s random delay
- DD_SECURITY_SCAN_CONCURRENCY=4 # scan up to 4 digests in parallel
- DD_SECURITY_SCAN_BATCH_TIMEOUT=1800000 # abort remaining digest queue after 30m
- DD_SECURITY_SCAN_NOTIFICATIONS=true # opt in to cron-driven notificationsOn-demand scanning
You can trigger a security scan without waiting for the next scheduled cycle.
- Single container — UI: Click the shield button on a container row.
- Single container — API:
POST /api/v1/containers/:id/scanruns vulnerability scanning, signature verification (if configured), and SBOM generation (if configured). Rate-limited to 30 requests per minute. - All containers — UI: Click Scan All in the Security view toolbar. Issues one server-side bulk scan — produces a single scan cycle rather than N per-container requests.
- All containers — API:
POST /api/v1/containers/scan-allscans all watched containers in one bulk operation. Rate-limited to 1 request per 60 seconds per IP (admin bypass available). Streams progress over SSE and respects client-disconnect aborts.
Real-time progress is broadcast via SSE events dd:scan-started and dd:scan-completed. Each scan cycle (cron, on-demand single, or bulk) carries a stable cycleId (UUID v7). When the cycle finishes, a dd:security-scan-cycle-complete event is emitted with { cycleId, scannedCount, alertCount, startedAt, completedAt }.
The cycleId is used by digest-mode notification triggers to group all findings from one scan cycle into a single summary notification instead of one per container.
Security view update button
When a newer image is available for a container shown in the Security view, an inline Update button appears directly next to the vulnerability data. This lets you apply the update without leaving the security context.
The update button respects eligibility blockers. If a security-scan-blocked blocker is active (either the running image scan or the candidate image scan returned blocked-severity vulnerabilities), the button is locked. Use the force-update path to override.
The Security view uses the same release-notes and source-project controls as the Containers view. When Drydock knows the current image's release notes, they are shown even if no update is pending; when source metadata is available, the View project link points at the resolved source repository.
security-scan-blocked eligibility blocker
The security-scan-blocked hard blocker is raised when:
- The candidate update image scan (
container.security.updateScan) has statusblocked, or - The current running image scan (
container.security.scan) has statusblocked
Either condition prevents a manual update and causes POST /api/v1/containers/:id/update to return 409. Use the force-update path if you need to proceed anyway.
Dual-slot scanning
When a container has an available update, clicking Scan Now (or calling POST /api/v1/containers/:id/scan) scans both the current running image and the available update image in a single operation.
- Results for the current image are stored in
container.security.scan - Results for the update image are stored in
container.security.updateScan - The UI shows delta comparison badges indicating how vulnerabilities change between versions (e.g. +3 fixed, -1 new)
This differs from the pre-update Update Bouncer flow, which only scans the candidate image as a gate before applying an update. Dual-slot scanning is informational and does not block updates.
Vulnerability report export
The Security detail panel includes an Export section for downloading vulnerability scan results in CSV or JSON format — useful for sharing with upstream image maintainers or importing into issue trackers.
UI export
- Open the Security detail panel for a container (click any container row in the Security view)
- Select a format from the Export vulnerabilities dropdown: CSV or JSON
- Click Download Report
CSV format
The CSV includes one row per vulnerability with these columns:
| Column | Description |
|---|---|
| ID | CVE or advisory identifier (e.g. CVE-2024-1234) |
| Severity | CRITICAL, HIGH, MEDIUM, LOW, or UNKNOWN |
| Package | Affected package name |
| Version | Installed version |
| Fixed In | Version that resolves the vulnerability (empty if no fix available) |
| Title | Short vulnerability description |
| Target | Scan target (e.g. OS package db, language lockfile) |
| URL | Link to the advisory |
=, +, -, or @ are automatically escaped so the file is safe to open in spreadsheet applications.JSON format
The JSON export is an array of objects with the same fields as the CSV (id, severity, package, version, fixedIn, title, target, url).
API
Vulnerability data is also available programmatically:
- Single container:
GET /api/v1/containers/:id/vulnerabilities - All containers (paginated):
GET /api/v1/containers/security/vulnerabilities?limit=100&offset=0
SBOM download
SBOM documents can be downloaded from the Security detail panel or via the API:
- UI: Open the Security detail panel → click Download SBOM (works regardless of whether the SBOM viewer toggle is active)
- API:
GET /api/v1/containers/:id/sbom?format={format}whereformatisspdx-jsonorcyclonedx-json
GET /api/v1/containers/:id/sbom returns 503 Service Unavailable with the message "Security scanner is not configured". This distinguishes "feature intentionally off" from a real server error.Security digest notifications
On large inventories, a single scheduled scan can surface many vulnerable containers. By default each one would send a separate notification. Use SECURITYMODE=digest on a notification trigger to receive one summary per scan cycle instead of one per container.
environment:
- DD_SECURITY_SCAN_CRON=0 3 * * *
- DD_SECURITY_SCAN_NOTIFICATIONS=true
- DD_NOTIFICATION_SMTP_ALERTS_FROM=drydock@example.com
- DD_NOTIFICATION_SMTP_ALERTS_TO=security@example.com
- DD_NOTIFICATION_SMTP_ALERTS_SECURITYMODE=digest
# Optional: customize digest subject/body
# - DD_NOTIFICATION_SMTP_ALERTS_SECURITYDIGESTTITLE=Security scan complete: ${scan.alertCount} ${scan.containerNoun} with findings
# - DD_NOTIFICATION_SMTP_ALERTS_SECURITYDIGESTBODY=Found ${scan.alertCount} of ${scan.scannedCount} containers with alertsThe digest fires once per cycleId, grouped by severity. Cycles with zero findings never emit. The SECURITYMODE setting is independent of the update-notification MODE — a trigger can combine MODE=batch (update emails) with SECURITYMODE=digest (security emails) simultaneously.
Digest templates use the same safe ${…} interpolation as every other trigger template — they are not evaluated as JavaScript, so arbitrary expressions (arrow functions, array methods, arithmetic) resolve to an empty string rather than running code. The following scan.* variables are available:
| Variable | Description |
|---|---|
scan.alertCount | Containers with at least one finding |
scan.scannedCount | Containers scanned in the cycle |
scan.containerNoun | container when alertCount is 1, otherwise containers |
scan.criticalCount / scan.highCount / scan.mediumCount / scan.lowCount / scan.unknownCount | Container counts whose top severity is that level |
scan.criticalList / scan.highList | Pre-formatted, newline-joined - name: … lists for the critical and high tiers |
scan.startedAt / scan.completedAt | Cycle start/finish ISO timestamps |
scan.cycleId | The cycle's UUID v7 |
Full example (scanning + signatures + SBOM)
services:
trivy:
image: aquasec/trivy:latest
command: server --listen 0.0.0.0:4954
drydock:
image: codeswhat/drydock:latest
depends_on:
- trivy
environment:
- DD_SECURITY_SCANNER=trivy
- DD_SECURITY_BLOCK_SEVERITY=CRITICAL,HIGH
- DD_SECURITY_TRIVY_SERVER=http://trivy:4954
- DD_SECURITY_VERIFY_SIGNATURES=true
- DD_SECURITY_COSIGN_IDENTITY=https://github.com/CodesWhat/drydock/.github/workflows/release-cut.yml@refs/heads/main
- DD_SECURITY_COSIGN_ISSUER=https://token.actions.githubusercontent.com
- DD_SECURITY_SBOM_ENABLED=true
- DD_SECURITY_SBOM_FORMATS=spdx-json,cyclonedx-json
volumes:
- /var/run/docker.sock:/var/run/docker.sock