Every image you run is signed by you. CryptOS does not ship a signing key, and it does not ask you to trust one. You generate your own Secure Boot key, build the image with it (or sign a downloaded unsigned image with it), and enroll its certificate on the machines you run. The same certificate becomes the node's upgrade anchor, so the key that makes an image bootable is also the only key that can replace it later.
Important
The project signs nothing. A release carries the source, and unsigned
images: no Authenticode signature, no upgrade anchor, and no signature or
attestation of any kind. Nothing the project ships is signed by, carries, or
trusts a key the project generated. Sign the unsigned image with your own key
and cryptosctl image sign, which also adds your certificate as the node's
upgrade anchor, or build and sign from source by following this guide. See
Sign a downloaded unsigned image.
build/uki/sign.sh takes the key and certificate you hand it through
SB_KEY / SB_CERT and uses them three ways. They have to be the same key
and certificate for all three:
| Use | Done by | Checked by |
|---|---|---|
| Authenticode signature on the UKI | sbsign in build/uki/sign.sh |
the machine's firmware, against Secure Boot db |
Detached signature, cryptos-<arch>.uki.sig |
openssl dgst -sha256 -sign in build/uki/sign.sh |
the running node, at cryptosctl image stage |
Upgrade anchor stamped into /init |
build/squashfs/build.sh, when SB_CERT is set |
the node, when it verifies the next image's .sig |
cryptosctl image sign does all three to a UKI that is already built, with
the anchor added at sign time instead of compiled in (see
the sign-time anchor).
The anchor is compiled into the init binary as
internal/release.CertificateDER. It is never read from configuration or
disk, so an administrator cannot swap it at runtime. See
image-upgrade.md for the upgrade path it protects.
An image can also get its anchor when it is signed, after the build. The
signing step (cryptosctl image sign) appends a small uncompressed cpio archive to the UKI's .initrd
section holding one file, /upgrade-anchor.der: the signing certificate in
DER. The .initrd is covered by the Authenticode signature and measured into
PCR 11, so the anchor can't be changed without breaking both. The kernel
unpacks the archives in order and a later file replaces an earlier one with
the same path, so signing again with another key appends another archive and
the last one wins.
At boot, cryptos-switchroot (the initramfs /init) reads
/upgrade-anchor.der before it switches into the SquashFS root, because the
initramfs is gone once the real init runs. It passes the certificate to the
real init in the CRYPTOS_UPGRADE_ANCHOR environment variable, as standard
base64 of the DER. It always drops any CRYPTOS_UPGRADE_ANCHOR it inherited
first: the kernel turns key=value words on the command line into init's
environment, and the command line is not trusted. With no file, the variable
is not set. Init prefers the sign-time anchor over one compiled in at build
time, because the signing step is the last word on whose image it is.
Do this on the machine that will hold the key, ideally an offline or dedicated build host. Either of the two methods below produces the same three files:
| File | Encoding | Consumed by |
|---|---|---|
sb.key |
RSA private key, PEM | SB_KEY (sbsign --key, openssl dgst -sign) |
sb.crt |
certificate, PEM | SB_CERT (sbsign --cert, sbverify, the stamped anchor) |
sb.der |
certificate, raw DER | firmware db enrollment |
umask 077
mkdir -p ~/cryptos-sb && cd ~/cryptos-sb
openssl req -new -x509 -newkey rsa:2048 -sha256 -noenc -days 3650 \
-subj "/O=Example Org/CN=Example Org Secure Boot Signing 2026" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,digitalSignature,keyCertSign" \
-addext "extendedKeyUsage=codeSigning" \
-keyout sb.key -out sb.crt
openssl x509 -in sb.crt -outform DER -out sb.derCaution
-noenc needs OpenSSL 3.0 or later; on 1.1.1 use -nodes. The key is written
unencrypted because sbsign and sign.sh read it non-interactively; protect it
with file permissions and storage instead (see "Key custody" below).
The extensions match what cryptos-sbkey writes: a self-signed certificate
with the code-signing extended key usage.
The repository's own generator does the same thing without openssl:
task build # produces bin/cryptos-sbkey
bin/cryptos-sbkey --out-dir ~/cryptos-sb --cn "Example Org Secure Boot Signing 2026"Flags: --out-dir, --cn, --days (0, the default, means about 10 years),
--bits (2048, the default, or 4096) and --force to overwrite existing
files. sb.key is written as PKCS#8 PEM with mode 0600.
Use RSA. The anchor check refuses any other key type
(internal/release), and the detached signature is PKCS#1 v1.5 over SHA-256.
- RSA-2048 is the UEFI-mandated baseline and loads on every firmware. Choose it unless you have a reason not to.
- RSA-4096 works only on firmware that accepts RSA-4096 certificates in
db. Swaprsa:2048forrsa:4096(or pass--bits 4096), and confirm the target firmware boots a test image before you depend on it. Firmware that rejects the key refuses to boot the image.
ECDSA is not an option here, even though CryptOS uses ECDSA for its CA keys:
support for ECDSA in db is inconsistent across firmware vendors, and the
anchor check expects RSA. This key signs boot images and nothing else.
Give the certificate a long life: 10 years (-days 3650, or the
cryptos-sbkey default) is the recommended value. Expiry is not what limits
the key:
- The node does not enforce the anchor's validity dates. An expired anchor would otherwise block the one upgrade that could replace it.
- Most firmware, including EDK II/OVMF, has no trusted clock and does not
enforce the dates on
dbcertificates or image signatures either.
Changing the key is the expensive part (a fleet-wide db update and, without
the old key, a re-provision), so pick a lifetime that outlasts the nodes. Do
not copy the one-day validity the CI workflow uses; that key is thrown away
at the end of the run.
The detached signature and the stamped anchor work the same either way. What changes is whether the firmware also checks the image at every boot.
Secure Boot on, your cert in db |
Secure Boot off | |
|---|---|---|
| Firmware refuses a tampered or foreign image at boot | yes | no |
| Upgrades must be signed by your key | yes | yes (stamped anchor) |
| Setup needed per machine | enroll sb.der in db |
none |
Caution
Decide before installing a node. With the default STATEKEY=tpm, the state
partition key is sealed to PCR 7, which measures the Secure Boot state and the
db contents. Turning Secure Boot on or off, or changing db, after the node
is installed means the state key no longer unseals. The STATEKEY=nodeid
variant does not use the TPM and is not affected.
Skip this section if you run with Secure Boot off.
UEFI keeps three authority levels: PK (owns the platform and signs KEK
updates), KEK (signs db/dbx updates) and db (certificates the
firmware loads images from). dbx is the revocation list. You only need to
add your certificate to db; the existing PK, KEK and vendor db entries can
stay.
A VM with EFI firmware and Secure Boot enabled starts with the Microsoft and
VMware certificates in db, which do not cover your image. You add yours from
the VM's own firmware setup, reading the certificate from a virtual CD. These
steps were checked on ESXi 8.0.3.
The firmware's file browser reads only FAT file systems. On a CD it finds one
only as an El Torito boot image, so you put sb.der in a small FAT image and
make that image the CD's El Torito image. No extra virtual disk is needed.
Caution
A plain ISO 9660 CD holding sb.der does not work: the firmware reports
No File System Found. Build the CD as below.
Install the tools:
Linux (Debian or Ubuntu; on Fedora use sudo dnf install mtools xorriso)
sudo apt install mtools xorrisomacOS (Homebrew)
brew install mtools xorrisoThen, in the directory that holds sb.der, build sb-enrol.iso. The commands
are the same on Linux and macOS:
mkdir sb-enrol
mformat -i sb-enrol/sb-enrol.img -C -f 1440 -v SBCERT ::
mcopy -i sb-enrol/sb-enrol.img sb.der ::/cryptos-sb.der
xorriso -as mkisofs -o sb-enrol.iso -V SBCERT \
-e sb-enrol.img -no-emul-boot sb-enrolmformat -C -f 1440 creates a 1.44 MB FAT12 image, mcopy puts the
certificate in it, and xorriso -e ... -no-emul-boot makes that image the
CD's El Torito boot image.
Caution
The file on the image must end in a lowercase .der (or .cer). A name
stored only as an upper-case 8.3 name, such as SB.DER, is refused with
Unsupported file type. A name longer than eight characters, such as
cryptos-sb.der, keeps its lowercase long name on the FAT image.
Check the result before you upload it:
mdir -i sb-enrol/sb-enrol.img ::/
xorriso -indev sb-enrol.iso -report_el_torito plainTip
mdir lists cryptos-sb.der in lowercase at the end of its line, and the
xorriso report has an El Torito img path line naming /sb-enrol.img.
- Upload
sb-enrol.isoto a datastore the host can read. - Power the VM off. Under VM Options > Advanced > Configuration
Parameters, add
uefi.allowAuthBypass=TRUE. This lets the VM's firmware setup accept adbentry that is not signed by a KEK. - Under VM Options > Boot Options, check that the firmware is EFI and turn Secure Boot on.
- Attach
sb-enrol.isoto the VM's CD/DVD drive with Connect At Power On ticked. - Under VM Options > Boot Options, tick Force EFI setup, then power the VM on. It stops in the Boot Manager.
- Choose Enter setup, then Secure Boot Configuration > Custom Secure Boot Options > DB Options > Enroll DB > Enroll DB Using File.
- In the File Explorer, pick the volume labelled
SBCERT(its path ends inCDROM(...)), thencryptos-sb.der. - Choose Commit Changes and Exit.
- To check, open DB Options > Delete DB. Your certificate's common name is listed next to the Microsoft and VMware entries. Leave with Esc without ticking anything, then shut the VM down from the Boot Manager (Shut down the system).
With govc, steps 2 to 5 are:
govc vm.change -vm cryptos-node -e uefi.allowAuthBypass=TRUE
govc device.boot -vm cryptos-node -firmware efi -secure=true
govc device.cdrom.insert -vm cryptos-node -ds datastore1 ISO/sb-enrol.iso
govc device.boot -vm cryptos-node -setup
govc vm.power -on cryptos-nodeIf the CD/DVD drive is not set to connect at power on, govc device.ls -vm cryptos-node names it (for example cdrom-3000), and govc device.connect -vm cryptos-node cdrom-3000 connects it.
Warning
Remove uefi.allowAuthBypass as soon as the certificate is in db. While it
is set, anyone with access to the VM's console can change db from firmware
setup without a KEK signature.
- With the VM powered off, remove
uefi.allowAuthBypass(with govc:govc vm.change -vm cryptos-node -e uefi.allowAuthBypass=), detachsb-enrol.iso, and confirm Secure Boot is still on under **VM OptionsBoot Options**.
Important
Do this before the first boot of the CryptOS installer image, for the reason given in step 2 above.
- Copy
sb.derto a FAT32 USB stick. - Reboot into firmware setup, then Security > Secure Boot.
- Put Secure Boot in Setup Mode or Custom Mode (vendor wording varies).
- Choose Enroll key / Add to db / Append signature and select
sb.der. - Save, return Secure Boot to User Mode / Enabled, reboot.
Most enterprise firmware accepts a raw DER (.der / .cer) file here.
CryptOS has no shell, so run this from a live Linux USB on the target, with the firmware in Setup Mode:
Caution
Drop --append only if you mean to replace the platform keys entirely.
sbctl status # confirm Setup Mode
sbctl import-keys --db-cert ./sb.der # add your cert to db
sbctl enroll-keys --append # --append keeps the existing keysIf you own PK and KEK, build a signed db update:
cert-to-efi-sig-list -g "$(uuidgen)" sb.der sb.esl
sign-efi-sig-list -k KEK.key -c KEK.crt db sb.esl sb.auth
# Apply sb.auth through the firmware's "update db" path or efi-updatevar.The image build runs on a Linux host; see build/README.md
for the toolchain. Pass the key and certificate as environment variables, with
absolute paths:
export SB_KEY="$HOME/cryptos-sb/sb.key"
export SB_CERT="$HOME/cryptos-sb/sb.crt"
task image PLATFORM=vmware STATEKEY=nodeidtask image builds the kernel, the static tools and the rootfs, assembles the
UKI and signs it. To also wrap it in a bootable ISO, run task iso instead; it
runs task image first:
task iso PLATFORM=vmware STATEKEY=nodeidDrop STATEKEY=nodeid (or set STATEKEY=tpm) for the TPM-backed image; see
build/README.md
for when nodeid is appropriate.
Important
Keep SB_CERT set for the whole run. The certificate is read twice, at
different steps:
rootfs:buildstamps it into/initas the upgrade anchor, if it is set.uki:signsigns the finished UKI withSB_KEY/SB_CERT, and fails if either is missing.
Running the steps separately with SB_CERT unset during rootfs:build gives
you a correctly signed image with no anchor: it boots, but a node installed
from it serves the image upgrade RPCs as Unimplemented and can only be
replaced by a reinstall. Exporting both variables once, as above, avoids that.
Outputs in build/out/:
| File | What it is |
|---|---|
cryptos-amd64.uki.unsigned |
the assembled UKI before signing |
cryptos-amd64.uki |
the signed UKI (anchor inside, Authenticode signature on it) |
cryptos-amd64.uki.sig |
the detached signature image stage checks |
cryptos-amd64-vmware-nodeid-<build>.iso |
the installer ISO (task iso only; -nodeid is omitted for STATEKEY=tpm; <build> is the git-describe build tag from build/ci/artifact-name.sh) |
sign.sh already runs both checks below and fails the build if either fails.
Run them yourself before you ship an image to a node.
The Authenticode signature is from your certificate:
sbverify --cert "$SB_CERT" build/out/cryptos-amd64.uki
sbverify --list build/out/cryptos-amd64.uki # shows the signer subjectThe detached signature verifies against the certificate (not just the key):
openssl dgst -sha256 \
-verify <(openssl x509 -in "$SB_CERT" -pubkey -noout) \
-signature build/out/cryptos-amd64.uki.sig \
build/out/cryptos-amd64.ukiThe anchor was stamped. The rootfs tree stays under build/.work/ after the
build, and the anchor is the certificate's DER in base64:
grep -q -a -F "$(openssl x509 -in "$SB_CERT" -outform DER | base64 -w0)" \
build/.work/rootfs-amd64/init && echo "anchor stamped"Tip
Expected output: anchor stamped.
No output means the build carries no anchor.
On the target, with Secure Boot on, the machine should boot the image. If the
firmware refuses it, your certificate is not in db, or the image was signed
with a different key.
A node accepts a new image only if its .sig verifies against the anchor the
running image carries. Build every later version with the same SB_KEY
and SB_CERT, then stage and activate it as described in
image-upgrade.md:
Note
cryptosctl runs on Linux and macOS today. A Windows build is coming.
cryptosctl --endpoint pki-root.example.org:443 image stage \
--image build/out/cryptos-amd64.ukiThe signature is read from <image>.sig unless --signature names another
file. An image signed by any other key is refused before anything is written
to the ESP.
task image:unsigned runs the same chain as task image but stops at
uki:assemble, and task iso:unsigned wraps that UKI in an ISO. They never
sign, and they clear SB_CERT for rootfs:build, so no anchor is stamped even
if SB_KEY and SB_CERT are exported in your shell:
task iso:unsigned PLATFORM=vmware STATEKEY=nodeid| File | What it is |
|---|---|
cryptos-amd64.uki.unsigned |
the UKI, no Authenticode signature, no anchor |
cryptos-amd64-vmware-nodeid-unsigned-<build>.iso |
the installer ISO around it (-nodeid is omitted for STATEKEY=tpm; <build> is the git-describe build tag from build/ci/artifact-name.sh) |
This is the recipe behind the release assets. On a v* tag, CI runs
task iso:unsigned for STATEKEY=tpm and STATEKEY=nodeid with no Secure
Boot variables set, and attaches to the tag's release:
| Release file | What it is |
|---|---|
cryptos-amd64-vmware[-nodeid]-<build>.uki.unsigned |
the unsigned UKI for each state-key mode |
cryptos-amd64-vmware[-nodeid]-unsigned-<build>.iso |
the installer ISO around it |
cryptosctl-<os>-<arch>-<build> |
cryptosctl for linux and darwin on amd64 and arm64 |
build-manifest.json |
the commit, build tag and file names |
SHA256SUMS |
the SHA-256 of every other file |
Before anything is attached, build/release/check-unsigned.sh fails the run if
any UKI, on its own or inside an ISO, carries an Authenticode signature
(sbverify --list must say "No signature table present"), if any signature,
certificate or attestation file is present, or if SHA256SUMS does not cover
every file. There is nothing else to verify: no signature, attestation,
provenance or cosign bundle is made for any release file.
Check a download against SHA256SUMS:
sha256sum -c SHA256SUMS --ignore-missingor rebuild the same files from the tag with the commands above and compare the
hashes. The per-run key the CI smoke build uses on main signs and anchors only
images that are thrown away with the run; none of them is uploaded.
Check that a build carries no anchor with the grep from step 5 against your own
certificate, or, without one, by looking for any base64 X.509 certificate in
/init:
grep -q -a -E 'MII[A-Za-z0-9+/]{400,}' build/.work/rootfs-amd64/init \
&& echo "anchor present" || echo "no anchor"
sbverify --list build/out/cryptos-amd64.uki.unsigned # "No signature table present"cryptosctl image sign signs an unsigned UKI with your key: it adds your
certificate as the sign-time anchor, signs the result
for Secure Boot (Authenticode, the same result sbsign gives) and writes the
detached .sig that cryptosctl image stage checks. It needs no root, no
rebuild and no other tools, so it runs on Linux and macOS. A node running the
signed image takes in-place upgrades signed by the same key.
Important
Use cryptosctl image sign, not task uki:sign, for a downloaded image.
uki:sign makes the UKI bootable with Secure Boot on, but adds no anchor, so
a node running it serves the upgrade calls as Unimplemented.
Before you start
cryptosctlfrom the same release, and the release's unsigned UKI for your state-key mode, checked againstSHA256SUMSas above.- Your RSA key and certificate from step 1
(
sb.keyandsb.crt). The key must not be encrypted.
-
Sign the UKI. With no
--out, the output is the input name without.unsigned:cryptosctl image sign \ --image cryptos-amd64-vmware-nodeid-<build>.uki.unsigned \ --key sb.key --cert sb.crt
Expected output:
cryptos-amd64-vmware-nodeid-<build>.ukiandcryptos-amd64-vmware-nodeid-<build>.uki.signext to the input. Nothing is written unless the tool's own check of the result passes. -
Check the Secure Boot signature:
sbverify --cert sb.crt cryptos-amd64-vmware-nodeid-<build>.uki
Expected output:
Signature verification OK. -
Check the detached signature the node will check:
openssl x509 -in sb.crt -pubkey -noout > sb.pub openssl dgst -sha256 -verify sb.pub \ -signature cryptos-amd64-vmware-nodeid-<build>.uki.sig \ cryptos-amd64-vmware-nodeid-<build>.uki
Expected output:
Verified OK.
Signing an image again with another key appends a second anchor, and the last
one wins. The full-image suite's bridge job runs these steps on every image
pull request and release: it signs an unsigned candidate with a throwaway key,
checks it with sbverify --cert, boots it, and stages a second image signed
the same way.
cryptosctl image sign also takes the release's unsigned ISO. It signs the UKI
inside it exactly as above and writes the signed UKI back over both copies the
ISO carries: EFI/BOOT/BOOTX64.EFI in the EFI boot image, which the firmware
boots, and /cryptos.uki, which the installer copies to the disk. You get an
installer ISO for your key with no root, no xorriso and no rebuild.
Before you start
cryptosctlfrom the same release, and the release's unsigned ISO for your state-key mode, checked againstSHA256SUMSas above.- Your RSA key and certificate from step 1
(
sb.keyandsb.crt). The key must not be encrypted.
-
Sign the ISO. With no
--out, the output is the input name without-unsigned:cryptosctl image sign \ --image cryptos-amd64-vmware-nodeid-unsigned-<build>.iso \ --key sb.key --cert sb.crt
Expected output: three files next to the input, the signed ISO
cryptos-amd64-vmware-nodeid-<build>.iso, the signed UKI inside itcryptos-amd64-vmware-nodeid-<build>.uki, and that UKI's release signaturecryptos-amd64-vmware-nodeid-<build>.uki.sig. The command lists both places in the ISO it signed. Nothing is written unless the tool has read the new ISO back and checked every copy, the signature and the anchor. -
Check the Secure Boot signature and the detached signature on the UKI written beside the ISO, the same bytes as both copies inside it, with the commands from steps 2 and 3 above (
sbverify --cert sb.crt ...ukiandopenssl dgst -sha256 -verify sb.pub ...). -
Install nodes from the signed ISO. Keep the
.ukiand.uki.sig: that pair is whatcryptosctl image stagesends, and any later ISO or UKI you sign with the same key upgrades every node installed from this one.
Caution
The EFI boot image inside the ISO keeps its size, so the signed UKI has to
fit the free space in it. build/iso/build.sh leaves 2 MiB free, far more
than signing adds. An ISO built some other way without that room is refused,
and nothing is written.
The full-image suite's iso job runs these steps on every image pull request
and release. It builds two unsigned ISOs with the release recipe and signs each
with a throwaway key. It takes both copies out of the first with xorriso and
mcopy and checks them with sbverify --cert. Then it boots that ISO in QEMU
on a blank disk, installs a node from it over the maintenance API, and stages
the second ISO's UKI onto the node.
The key is the only thing that can produce an image your nodes will accept.
Caution
- Losing the key means re-provisioning to change anchors. A node checks the next image against the anchor compiled into the image it is running, and nothing on the node can replace that anchor. Without the key you cannot sign an image it will stage, so the only way onto a new key is a reinstall, which reformats the state partition and destroys the CA key.
- A leaked key lets anyone with admin access to a node install an image of their choosing, and, with Secure Boot on, boot it on any machine that trusts your certificate. Treat it like a CA key.
- Keep it offline when you are not building: an encrypted backup in at least two places, and the working copy on a build host you control, readable only by the account that runs the build.
- Never commit it, never put it in CI secrets for a public repository, and do not reuse it for anything else.
Caution
On a STATEKEY=tpm node, only steps 1 and 2 below are safe without a
re-provision. See the caution after step 2 for what steps 3 through 5 cost
on that mode, and what a re-provision there actually loses.
Rotation does not need a reinstall. It needs one extra image, built so its
compiled-in anchor moves to the new key before anything on the machine
checks the new key's Authenticode signature. Call that extra image the
bridge: it is signed, Authenticode and detached signature both, with the
key already in every machine's db, so it stages and boots exactly like any
other upgrade, with no firmware work first. Its anchor, though, is the new
certificate -- so once it is running, the next image the node will accept has
to carry the new key's detached signature.
Work through it with two key pairs: the one already in the fleet
(sb-2026.key / sb-2026.crt, already in db, already the running image's
anchor) and the one you are rotating to (sb-2027.key / sb-2027.crt /
sb-2027.der).
rootfs:build stamps whatever SB_CERT is set to as the anchor;
uki:sign signs the UKI and the detached signature with SB_KEY / SB_CERT.
Run them as separate task invocations so the two can disagree -- one task image run cannot do this, since it uses the same environment throughout.
kernel:build still needs the fleet's PLATFORM (it selects that platform's
drivers; nothing about rotation changes that):
export SB_CERT="$HOME/cryptos-sb/sb-2027.crt" # the new cert becomes the anchor
task kernel:build PLATFORM=vmware # match the fleet's platform
task cryptsetup:build
task e2fsprogs:build
task sgdisk:build
task mkfsvfat:build
task rootfs:build STATEKEY=nodeid
task uki:assemble
export SB_KEY="$HOME/cryptos-sb/sb-2026.key" # Authenticode + detached sig: the OLD key
export SB_CERT="$HOME/cryptos-sb/sb-2026.crt"
task uki:signTip
STATEKEY here only sets the first-boot default this image would format a
brand-new node with -- an upgrade reads its mode from the state partition's
LUKS header and ignores it (see State-key mode).
It does not need to match the fleet for a rotation build; it would only
matter if this same image were later used to provision a node from scratch.
Check what came out before shipping it, the same way as step 5
-- but name the certificate explicitly. By the time you get here $SB_CERT
is the 2026 certificate, so reusing it in the anchor check would check for
the wrong one:
sbverify --list build/out/cryptos-amd64.uki # names the 2026 certificate
grep -q -a -F "$(openssl x509 -in "$HOME/cryptos-sb/sb-2027.crt" -outform DER | base64 -w0)" \
build/.work/rootfs-amd64/init && echo "anchor stamped (2027)"cryptosctl --endpoint pki-root.example.org:443 image stage \
--image build/out/cryptos-amd64.uki
cryptosctl --endpoint pki-root.example.org:443 image activate \
--confirm "Example Root CA G1"Nothing about db changes here. The bridge's Authenticode signature is the
old key, already enrolled everywhere, and its detached signature verifies
against the running node's current (2026) anchor like any ordinary upgrade.
After the reboot, the node's compiled-in anchor is the 2027 certificate: the
next image it will accept has to carry a detached signature from the new key.
Record this image's digest now, before building anything else:
sha256sum build/out/cryptos-amd64.ukicryptosctl image status only ever reports a raw SHA-256, with no label for
which key built it. This is the only record of which digest is the bridge
versus the real image from step 4, once both exist.
Caution
On a STATEKEY=tpm node, stop here unless a re-provision is acceptable.
Steps 1 and 2 are safe: db has not changed, and the bridge's Authenticode
signature is still the key that verified the image before it, so PCR 7 --
which holds only "for an image signed with the same Secure Boot
key" -- does not move. Steps 3, 4 and 5
below each move it on a tpm node, and image stage's reseal has no way
to follow: it copies the TPM's current PCR values into every sealed copy
and only overrides PCR 11, so a PCR 7 that will differ at the next boot is
never accounted for.
- Step 3 (enrolling the 2027 certificate into
db) changes whatdbholds, which PCR 7 measures directly. - Step 4 (booting the image whose Authenticode is the 2027 key) moves
PCR 7 again, independently of step 3: PCR 7 records which certificate in
dbverified the boot, not just whatdbcontains, so switching which key verifies the boot changes it even with both certificates already enrolled. - Step 5 (removing the 2026 certificate, or adding it to
dbx) changesdbagain, the same way step 3 does.
Any one of those breaks the unseal at the next boot. The one way to rotate
on a tpm node without a re-provision is to keep shipping split builds
indefinitely -- Authenticode always the 2026 key, only the detached
signature and the anchor moving forward (a bridge of a bridge, and so on).
That rotates the upgrade anchor and nothing else: db and the Authenticode
key never move, so it does not help if the 2026 key itself was ever
exposed -- a reinstall is the only fix for that, on a tpm node. nodeid
and kms nodes do not need this workaround at all: steps 1 through 5 plus
dbx already move the Authenticode key and let the exposed certificate be
revoked, with no reinstall required.
What a re-provision costs on a tpm node: it is not a reinstall that
keeps the CA. The state partition has one keyslot, opened only by the
TPM-sealed key -- there is no recovery passphrase to fall back to -- and CA
key export is refused on a tpm node by design, because a TPM-sealed key
is not portable. With no exported copy and no way to reopen the old
partition, re-provisioning means a new CA key and re-issuing everything
the old one signed, not a reinstall that carries the identity forward.
nodeid and kms nodes do not reseal anything and are not affected by any
of this: only tpm mode binds the state key to the booted image.
On a STATEKEY=tpm node, this is the step the caution above is about --
read it before going further.
Only now does any machine need firmware work: add sb-2027.der to db,
the same way as enrolling the first certificate
(sbctl enroll-keys --append, or the firmware / ESXi steps) -- always
appending, never replacing. Leave the 2026 certificate in db.
Warning
A node with Secure Boot on refuses to boot any image whose Authenticode
signature is not in db. At every point in this procedure, both ESP slots
-- active and previous -- have to hold images signed by a key db still
trusts, or an image rollback onto the previous slot
can leave the node with nothing bootable. That is why the 2026 certificate
stays in db past this step: the previous slot still holds a
2026-Authenticode image (the bridge) until
one more upgrade lands.
Build the 2027 image the ordinary way -- Authenticode, detached signature and
anchor all from the same key, no split steps. PLATFORM still has to match
the fleet:
export SB_KEY="$HOME/cryptos-sb/sb-2027.key"
export SB_CERT="$HOME/cryptos-sb/sb-2027.crt"
task image PLATFORM=vmware STATEKEY=nodeidcryptosctl --endpoint pki-root.example.org:443 image stage \
--image build/out/cryptos-amd64.uki
cryptosctl --endpoint pki-root.example.org:443 image activate \
--confirm "Example Root CA G1"Its detached signature verifies against the bridge's anchor (the 2027
certificate), so image stage accepts it the same way it would any ordinary
upgrade. Its Authenticode signature needs the 2027 certificate in db, which
step 3 already added. After this activation, the previous slot holds the
bridge (2026 Authenticode, 2027 anchor) and the active slot holds this image
(2027 Authenticode, 2027 anchor). Record this image's digest too
(sha256sum build/out/cryptos-amd64.uki), so image status afterward can
tell it apart from the bridge's.
The previous slot still holds the bridge, a 2026-Authenticode image, so
leaving sb-2026.crt enrolled in db is not a loose end -- it is what keeps
that slot bootable. Removing it is optional, and only safe once neither
the active nor the previous digest in cryptosctl image status equals the
bridge's sha256sum from step 2. Staging always moves the current active image
into previous before writing the incoming one, so the bridge's digest does
not sit in both slots at once -- after step 4 it is the previous digest; one
more ordinary upgrade moves the step-4 image into previous instead, and only
then has the bridge aged out of the ESP entirely. Once it has, remove
sb-2026.crt from db on each machine, and add it to dbx instead if the
key was ever exposed.
Rollback works the same as any other
upgrade: cryptosctl image rollback followed by image activate puts the
retained image back. Rolling back from the real image (step 4) lands on the
bridge, whose anchor is still the 2027 certificate, so staging the real image
again afterward works without rebuilding anything. Rolling back from the
bridge (step 2) lands on the pre-rotation image, whose anchor is the 2026
certificate again -- rotation has to restart from a new bridge. Either way,
per the warning in step 3, do not remove the 2026 certificate from db until
you are done rolling back as well as rolling forward.