container-source-policy
Generate BuildKit source policies that make Docker builds reproducible and secure — without modifying your Dockerfiles.
- 📌 Pin images and URLs to immutable checksums
- 🛡️ Harden builds with Docker Hardened Images (fewer CVEs, smaller footprint)
- ✅ Validate existing policies against Dockerfiles (coming soon)
See the BuildKit documentation on build reproducibility for more details on source policies.
Quick start
container-source-policy pin --stdout Dockerfile > source-policy.json
EXPERIMENTAL_BUILDKIT_SOURCE_POLICY=source-policy.json docker buildx build -t my-image:dev .Note:
EXPERIMENTAL_BUILDKIT_SOURCE_POLICYis the environment variable used by Docker Buildx to specify a source policy file.
Install
Run directly without installing (recommended):
# npm/bun
npx container-source-policy --help
bunx container-source-policy --help
# Python
uvx container-source-policy --help
# Ruby (requires RubyGems 3.3+)
gem exec container-source-policy --helpOr install globally:
# Go (build from source)
go install github.com/tinovyatkin/container-source-policy@latest
# npm
npm i -g container-source-policy
# Python
pipx install container-source-policy
# Ruby
gem install container-source-policyUsage
Generate a policy for one or more Dockerfiles:
container-source-policy pin --stdout Dockerfile Dockerfile.ci > source-policy.jsonRead the Dockerfile from stdin:
cat Dockerfile | container-source-policy pin --stdout -Write directly to a file:
container-source-policy pin --output source-policy.json DockerfileDocker Hardened Images (DHI)
Use --prefer-dhi to pin Docker Hub library images to their Docker Hardened Images equivalents when available:
# First, login to dhi.io with your Docker Hub credentials
docker login dhi.io
# Then use --prefer-dhi to prefer hardened images
container-source-policy pin --prefer-dhi --stdout DockerfileThis converts eligible images (e.g., alpine:3.21, node:22, golang:1.23) to their dhi.io equivalents, which are minimal, security-hardened
versions with fewer vulnerabilities.
- Only Docker Hub library images (
alpine,node,golang, etc.) are eligible - Images not available on dhi.io silently fall back to docker.io
- Non-library images (
ghcr.io/*,docker.io/myorg/*) are unchanged - The policy selector still matches the original reference, so your Dockerfile works unchanged
Example output with --prefer-dhi:
{
"selector": { "identifier": "docker-image://golang:1.23" },
"updates": { "identifier": "docker-image://dhi.io/golang:1.23@sha256:..." }
}Then pass the policy to BuildKit / Buildx via the environment variable:
EXPERIMENTAL_BUILDKIT_SOURCE_POLICY=source-policy.json docker buildx build .Or use buildctl directly with the --source-policy-file flag:
buildctl build --frontend dockerfile.v0 --local dockerfile=. --local context=. --source-policy-file source-policy.jsonWhat gets pinned
Container images (FROM, COPY --from, ONBUILD)
- Looks at
FROM …,COPY --from=<image>, and theirONBUILDvariants across all provided Dockerfiles. - Skips:
FROM scratch-
FROM <stage>/COPY --from=<stage>references to a previous named build stage -
COPY --from=0numeric stage indices -
FROM ${VAR}/COPY --from=${VAR}(unexpanded ARG/ENV variables) - images already written as
name@sha256:…
- Resolves the image manifest digest from the registry and emits BuildKit
CONVERTrules of the form:-
docker-image://<as-written-in-Dockerfile>→docker-image://<normalized>@sha256:…
-
HTTP sources (ADD, ONBUILD ADD)
- Looks at
ADD <url> …andONBUILD ADD <url> …instructions with HTTP/HTTPS URLs. - Skips:
-
ADD --checksum=… <url>(already pinned) - URLs containing unexpanded variables (
${VAR},$VAR) - Git URLs (handled separately, see below)
- Volatile content (emits warning): URLs returning
Cache-Control: no-store,no-cache,max-age=0, or expiredExpiresheaders
-
- Fetches the checksum and emits
CONVERTrules withhttp.checksumattribute. -
Respects
Varyheader: captures request headers that affect response content (e.g.,User-Agent,Accept-Encoding) and includes them in the policy ashttp.header.*attributes to ensure reproducible builds.
Optimized checksum fetching — avoids downloading large files when possible:
-
raw.githubusercontent.com: extracts SHA256 from ETag header - GitHub releases: uses the API
digestfield (setGITHUB_TOKENfor higher rate limits) - S3: uses
x-amz-checksum-sha256response header (by sendingx-amz-checksum-mode: ENABLED) - Fallback: downloads and computes SHA256
Git sources (ADD, ONBUILD ADD)
- Looks at
ADD <git-url> …andONBUILD ADD <git-url> …instructions with Git repository URLs. - Supports various Git URL formats:
https://github.com/owner/repo.git#refgit://host/path#refgit@github.com:owner/repo#refssh://git@host/path#ref
- Skips URLs containing unexpanded variables (
${VAR},$VAR) - Uses
git ls-remoteto resolve the ref (branch, tag, or commit) to a commit SHA - Emits
CONVERTrules withgit.checksumattribute (full 40-character commit SHA)
Example: ADD https://github.com/cli/cli.git#v2.40.0 /dest pins to commit 54d56cab...
Development
make build
make test
make lintUpdate integration-test snapshots:
UPDATE_SNAPS=true go test ./internal/integration/...Repository layout
-
cmd/container-source-policy/cmd/: CLI commands (urfave/cli) -
internal/dockerfile: Dockerfile parsing (FROMandADDextraction) -
internal/registry: registry client (image digest resolution) -
internal/dhi: Docker Hardened Images reference mapping -
internal/http: HTTP client (URL checksum fetching with optimizations) -
internal/git: Git client (commit SHA resolution via git ls-remote) -
internal/policy: BuildKit source policy types and JSON output -
internal/pin: orchestration logic forpin -
internal/integration: end-to-end tests with mock registry/HTTP server and snapshots -
packaging/: wrappers for publishing prebuilt binaries to npm / PyPI / RubyGems
Packaging
See packaging/README.md for how the npm/PyPI/Ruby packages are assembled from GoReleaser artifacts.