Encryption Key Rotation¶
The API servers reload /etc/kubernetes/enc/encryption-config.yaml whenever
it changes (encryption-provider-config-automatic-reload in
ansible/templates/kubeadm.yaml.j2), so a rotation is a file edit on each
control plane and one rewrite of every Secret; what the key protects and how it
is generated is in Encryption at rest.
The first key in the list encrypts, every key in the list decrypts, and a
Secret's ciphertext names the key it was written with. Two things follow: the
new key goes in above the old one, and the cluster has to end up with the new
key under the name the Butane template renders (key1 in
ansible/templates/butane_node_config.yaml.j2), because a control plane
provisioned later gets exactly that file and can read nothing written under
another name. That is why the Secrets are rewritten twice.
Reading or writing the file from a shell fires TetragonSensitiveFileAccess;
that is the alert doing its job, not a problem to fix.
Runbook¶
-
Generate the new key beside the old one, in the shape
config.yaml'spasswordlookup produces: 32 letters and digits, stored in plain text, and base64-encoded where it goes into the config. -
On every control plane, add the new key as
key2abovekey1in/etc/kubernetes/enc/encryption-config.yaml; the rest of the file stays as it is. -
Wait for every API server to reload. Each reloads its own file, so ask each one by its
ansible_hostrather than through the VIP; astatus="failure"count means the file did not parse and the old config is still in effect. -
Rewrite every Secret so it is encrypted with
key2. A conflict on a Secret something updated mid-stream is harmless: run it again. -
On every control plane, replace
key1's secret with the new key and move it back abovekey2; both entries now hold the new key. Wait for the reload as in step 3. -
Rewrite every Secret again, so every ciphertext names
key1. -
On every control plane, remove
key2. The file now matches what Ignition writes for a new control plane, and nothing on any node holds the old key. -
Make the new key the one
make configrenders, and verify that the Butane config of every control plane carries it askey1;config.yamlreads the file back instead of generating a new value, so nothing else changes. The Ignition JSON cannot be grepped, since Butane gzips file contents, so the check reads the Butane YAML it was transpiled from inoutput/tmp/.mv output/credentials/encryption_key.new output/credentials/encryption_key make config && grep -c "$(tr -d '\n' < output/credentials/encryption_key | base64)" output/tmp/butane/butane-*.yamlOne match per control plane, none per worker or installer config.
-
Confirm against etcd that a Secret's ciphertext names the new key, with the same
etcdctlflags as Taking one by hand; the value starts withk8s:enc:secretbox:v1:key1:.
Store the new key with the rest of output/credentials/ wherever the old one
is kept, and keep the old one next to the etcd snapshots taken before the
rotation: those restore only with the key they were written under.