Single image
Use a tag or digest when the target scope should stay narrow.
layerleak scan mongo:latest
layerleak scan repo/app@sha256:...
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.
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.
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.
Use a tag or digest when the target scope should stay narrow.
layerleak scan mongo:latest
layerleak scan repo/app@sha256:...
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.
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
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.
30s,
2m, 1h). Manifest, config, tag, and auth requests use
LAYERLEAK_HTTP_TIMEOUT; each layer transfer uses
LAYERLEAK_BLOB_TIMEOUT.
LAYERLEAK_REGISTRY_BASE_URL and
LAYERLEAK_REGISTRY_AUTH_URL are optional. Leave them unset unless
forcing traffic through a proxy or alternate auth endpoint.
ALLOWED_PRIVATE_* lists; schemes, paths, credentials, and wildcards
are rejected.
LAYERLEAK_API_ADDR to 0.0.0.0:8080.
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.
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.
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.
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.
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
/health/livez/readyz/api/v1/scans/api/v1/scans/{id}/api/v1/repositories/api/v1/repositories/{repository}/scans/api/v1/repositories/{repository}/findings/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.
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.
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