Runbook: CA key ceremony (m-of-n)
Generating or rotating a Certificate Authority key is the most consequential operation in trstctl: whoever controls a CA key can mint trust. trstctl gates CA-key operations behind an m-of-n key ceremony — the key is created only after a configured number of distinct custodians approve — so no single operator can unilaterally stand up or rotate a CA.
Maturity note. Root/intermediate ceremonies, offline-root import, zero-downtime rotation, cross-signing, offline-root re-key, and online break-glass issue/rotation/cross-sign are all served over REST. Online CA private keys stay in the isolated signer process; the offline root key never enters trstctl. The issuing CA's key is persisted and sealed at rest (R3.2), so the signer reloads it after a restart instead of silently rotating the CA (see Configuration -> Signer and disaster recovery).
POST /api/v1/breakglass/reconcileverifies signed emergency bundles into the audit chain after recovery. HelmexternalKMSwires signer key-store custody; non-extractable HSM/KMS-resident custody remains future work (see Current limitations and the incident-response runbook).
The model
A ceremony has a purpose (the exact key operation and resource it authorizes)
and a threshold m — the number of distinct custodian approvals required.
Custodians approve independently; the CA-key operation is refused until quorum is
reached (ErrQuorumNotMet), refused if the purpose does not match the requested
operation (ErrKeyCeremonyPurposeMismatch), and refused if the ceremony was
already used (ErrKeyCeremonyNotPending).
Purpose values are deliberately concrete:
| Purpose format | Authorizes |
|---|---|
root:<sha256-of-ca-spec> |
One new root CA matching the reviewed CASpec. |
intermediate:<parent-ca-id>:<sha256-of-ca-spec> |
One intermediate under that parent, matching CASpec. |
offline-root:<sha256-of-root-cert-der>:root:<sha256-of-ca-spec> |
Importing a public, self-signed offline root certificate matching CASpec. |
offline-intermediate:<parent-ca-id>:<sha256-of-ca-spec> |
Generating a signer-held CSR under the offline root, then importing the signed intermediate. |
import-existing-ca:<signer-handle>:<sha256-of-chain-der>:root:<sha256-of-ca-spec> |
Importing an existing certificate chain, bound to the named signer-held key handle. |
cross-sign:<ca-id>:<sha256-of-target-cert-der> |
One cross-signature from that CA over the target certificate. |
offline-root-rekey:<sha256-of-authority/successor/both-cross-certs/reason/spec> |
Importing an offline-root successor plus both direction-specific cross-certificates; private keys stay on the disconnected systems. |
offline-cross-sign:<sha256-of-authority/target/cross-cert> |
Importing an already-produced offline-root cross-certificate for a target. |
In the hierarchy manager, StartCeremony opens an m-of-n ceremony and
Approve records one de-duplicated custodian approval. Every procedure below
collects approvals the same way: each custodian calls
POST /api/v1/ca/ceremonies/{id}/approvals with their own token (the opener
cannot approve their own ceremony); every approval is auditable and emits
ca.ceremony.approved.
CA-mutating calls (CreateRoot, Rotate, CrossSignAuthority, and the rest) are
gated on purpose-bound quorum: each locks the pending ceremony, checks quorum
and exact purpose, and marks it completed in the same transaction as the
mutation. Consuming a ceremony on success means it cannot be reused; cross-signing
is gated too, since it also extends trust (it mints a CA certificate under your
signing CA).
The ceremony and its approvals are tenant-scoped rows under row-level security:
ca_key_ceremonies (with the threshold) and ca_ceremony_approvals.
Procedure: standing up a new CA
Convene the custodians. Choose n trusted custodians and a threshold m (e.g. 3-of-5); see custodian hygiene below for sizing.
Open the ceremony for the reviewed root or intermediate spec:
POST /api/v1/ca/ceremonieswith bearer auth carryingissuers:writeand anIdempotency-Keyheader.{ "operation": "create_root", "threshold": 2, "spec": { "common_name": "Example Root CA", "ttl_seconds": 315360000, "signature_algorithm": "ECDSA-P256", "max_path_len": 1, "permitted_dns_domains": ["example.internal"] } }For an intermediate, use
"operation": "create_intermediate"and include"parent_id": "<root-ca-id>"; the server derivesintermediate:<parent-ca-id>:<sha256-of-ca-spec>from the same request the CA operation will execute.Collect approvals at
POST /api/v1/ca/ceremonies/{id}/approvals(see above).Create the CA. Once m custodians have approved, call
POST /api/v1/ca/authorities/rootsorPOST /api/v1/ca/authorities/intermediateswith theceremony_idand the same reviewed spec. Before quorum this fails closed withErrQuorumNotMet; for a mismatched resource it fails closed withErrKeyCeremonyPurposeMismatch.Distribute trust. Publish the new CA certificate to relying parties. Verify: for an intermediate, the chain resolves to its parent.
Record the ceremony in your change-management system alongside the audit trail.
Leaf issuance (once an intermediate exists) is served at
POST /api/v1/ca/authorities/{id}/issue (CSR PEM, validity, a certs:issue
token); the CA key still signs inside the isolated signer process.
Procedure: offline root with served intermediate
Use this when the root key lives outside the control plane and only comes online for ceremonies.
Create the offline root certificate outside trstctl. Keep the root private key on the offline system. Export only the public root certificate PEM.
Open the offline-root import ceremony with the public root certificate and reviewed
CASpec:{ "operation": "import_offline_root", "threshold": 2, "certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", "spec": { "common_name": "Example Offline Root CA", "ttl_seconds": 315360000, "signature_algorithm": "ECDSA-P256", "max_path_len": 1, "permitted_dns_domains": ["example.internal"] } }Collect approvals at
POST /api/v1/ca/ceremonies/{id}/approvals.Import the public offline root with
POST /api/v1/ca/authorities/offline-roots(sameceremony_id,certificate_pem,spec). The server accepts exactly one certificate PEM, rejects private-key blocks, verifies the root is self-signed and CA-capable, and stores the authority with no signer handle.Open the offline-intermediate ceremony with operation
create_offline_intermediate,parent_idset to the imported root authority, and the intermediateCASpec; collect approvals as above.Generate the signer-held CSR with
POST /api/v1/ca/authorities/{offline-root-id}/offline-intermediates/csr; the signer creates and keeps the intermediate private key, returning only a CSR PEM plus signer handle.Sign the CSR on the offline root system: move only the CSR over, sign it as a CA certificate under the offline root, and bring back only the signed certificate PEM.
Import the offline-signed intermediate with
POST /api/v1/ca/authorities/{offline-root-id}/offline-intermediates; the server verifies the certificate chains to the offline root, matches the reviewedCASpec, obeys path-length constraints, and contains the exact public key from the CSR.
Leaf issuance then uses POST /api/v1/ca/authorities/{intermediate-id}/issue.
Issuing directly from the imported offline root fails closed (no signer handle).
Procedure: importing an existing signer-backed CA chain
Use this when an existing root/intermediate certificate should become a served trstctl authority whose private key lives behind a signer handle. Never paste private-key PEM into the API or UI.
Pre-provision the signer handle. The signer holds the CA private key under a handle constrained to CA signing; the control plane uses only that handle and its public key.
Export the public CA chain. Put the imported authority certificate first, followed by its issuer chain up to a self-signed root (for a root import, just the self-signed root certificate). The PEM bundle must contain only
CERTIFICATEblocks.Open the import ceremony with the chain, signer handle, and reviewed
CASpec:{ "operation": "import_existing_ca", "threshold": 2, "certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", "signer_handle": "customer-existing-ca", "spec": { "common_name": "Example Imported Issuing CA", "ttl_seconds": 71280000, "signature_algorithm": "ECDSA-P256", "max_path_len": 0, "permitted_dns_domains": ["example.internal"] } }Collect approvals at
POST /api/v1/ca/ceremonies/{id}/approvals.Import the CA with
POST /api/v1/ca/authorities/imported(sameceremony_id,certificate_pem,signer_handle,spec). The server verifies the first certificate is a usable CA, the chain reaches a self-signed root, the reviewed profile matches, and the certificate's public key exactly matches the signer-held key. The stored authority holds only public chain metadata plus the signer handle.
Leaf issuance then uses POST /api/v1/ca/authorities/{imported-ca-id}/issue.
Procedure: rotating a CA
Zero-downtime rotation: create or import the successor CA under the normal ceremony rules, then activate it behind the predecessor's stable issue URL.
- Create or import the successor CA with the same kind, parent, path-length, DNS, and EKU constraints as the predecessor, using whichever ceremony procedure above applies.
- Activate the overlap window with
POST /api/v1/ca/authorities/{predecessor-id}/rotate(JSON body:successor_id). The server marks the predecessorsuperseded, recordsreplaces_id, emitsca.authority.rotated, and keeps the predecessor issue URL live while routing new issuance to the successor. - Verify both issue URLs: the predecessor URL still answers but its chain now verifies to the successor, and the successor URL issues directly.
- If your hierarchy requires cross-signing the new CA, open a separate
cross-sign:<ca-id>:<sha256-of-target-cert-der>ceremony and collect its m approvals as above. Submit the ceremony id and target certificate toPOST /api/v1/ca/authorities/{issuer-id}/cross-sign, refused until quorum and exact target-certificate match. Verify the returned certificate independently against the issuer before distributing the new chain. - Retire the old key per your policy (and per the incident-response runbook if the rotation is compromise-driven).
Procedure: renewing or re-keying a signer-backed CA
Use re-key to give an authority a fresh CA key/certificate in the same logical lane. The served path covers signer-backed online roots/intermediates; offline-root private operations stay outside the binary, though the import path verifies their public successor and cross-certificates.
Start a ceremony with
POST /api/v1/ca/ceremonies:{ "operation": "rekey_ca", "authority_id": "<ca-authority-id>", "threshold": 2, "spec": { "common_name": "Reviewed re-key" } }The ceremony purpose is
rotation:<ca-authority-id>, so it cannot be replayed against a different CA.Collect approvals at
POST /api/v1/ca/ceremonies/{id}/approvals.Activate the re-key with
POST /api/v1/ca/authorities/{id}/rekey:{ "ceremony_id": "<rekey-ceremony-id>", "ttl_seconds": 7776000, "reason": "planned CA renewal" }The server creates a fresh signer-held CA key, issues a replacement certificate matching the predecessor's common name, DNS constraints, EKUs, and path length; emits
ca.authority.rekeyed; marks the predecessorsuperseded; and recordsreplaces_idon the successor.Verify: both the predecessor and successor issue URLs return chains signed by the new successor CA.
Procedure: re-keying an offline root
The disconnected root system performs every private operation; trstctl receives only public certificates and refuses a private-key PEM block.
- On the offline systems, create a self-signed successor root with constraints no wider than the predecessor, and produce both cross-certificates (successor-by-predecessor and predecessor-by-successor).
- Start
operation=rekey_offline_rootatPOST /api/v1/ca/ceremonies. Bindauthority_id, the successor incertificate_pem, the two direction-specific certificates incross_certificate_pemandreverse_cross_certificate_pem, the reason, threshold, and exactCASpec. - Collect approvals at
POST /api/v1/ca/ceremonies/{ceremony-id}/approvals. - POST the same public package plus
ceremony_idto/api/v1/ca/authorities/{predecessor-id}/offline-rekey. trstctl verifies both signatures, overlapping validity, key usages, EKUs, DNS/path constraints, subject/public keys, SKI, and AKI before superseding the predecessor and recordingca.authority.rekeyed. - Verify both returned cross chains with a client independent of trstctl, for
example
openssl verify, before distributing either trust path. - To import a target CA cross-certificate produced by the new offline root,
open
operation=import_offline_cross_signbound to the successor authority, target, and cross-certificate; collect quorum; then POST the same public values to/api/v1/ca/authorities/{successor-id}/offline-cross-signs.
Procedure: online break-glass issue and CA rotation
Bootstrap the authority via ca ceremonies start / ca ceremonies approve /
ca authorities create-root while break-glass is off; save the certificate and
signer handle, derive the public-key file, then enable the online block (one
tenant, a distinct operator-subject roster, threshold at least two — see
Configuration).
- Start an issuance ceremony with
trstctl-cli breakglass issue-ceremony -f issue-intent.json. The intent carries request id, subject, CSR, reason, and TTL, but no approver names. - Collect approvals: distinct configured operators each run
trstctl-cli ca ceremonies approve <ceremony-id>with their own token. - Add only
ceremony_idto the exact approved intent and runtrstctl-cli breakglass issue -f issue.json. Any changed CSR/reason/TTL, sub-quorum, wrong tenant, or reused ceremony fails closed. - Rotate with the same two-phase pattern:
breakglass rotation-ceremony, approvals, thenbreakglass rotate. Independently validate the returned new-by-previous and previous-by-new cross-certificates and retain both verifier certificates for the overlap window. - Cross-sign an external target with
breakglass cross-sign-ceremony, approvals, thenbreakglass cross-sign. The target certificate digest is part of the ceremony purpose, so swapping the target after approval fails.
Custodian hygiene
- Custodians should be distinct people with independent credentials; do not let one operator hold multiple custodian identities.
- Choose m and n so the loss of one custodian is recoverable but a single compromise cannot mint trust.
- Every approval is a logged, attributable action recorded against the ceremony.
See Current limitations for remaining local key-custody qualifications, and Disaster recovery for CA-key loss handling.