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 scanner provider | trivy | disabled |
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) | 120000 |
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_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) | 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). Per-container label dd.security.gate=off also works but only for that container. | off | empty (gate enabled) |
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=120000Server 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_BLOCK_SEVERITY=CRITICAL,HIGH
- DD_SECURITY_TRIVY_SERVER=http://trivy:4954
- DD_SECURITY_TRIVY_TIMEOUT=120000Advisory-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=NONEBlocked 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 generates Software Bill of Materials (SBOM) documents for candidate images during the Update Bouncer flow. SBOMs are persisted in container.security.sbom and available via the API.
| 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 |
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.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.
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