How the keys that sign Kryptik releases are made, kept, used, replaced and
given up. They live in the repository's protected release environment, and
only a release tag's build signs with them, after the maintainer approves the
run. The build never makes these keys (build/lib/release-keys.sh).
| Key | Signs | Where it lives | If it is stolen |
|---|---|---|---|
kryptik-release (Ed25519) |
every release's manifest, and the checksums of its install media | the release environment's KRYPTIK_KEY_MEDIUM secret, and the backup |
the thief can sign a release that every machine installs |
kryptik-latest (Ed25519) |
the channel's "this release is current" statement, daily | the github-pages environment's KRYPTIK_LATEST_KEY secret, which only main's workflows reach, KRYPTIK_KEY_MEDIUM, and the backup |
machines can be held on an old release, and where the thief can also hand them statements, as a net zone they hold can, nothing reports it; nothing can be installed with it |
kryptik-sb (RSA, X.509) |
the kernels, for Secure Boot | KRYPTIK_KEY_MEDIUM, and the backup |
the thief can sign kernels that machines which enrolled it will boot |
kryptik-testctl (Ed25519) |
a control disk that arms an unattended install or recovery on a machine booting a release's medium (the suites' installs) | the release-tests environment's KRYPTIK_TESTCTL_KEY secret, and the backup |
the thief can wipe a disk on a machine they boot a release's medium on with a control disk attached; nothing can be signed or installed with it |
| the module key | the kernel's modules | nowhere: each kernel build makes one and throws it away | nothing to steal |
An image trusts the three Ed25519 keys through its anchor,
/usr/share/kryptik/trust/release-signers. Each key is listed under its own
name and held to its own namespaces: the release key to kryptik-release for
manifests, kryptik-media for the media's checksums and kryptik-image for
the lists of software images, the statement key to
kryptik-latest, the control-disk key to kryptik-testctl. So a statement
key cannot sign a release or its media, a control-disk key can arm nothing
but an install, and no signature passes for one of another kind. A machine
trusts the Secure Boot key once its certificate is enrolled in the machine's
firmware.
The public halves are in the tree, build/config/release/release-signers and
build/config/release/kryptik-sb.crt: a release's root is bound to that
anchor, and a download can be compared with it.
A release tag's Distro run builds the system as any run does, then binds each
release's root and kernels to the public halves alone
(KRYPTIK_MEDIA_PHASE=bind). That job ran every upstream build script as
root, so no private key reaches it. The sign job then waits for the
maintainer's approval and takes the key medium from the release environment
on a fresh runner. It treats what the build job handed over as hostile: it
unpacks only the bound releases and the stamps, refuses anything in them that
is not a plain file or directory, takes the versions from the tag rather than
from the build job, and checks the bound root against the medium. Then, as an
unprivileged user, it signs and assembles the media with host tools
(KRYPTIK_MEDIA_PHASE=sign), checks that no line of a private key is in
anything it made or logged, and deletes the medium. Only v* tags may use the
environment. The acceptance parts take the control-disk key from
release-tests the same way, so no artifact carries it, and the part that
runs sysroot programs on the host never gets it.
A release is therefore as trustworthy as the maintainer's GitHub account and
the runners that build it: whoever can push a v* tag and approve its run
can sign a release. Keep the account behind a passkey or a hardware security
key.
Once, on a machine you trust.
-
Make the environments.
releasetakes onlyv*tags and waits for a reviewer;release-teststakes onlyv*tags;github-pages, where the channel workflow signs statements on its schedule with no one to approve, takes onlymain. A run that names an environment before it exists creates one with no rules, so these come first:me="$(gh api user --jq .id)" gh api -X PUT repos/{owner}/{repo}/environments/release --input - <<EOF {"reviewers": [{"type": "User", "id": ${me}}], "deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}} EOF gh api -X PUT repos/{owner}/{repo}/environments/release-tests --input - <<EOF {"deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}} EOF for e in release release-tests; do gh api -X POST "repos/{owner}/{repo}/environments/$e/deployment-branch-policies" -f name='v*' -f type=tag done gh api -X PUT repos/{owner}/{repo}/environments/github-pages --input - <<EOF {"deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}} EOF gh api -X POST repos/{owner}/{repo}/environments/github-pages/deployment-branch-policies -f name=main -f type=branch
-
Run
tools/make-release-keys.sh BACKUP-DIRfrom the checkout. It refuses to go on unlessreleasehas a reviewer and a tag policy andgithub-pagesdeploys frommainalone. It asks for a passphrase for the backup and makes the four keys in a temporary directory, with no passphrase of their own, since the workflow signs unattended. It writesBACKUP-DIR/kryptik-keys.tar.gz.encand checks that the backup opens before anything leaves the machine. Only then does it set the three secrets:KRYPTIK_KEY_MEDIUMinrelease: the medium the sign job unpacks, every file its owner's alone, without the control-disk key;KRYPTIK_TESTCTL_KEYinrelease-tests;KRYPTIK_LATEST_KEYingithub-pages. A repository secret would reach every branch's workflows, and nothing approves this key's runs.
It copies
release-signersandkryptik-sb.crttobuild/config/release/. The temporary directory goes when it exits. -
Commit
build/config/release/, and keep the backup in a second place as well (a password manager's file store and a USB stick, say). It opens withopenssl enc -d -aes-256-cbc -pbkdf2 -iter 1000000 -in kryptik-keys.tar.gz.enc | tar -xz.
Losing kryptik-release without a backup means no installed machine can be
updated again. A new key can only reach them in a release signed by the old
one, so each machine would have to be reinstalled from a medium carrying a
new anchor.
For each release:
- Tag a commit on main whose CI and Distro runs passed, and push the tag:
git tag v1.0.1 <commit> && git push origin v1.0.1. - The run checks the tag, the pins, the sources and CI on the commit
(releases), builds from nothing, binds, and
waits: approve the
signjob's deployment toreleaseon the run's page, within the week its bound releases are kept. - It signs, runs every suite on the signed media and drafts the release with
its export, source and acceptance logs. Read the draft, then publish it:
gh release edit v1.0.1 --draft=false --latest. - Publish it into the channel: run the
Update channelworkflow with the tag (gh workflow run channel.yml -f release=v1.0.1). Its daily schedule signs the statement again; a machine that hears nothing for 30 days says so.
A rehearsal runs the whole flow with a throwaway medium made for that run:
binding, the sign job without its approval, and every suite on the signed
media. Rehearse at the commit you mean to tag before a release whose build,
stage 06 or suites changed since the last rehearsal. A fault found there
costs a run; one found under the tag costs a version number. Run it from a
branch of its own, since a dispatch on main would cancel main's run in flight
(both are in one concurrency group):
git push origin <commit>:refs/heads/rehearse-production
gh workflow run distro.yml --ref rehearse-production -f role=productionA production image can also be built on one machine with a key medium
(building). That machine then holds the keys while its chroot
runs; KRYPTIK_MEDIA_PHASE=bind and then sign split the build as the
workflow does.
A production release is numbered MAJOR.MINOR.PATCH, such as 1.0.3: digits
only, with no leading zeros. Raise the patch number for fixes, the minor for
new features, and the major for a change that needs a reinstall. The build
refuses any other form for a production image, and a missing version too. A
machine installs only a release newer than the one it runs, ordered as
sort -V orders them, so 1.0.10 comes after 1.0.9.
A machine boots Kryptik's signed kernels once kryptik-sb.crt is in its
firmware's database of allowed keys (db). The USB medium carries the
certificate at /kryptik/kryptik-sb.crt and the ISO at its root, the release
record has it in DER form as well, and the tree keeps it in
build/config/release/. How to enrol it depends on the firmware; most setup
screens can enrol a key from a file on a FAT volume.
A machine accepts only what the anchor of the release it runs lists, and the
channel names one release at a time. So a new key has to arrive in a release
the old key signed, whose anchor lists both keys, and that release has to
stay the channel's current one until the machines you care about have
installed it. The build accepts an anchor that lists a name more than once,
as long as no key is listed twice. Each change updates the secret, the
backup and build/config/release/.
- Statement key. Make the new key as above. Ship a release whose anchor
lists both the old and the new
kryptik-latest, and keep signing statements with the old key until every machine runs it. Then switchKRYPTIK_LATEST_KEYto the new key, and drop the old line from a later release's anchor. - Release key. Make the new key as above. Ship release N+1, signed by the old key, with an anchor that lists both, and keep it the channel's current release until the machines you care about have installed it. Then sign N+2 with the new key and drop the old line from its anchor. A machine still on N after that cannot take N+2: install N+1 on it from a payload on a disk, and it updates as usual from there.
- Secure Boot key. Enrol the new certificate on every machine before you ship kernels signed by it. Remove the old certificate from db, or add it to dbx, once no machine needs it.
A stolen GitHub account, or a way into the release environment, is a stolen
release key: revoke the account's sessions and tokens first.
- Statement key. Machines can be held on an old release, which a hostile network can do anyway, and they report it after 30 days, unless the thief can also hand them statements, as a net zone they hold can: fresh ones naming the old release keep the report from appearing. Replace the key as above.
- Release key. The thief can sign a release that machines will install. Machines trust no other key yet, so your answer has to be signed with the stolen one too. Make a new release key, and ship a release, signed by the stolen key, whose anchor lists only the new one. It is a race, since the thief can ship a competing release until machines install yours: publish it and tell users to update at once. A machine that installed the thief's release has to be reinstalled from a medium you made.
- Secure Boot key. The thief can sign kernels that enrolled machines will boot. Add its certificate to each machine's dbx and enrol a new one.