Skip to content
ConfigurationUpdate Bouncer

Update Bouncer

Run security scanning, signature verification, and SBOM generation before applying container updates.

Update Bouncer runs security scanning in a safe-pull flow:

  1. Candidate image is scanned before update
  2. Update is blocked when CVEs match configured blocking severities
  3. Scan result is stored in container.security.scan and exposed in API/UI

Enablement

Security scanning is disabled by default. To enable it, set:

DD_SECURITY_SCANNER=trivy

Variables

Env varRequiredDescriptionSupported valuesDefault value when missing
DD_SECURITY_SCANNER🔴Enable one scanner or run both and merge duplicate findingstrivy, grype, bothdisabled
DD_SECURITY_BACKENDScanner execution backendcommand, docker, remotecommand
DD_SECURITY_AVAILABILITY_POLICYBehavior when a scan cannot complete. block is fail-closed; warn records an audit warning and permits the update. Detected blocking findings always block.block, warnblock
DD_SECURITY_DOCKER_SOCKETDocker/Podman socket used to create scanner workers when no host is setabsolute socket path/var/run/docker.sock
DD_SECURITY_DOCKER_HOSTDocker API/socket-proxy host for scanner workershostname/IPempty
DD_SECURITY_DOCKER_PORTDocker API/socket-proxy port1..655352375
DD_SECURITY_DOCKER_PROTOCOLDocker API protocolhttp, httpshttp
DD_SECURITY_DOCKER_NETWORKDocker network joined by workers. Set this to the Compose network when a remote worker must resolve a Trivy service name.Docker network namebridge
DD_SECURITY_DOCKER_CACHE_VOLUMENamed-volume prefix for daemon-side provider cachessafe Docker volume prefixdrydock-scanner-cache
DD_SECURITY_BLOCK_SEVERITYBlocking severities (comma-separated). Set to NONE for advisory-only mode (scan without blocking updates)NONE, or any of UNKNOWN,LOW,MEDIUM,HIGH,CRITICALCRITICAL,HIGH
DD_SECURITY_TRIVY_SERVERTrivy server URL (enables client/server mode)URLempty (local CLI mode)
DD_SECURITY_TRIVY_COMMANDTrivy command path for local CLI mode (must not contain shell metacharacters or .. path traversal)executable pathtrivy
DD_SECURITY_TRIVY_TIMEOUTTrivy command timeout in millisecondsinteger (>=1000)600000
DD_SECURITY_TRIVY_ARGSAdditional Trivy arguments as a JSON string array. Output, timeout, server, scanner, and severity flags cannot be overridden.JSON string array[]
DD_SECURITY_TRIVY_IMAGE_SRCOverride 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.stringempty (Trivy auto-detects)
DD_SECURITY_TRIVY_WORKER_IMAGEDigest-pinned Trivy worker used by docker/remote backendsOCI image pinned with @sha256:release-pinned Trivy image
DD_SECURITY_GRYPE_COMMANDGrype command for command backendexecutable pathgrype
DD_SECURITY_GRYPE_TIMEOUTGrype scan timeout in millisecondsinteger (>=1000)600000
DD_SECURITY_GRYPE_ARGSAdditional Grype arguments as a JSON string array. JSON/file output flags cannot be overridden.JSON string array[]
DD_SECURITY_GRYPE_WORKER_IMAGEDigest-pinned Grype worker used by Docker backendOCI image pinned with @sha256:release-pinned Grype image
DD_SECURITY_SCAN_CRONCron expression for scheduled background security scanscron expressiondisabled
DD_SECURITY_SCAN_JITTERRandom jitter delay (ms) before each scheduled scan startsinteger (>=0)60000
DD_SECURITY_SCAN_CONCURRENCYMax digest scans processed in parallel during scheduled scansinteger (>=1)4
DD_SECURITY_SCAN_BATCH_TIMEOUTTotal scheduled scan batch timeout in milliseconds (0 disables timeout)integer (>=0)1800000
DD_SECURITY_SCAN_DIGEST_CACHE_MAX_ENTRIESMax 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_NOTIFICATIONSWhen true, findings from scheduled (cron) scans are routed through notification triggers. On-demand scans always notify.true / falsefalse
DD_SECURITY_GATE_MODEOverride 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, offon
DD_SECURITY_GATE_RELATIVEWhen 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 / falsefalse
DD_SECURITY_PRUNE_ONBLOCKPrune a pulled candidate image when Update Bouncer blocks ittrue / falsetrue

Trivy modes

Client mode (local CLI)

Use this mode when the trivy binary is available inside the drydock runtime.

As of v1.3.0, the official drydock image includes both trivy and cosign. No custom image is needed for local CLI mode.
No Docker socket required. Trivy uses its default source order (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=600000

The 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=600000

remote 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:/store

The 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=NONE

No-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=true

The 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 varRequiredDescriptionSupported valuesDefault value when missing
DD_SECURITY_VERIFY_SIGNATURESEnable signature verification gatetrue / falsefalse
DD_SECURITY_COSIGN_KEYPath to cosign public key filefile pathempty (keyless / Sigstore)
DD_SECURITY_COSIGN_COMMANDCosign command pathexecutable pathcosign
DD_SECURITY_COSIGN_TIMEOUTCosign command timeout in millisecondsinteger (>=1000)60000
DD_SECURITY_COSIGN_IDENTITYCertificate identity for keyless verificationstringempty
DD_SECURITY_COSIGN_ISSUEROIDC issuer for keyless verificationstringempty
When 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.
The examples below use a direct Docker socket mount for brevity. For production deployments, use a socket proxy instead — all security features work identically with proxy-based connections.

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.sock

Keyless 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.sock

SBOM 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 varRequiredDescriptionSupported valuesDefault value when missing
DD_SECURITY_SBOM_ENABLEDEnable SBOM generationtrue / falsefalse
DD_SECURITY_SBOM_FORMATSComma-separated list of SBOM formatsspdx-json, cyclonedx-jsonspdx-json
DD_SECURITY_SBOM_GENERATORProvider-neutral SBOM generatorauto, trivy, syftauto
DD_SECURITY_SYFT_COMMANDSyft command for command backendexecutable pathsyft
DD_SECURITY_SYFT_TIMEOUTSyft generation timeout in millisecondsinteger (>=1000)600000
DD_SECURITY_SYFT_ARGSAdditional Syft arguments as a JSON string array. Output/file flags cannot be overridden.JSON string array[]
DD_SECURITY_SYFT_WORKER_IMAGEDigest-pinned Syft worker for Docker backendOCI 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.sock
SBOM documents are retrievable per-container via GET /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_CRON and 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 (6h in 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 notifications

On-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/scan runs 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-all scans 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 status blocked, or
  • The current running image scan (container.security.scan) has status blocked

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

  1. Open the Security detail panel for a container (click any container row in the Security view)
  2. Select a format from the Export vulnerabilities dropdown: CSV or JSON
  3. Click Download Report

CSV format

The CSV includes one row per vulnerability with these columns:

ColumnDescription
IDCVE or advisory identifier (e.g. CVE-2024-1234)
SeverityCRITICAL, HIGH, MEDIUM, LOW, or UNKNOWN
PackageAffected package name
VersionInstalled version
Fixed InVersion that resolves the vulnerability (empty if no fix available)
TitleShort vulnerability description
TargetScan target (e.g. OS package db, language lockfile)
URLLink to the advisory
CSV fields are sanitized against formula injection — values starting with =, +, -, 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} where format is spdx-json or cyclonedx-json
When the security scanner is disabled or misconfigured, 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 alerts

The 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:

VariableDescription
scan.alertCountContainers with at least one finding
scan.scannedCountContainers scanned in the cycle
scan.containerNouncontainer when alertCount is 1, otherwise containers
scan.criticalCount / scan.highCount / scan.mediumCount / scan.lowCount / scan.unknownCountContainer counts whose top severity is that level
scan.criticalList / scan.highListPre-formatted, newline-joined - name: … lists for the critical and high tiers
scan.startedAt / scan.completedAtCycle start/finish ISO timestamps
scan.cycleIdThe 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