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.

Install github.com/brumbelow/layerleak@latest
Runtime Go 1.25.7+
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.

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

Bare repository names enumerate public tags, resolve each tag to a digest, and group duplicate digests.

layerleak scan mongo
layerleak scan quay.io/prometheus/busybox

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.

Detection uses native detectors for 60+ secret types, with TruffleHog defaults as a fallback layer. 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_MAX_FILE_BYTES=1048576
LAYERLEAK_MAX_LAYER_BYTES=536870912
LAYERLEAK_MAX_LAYER_ENTRIES=50000
LAYERLEAK_MAX_MANIFEST_BYTES=0
LAYERLEAK_MAX_CONFIG_BYTES=0
LAYERLEAK_MAX_TAG_RESPONSE_BYTES=8388608
LAYERLEAK_MAX_REPOSITORY_TAGS=0
LAYERLEAK_MAX_REPOSITORY_TARGETS=0
LAYERLEAK_REGISTRY_REQUEST_ATTEMPTS=2
LAYERLEAK_DATABASE_URL=postgres://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. LAYERLEAK_HTTP_TIMEOUT uses Go duration syntax (30s, 2m, 1h) and applies to every registry request: manifests, blobs, tag pages, and auth tokens.
  • 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.
  • 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.

A scan saves a JSON file under LAYERLEAK_FINDINGS_DIR. If unset, it defaults to findings/ beside the nearest go.mod, falling back to the current working directory.

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.

05

Postgres persistence

Migrations are manual on purpose. The scanner does not create or upgrade schema at runtime. Use PostgreSQL server >= 16.13.

psql "$LAYERLEAK_DATABASE_URL" -f migrations/0001_initial.up.sql
psql "$LAYERLEAK_DATABASE_URL" -f migrations/0002_finding_occurrence_metadata.up.sql
psql "$LAYERLEAK_DATABASE_URL" -f migrations/0003_scan_runs.up.sql

The published container image ships a rerunnable helper that enforces server version >= 16.13 and exits non-zero on partial migration state:

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

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 Use a dedicated database or schema for layerleak. To clear stored data, drop the schema or database rather than deleting individual rows.
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
  • 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 and optional platform. The response includes scan_run_id when Postgres persistence is enabled, plus the same redacted result schema produced by the CLI.

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

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.
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, and api services. The db service baseline is pinned to postgres:16.13-alpine.

docker compose --profile manual run --rm migrate
docker compose up -d api

In Dockge or Komodo, import the same Compose file and run the migrate service once before enabling the long-running api service.

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. Nothing is sent to any registry or database.

layerleak scan vulnerableHost:latest --platform linux/amd64

Open the simulated demo