π The OS / engine for CryptOS-PKI β an immutable, API-driven, high-assurance PKI operating system in the Talos Linux tradition.
Builds a signed Unified Kernel Image (UKI): hardened kernel + Go-based PID 1 + read-only SquashFS rootfs + TPM-unsealed encrypted state partition. A single image boots into a Root, Intermediate, or Issuing CA role based on its machine config. No SSH, no shell, no interactive access. Private keys are TPM-bound and never live on disk in the clear.
Warning
π§ Pre-1.0: any release can change fundamentally. CryptOS is pre-1.0. Until v1.0.0, any release may change configuration, APIs, on-disk and state formats, trust setup, and upgrade paths, sometimes with no migration path. If you run it in production, you accept that risk. Read each release's upgrade notes before you upgrade.
- πͺ¨ Immutable rootfs β SquashFS, read-only. Persistent state only on the encrypted partition, unsealed by the local TPM.
- π TPM-bound identity β CA private keys are created inside the TPM and never leave it. ECDSA P-384 for Roots, P-256 for Issuing CAs. An RSA-3072 or RSA-4096 CA key is held in the TPM the same way when the TPM implements that size; otherwise key creation refuses it rather than fall back.
- π« No interactive access β no SSH, no shell, no usernames/passwords. No web frontend in the image either. Management is
cryptosctlover mTLS gRPC, or the Fleet Manager (which talks the same mTLS gRPC). - π RFC-strict β TLS 1.3 (RFC 8446), X.509 (RFC 5280), and every protocol adapter follows its RFC to the letter.
- π Declarative β machine config in YAML (
apiVersion: cryptos.dev/v1alpha1), applied viaApplyConfig. No click-ops. - π§ͺ Stdlib-only on the cert path β
crypto/x509,crypto/tls,crypto/ecdsa,crypto/rand,golang.org/x/crypto. Nocfssl, nosmallstep, no PKI wrappers β ever.
proto/cryptos/node/v1/ # the node API (cryptos.node.v1) this OS serves
gen/go/cryptos/node/v1/ # generated Go stubs (package nodev1); `task generate`, never hand-edited
cmd/
init/ # PID 1 binary; becomes /init in the SquashFS
cryptosctl/ # operator CLI (the only management surface on a standalone node)
cryptos-install/ # bare-metal disk installer (GPT + ESP + UKI)
cryptos-sbkey/ # Secure Boot signing key + cert generator (for db enrollment)
cryptos-switchroot/ # shim /init: loop-mounts the SquashFS root and pivots into it
internal/
init/ # supervisor + boot bring-up
netlink/ # NIC bring-up via rtnetlink
mounts/ # early mount sequence
timesync/ # client-only SNTPv4: boot step, periodic slew, clock floor
tpm/ # go-tpm wrapper, SRK provisioning, crypto.Signer impl
ca/ # RFC 5280 cert template builder
ceremony/ # first-boot ceremony state machine
storage/
luks/ # TPM-sealed LUKS2 open/format
etcd/ # embedded etcd config + schema
grpc/ # mTLS gRPC server, RPC handlers
node/ # typed etcd state layer + gRPC Identity/Status/Config providers
install/ # bare-metal disk provisioning (partition plan + UKI install)
switchroot/ # SquashFS-root pivot sequence (loop-mount + switch_root)
audit/ # hash-chained audit log
config/ # machine config parser + validator
bootstrap/ # bootstrap admin cert loading + first-ceremony rotation
build/ # kernel config, UKI assembly recipes, SquashFS templates
test/kind/ # kind + cert-manager ACME end-to-end harness (task e2e:kind)
test/image/ # full-image suite: build, run and coverage scripts (task e2e:image)
test/integration/ # QEMU + swtpm tests against the booted image, including the full-image suite
testdata/configs/ # sample machine configs
Requires Go 1.26.8+ (the go line in go.mod; an older local Go downloads that toolchain on first use), go-task, golangci-lint, golic, buf (proto lint and codegen), and (for integration testing) qemu-system-x86_64 + swtpm + OVMF. task test also runs the TPM-held RSA CA end-to-end test against swtpm when it is installed, because the in-process TPM simulator implements RSA-2048 only; without swtpm that test skips locally and fails in CI.
task ci # fmt + proto lint + generated-code check + lint + vet + test + build
task generate # regenerate gen/go from proto/ with the pinned plugins (task tools)
task build # produces bin/init and bin/cryptosctl, stamped with the build identity
task license # re-inject Apache 2.0 headers via golic
task e2e:kind # Linux + docker: cert-manager in kind gets a certificate over ACME
task e2e:image # Linux + KVM + docker: boot the image as a Root and an Intermediate and run the full suitetask e2e:kind builds a Root and an ACME-serving Intermediate in-process (software keys, no TPM), stands up a kind cluster with cert-manager and Contour, and checks that a Certificate goes Ready with a chain to the root and renews to a new serial. It downloads pinned, checksum-checked kind, kubectl and manifests (test/kind/versions.env), needs sudo once to add a /etc/hosts line for the test name, and skips when docker isn't available.
task e2e:image builds a coverage-instrumented variant of the image (test/image/build.sh: the same kernel and rootfs, with /init built with -cover and a throwaway upgrade anchor, plus a signed successor image for the upgrade step; release and CI images are never built with -cover), boots it in QEMU with swtpm and OVMF as a software-key Root and a TPM-key Intermediate, and runs the steps in test/integration/image_suite_test.go against them with cryptosctl: the ceremony, sign-subordinate, leaf issuance, nginx with OCSP stapling, revocation, the ACME and EST protocol switch, cert-manager over ACME in kind, EST enrolment, re-certify, escrow, an in-place image upgrade on both nodes and the console reset. Each step reports pass, fail or skip in summary.md, and the merged coverage lands in coverage.html. It needs qemu, swtpm, OVMF, mtools, sgdisk, docker and sudo for one /etc/hosts line, and skips on a host without them.
bin/cryptosctl version prints the version, commit, and build date the binary was built from (git describe --tags --always --dirty; see build/README.md); pass --endpoint to also show the node's version.
The image pipeline (task image β hardened kernel build, SquashFS rootfs, UKI assembly + Secure Boot signing) has draft recipes under build/ that run on a Linux build host; see build/README.md. They are written but not yet executed end to end. The QEMU + swtpm integration harness lands in a subsequent PR.
CryptOS ships no signing key and trusts none that the project generated. You generate your own RSA Secure Boot key and certificate (with openssl or cryptos-sbkey), then build with it:
export SB_KEY=/path/to/sb.key SB_CERT=/path/to/sb.crt
task image PLATFORM=vmware STATEKEY=nodeid # or: task iso PLATFORM=vmware STATEKEY=nodeidImportant
Keep SB_CERT set for the whole run: rootfs:build stamps it into the image as the upgrade anchor, and uki:sign signs the UKI and writes the detached .uki.sig with the same key. A node only stages later images signed by that key, so losing the key means re-provisioning to change anchors. Enroll the certificate in firmware db (vSphere, firmware UI, sbctl, or efitools) to boot with Secure Boot on, or run with Secure Boot off and rely on the stamped anchor for upgrades.
Public release assets are unsigned, or a build recipe only. docs/secure-boot.md is the full guide: key generation, enrollment, building, verifying with sbverify and openssl, upgrades, and key custody.
Each v* tag attaches these to its GitHub Release, all built by task iso:unsigned with no Secure Boot variables set:
| Asset | What it is |
|---|---|
cryptos-amd64-vmware.uki.unsigned, cryptos-amd64-vmware-nodeid.uki.unsigned |
the UKI (TPM-backed and STATEKEY=nodeid variants), with no Secure Boot signature and no upgrade anchor |
cryptos-amd64-vmware-unsigned.iso, cryptos-amd64-vmware-nodeid-unsigned.iso |
the same UKIs wrapped in a UEFI-bootable installer ISO |
cryptosctl-{linux,darwin}-{amd64,arm64} |
the static CLI, stamped with the release version |
SHA256SUMS |
SHA-256 of every asset above |
Important
They are for evaluation with Secure Boot off. A node installed from one cannot be upgraded in place (it has no anchor), so for real use build with your own key as above. task image:unsigned and task iso:unsigned reproduce the assets locally; they clear SB_CERT for rootfs:build and never sign, even if SB_KEY/SB_CERT are exported.
GitHub Actions:
ci-go(ci-go.yml) βtask ci(format, proto lint, generated-code check, lint, vet, test, build) on every pull request + push tomain, on a GitHub-hosted Linux runner, withswtpminstalled for the TPM-held RSA CA test. Draft pull requests are skipped; CI runs when the PR is marked ready.
After a stacked pull request is retargeted onto main, CI starts on its next push, or when it is toggled to draft and back to ready.
-
ci-image(ci-image.yml) β builds the UKI on a GitHub-hosted runner (amd64 onubuntu-latest, arm64 onubuntu-24.04-arm), installing the kernel /ukify/sbsigntoolchain per run. Runs on push tomain, tags, and manual dispatch; useworkflow_dispatchon a branch to validate image changes before merging. Onmainit signs with a per-run ephemeral key as a smoke test and uploads nothing. On av*tag it builds the unsigned release assets and attaches them to the tag's release (a draft, marked pre-release for-alpha/-beta/-rctags, if none exists yet); dispatch withrelease_assetsbuilds them without publishing. -
ci-kind-acme(ci-kind-acme.yml) βtest/kind/run.sh(thetask e2e:kindharness) on pull requests that touch the ACME, config, node or e2e code, on a GitHub-hosted runner. Drafts are skipped, and it isn't a required check. -
ci-e2e-image(ci-e2e-image.yml) β the full-image suite (task e2e:image) on a GitHub-hosted runner, with KVM when the runner has it: nightly, on pull requests that touch the image build, the boot, or the protocol and config code, and on manual dispatch. The step table and per-package coverage go into the job summary and the HTML report into the run's artifacts. Drafts are skipped, and it isn't a required check.
A CA node has exactly two ways to be managed:
| When | What | |
|---|---|---|
cryptosctl |
Always β and the only option on a standalone (unlinked) node. | Local UNIX socket on the node for break-glass; remote mTLS gRPC for everything else. |
| Fleet Manager | Optional. When you want a web UI or multi-node view. | The manager/ backend serves the web/ frontend; talks to nodes via the same mTLS gRPC API. |
There is no third surface. The OS image ships no web frontend β neither source nor compiled β by design.
Remote cryptosctl checks the node's management certificate against --trust. Before the node has a CA, that certificate is self-signed and regenerated on every boot, so you pin it: the console shows its SHA-256, and cryptosctl trust fetch --expect-sha256 <fingerprint> saves the pin only when it matches. Once the node has a CA, the certificate is signed by it and --trust is your root certificate, which survives reboots. docs/management-trust.md covers both.
An intermediate can get a fresh certificate for the key it already holds, for example one that carries CRL, OCSP and caIssuers pointers after the parent's revocation_base_url was set: cryptosctl ca get-renewal-csr, ca sign-subordinate on the parent, then ca submit-renewed-cert. No re-key and no reboot. docs/subordinate-recertify.md has the procedure and the openssl checks.
Every issued certificate comes from a named profile in the machine config. docs/certificate-profiles.md is the field reference. It also covers how a certificate's validity is capped at the issuing CA's own notAfter (or refused, with validity_policy: reject), and the warnings cryptosctl prints when that happens.
A node keeps every certificate it issues. cryptosctl ca list-issued lists them, ca get-issued --serial <hex> prints one as PEM with its chain up to the root and its status (valid, revoked or expired), and ca revoke revokes one. docs/issued-certificates.md covers all three.
Every call to a node's API, except the status polling from the console and the Fleet Manager, is recorded in its hash-chained audit log. cryptosctl audit list reads it, with time, call and actor filters, and audit verify checks the signatures and the chain and exits non-zero when it is broken. docs/audit-log.md covers both.
A node keeps its clock in sync over SNTP with the servers in network.ntp_servers (or its DHCP lease), and refuses to sign certificates until the first sync when a time source is configured. CRL and OCSP are never held up. docs/time-sync.md covers the servers, the signing gate and its pki.allow_unsynced_clock override, and the Clock: status line.
Issuing LDAPS and KDC certificates to Active Directory domain controllers, including certreq on Server Core, is covered in docs/active-directory.md. docs/active-directory-root-gpo.md distributes the root to domain members with Group Policy.
Network devices that cannot run ACME, such as Cisco IOS and IOS-XE trustpoints, enrol over SCEP (RFC 8894): each initial enrolment is authorized by a one-time challenge from cryptosctl scep challenge mint, and renewal by the device's current certificate. docs/scep.md covers switching it on, the per-profile key floor, the RA certificate and the approval queue.
Most config apply changes report requires_reboot=true. Restart the node through its orderly shutdown rather than a hypervisor hard reset:
Note
cryptosctl runs on Linux and macOS today. A Windows build is coming.
cryptosctl --endpoint pki-root.example:443 reboot --confirm "Example Root CA G1"
cryptosctl --endpoint pki-root.example:443 reboot --confirm "Example Root CA G1" --power-off--confirm must be the node's CA common name, and over mTLS the call needs the bootstrap admin client certificate. The node replies, then stops its listeners, closes etcd and the audit log, unmounts and locks the state volume, and restarts (or powers off). A hard reset skips all of that. The management certificate gets a new key on every boot, so refresh a pinned --trust afterwards; a --trust that holds the CA keeps working.
The same orderly shutdown also runs, without the API, when:
- the ACPI power button is pressed (a hypervisor guest shutdown that goes through ACPI, such as
virsh shutdown). The node powers off. - Ctrl-Alt-Del reaches the console (for example, the vSphere console's "Send Ctrl+Alt+Delete"; the VMware image carries the PS/2 keyboard driver for this). The node reboots.
- PID 1 receives
SIGTERMorSIGINT. The node reboots.
The image ships no guest tools, so a vSphere "Shut Down Guest OS" or "Restart Guest OS" request is not available. Use cryptosctl reboot, or the console's Ctrl+Alt+Delete.
Pre-alpha. Phase 1 scaffolding has landed; subsystem implementation is in progress.
- πͺ¨ Phase 1 β Core OS + single-node Root CA MVP. Boot a UKI in QEMU +
swtpm, generate a TPM-resident ECDSA P-384 Root key, self-sign an RFC 5280-strict Root cert, validate viacryptosctl. - π Phase 2 β Role-aware API + protocol adapters + Fleet Manager. Root / Intermediate / Issuing role split, ACME / SCEP / EST / WSTEP / RFC 3161 / OCSP / CRL.
- π‘οΈ Phase 3 β Pool, HA, extensions, isolation, recovery. 2-node HA pairs (Infoblox-style failover, VRRPv3 VIP), multi-Root topology (configurable depth, default cap 3), Fleet Manager linkage protocol, Talos-style signed late-binding extensions, disaster-recovery escrow.
The gRPC API this OS serves is defined here, in proto/cryptos/node/v1 (package cryptos.node.v1). The Go stubs live under gen/go/cryptos/node/v1 (import github.com/CryptOS-PKI/cryptos-node/gen/go/cryptos/node/v1, package nodev1), and the Fleet Manager imports them from there. Change a .proto, run task generate, and commit the regenerated tree in the same change; task ci fails when gen/ is stale. task proto:breaking checks a change against main.
- π°οΈ
managerβ Fleet Manager backend (optional). - π¨
webβ Fleet Manager web frontend (optional, served bymanager/).
Apache License 2.0. Copyright The CryptOS Authors.