Operator notes

Scan public OCI images without a daemon.

layerleak reads registry manifests, image config, build history, and decompressed filesystem layers directly. It is read-only, runs as a single Go binary, and keeps actionable findings separated from likely test fixtures and example placeholders.

This development copy documents the planned v1.1 line. The currently published canonical stable CLI is v1.0.0. The v1.1 changelog and OpenAPI version remain frozen so an accepted release candidate can be promoted from the same source commit.

Install github.com/brumbelow/layerleak@latest
Runtime Go 1.25.13+
Persistence PostgreSQL 16.13+
Default exposure redacted findings
01

Quickstart

The module root is the supported CLI entrypoint. Install or build from there, then scan a public image reference. GOBIN or GOPATH/bin must be on PATH after go install.

go install github.com/brumbelow/layerleak@latest
layerleak --help
layerleak scan library/nginx:latest --format json

Source builds use the same module:

git clone https://github.com/brumbelow/layerleak.git
cd layerleak
go build -o layerleak .
./layerleak scan alpine:latest --platform linux/amd64

To pin a published release, replace @latest with the v1.x.y tag you want. The module path is github.com/brumbelow/layerleak, so @latest resolves to the highest published v1.x.y tag, currently v1.0.0. A planned command such as go install github.com/brumbelow/layerleak@v1.1.0-rc.1 becomes available only after that tag is published. Module-installed binaries report that version through layerleak --version; local checkout builds report the version Go embeds for the checkout, falling back to dev when no module version is available.

02

Scan model

layerleak supports public images on OCI-compatible registries including Docker Hub, GHCR, Quay, GCR, MCR, Amazon ECR Public, and self-hosted registries. It does not require a local Docker daemon and does not verify discovered secrets.

Single image

Use a tag or digest when the target scope should stay narrow.

layerleak scan mongo:latest
layerleak scan repo/app@sha256:...

Repository sweep

A bare repository scans latest. Add --all-tags to explicitly enumerate public tags, resolve each one to a digest, and group duplicate digests.

layerleak scan mongo
layerleak scan quay.io/prometheus/busybox --all-tags

Multi-arch indexes can be narrowed with --platform os/arch[/variant]. Attestation and provenance manifests (for example application/vnd.in-toto+json) are skipped instead of counted as platform scan failures.

Repository sweeps accept per-command scope flags that override the matching environment variables: --tag-page-size for tag-list page size, --max-repository-tags to cap how many tags are enumerated, and --max-repository-targets to cap how many distinct digests are resolved. --max-repository-tags and --max-repository-targets accept 0 to disable the cap; --tag-page-size must be greater than zero. These flags are non-interactive and safe for automation.

Detection uses Layerleak's native detector set. Findings on paths that look like tests, fixtures, examples, specs, or e2e suites are kept as suppressed findings rather than counted as actionable.

03

Configuration

Defaults live in .env.example and the Go config loader. Positive MAX_* caps fail closed when exceeded, while preserving the partial results produced before the failure.

LAYERLEAK_LOG_LEVEL=info
LAYERLEAK_FINDINGS_DIR=findings
LAYERLEAK_API_ADDR=127.0.0.1:8080
LAYERLEAK_PERSIST_RAW_SECRETS=0
LAYERLEAK_TAG_PAGE_SIZE=100
LAYERLEAK_HTTP_TIMEOUT=30s
LAYERLEAK_BLOB_TIMEOUT=10m
LAYERLEAK_SCAN_TIMEOUT=30m
LAYERLEAK_MAX_FILE_BYTES=1048576
LAYERLEAK_MAX_LAYER_BYTES=536870912
LAYERLEAK_MAX_LAYER_ENTRIES=50000
LAYERLEAK_MAX_IMAGE_LAYERS=512
LAYERLEAK_MAX_IMAGE_MANIFESTS=64
LAYERLEAK_MAX_IMAGE_LAYER_BYTES=4294967296
LAYERLEAK_MAX_IMAGE_ARTIFACTS=250000
LAYERLEAK_MAX_RETAINED_BYTES=1073741824
LAYERLEAK_MAX_MANIFEST_BYTES=8388608
LAYERLEAK_MAX_CONFIG_BYTES=8388608
LAYERLEAK_MAX_TAG_RESPONSE_BYTES=8388608
LAYERLEAK_MAX_FINDINGS_PER_SCAN=10000
# Raw retention stops with partial coverage at this aggregate byte bound.
LAYERLEAK_MAX_RAW_FINDING_BYTES=67108864
LAYERLEAK_MAX_REPOSITORY_TAGS=1000
LAYERLEAK_MAX_REPOSITORY_TARGETS=250
LAYERLEAK_REGISTRY_REQUEST_ATTEMPTS=2
LAYERLEAK_REGISTRY_MAX_REDIRECTS=3
LAYERLEAK_MAX_AUTH_RESPONSE_BYTES=1048576
LAYERLEAK_ALLOWED_PRIVATE_REGISTRY_HOSTS=
LAYERLEAK_ALLOWED_PRIVATE_AUTH_HOSTS=
LAYERLEAK_REGISTRY_BASE_URL=
LAYERLEAK_REGISTRY_AUTH_URL=
LAYERLEAK_DATABASE_URL=postgres://postgres:***@localhost:5432/layerleak?sslmode=disable
  • Limits. Settings with a non-negative semantic (MAX_LAYER_BYTES, MAX_LAYER_ENTRIES, MAX_MANIFEST_BYTES, MAX_CONFIG_BYTES, MAX_TAG_RESPONSE_BYTES, MAX_REPOSITORY_TAGS, MAX_REPOSITORY_TARGETS) accept 0 to disable the cap. MAX_FILE_BYTES must be greater than zero.
  • Timeouts. Durations use Go syntax (30s, 2m, 1h). Manifest, config, tag, and auth requests use LAYERLEAK_HTTP_TIMEOUT; each layer transfer uses LAYERLEAK_BLOB_TIMEOUT.
  • Registry overrides. LAYERLEAK_REGISTRY_BASE_URL and LAYERLEAK_REGISTRY_AUTH_URL are optional. Leave them unset unless forcing traffic through a proxy or alternate auth endpoint.
  • Private destinations. Private registry and auth hosts are blocked by default. Exact, kind-specific exceptions can be set with the two ALLOWED_PRIVATE_* lists; schemes, paths, credentials, and wildcards are rejected.
  • API container. The published image defaults LAYERLEAK_API_ADDR to 0.0.0.0:8080.
04

Results

Actionable findings remain in findings and drive the non-zero scan exit status. Likely test, example, fixture, and spec placeholders are emitted as suppressed findings with disposition metadata, and do not count toward total_findings.

Each usable scan result produces two JSON artifacts with the same generated basename under LAYERLEAK_FINDINGS_DIR. The compatible findings array remains at findings/<basename>.json. A versioned, always-redacted record at findings/scans/<basename>.json adds image identity, status, coverage, diagnostics, counts, creation time, and the PostgreSQL persistence outcome. If the directory is unset, it defaults to findings/ beside the nearest go.mod, falling back to the current working directory.

Every result reports status as completed, partial, or failed, plus concrete coverage counters and diagnostics. Incomplete work fails by default; --allow-partial accepts a usable partial result without hiding the missing coverage.

Per-occurrence fields

detector_name confidence disposition disposition_reason source_type platform file_path layer_digest line_number source_location context_snippet present_in_final_image

Per-finding fields

manifest_digest fingerprint redacted_value occurrence_count actionable_occurrence_count suppressed_occurrence_count first_seen_at last_seen_at

Saved files and Postgres writes default to redacted values and redacted context snippets. Raw secret values and raw snippets are written only when LAYERLEAK_PERSIST_RAW_SECRETS=1. API responses stay redacted even if a writer persisted raw values; the companion scan record is always redacted. The two local files are published independently, so a write failure retains any artifact already published and returns an operational error.

05

Postgres persistence

Migrations are explicit. The API does not create or upgrade schema at runtime and does not become ready until schema version 0004 is installed. Use PostgreSQL server >= 16.13.

export LAYERLEAK_MIGRATIONS_DIR="$PWD/migrations"
go run ./cmd/migrate
go run ./cmd/migrate

The native command is rerunnable, holds a PostgreSQL advisory lock, records checksums in a migration ledger, and refuses drift, gaps, or partial legacy state. The published image contains the same command:

docker run --rm \
  -e LAYERLEAK_DATABASE_URL="$LAYERLEAK_DATABASE_URL" \
  --entrypoint /usr/local/bin/layerleak-migrate-up \
  ghcr.io/brumbelow/layerleak:latest

Current state is deduplicated by (manifest_digest, fingerprint). Append-only scan history lives in scan_runs as a redacted snapshot of the public result JSON, regardless of LAYERLEAK_PERSIST_RAW_SECRETS.

Safe purge Disable LAYERLEAK_PERSIST_RAW_SECRETS on every database writer and restart or stop those processes first. The layerleak-purge-raw-secrets --confirm command then clears only opted-in raw values and snippets while preserving redacted findings and scan history.
06

HTTP API

The API binary under cmd/api is JSON-only and Postgres-backed. It requires LAYERLEAK_DATABASE_URL; it does not serve from local findings files.

go run ./cmd/api
  • GET/health
  • GET/livez
  • GET/readyz
  • POST/api/v1/scans
  • GET/api/v1/scans/{id}
  • GET/api/v1/repositories
  • GET/api/v1/repositories/{repository}/scans
  • GET/api/v1/repositories/{repository}/findings
  • GET/api/v1/findings/{id}

POST /api/v1/scans is synchronous and accepts a JSON body with reference, optional platform, and optional all_tags. A bare reference means latest unless all_tags is true. The response includes scan_run_id plus the same redacted result schema produced by the CLI. Incomplete or limit-bounded work returns HTTP 422 with scan_incomplete or scan_limit_exceeded and includes a usable persisted result when one exists.

If a usable redacted result exists but PostgreSQL persistence fails, the endpoint returns HTTP 503 with storage_unavailable, neutral public wording, and the actual result status and coverage. It omits scan_run_id unless persistence succeeded.

Repository scan and finding list endpoints accept an optional ?registry= query parameter (defaults to docker.io), plus ?limit= and ?offset= pagination (limit defaults to 50 and is capped at 200). Findings list also accepts ?disposition=actionable|suppressed|all; omitted disposition returns actionable findings only.

No built-in auth The API does not include an authentication layer. Deploy it on a private network and front it with your own authn/authz gateway or reverse proxy policy.

Open the versioned OpenAPI 3.1 specification.

07

Container & Compose

The published image runs the API by default. It binds 0.0.0.0:8080 and expects LAYERLEAK_DATABASE_URL.

docker pull ghcr.io/brumbelow/layerleak:latest
docker run --rm \
  -p 8080:8080 \
  -e LAYERLEAK_DATABASE_URL='postgres://<user>:<pass>@<host>:5432/layerleak?sslmode=disable' \
  ghcr.io/brumbelow/layerleak:latest

The repository ships a Compose stack in docker-compose.yml with db, migrate, purge-raw-secrets, and api services. The db service baseline is pinned to postgres:16.13-alpine.

cp .env.example .env
# Set LAYERLEAK_DB_PASSWORD in .env.
docker compose config

docker compose --profile tools run --rm migrate
docker compose up -d api
curl --fail http://127.0.0.1:8080/readyz

In Dockge or Komodo, import the same Compose file and run the migrate service once before enabling the long-running api service. Compose binds the host port to 127.0.0.1 by default; change LAYERLEAK_API_HOST only when an authenticated network edge is ready.

08

Browser demo

The Pages demo is fully static. It replays a fixed transcript and renders a synthetic Postgres-style snapshot from versioned fixture data in the repository. Raw-value columns are empty to match default storage, and the transcript shows the current result status, coverage, findings-array path, and companion scan-record path. Nothing is sent to any registry or database.

layerleak scan vulnerableHost:latest --platform linux/amd64

Open the simulated demo