azu/dockerfile-pin
azu/dockerfile-pin has 92 GitHub stars and 4 forks. Star changes are not measured yet. Last checked 2026-11-14 12:27 UTC. Last push 2026-11-12.
Gemessener Sternzuwachs
- in 24h
- Verlauf wird gesammelt
- in 7 days
- Verlauf wird gesammelt
- in 15 days
- Verlauf wird gesammelt
- in 1 month
- Verlauf wird gesammelt
Nettoänderungen in überlappenden Zeiträumen, keine Tagessummen. Fehlende Messwerte bleiben leer. Sterne zeigen Interesse, nicht Qualität.
Über uns
A CLI tool for digest pinning — adds @sha256:<digest> to Dockerfile, docker-compose.yml, and GitHub Actions to prevent supply chain attacks.
SicherheitREADME.md
Originaldockerfile-pin
A CLI tool that adds @sha256:<digest> to FROM and COPY --from= lines in Dockerfiles, image fields in docker-compose.yml, and Docker image references in GitHub Actions and GitLab CI files to prevent supply chain attacks.
Install
curl
# Download binary (macOS Apple Silicon)
curl -sL "https://github.com/azu/dockerfile-pin/releases/latest/download/dockerfile-pin_darwin_arm64.tar.gz" | tar xz
sudo mv dockerfile-pin /usr/local/bin/
# Download binary (Linux amd64)
curl -sL "https://github.com/azu/dockerfile-pin/releases/latest/download/dockerfile-pin_linux_amd64.tar.gz" | tar xz
sudo mv dockerfile-pin /usr/local/bin/
aqua
aqua init
aqua generate -i azu/dockerfile-pin
aqua i
aqua exec -- dockerfile-pin --help
Go
go install github.com/azu/dockerfile-pin@latest
See GitHub Releases for all platforms.
Usage
Run
Add digests to Dockerfile FROM lines, docker-compose.yml image fields, and GitHub Actions and GitLab CI Docker image references.
By default, shows changes without modifying files (dry-run).
# Preview changes (dry-run, default)
dockerfile-pin run
# Actually write changes to files
dockerfile-pin run --write
# Preview a specific file
dockerfile-pin run -f path/to/Dockerfile
# Preview docker-compose.yml
dockerfile-pin run -f docker-compose.yml
# Preview multiple files using glob
dockerfile-pin run --glob '**/Dockerfile*'
# Multiple patterns with brace expansion
dockerfile-pin run --glob '**/{Dockerfile,Dockerfile.*,docker-compose.yml,compose.yaml}'
# Ignore specific images (glob patterns, repeatable)
dockerfile-pin run --ignore-images "mcr.microsoft.com/**"
# Re-resolve existing digests
dockerfile-pin run --write --update
# Skip images built within the last 7 days
dockerfile-pin run --write --update --min-age 7
Dockerfile
Before:
FROM node:20.11.1
FROM python:3.12-slim AS builder
FROM scratch
COPY --from=nginx:1.27 /etc/nginx /etc/nginx
COPY --from=builder /app /app
After:
FROM node:20.11.1@sha256:e06aae17c40c7a6b5296ca6f942a02e6737ae61bbbf3e2158624bb0f887991b5
FROM python:3.12-slim@sha256:3d5ed973e45820f5ba5e46bd065bd88b3a504ff0724d85980dcd05eab361fcf4 AS builder
FROM scratch
COPY --from=nginx:1.27@sha256:6784fb08b4b7c3b6bcd3f4a1b4d1b1f3e3b7a7ca42ec3e0d9df8a97a2c9a3b1d /etc/nginx /etc/nginx
COPY --from=builder /app /app
docker-compose.yml
Before:
services:
web:
image: node:20.11.1
ports:
- "3000:3000"
db:
image: postgres:16.2
environment:
POSTGRES_PASSWORD: secret
app:
build: .
image: myapp:latest
After:
services:
web:
image: node:20.11.1@sha256:e06aae17c40c7a6b5296ca6f942a02e6737ae61bbbf3e2158624bb0f887991b5
ports:
- "3000:3000"
db:
image: postgres:16.2@sha256:4aea012537edfad80f98d870a36e6b90b4c09b27be7f4b4759d72db863baeebb
environment:
POSTGRES_PASSWORD: secret
app:
build: .
image: myapp:latest # skipped (has build directive)
GitHub Actions
Before:
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:24
services:
db:
image: postgres:18
steps:
- uses: docker://ghcr.io/astral-sh/uv:latest
- uses: actions/checkout@v4
After:
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:24@sha256:bb20cf73b3ad7212834ec48e2174cdcb5775f6550510a5336b842ae32741ce6c
services:
db:
image: postgres:18@sha256:a9abf4275f9e99bff8e6aed712b3b7dfec9cac1341bba01c1ffdfce9ff9fc34a
steps:
- uses: docker://ghcr.io/astral-sh/uv:latest@sha256:90bbb3c16635e9627f49eec6539f956d70746c409209041800a0280b93152823
- uses: actions/checkout@v4 # not a Docker image, skipped
GitLab CI
Before:
default:
image:
name: node:24
entrypoint: [""]
test:
services:
- postgres:18
- name: redis:7
alias: cache
script:
- npm test
deploy:
image: $CI_REGISTRY_IMAGE:latest
script:
- ./deploy.sh
After:
default:
image:
name: node:24@sha256:bb20cf73b3ad7212834ec48e2174cdcb5775f6550510a5336b842ae32741ce6c
entrypoint: [""]
test:
services:
- postgres:18@sha256:a9abf4275f9e99bff8e6aed712b3b7dfec9cac1341bba01c1ffdfce9ff9fc34a
- name: redis:7@sha256:90bbb3c16635e9627f49eec6539f956d70746c409209041800a0280b93152823
alias: cache
script:
- npm test
deploy:
image: $CI_REGISTRY_IMAGE:latest # built from a CI variable, skipped
script:
- ./deploy.sh
Check
Validate that digests are present and exist in the registry.
# Check a single Dockerfile
dockerfile-pin check -f Dockerfile
# Check multiple files
dockerfile-pin check --glob '**/Dockerfile*'
# Multiple patterns with brace expansion
dockerfile-pin check --glob '**/{Dockerfile,Dockerfile.*,dockerfile_*.tmpl,docker-compose.yml,compose.yaml}'
# Syntax check only (no registry queries)
dockerfile-pin check --syntax-only
# JSON output for CI
dockerfile-pin check --format json
# Ignore specific images (glob patterns, repeatable)
dockerfile-pin check --ignore-images "scratch"
dockerfile-pin check --ignore-images "ghcr.io/myorg/*" --ignore-images "mcr.microsoft.com/**"
Verify tags (--verify-tag)
By default, check only verifies that the pinned digest exists in the registry.
It does not verify that the tag points to that digest, so FROM node:24@sha256:<digest-of-node-22> passes.
--verify-tag also fetches the digest that each tag points to now (a HEAD request for the tag, the same lookup run uses) and fails when it differs from the pinned digest.
dockerfile-pin check --verify-tag
FAIL Dockerfile:2 FROM busybox:1.37@sha256:73aaf0... tag points to a different digest: sha256:a7dbc3...
This option is off by default because tags are mutable.
When an upstream image is rebuilt, the tag moves to a new digest and this check fails even though nothing in your repository changed.
This happens to moving tags like node:24 and also to specific tags like node:24.11.1, because Docker Official Images are rebuilt when their base images are updated.
Run dockerfile-pin run --write --update to update the digests, or exclude such images with --ignore-images.
This is not the same as verifying a version comment in pinact.
A registry only returns the digest a tag points to now, not the digests it pointed to in the past.
So a failure means "the tag and the pinned digest do not match now", and --verify-tag cannot tell whether the upstream image was updated or the digest was replaced with a different image's digest.
References without a tag (e.g., node@sha256:...) are reported as OK with no tag to verify.
--verify-tag cannot be combined with --syntax-only.
Output:
FAIL Dockerfile:1 FROM node:20.11.1 missing digest
OK Dockerfile:3 FROM python:3.12@sha256:abc123...
SKIP Dockerfile:5 FROM scratch scratch image
Exit code is 1 when any check fails (configurable with --exit-code).
Configuration File
Create .dockerfile-pin.yaml (or .dockerfile-pin.yml) in your project root to configure ignore rules:
# .dockerfile-pin.yaml
min-age: 7 # Skip images built within the last 7 days
ignore-images:
- "ghcr.io/myorg/*" # Ignore all images under myorg
- "!ghcr.io/myorg/public-*" # But still check public-* images
- "*.dkr.ecr.*.amazonaws.com/**" # Ignore all ECR images
- "mcr.microsoft.com/**" # Ignore all Microsoft container images
- "scratch" # Ignore exact image name
Config file patterns are merged with --ignore-images CLI flags. CLI flags are evaluated after config file patterns, so they take precedence (last match wins).
Pattern Syntax
Patterns use glob matching (doublestar syntax):
| Pattern | Matches | Does not match |
|---|---|---|
scratch |
scratch |
scratch:latest |
node:* |
node:20, node:latest |
node:20@sha256:... |
ghcr.io/myorg/* |
ghcr.io/myorg/app:v1 |
ghcr.io/myorg/sub/app:v1 |
ghcr.io/myorg/** |
ghcr.io/myorg/app:v1, ghcr.io/myorg/sub/app:v1 |
ghcr.io/other/app:v1 |
*.dkr.ecr.*.amazonaws.com/* |
123.dkr.ecr.us-east-1.amazonaws.com/app:v1 |
Negation patterns (prefixed with !) override previous matches:
ignore-images:
- "ghcr.io/myorg/*" # Ignore all
- "!ghcr.io/myorg/public-*" # But check public-* images
Supported Patterns
Dockerfiles
| Pattern | Supported |
|---|---|
FROM image:tag |
Yes |
FROM image:tag AS name |
Yes |
FROM --platform=linux/amd64 image:tag |
Yes |
FROM image:tag@sha256:... (already pinned) |
Skipped (use --update to refresh) |
FROM scratch |
Skipped |
FROM <stage-name> (multi-stage ref) |
Skipped |
ARG VERSION=1.0 + FROM image:${VERSION} |
Yes (expanded from default) |
ARG BASE + FROM ${BASE} (no default) |
Skipped with warning |
FROM ghcr.io/org/image:tag |
Yes |
FROM registry:5000/image:tag |
Yes |
COPY --from=image:tag |
Yes |
COPY --from=image:tag@sha256:... (already pinned) |
Skipped (use --update to refresh) |
COPY --from=<stage-name> (multi-stage ref) |
Skipped |
COPY --from=0 (stage index) |
Skipped |
COPY --from=scratch |
Skipped |
COPY --from=image:${TAG} |
Skipped (BuildKit does not expand variables here) |
COPY /src /dst (build context) |
Nothing to pin |
ONBUILD COPY --from=image:tag |
Yes |
# escape= directive |
Honored |
ADD --from=..., RUN --mount=...,from=... |
Not supported |
An ONBUILD COPY --from=name is resolved as an image even when a stage in the same
file shares that name. The trigger does not run in this build: it is recorded into the
image config and executed later, inside whichever build uses this image as its base,
against that Dockerfile's stages — the ones declared here are gone by then.
A digest makes a name an image even when a build stage shares it: BuildKit matches the
whole value against its stage names, so COPY --from=nginx@sha256:... finds no stage
named nginx and is resolved from the registry. The same holds for FROM.
A COPY --from=<name> that matches no build stage is treated as an image, the same
way FROM ubuntu is. A named build context
(docker buildx build --build-context name=...) is written the same way and cannot
be told apart from the Dockerfile alone; use --ignore-images to exclude one.
Variables are not expanded in COPY --from=: BuildKit reads the value verbatim and
the build fails with failed to parse stage name (moby/buildkit#2374),
so such a reference is reported as skipped rather than pinned. FROM does expand them.
docker-compose.yml
| Pattern | Supported |
|---|---|
image: node:20 |
Yes |
image: node:20@sha256:... |
Skipped (use --update) |
Service with build: directive |
Skipped |
Service without image: key |
Skipped |
GitHub Actions workflow files (.github/workflows/*.yml)
| Pattern | Supported |
|---|---|
jobs.<id>.container.image: node:20 |
Yes |
jobs.<id>.container: node:20 (string shorthand) |
Yes |
jobs.<id>.services.<id>.image: postgres:16 |
Yes |
jobs.<id>.steps[*].uses: docker://image:tag |
Yes |
jobs.<id>.steps[*].uses: actions/checkout@v4 |
Skipped (not a Docker image) |
GitHub Actions action files (action.yml)
| Pattern | Supported |
|---|---|
runs.image: 'docker://debian:stretch-slim' |
Yes |
runs.image: 'Dockerfile' |
Skipped (local Dockerfile) |
GitLab CI files (.gitlab-ci.yml)
The default search finds only .gitlab-ci.yml, the one name GitLab reads. The layouts below are also recognised when named with -f or --glob, but are not searched for by default, because templates is an ordinary directory name that other tools use and the .gitlab/ci convention is not something GitLab defines.
| Layout | Recognised |
|---|---|
.gitlab-ci.yml |
Yes, and found by default |
*.gitlab-ci.yml (a pipeline split into parts) |
Yes, with -f or --glob |
templates/<name>.yml (CI component) |
Yes, with -f or --glob |
templates/<name>/template.yml (CI component) |
Yes, with -f or --glob |
.gitlab/ci/**/*.yml (pulled in with include: local:) |
Yes, with -f or --glob |
| Any other name | Treated as a compose file |
The two component rows are matched from the root, because GitLab requires a component's templates directory to be at the root of the repository; name such a file by a path relative to that root, not an absolute one or one reaching up with ... The compose filenames keep their own handling wherever they sit, so a templates/docker-compose.yml is still a compose file.
The keywords read inside a recognised file are the following.
| Pattern | Supported |
|---|---|
<job>.image: node:20 |
Yes |
<job>.image.name: node:20 |
Yes |
<job>.services[*]: postgres:16 |
Yes |
<job>.services[*].name: postgres:16 |
Yes |
default.image and default.services |
Yes |
Root-level image and services (deprecated by GitLab) |
Yes |
Hidden job templates (.build:) |
Yes |
image: $CI_REGISTRY_IMAGE:tag |
Skipped (built from a CI variable) |
image: node:20@sha256:... |
Skipped (use --update) |
Files with a component spec: header document |
Yes (all YAML documents are read) |
Global keywords (variables, workflow, include, stages, spec) |
Not scanned |
CI Integration
Check (PR validation)
Validate that all images are pinned on every pull request.
With aqua (if your project already uses aqua, add azu/dockerfile-pin to your aqua.yaml):
# .github/workflows/dockerfile-check.yml
name: Dockerfile Digest Check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquaproj/aqua-installer@v3
with:
aqua_version: v2.45.0
- run: dockerfile-pin check
Without aqua:
# .github/workflows/dockerfile-check.yml
name: Dockerfile Digest Check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dockerfile-pin
run: |
curl -sL "https://github.com/azu/dockerfile-pin/releases/latest/download/dockerfile-pin_linux_amd64.tar.gz" | tar xz -C /usr/local/bin
- run: dockerfile-pin check
dockerfile-pin check exits with code 1 if any image is missing a digest.
When -f and --glob are omitted, it auto-detects target files using git ls-files filtered by the default glob pattern:
**/{Dockerfile,Dockerfile.*,docker-compose*.yml,docker-compose*.yaml,compose.yml,compose.yaml,action.yml,action.yaml,.gitlab-ci.yml,.github/workflows/*.yml,.github/workflows/*.yaml}
Outside a git repository, it falls back to the same glob pattern with common directories (node_modules, vendor) excluded.
Pin (migration)
Run locally to add digests to all Dockerfiles, compose files, GitHub Actions files, and GitLab CI files:
# Preview changes
dockerfile-pin run
# Apply changes
dockerfile-pin run --write
Private registries
For private registries (GCR, GHCR, ECR), configure Docker credentials before running:
# GHCR
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# GCR
- uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud auth configure-docker
dockerfile-pin uses ~/.docker/config.json for authentication, so any docker login or credential helper works.
How It Works
- Uses go-containerregistry (crane) for registry API calls
- Uses BuildKit's Dockerfile parser for accurate FROM line parsing
runresolves digests via HEAD requests (does not count against Docker Hub pull rate limits)checkverifies digest existence via HEAD requests- Authenticates using
~/.docker/config.json(supports Docker Hub, GHCR, GCR, ECR, etc.)
Digest Updates
--update (-u) re-resolves each tag against the registry and replaces the existing digest with the current digest of that tag. The tag itself is not changed.
# Re-resolve all pinned digests from the registry
dockerfile-pin run --write --update
--min-age
--min-age N skips images whose build date is within the last N days. This acts as a cooldown period — only pin to images that have been stable for at least N days.
# Skip images built within the last 7 days
dockerfile-pin run --write --update --min-age 7
The build date is read from the image's OCI config (Created field). Images with no creation timestamp (e.g., reproducible builds) are not skipped.
--min-age can also be set in the configuration file:
# .dockerfile-pin.yaml
min-age: 7
CLI flag takes precedence over the configuration file value.
For automated ongoing digest updates, use Renovate which understands the image:tag@sha256:digest format.
License
MIT