feat(images): build on rootless BuildKit over mTLS; verification moves into Containerfile verify stages
Operator decision on PR #81: no CI job may reach a privileged daemon. The act runner is untouched. - platform/buildkit (+ clusters/base/buildkit.yaml): moby/buildkit v0.34.1-rootless pinned by digest, uid 1000, not privileged; caps drop ALL + SETUID/SETGID, seccomp/AppArmor Unconfined, read-only rootfs (each measured under podman). cert-manager CA + server/client certs, ClusterIP :1234, default-deny with ingress only from the act-runner pod (DinD masquerades jobs to it). No Kyverno exception: every applied policy matches app-namespace=true only. - Each Containerfile names its shipped stage and ends in a `verify` stage holding every former `docker run` assertion (uid 65532, RUN --network=none, writable-path inventory standing in for the read-only rootfs). The old `&& echo` checks that could never fail are real assertions now. - images.sh drives buildkitd with a pinned buildx (sha256-checked) via bake; PRs build verify targets with agent-runner FROM the in-run toolbox (target: context); main verifies, then pushes. Fails loudly when the builder secrets are absent or buildkitd is unreachable. - infra/gitea/buildkit-client-secret.sh copies the client cert into Actions secrets. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
65a7769f55
commit
310deb0f7d
28 files changed
+1490
-608
No files matched your search
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copy the image builder's CLIENT certificate from the cluster into the three repository Actions
|
||||
# secrets the image pipeline reads: BUILDKIT_CA, BUILDKIT_CLIENT_CERT, BUILDKIT_CLIENT_KEY.
|
||||
#
|
||||
# ./infra/gitea/buildkit-client-secret.sh # copy and verify
|
||||
# ./infra/gitea/buildkit-client-secret.sh --check # compare only; changes nothing
|
||||
#
|
||||
# RUNNING IT IS AN OPERATOR ACTION. It writes repository secrets.
|
||||
#
|
||||
# WHERE THE CERTIFICATE COMES FROM. platform/buildkit/certificates.yaml has cert-manager issue
|
||||
# `buildkit-client-ci` from a CA that exists only for the builder; buildkitd admits a connection
|
||||
# only with a certificate signed by that CA. CI jobs hold NO cluster credential (R-SEC-02), so they
|
||||
# cannot read the Secret themselves: this script is the one bridge, run by a human who has both
|
||||
# a kube context and the Gitea credential.
|
||||
#
|
||||
# WHEN TO RUN IT
|
||||
# 1. once, after Flux has applied platform/buildkit (Kustomization `buildkit` Ready, and
|
||||
# `kubectl -n buildkit get certificate buildkit-client-ci` READY=True);
|
||||
# 2. after every renewal of that certificate (duration 1y, renewed 30 days early). Until then
|
||||
# the image pipeline fails LOUDLY at its builder preflight — it never skips.
|
||||
# `--check` tells you whether the copy is stale.
|
||||
#
|
||||
# NOTHING SECRET IS PRINTED. The key goes from `kubectl get secret` into the Gitea secrets API
|
||||
# through shell variables and nowhere else. The script prints only names, sizes and the
|
||||
# certificate's subject and expiry (public data).
|
||||
#
|
||||
# NEEDS: kubectl with a context for the cluster (default k3s-prod), jq, curl, openssl, base64,
|
||||
# and the Gitea credential `git` already has stored for the host — the same requirements as
|
||||
# infra/gitea/conformance-trigger-kubeconfig.sh, run from the same place.
|
||||
set -euo pipefail
|
||||
HOST=gitea.home.keithjackson.dev; OWNER=Home-Network; REPO=home-network-k3s-impl
|
||||
NS=buildkit; SECRET=buildkit-client-ci; KCTX="${KCTX:-k3s-prod}"
|
||||
CHECK=0
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--check) CHECK=1 ;;
|
||||
-h|--help) sed -n '2,/^set -euo/p' "${BASH_SOURCE[0]}" | sed '$d' | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "unknown argument: $a" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
for t in kubectl jq curl openssl base64 git; do command -v "$t" >/dev/null || die "missing required tool: $t"; done
|
||||
|
||||
echo "==> reading Secret ${NS}/${SECRET} (context ${KCTX}; values not shown)"
|
||||
js="$(kubectl --context "$KCTX" -n "$NS" get secret "$SECRET" -o json)" \
|
||||
|| die "cannot read ${NS}/${SECRET} — is platform/buildkit deployed and the Certificate Ready?"
|
||||
CA="$(jq -r '.data["ca.crt"] // empty' <<<"$js" | base64 -d)"
|
||||
CRT="$(jq -r '.data["tls.crt"] // empty' <<<"$js" | base64 -d)"
|
||||
KEY="$(jq -r '.data["tls.key"] // empty' <<<"$js" | base64 -d)"
|
||||
unset js
|
||||
[ -n "$CA" ] && [ -n "$CRT" ] && [ -n "$KEY" ] || die "${NS}/${SECRET} lacks ca.crt/tls.crt/tls.key"
|
||||
|
||||
# Sanity before anything is written: the leaf is a CLIENT cert issued by this CA, and the key
|
||||
# belongs to it. A wrong pairing would otherwise surface only as a TLS failure in CI.
|
||||
openssl verify -CAfile <(printf '%s\n' "$CA") <(printf '%s\n' "$CRT") >/dev/null 2>&1 \
|
||||
|| die "tls.crt does not verify against ca.crt"
|
||||
openssl x509 -noout -ext extendedKeyUsage <<<"$CRT" 2>/dev/null | grep -q "TLS Web Client Authentication" \
|
||||
|| die "tls.crt is not a client-auth certificate"
|
||||
[ "$(openssl x509 -noout -pubkey <<<"$CRT" | openssl sha256)" = "$(openssl pkey -pubout <<<"$KEY" 2>/dev/null | openssl sha256)" ] \
|
||||
|| die "tls.key does not match tls.crt"
|
||||
echo " $(openssl x509 -noout -subject <<<"$CRT") / $(openssl x509 -noout -enddate <<<"$CRT")"
|
||||
|
||||
creds="$(printf 'protocol=https\nhost=%s\n\n' "$HOST" | git credential fill)"
|
||||
U="$(sed -n 's/^username=//p' <<<"$creds")"; P="$(sed -n 's/^password=//p' <<<"$creds")"; unset creds
|
||||
[ -n "$U" ] && [ -n "$P" ] || die "no stored credential for $HOST"
|
||||
echo "==> authenticating to Gitea as '${U}' (secret not shown)"
|
||||
api() { curl -sS --config <(printf 'user = "%s:%s"\n' "$U" "$P") "$@"; }
|
||||
API="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/actions/secrets"
|
||||
|
||||
if [ "$CHECK" -eq 1 ]; then
|
||||
# Gitea never returns a secret's value, only its name and creation time. "Stale" therefore
|
||||
# means: the secret was created before the certificate's current notBefore.
|
||||
names="$(api "$API" | jq -c '[.[]? | {name, created_at}]')"
|
||||
nb="$(date -u -d "$(openssl x509 -noout -startdate <<<"$CRT" | cut -d= -f2)" +%s)"
|
||||
rc=0
|
||||
for n in BUILDKIT_CA BUILDKIT_CLIENT_CERT BUILDKIT_CLIENT_KEY; do
|
||||
c="$(jq -r --arg n "$n" '.[] | select(.name==$n) | .created_at' <<<"$names")"
|
||||
if [ -z "$c" ]; then echo " MISSING $n"; rc=1
|
||||
elif [ "$(date -u -d "$c" +%s)" -lt "$nb" ]; then echo " STALE $n (stored $c, certificate re-issued since)"; rc=1
|
||||
else echo " current $n (stored $c)"; fi
|
||||
done
|
||||
exit "$rc"
|
||||
fi
|
||||
|
||||
put() { # name value
|
||||
local code
|
||||
code="$(api -o /dev/null -w '%{http_code}' -X PUT -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg d "$2" '{data:$d}')" "${API}/$1")"
|
||||
[ "$code" = 201 ] || [ "$code" = 204 ] || die "PUT $1 failed: HTTP ${code}"
|
||||
echo " stored $1 (${#2} bytes, value not shown)"
|
||||
}
|
||||
echo "==> storing repository Actions secrets"
|
||||
put BUILDKIT_CA "$CA"
|
||||
put BUILDKIT_CLIENT_CERT "$CRT"
|
||||
put BUILDKIT_CLIENT_KEY "$KEY"
|
||||
unset CA CRT KEY
|
||||
|
||||
# VERIFY BY READING BACK (names only — Gitea never returns values). curl -sS exits 0 on an HTTP
|
||||
# error, which is how set-ci-token.sh once claimed a store that never happened.
|
||||
names="$(api "$API" | jq -r '[.[]?.name] | join(" ")')"
|
||||
for n in BUILDKIT_CA BUILDKIT_CLIENT_CERT BUILDKIT_CLIENT_KEY; do
|
||||
grep -qw "$n" <<<"$names" || die "stored $n but it is NOT listed (${names}) — refusing to claim success"
|
||||
done
|
||||
echo "==> stored and VERIFIED present. Re-run the images workflow (workflow_dispatch) to use them."
|
||||
Reference in new issue
Block a user