diff --git a/.gitea/workflows/build_push.yml b/.gitea/workflows/build_push.yml index 4f46187..d013b01 100644 --- a/.gitea/workflows/build_push.yml +++ b/.gitea/workflows/build_push.yml @@ -126,8 +126,9 @@ jobs: # The kubeconfig embeds the k3s server CA. If the cluster's CA has # been rotated since the secret was created, kubectl cannot verify # the server. The script refreshes the CA, but only if it matches - # the fingerprint pinned in the secret - see the script for why. - bash scripts/kube-ca-refresh.sh "${{ secrets.KUBE_SERVER_CA_FINGERPRINT }}" + # the fingerprint pinned in scripts/cluster-ca.sha256 - see the + # script for why. + bash scripts/kube-ca-refresh.sh - name: Install kubectl run: | diff --git a/scripts/cluster-ca.sha256 b/scripts/cluster-ca.sha256 new file mode 100644 index 0000000..3f6664b --- /dev/null +++ b/scripts/cluster-ca.sha256 @@ -0,0 +1,17 @@ +# SHA-256 fingerprint of the cluster's API server CA (the k3s server-ca.crt). +# +# This is a PUBLIC value, not a secret - it can be read straight off the +# certificate. It lives in the repo rather than in a Gitea secret so that +# changing it is a reviewable commit, and so nobody with secret-write access +# can silently repoint the CI runner at a different CA. +# +# The deploy job fetches the cluster's CA over an unverified connection and +# compares its fingerprint against this value. A CA that does not match is +# refused, so an attacker able to intercept the fetch cannot substitute +# their own. +# +# To refresh it after a legitimate CA rotation: +# openssl x509 -in /var/lib/rancher/k3s/server/tls/server-ca.crt # -noout -fingerprint -sha256 +# and update the line below (colons are stripped before comparison, so either +# format works). +d23a17bb9c5f7c66d15e15635d6bb928e3a34f38142705e4e6516075a5f35907 diff --git a/scripts/kube-ca-refresh.sh b/scripts/kube-ca-refresh.sh index f513919..73a6b09 100755 --- a/scripts/kube-ca-refresh.sh +++ b/scripts/kube-ca-refresh.sh @@ -12,13 +12,21 @@ # # The refresh is PINNED: the server's CA is fetched over an unverified # connection, but it is only used if its SHA-256 fingerprint matches the value -# in the KUBE_SERVER_CA_FINGERPRINT secret. An attacker able to intercept -# the fetch cannot use it - their CA will not hash to the pinned fingerprint. +# pinned in scripts/cluster-ca.sha256. An attacker able to intercept the +# fetch cannot use it - their CA will not hash to the pinned fingerprint. # The runner has cluster-admin credentials, so trusting an unverified CA here # would be a privilege-escalation hole. # +# The fingerprint is a PUBLIC value - it can be read straight off the +# certificate - so it lives in the repo rather than in a secret. That way a +# change to it is a reviewable commit, and nobody with secret-write access +# can silently repoint the CI runner at a different CA. +# +# The expected fingerprint is read from scripts/cluster-ca.sha256 next to +# this script. It can be overridden with the first argument. +# # Usage: -# kube-ca-refresh.sh +# kube-ca-refresh.sh [expected-fingerprint] # # Exit codes: # 0 the cluster is reachable @@ -26,7 +34,12 @@ set -euo pipefail +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) EXPECTED="${1:-}" +if [ -z "$EXPECTED" ] && [ -f "$SCRIPT_DIR/cluster-ca.sha256" ]; then + # The file may carry comments; take the last non-comment line. + EXPECTED=$(grep -vE '^[[:space:]]*(#|$)' "$SCRIPT_DIR/cluster-ca.sha256" | tail -1 | tr -d '[:space:]') +fi if kubectl version --request-timeout=15s >/dev/null 2>&1; then echo "cluster reachable" @@ -36,8 +49,8 @@ fi if [ -z "$EXPECTED" ]; then echo "ERROR: the kubeconfig cannot verify the API server, and no expected CA" >&2 echo " fingerprint was supplied, so the CA cannot be refreshed safely." >&2 - echo " Set KUBE_SERVER_CA_FINGERPRINT to the SHA-256 fingerprint of the" >&2 - echo " cluster's CA:" >&2 + echo " and no pinned CA fingerprint could be found." >&2 + echo " Set one in scripts/cluster-ca.sha256:" >&2 echo " openssl x509 -in server-ca.crt -noout -fingerprint -sha256" >&2 exit 1 fi @@ -72,7 +85,7 @@ echo "actual CA fingerprint: $ACTUAL" if [ "$ACTUAL" != "$WANTED" ]; then echo "ERROR: the cluster CA does not match the pinned fingerprint." >&2 echo " Refusing to use it. Either the CA has been rotated and the" >&2 - echo " fingerprint secret needs updating, or the connection was" >&2 + echo " pinned fingerprint needs updating, or the connection was" >&2 echo " intercepted." >&2 exit 1 fi