Repository navigation
fix(init): read the state-key mode from the volume after the first boot - #305
Merged
Merged
Conversation
A node whose state_key.mode came from its machine config and differed from the image's build-time default could not come back from any reboot. init resolved the mode before unlock from the ESP config stage only, and the stage exists only on the first boot, so every later boot fell back to the build-time mode and opened the volume with the wrong protector. Later boots now derive the mode from the LUKS2 header the volume was sealed with: a cryptos-tpm2 token means tpm, a cryptos-kms token means kms (with the endpoint and trust it carries), and no protector token means nodeid. The first boot still takes it from the staged config, else the build-time default. The multi-token TPM header left by an upgrade still reads as tpm. The KMS token carried no type or keyslots, which LUKS2 requires, so cryptsetup refused to import it and kms mode could not finish a first boot. It now has type cryptos-kms and the state keyslot. The mode is fixed at install. ApplyConfig on a running node and the re-provision installer refuse a config naming another state_key.mode with FailedPrecondition, and a boot whose persisted config disagrees logs it and keeps the sealed mode. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Bugs5382
force-pushed
the
fix/297-state-key-mode-before-unlock
branch
from
September 30, 2026 16:30
a026075 to
beac95e
Compare
4 tasks done
Contributor
Author
|
Merging for the v0.1.0 test run: CI green and mergeable at merge time. |
This was referenced Sep 30, 2026
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.
What and why
A node whose
state_key.modecomes from its machine config, and differs from the image's build-time default, can't come back from any reboot after its first boot. For example,nodeidon aSTATEKEY=tpmimage.Root cause.
bootresolved the mode before unlock withpreUnlockStateKey, which reads only the ESP config stage.loadOrSeedConfigdeletes that stage on the first boot. On every later boot the function returned the zeroStateKey, init fell back toStateKeyMode(the build-time default), andOpenStateVolumeused the wrong protector. For example, it looked for a TPM token on anodeidvolume, which has none, or derived a UUID key for a TPM-sealed volume. Withpanic=1the node reboot-loops.Fix: read the mode from the LUKS2 header (option a). The volume records how it was sealed, so later boots derive the mode from its token types:
cryptos-tpm2token (one per bootable image after an upgrade or rollback)tpmcryptos-kmstokenkms, with the endpoint and trust bundle the token carriesnodeid(it persists none)The first boot still takes the mode from the staged config, else the build-time default. I chose this over persisting the mode on the ESP (option b) because the header can't drift from how the volume was actually sealed. It adds no new file for an upgrade or a rollback to keep in step, and the multi-token TPM header from #264/#269 keeps reading as
tpm.The pre-unlock sequence moved into a small seam,
stateUnlockerininternal/init/stateunlock.go, so it can be tested end to end: first boot, later boots, and opening the volume.bootcalls it in place of the inline code. The state device is now resolved before the backends are built (the TPM is opened afterIsLUKS, not before).Found while testing: KMS mode could not finish a first boot. Its LUKS2 token had no
typeorkeyslots, which LUKS2 requires, so realcryptsetup token importrefused it (Failed to import token from file.). The token now hastype: cryptos-kmsand the state keyslot. That type is also how a later boot recognizes a KMS volume. No KMS node can exist with the old token, because none could get through its first boot.Changing the mode on an installed node is refused. The mode is fixed at install.
ApplyConfigon a running node (node.ConfigStore.WithSealedStateKeyMode) refuses a config that names a differentstate_key.modewithFailedPrecondition, and nothing is written.So the node can't end up unbootable. To change the mode, reinstall.
Closes #297
Related: #296 (where it was found; its image suite skips the Root upgrade step for this bug, and that step can be re-enabled once #296 rebases onto this), #264, #269.
Docs:
build/README.md(theSTATEKEYsection says the config can override it, the mode is fixed at install, and a change is refused) anddocs/image-upgrade.md(a new "State-key mode" section: an upgrade never changes the mode). Companion docs PR: CryptOS-PKI/docs#52 updates thestate_keyreference and the sealed-keys concept page, which today tells readers to match the mode to the image because of this bug. The README needs no change.Verification
golangci-lint: 0 issues;go vet;gofmt;task license)go test ./...)How this was verified
Tests first.
internal/init/stateunlock_test.gowas written against the seam while it still had the old behaviour. It failed for the reported reason:tpm image, nodeid configured, boot 2:no cryptos-tpm2 token recovers the key ... Token is not in use.nodeid image, tpm configured, boot 2:luks: Open: ... No key available with this passphrase.kmscases, boot 1:ImportToken: ... Failed to import token from file.All pass after the fix.
New tests:
TestStateUnlock_ConfiguredModeSurvivesReboots, against an in-memory LUKS2 header. Cases: tpm image with nodeid configured, nodeid image with tpm, tpm or nodeid image with kms, and no mode configured on either image. Each case runs a first boot and two later boots, and each boot checks the mode, the protector, and that the volume opens with the right key.TestStateUnlock_ConfiguredModeSurvivesReboots_RealCryptsetup: the same cases against a real LUKS2 header in a file. The key check usesluksOpen --test-passphrase, so it needs no root.TestStateUnlock_UpgradedTPMVolume_RealCryptsetup: after a reseal adds a second TPM token and the install copy stops unsealing, the volume still reads astpmand opens with the other copy.TestStateUnlock_LaterStageCannotChangeTheMode: a stage that shows up on a later boot doesn't override the sealed mode.TestSealedStateKey: the derivation table, including foreign token types and the TPM+KMS refusal.TestStateKeyCheckSealed,TestConfigStoreApply_RefusesStateKeyModeChange,TestReprovisioner_RefusesStateKeyModeChange: a mode change is refused withFailedPrecondition, and nothing is persisted or rebooted.Full gate:
task ci(fmt, lint, vet, test, build) passes locally, with cryptsetup 2.7.0 present so the real-cryptsetup tests ran.QEMU + swtpm:
Debug (
qemu-dev) UKIs built locally frommain(0c657fb) and from this branch (3c1d744). Each ran in QEMU (TCG, OVMF, swtpm, SMBIOS UUID set) from a pre-installed disk withstate_key.modestaged on the ESP. The run is a first boot plus two reboots on the same disk and TPM state. "Up" means the mTLS listener answered.main,STATEKEY=tpmnodeidstate key mode: nodeidstate key mode: tpm,OpenStateVolume: no cryptos-tpm2 token recovers the keymain,STATEKEY=nodeidtpmstate key mode: tpmstate key mode: nodeid,OpenStateVolume: recover key: nodeid: ...STATEKEY=tpmnodeidnodeidthe volume is sealed in nodeid modeSTATEKEY=nodeidtpmtpmthe volume is sealed in tpm modeThe
mainruns reproduce the reported bug in both directions. The branch runs come back on every reboot with the mode the volume was sealed with. KMS wasn't booted, because there's no KMS endpoint in this setup. It's covered by the unit and real-cryptsetup tests. The #296 image suite's skipped Root upgrade step wasn't re-enabled here, because #296 isn't onmainand this PR leaves it alone. It can be turned back on when #296 rebases onto this.Before merging: add a closing comment summarizing what was actually done in this PR
(not just the checked boxes).