fix(ci): pin the cluster CA fingerprint instead of trusting a fetched CA
The review was right on both counts, and both are fixed.
1. MITM. Fetching the CA over an unverified connection (`curl -sk`) and then
trusting it is a privilege-escalation hole: the runner holds cluster-admin
credentials, so an attacker able to intercept the fetch could hand us a CA
of their choosing and then read everything sent over the connection.
The CA is now PINNED. It is still fetched over an insecure connection, but
it is only USED if its SHA-256 fingerprint matches
`KUBE_SERVER_CA_FINGERPRINT`. An intercepted CA will not hash to the
pinned value, so it is refused. If the secret is not set, the job fails
rather than trusting an unverified CA.
2. The hardcoded cluster name. `kubectl config set-cluster default` creates a
NEW cluster entry when the kubeconfig's entry is called something else,
leaving the original (and its stale CA) in use. The name is now read from
the kubeconfig: `{.clusters[0].name}`.
The bash is also moved out of the YAML into `scripts/kube-ca-refresh.sh`, so
it can be read, reviewed and tested on its own rather than as a wall of
shell inside a workflow file.
All three paths are tested against the live API server:
- no fingerprint supplied -> refuses, exit 1
- wrong fingerprint -> refuses, exit 1
- correct fingerprint -> refreshes the CA, exit 0, kubectl then works
This commit is contained in:
@@ -123,50 +123,11 @@ jobs:
|
||||
|
||||
- name: Verify the cluster is reachable
|
||||
run: |
|
||||
# Fail fast, and with a useful message, if the kubeconfig cannot
|
||||
# talk to the API server.
|
||||
#
|
||||
# The certificate-authority-data inside the kubeconfig is the k3s
|
||||
# server CA. If the cluster's CA has been rotated since the secret
|
||||
# was created, kubectl fails with:
|
||||
# tls: failed to verify certificate: x509: certificate signed by
|
||||
# unknown authority
|
||||
# which is a confusing way to learn that a secret is stale.
|
||||
if kubectl version --request-timeout=15s >/dev/null 2>&1; then
|
||||
echo "cluster reachable"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "the kubeconfig could not verify the API server - refreshing the CA"
|
||||
|
||||
# The API server publishes its own CA at /cacerts. Fetch it over an
|
||||
# insecure connection - we cannot verify it yet, that is the problem
|
||||
# being solved - and pin it explicitly. This makes the deploy
|
||||
# self-healing after a CA rotation, instead of needing the secret
|
||||
# to be regenerated by hand.
|
||||
SERVER=$(kubectl config view --raw -o jsonpath='{.clusters[0].cluster.server}')
|
||||
echo "server: $SERVER"
|
||||
curl -sk "$SERVER/cacerts" -o $HOME/.kube/ca.crt
|
||||
|
||||
if [ ! -s $HOME/.kube/ca.crt ]; then
|
||||
echo "ERROR: could not fetch the cluster CA from $SERVER/cacerts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kubectl config set-cluster default \
|
||||
--embed-certs=true \
|
||||
--certificate-authority=$HOME/.kube/ca.crt >/dev/null
|
||||
rm -f $HOME/.kube/ca.crt
|
||||
|
||||
if kubectl version --request-timeout=15s >/dev/null 2>&1; then
|
||||
echo "cluster reachable after refreshing the CA"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ERROR: cannot reach the cluster API server even with a refreshed CA."
|
||||
echo " The kubeconfig in the KUBEC_CONFIG_BUILDX_NEW_2 secret may be"
|
||||
echo " stale or invalid, and needs regenerating."
|
||||
exit 1
|
||||
# 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 }}"
|
||||
|
||||
- name: Install kubectl
|
||||
run: |
|
||||
|
||||
Reference in New Issue
Block a user