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]>
105 lines
5.7 KiB
Bash
Executable File
105 lines
5.7 KiB
Bash
Executable File
#!/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."
|