
layerleak the Docker Hub Secret Scanner
Layerleak is a read-only OCI image secret scanner. It resolves public image references without a Docker daemon, verifies downloaded content against OCI digests, reconstructs layer state, inspects deleted artifacts and image metadata, and returns redacted, provenance-rich findings.
It supports Docker Hub, GHCR, Quay, GCR, MCR, Amazon ECR Public, and other OCI-compatible registries. Results can be saved as JSON and persisted in PostgreSQL for the bundled API.
This development tree documents the 3.0.0 line on the
github.com/brumbelow/layerleak/v3 module path. The release-candidate command
shown below becomes installable only after that exact tag is published. The
3.0.0 labels in the changelog and OpenAPI document are intentionally frozen so
an accepted release candidate can be promoted from the same source commit.
Layerleak scans untrusted image content, so its defaults are intentionally bounded and fail closed:
partial or failed, never as a clean
scan.Layerleak does not verify whether a detected credential is live. The API has no
authorization model: it is open by default, and the opt-in
LAYERLEAK_API_BEARER_TOKENS shared-token check is defence in depth for the
/api/ paths, not access control. Expose it only on a trusted network or
behind an authenticated gateway either way.
Layerleak requires Go 1.27.1 or newer.
go install github.com/brumbelow/layerleak/v3@latest
layerleak --version
layerleak --help
The /v3 module path is the canonical install target; the installed binary is
still named layerleak. Pin a stable or release candidate explicitly when
reproducibility matters:
go install github.com/brumbelow/layerleak/[email protected]
go install github.com/brumbelow/layerleak/[email protected]
@latest on the /v3 path selects the highest published v3 release. Go
excludes prereleases from @latest once a stable version exists, so while only
release candidates are published @latest resolves to the newest candidate.
The root path github.com/brumbelow/layerleak without /v3 stays at v1.0.0
forever; always include /v3. Checkout builds report a VCS-derived development
version, including a dirty-worktree marker, or dev without VCS information.
Release-installed binaries report the resolved module version through
layerleak --version.
Every release also attaches prebuilt CLI archives, so no Go toolchain is
needed: layerleak_<version>_<os>_<arch>.tar.gz for linux and darwin on
amd64 and arm64 (containing layerleak, LICENSE and
THIRD_PARTY_NOTICES.md) and layerleak_<version>_windows_amd64.zip
(layerleak.exe). Each release carries a layerleak_<version>_checksums.txt
signed keylessly by the release workflow
(layerleak_<version>_checksums.txt.sigstore.json) and a build-provenance
attestation for every archive. Verify before you run:
version=v3.0.0
archive="layerleak_${version}_linux_amd64.tar.gz"
checksums="layerleak_${version}_checksums.txt"
base="https://github.com/brumbelow/layerleak/releases/download/${version}"
curl --fail --location --proto '=https' --tlsv1.2 --remote-name-all \
"${base}/${archive}" "${base}/${checksums}" "${base}/${checksums}.sigstore.json"
sha256sum --check --ignore-missing "${checksums}"
cosign verify-blob \
--bundle "${checksums}.sigstore.json" \
--certificate-identity 'https://github.com/Brumbelow/layerleak/.github/workflows/container-release.yml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"${checksums}"
gh attestation verify "${archive}" --repo Brumbelow/layerleak \
--signer-workflow Brumbelow/layerleak/.github/workflows/container-release.yml \
--source-ref refs/heads/main --deny-self-hosted-runners
tar -xzf "${archive}" layerleak
./layerleak version
The signature proves the checksums file came from this repository's release
workflow on main, the checksum binds the archive to that file, and the
attestation independently binds the archive to the workflow run that built it.
release-manifest.json on the release lists every archive and the digest of
the binary inside it. A stable release rebuilds the accepted release
candidate's commit with the stable version string and refuses to publish
unless the rebuild reproduces the candidate's binaries byte for byte, so
layerleak version reports exactly the tag you downloaded.
The repository root is a composite action that downloads the archive for the
runner, verifies it as above (checksum, the checksums file's cosign bundle with
the cosign release pinned in scripts/release-tools.sh, and the archive
attestation) and runs layerleak scan. Pin it to a release tag; the matching
archives are downloaded from that release:
name: Image secrets
on: { push: { branches: [main] } }
permissions: { contents: read, security-events: write }
jobs:
layerleak:
runs-on: ubuntu-24.04
steps:
- id: scan
uses: brumbelow/[email protected]
with: { image: "ghcr.io/${{ github.repository }}:${{ github.sha }}", fail-on: none }
- uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
if: ${{ always() && steps.scan.outputs.sarif-file != '' }}
with: { sarif_file: "${{ steps.scan.outputs.sarif-file }}" }
| Input | Default | Meaning |
|---|---|---|
image | required | Image reference to scan; private registries use LAYERLEAK_REGISTRY_USERNAME/LAYERLEAK_REGISTRY_PASSWORD in the step env. |
version | the tag in uses: | Release whose archive is downloaded; set it only when uses: names a branch or commit. |
format | sarif | summary, json or sarif. |
fail-on | low | Passed to --fail-on; none reports without failing the step. |
allow-partial | false | Passes --allow-partial. |
platform | every linux manifest | Passed to --platform. |
output-file | $RUNNER_TEMP/layerleak/results.<ext> | Where the formatted result is written. |
extra-args | empty | Further layerleak scan flags, split on whitespace and appended last (for example --all-tags --max-repository-tags 50). |