Rekey unseal keys too, and stop losing the key that unseals vault-unseal - #86
Merged
Merged
Conversation
rotate-keys.sh covered the recovery-key path only, because the local
cluster is auto-unsealed and its shares are recovery keys. The
unseal-key path had no coverage at all. --unseal-keys runs the same
ceremony against sys/rekey rather than sys/rekey-recovery-key, and
tests/key-rotation exercises it against vault-unseal, which is the
Shamir-sealed Vault underneath the profile.
Which kind of key a Vault has depends only on how it is sealed, and
migrate-seal.sh turns each into the other without changing their values,
so this is one ceremony against two endpoints rather than two scripts.
The CLI calls the second one "barrier" and makes it the default target,
which is worth knowing: `vault operator rekey` with no arguments, run
against an auto-unsealed cluster, addresses a set of keys that cluster
does not use.
THE PREREQUISITE, WHICH TURNED OUT TO BE A BUG
Rekeying needs a quorum of the current shares, and vault-unseal had
none. bootstrap-dev-cluster.sh held its unseal key and root token in
shell variables and never wrote them down.
That made a single `docker compose restart vault-unseal` unrecoverable,
which is not hypothetical -- it is what a Docker Desktop restart or a
host reboot does. And it does not fail gracefully. vault-unseal comes
back sealed, and a cluster node restarted after that does not come back
sealed, it fails to start:
error parsing Seal configuration: ... 503 * Vault is sealed
with no key anywhere to fix it. The only route back was `make destroy`,
which is to say: losing the cluster because the container providing its
seal was restarted. This is the same shape as the recovery keys the
bootstrap used to discard, fixed in v0.15, in the one place where it
costs more.
docker/dev/.unseal-keys.json is written 0600 and gitignored, and the
suite restarts vault-unseal on every run to prove the kept keys open it
and that a cluster node auto-unseals against it afterwards.
ONE STALE SHARE PROVES NOTHING
The suite rekeys vault-unseal twice -- 1-of-1 to 5-of-3, then again --
so that a full quorum of the superseded generation exists to test with.
Vault accepts unseal shares and only validates the combination once the
threshold is reached, so a single stale share returns success and 1/3
progress. An assertion built on one old share would pass whether or not
the rekey did anything, and would leave that progress behind to break
the next unseal. I found that by writing exactly that assertion first.
tests/key-rotation goes from 28 assertions to 46. Three mutations, all
watched to fail; the table records what each actually broke rather than
what it was aimed at, because U3 missed the two assertions it was
pointed at and was caught by others.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The security scan failed on the Shamir rekey change with two
generic-api-key findings, both false. Vault returns key shares under
unseal_keys_b64 or recovery_keys_b64 depending on how the Vault is
sealed, and rotate-keys.sh picks between them with a variable:
KEY_FIELD="unseal_keys_b64"
which the rule reads as KEY = <high-entropy value>, entropy 3.5, for a
string that appears verbatim in Vault's own documentation.
There was no .gitleaks.toml at all until now -- the run logs "no gitleaks
config found in path .gitleaks.toml, using default gitleaks config" -- so
this adds one that keeps every default rule via useDefault and records
the single accepted finding, on the same terms .trivyignore.yaml sets: an
entry states what it permits and why.
The allowlist is anchored to those two exact strings rather than to the
file, the line or the rule. That distinction is the whole point, and it
is verified rather than asserted: with the config in place, a real Vault
service token and a high-entropy generic secret in the same file, on
adjacent lines, are both still reported. Only the field names are muted.
Reproduced locally against the same commit CI scanned, with the same
gitleaks version, before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
rotate-keys.shcovered the recovery-key path only, because the local cluster is auto-unsealed and its shares are recovery keys. The unseal-key path had no coverage at all.--unseal-keysruns the same ceremony againstsys/rekeyinstead ofsys/rekey-recovery-key, exercised againstvault-unseal— the Shamir-sealed Vault underneath the profile. Which kind of key a Vault has depends only on how it's sealed, andmigrate-seal.shturns each into the other without changing their values, so this is one ceremony against two endpoints rather than two scripts.--recovery-keyssys/rekey-recovery-key/*vault operator rekey -target=recovery--unseal-keyssys/rekey/*vault operator rekeyThe CLI calls the second one barrier and makes it the default target — so
vault operator rekeywith no arguments, run against an auto-unsealed cluster, addresses a set of keys that cluster does not use.The prerequisite turned out to be a bug
Rekeying needs a quorum of the current shares, and
vault-unsealhad none.bootstrap-dev-cluster.shheld its unseal key and root token in shell variables and never wrote them down.That made a single
docker compose restart vault-unsealunrecoverable — which is what a Docker Desktop restart or a host reboot does. And it doesn't fail gracefully.vault-unsealcomes back sealed, and a cluster node restarted afterwards doesn't come back sealed either; it fails to start:with no key anywhere to fix it. The only route back was
make destroy— losing the cluster because the container providing its seal was restarted.This is the same shape as the recovery keys the bootstrap used to discard, fixed in v0.15, in the one place where it costs more.
docker/dev/.unseal-keys.jsonis written 0600 and gitignored now, and the suite restartsvault-unsealon every run to prove the kept keys open it and that a cluster node auto-unseals against it afterwards.One stale share proves nothing
The suite rekeys
vault-unsealtwice — 1-of-1 to 5-of-3, then again — so a full quorum of the superseded generation exists to test with.Vault accepts unseal shares and only validates the combination once the threshold is reached, so a single stale share returns success and 1/3 progress. An assertion built on one old share would pass whether or not the rekey did anything — and would leave that progress behind to break the next unseal. I know because I wrote exactly that assertion first and watched it lie to me.
Testing
tests/key-rotationgoes from 28 assertions to 46. Three mutations, all watched to fail.vault-unseal's keys are not kept, as before this change--unseal-keysaddresses the recovery endpointrecovery_keys_b64U3 is recorded for what it actually broke rather than what it was aimed at: it missed both assertions about handing the wrong key file to the wrong mode, because the run was refused anyway for a different reason and the refusal happened to satisfy them. The table records observations, not intentions.
Still uncovered
A rekey of a Shamir-sealed cluster — several nodes, each needing the new shares — as opposed to the single-node
vault-unseal. And migration between two cloud KMS providers, which needs a cloud account.🤖 Generated with Claude Code