Skip to content

azu/dockerfile-pin

Repository files navigation

dockerfile-pin

A CLI tool that adds @sha256:<digest> to FROM lines in Dockerfiles, image fields in docker-compose.yml, and Docker image references in GitHub Actions 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 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 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

After:

FROM node:20.11.1@sha256:e06aae17c40c7a6b5296ca6f942a02e6737ae61bbbf3e2158624bb0f887991b5
FROM python:3.12-slim@sha256:3d5ed973e45820f5ba5e46bd065bd88b3a504ff0724d85980dcd05eab361fcf4 AS builder
FROM scratch

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

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/**"

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

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)

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,.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, and GitHub Actions 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
  • run resolves digests via HEAD requests (does not count against Docker Hub pull rate limits)
  • check verifies 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

About

A CLI tool for digest pinning — adds @sha256:<digest> to Dockerfile, docker-compose.yml, and GitHub Actions to prevent supply chain attacks.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Languages