Skip to content

Commit 5def835

Browse files
wrkodeclaude
andauthored
chore(build): digest-pin base images and SHA-pin workflow actions (kairos#4203) (#24)
Supply-chain hardening: make the image build and CI reproducible and tamper-evident by content-addressing every external dependency. - Dockerfile + Makefile: digest-pin all base images (hadron, kairos-init, golang static+alpine builders, alpine) as image:tag@sha256:<index-digest>. The multi-arch INDEX digest is used so cross-arch builds still resolve the right manifest. KAIROS_BASE_IMAGE/KAIROS_INIT_IMAGE are pinned in BOTH the Dockerfile ARG defaults and the Makefile defaults, since CI's make image passes the Makefile value (closing the override path). - ci.yml + release.yml: SHA-pin the remaining GitHub Actions (checkout, setup-go, setup-buildx, login, golangci-lint) with a # vN comment, matching the already SHA-pinned attest/sbom actions. No floating action tags remain. - make verify-pins (wired into the CI gates job): fails loud if a base-image digest drifts between Dockerfile and Makefile, or is not digest-pinned, so a two-site re-pin mistake cannot land silently. Reviewed by security-architect (gate passed, no blockers). The crane download stays version+sha256-verified (consistent with the other binary downloads); the control-plane image crane pull digest-pinning remains tracked as ADR-16 P5, explicitly out of scope here. Signed-off-by: William Rizzo <william.rizzo@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2bc0e9d commit 5def835

4 files changed

Lines changed: 54 additions & 28 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ jobs:
2424
timeout-minutes: 15
2525
steps:
2626
- name: Checkout
27-
uses: actions/checkout@v5
27+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
2828

2929
- name: Set up Go
30-
uses: actions/setup-go@v6
30+
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
3131
with:
3232
go-version-file: go.mod
3333
check-latest: false
3434

35+
- name: verify base-image pins (no digest drift)
36+
run: make verify-pins
37+
3538
- name: gofmt -s (check only)
3639
run: make fmt-check
3740

@@ -45,7 +48,7 @@ jobs:
4548
run: make build
4649

4750
- name: golangci-lint
48-
uses: golangci/golangci-lint-action@v9
51+
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
4952
with:
5053
# Pin to the version validated locally against the v2 config.
5154
version: v2.12.2
@@ -61,7 +64,7 @@ jobs:
6164
minors: ${{ steps.minors.outputs.minors }}
6265
steps:
6366
- name: Checkout
64-
uses: actions/checkout@v5
67+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
6568

6669
- name: Extract SupportedMinors -> JSON matrix
6770
id: minors
@@ -97,10 +100,10 @@ jobs:
97100
minor: ${{ fromJSON(needs.discover.outputs.minors) }}
98101
steps:
99102
- name: Checkout
100-
uses: actions/checkout@v5
103+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
101104

102105
- name: Set up Docker Buildx
103-
uses: docker/setup-buildx-action@v4
106+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
104107

105108
- name: Resolve Kubernetes patch + commit + crictl versions
106109
id: ver
@@ -254,16 +257,16 @@ jobs:
254257
minor: ${{ fromJSON(needs.discover.outputs.minors) }}
255258
steps:
256259
- name: Checkout
257-
uses: actions/checkout@v5
260+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
258261

259262
- name: Set up Go
260-
uses: actions/setup-go@v6
263+
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
261264
with:
262265
go-version-file: go.mod
263266
check-latest: false
264267

265268
- name: Set up Docker Buildx
266-
uses: docker/setup-buildx-action@v4
269+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
267270

268271
- name: Resolve Kubernetes patch + commit + crictl versions
269272
id: ver

.github/workflows/release.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
latest_minor: ${{ steps.minors.outputs.latest_minor }}
4242
steps:
4343
- name: Checkout
44-
uses: actions/checkout@v5
44+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
4545

4646
- name: Extract SupportedMinors -> JSON matrix
4747
id: minors
@@ -78,13 +78,13 @@ jobs:
7878
minor: ${{ fromJSON(needs.discover.outputs.minors) }}
7979
steps:
8080
- name: Checkout
81-
uses: actions/checkout@v5
81+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
8282

8383
- name: Set up Docker Buildx
84-
uses: docker/setup-buildx-action@v4
84+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
8585

8686
- name: Log in to ghcr.io
87-
uses: docker/login-action@v4
87+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
8888
with:
8989
registry: ghcr.io
9090
username: ${{ github.actor }}
@@ -216,10 +216,10 @@ jobs:
216216
attestations: write # write provenance + SBOM attestations
217217
steps:
218218
- name: Checkout
219-
uses: actions/checkout@v5
219+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
220220

221221
- name: Set up Go
222-
uses: actions/setup-go@v6
222+
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
223223
with:
224224
go-version-file: go.mod
225225

Dockerfile

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,30 @@
2020
# (https://kairos.io/docs/reference/auroraboot/). See samples/README.md.
2121

2222
# ----------------------------------------------------------------------------
23-
# Versions: pinned by default. Bump together via build args; CI should pin via
24-
# image digest as well once a release pipeline lands.
23+
# Versions: pinned by default. Bump together via build args. Base images are
24+
# digest-pinned below (kairos#4203) for a reproducible, tamper-evident supply chain.
2525
# ----------------------------------------------------------------------------
2626
# Pure upstream Hadron base (musl). kairos-init transforms it into a Kairos OS in
2727
# the final stage, exactly as the canonical Kairos image build does.
28-
ARG KAIROS_BASE_IMAGE=ghcr.io/kairos-io/hadron:v0.4.0
28+
# All base images are DIGEST-pinned (image:tag@sha256:...) for a reproducible,
29+
# tamper-evident supply chain (kairos#4203). The tag is kept for readability; the
30+
# digest is the multi-arch index digest, so cross-arch builds still resolve the
31+
# right manifest. When bumping a tag, re-resolve the digest
32+
# (docker buildx imagetools inspect <image:tag>) and update both here and in the
33+
# Makefile defaults that override these ARGs.
34+
ARG KAIROS_BASE_IMAGE=ghcr.io/kairos-io/hadron:v0.4.0@sha256:1e19d9cd5a70dfc6940f58d899e72f6776f4d64934cd6f402f4a4186ccc40d4d
2935
# kairos-init that matches the version Kairos itself uses to build Hadron. Older
3036
# pins (e.g. v0.6.0) cannot regenerate Hadron's initramfs (dracut -f ... fails).
31-
ARG KAIROS_INIT_IMAGE=quay.io/kairos/kairos-init:v0.14.6
32-
ARG GO_BUILDER_IMAGE=golang:1.26.4-alpine
37+
ARG KAIROS_INIT_IMAGE=quay.io/kairos/kairos-init:v0.14.6@sha256:e53eb7e5ada035e7e192f072f9e041ca5d60440ecf8c766c32e7d95253b293e7
38+
ARG GO_BUILDER_IMAGE=golang:1.26.4-alpine@sha256:3ad57304ad93bbec8548a0437ad9e06a455660655d9af011d58b993f6f615648
3339
ARG TARGETARCH=amd64
3440

3541
# containerd and kubelet are ALWAYS built fully static from source: Hadron is
3642
# musl, so the glibc-linked official releases cannot exec. A fully static binary
3743
# runs on musl AND glibc. STATIC_BUILDER is the from-source toolchain (Debian Go,
3844
# matching the validated recipe); patch-pinned (matches GO_BUILDER_IMAGE's pin
3945
# level) so the from-source compiler is reproducible across Go patch releases.
40-
# Digest-pinning all base images remains a tracked backlog item.
41-
ARG STATIC_BUILDER_IMAGE=golang:1.26.4
46+
ARG STATIC_BUILDER_IMAGE=golang:1.26.4@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
4247

4348
# Kubernetes (must be within the supported window the provider enforces at
4449
# runtime: 1.34 / 1.35 / 1.36 as of 2026).
@@ -93,7 +98,7 @@ RUN go build \
9398
# kubeadm/kubectl/crictl are already static and run on musl unchanged. kubelet is
9499
# NOT downloaded here -- it is built static from source (see kubelet-build).
95100
# ----------------------------------------------------------------------------
96-
FROM alpine:3.21 AS k8s-binaries
101+
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS k8s-binaries
97102
ARG KUBERNETES_VERSION
98103
ARG CRICTL_VERSION
99104
ARG TARGETARCH
@@ -127,7 +132,7 @@ RUN set -eux; \
127132
# (runc, CNI). containerd is built static from source (see containerd-build);
128133
# runc and the CNI plugins are already static and run on musl unchanged.
129134
# ----------------------------------------------------------------------------
130-
FROM alpine:3.21 AS runtime-binaries
135+
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS runtime-binaries
131136
ARG RUNC_VERSION
132137
ARG CNI_PLUGINS_VERSION
133138
ARG TARGETARCH
@@ -231,7 +236,7 @@ RUN set -eux; \
231236
# image and imported into containerd at boot, so a first boot converges with NO
232237
# registry access (air-gap). CNI is intentionally NOT bundled (operator's choice).
233238
# ----------------------------------------------------------------------------
234-
FROM alpine:3.21 AS image-bundler
239+
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS image-bundler
235240
ARG KUBERNETES_VERSION
236241
ARG TARGETARCH
237242
# Pinned; crane is a static Go binary that runs on musl.

Makefile

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ CNI_PLUGINS_VERSION ?= v1.8.0
2525
# Kairos OS base the image is built FROM. Defaults to the pure upstream Hadron
2626
# (musl) OS, which kairos-init transforms into a Kairos system in the final stage
2727
# (mirroring the canonical Kairos image build). Override to test another base.
28-
KAIROS_BASE_IMAGE ?= ghcr.io/kairos-io/hadron:v0.4.0
28+
# DIGEST-pinned (kairos#4203); must match the Dockerfile ARG defaults. Re-resolve
29+
# with `docker buildx imagetools inspect <image:tag>` when bumping the tag.
30+
KAIROS_BASE_IMAGE ?= ghcr.io/kairos-io/hadron:v0.4.0@sha256:1e19d9cd5a70dfc6940f58d899e72f6776f4d64934cd6f402f4a4186ccc40d4d
2931
# kairos-init image used in the final stage. Defaults to the Dockerfile's pin
3032
# (the version Kairos itself uses to build Hadron); override for testing.
31-
KAIROS_INIT_IMAGE ?= quay.io/kairos/kairos-init:v0.14.6
33+
KAIROS_INIT_IMAGE ?= quay.io/kairos/kairos-init:v0.14.6@sha256:e53eb7e5ada035e7e192f072f9e041ca5d60440ecf8c766c32e7d95253b293e7
3234
IMAGE ?= kairos-kubeadm:$(VERSION)
3335

3436
# e2e (ADR-13). The node image FROM-derives the kairos-kubeadm base so it reuses
@@ -39,7 +41,7 @@ BASE_IMAGE ?= kairos-kubeadm:$(KUBERNETES_VERSION)
3941
E2E_NODE_IMAGE ?= kairos-kubeadm-e2e-node:$(KUBERNETES_VERSION)
4042
E2E_TIMEOUT ?= 40m
4143

42-
.PHONY: all build test vet fmt fmt-check lint tidy image clean e2e-node-image e2e
44+
.PHONY: all build test vet fmt fmt-check lint tidy verify-pins image clean e2e-node-image e2e
4345

4446
all: build
4547

@@ -71,6 +73,22 @@ lint:
7173
tidy:
7274
$(GO) mod tidy
7375

76+
## verify-pins: fail if a digest-pinned base image drifts between Dockerfile and
77+
## Makefile, or is not digest-pinned at all (kairos#4203 supply-chain guard).
78+
verify-pins:
79+
@fail=0; \
80+
for var in KAIROS_BASE_IMAGE KAIROS_INIT_IMAGE; do \
81+
df="$$(sed -n "s/^ARG $$var=//p" Dockerfile | head -1)"; \
82+
mk="$$(sed -n "s/^$$var[[:space:]]*?=[[:space:]]*//p" Makefile | head -1)"; \
83+
case "$$df" in *@sha256:*) : ;; *) echo "FAIL: Dockerfile $$var is not digest-pinned: '$$df'"; fail=1 ;; esac; \
84+
if [ "$$df" != "$$mk" ]; then \
85+
echo "FAIL: $$var drift:"; echo " Dockerfile: $$df"; echo " Makefile: $$mk"; fail=1; \
86+
else \
87+
echo "ok: $$var = $$df"; \
88+
fi; \
89+
done; \
90+
if [ "$$fail" != 0 ]; then echo "re-resolve with: docker buildx imagetools inspect <image:tag>"; exit 1; fi
91+
7492
## image: build the Kairos image bundling provider + kubeadm + containerd
7593
## (requires Docker; supply-chain-verified downloads inside the Dockerfile).
7694
image:

0 commit comments

Comments
 (0)