fix(ci): pin the cluster CA in the repo rather than in a secret
The CA fingerprint is not a secret. It is a public value that anyone can read straight off the certificate, so pinning it in a Gitea secret was the wrong shape: - A change to it would be invisible - no commit, no review. - Nobody with secret-write access could silently repoint the CI runner at a different CA. It now lives in `scripts/cluster-ca.sha256`, so changing it is a reviewable commit. The script reads it from there, and still accepts an override as its first argument. This also removes the need for a `KUBE_SERVER_CA_FINGERPRINT` secret, which the bot token cannot create anyway - the Actions secrets API is owner-only. The behaviour is unchanged: the CA is fetched over an unverified connection but only USED if it matches the pinned fingerprint, so an intercepted CA is refused. Re-tested against the live API server - the matching fingerprint refreshes the CA and kubectl then works; a wrong one is refused and the stale CA is left in place.
This commit is contained in:
@@ -126,8 +126,9 @@ jobs:
|
|||||||
# The kubeconfig embeds the k3s server CA. If the cluster's CA has
|
# The kubeconfig embeds the k3s server CA. If the cluster's CA has
|
||||||
# been rotated since the secret was created, kubectl cannot verify
|
# been rotated since the secret was created, kubectl cannot verify
|
||||||
# the server. The script refreshes the CA, but only if it matches
|
# the server. The script refreshes the CA, but only if it matches
|
||||||
# the fingerprint pinned in the secret - see the script for why.
|
# the fingerprint pinned in scripts/cluster-ca.sha256 - see the
|
||||||
bash scripts/kube-ca-refresh.sh "${{ secrets.KUBE_SERVER_CA_FINGERPRINT }}"
|
# script for why.
|
||||||
|
bash scripts/kube-ca-refresh.sh
|
||||||
|
|
||||||
- name: Install kubectl
|
- name: Install kubectl
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,13 +12,21 @@
|
|||||||
#
|
#
|
||||||
# The refresh is PINNED: the server's CA is fetched over an unverified
|
# 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
|
# 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
|
# pinned in scripts/cluster-ca.sha256. An attacker able to intercept the
|
||||||
# the fetch cannot use it - their CA will not hash to the pinned fingerprint.
|
# 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
|
# The runner has cluster-admin credentials, so trusting an unverified CA here
|
||||||
# would be a privilege-escalation hole.
|
# 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:
|
# Usage:
|
||||||
# kube-ca-refresh.sh <expected-fingerprint>
|
# kube-ca-refresh.sh [expected-fingerprint]
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
# 0 the cluster is reachable
|
# 0 the cluster is reachable
|
||||||
@@ -26,7 +34,12 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
EXPECTED="${1:-}"
|
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
|
if kubectl version --request-timeout=15s >/dev/null 2>&1; then
|
||||||
echo "cluster reachable"
|
echo "cluster reachable"
|
||||||
@@ -36,8 +49,8 @@ fi
|
|||||||
if [ -z "$EXPECTED" ]; then
|
if [ -z "$EXPECTED" ]; then
|
||||||
echo "ERROR: the kubeconfig cannot verify the API server, and no expected CA" >&2
|
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 " 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 " and no pinned CA fingerprint could be found." >&2
|
||||||
echo " cluster's CA:" >&2
|
echo " Set one in scripts/cluster-ca.sha256:" >&2
|
||||||
echo " openssl x509 -in server-ca.crt -noout -fingerprint -sha256" >&2
|
echo " openssl x509 -in server-ca.crt -noout -fingerprint -sha256" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -72,7 +85,7 @@ echo "actual CA fingerprint: $ACTUAL"
|
|||||||
if [ "$ACTUAL" != "$WANTED" ]; then
|
if [ "$ACTUAL" != "$WANTED" ]; then
|
||||||
echo "ERROR: the cluster CA does not match the pinned fingerprint." >&2
|
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 " 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
|
echo " intercepted." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user