A ctlplne studio product

Disconnected edge CA key custody

An edge CA is allowed to sign while its host cannot reach the control plane, so its private key is the dangerous part. The safe default puts that key in a TPM2 object that cannot be exported. The agent persists only a JSON handle containing the provider name, opaque object ID, algorithm, and public key.

TPM2 default

Generate generation 1 of the segment key and its CSR:

trstctl-agent \
  --edge-csr \
  --edge-tenant 77777777-7777-7777-7777-777777777777 \
  --edge-segment 88888888-8888-8888-8888-888888888888 \
  --edge-cn bunker-01-edge-ca \
  --edge-key-provider tpm2 \
  --edge-key-generation 1 \
  --edge-key-handle-out /var/lib/trstctl/edge-ca.keyref.json \
  --edge-csr-out /var/lib/trstctl/edge-ca.csr

--edge-tpm-path can name /dev/tpmrm0, /dev/tpm0, or a swtpm Unix socket. Owner/key authorization values come only from --edge-tpm-owner-auth-file and --edge-tpm-key-auth-file; they are read as bytes and wiped after the device opens. Repeating the same tenant, segment, and generation reconciles the same persistent object after a lost response. Use a new generation label when intentionally rotating the edge CA.

The command prints the CSR as base64 and the exact attestation challenge. A separate TPM attestation identity key (AIK) must already have a certificate chain issued by the operator's vetted TPM attestation authority. Its private key stays in the TPM; the public chain is what the segment policy pins. The AIK handle file has the same public-only JSON shape as edge-ca.keyref.json. Provision the AIK under a different durable operation label from the edge CA, then obtain its AIK-profile certificate from that authority. Do not sign an arbitrary host-generated AIK public key without the authority's device enrollment and endorsement checks. A local QA authority may issue a disposable AIK certificate for a local swtpm; that qualifies only the emulator contract. To create the separate TPM-resident AIK key and a proof-of-possession CSR with the shipped agent, use the same tenant and segment but a distinct generation:

trstctl-agent \
  --edge-csr \
  --edge-tenant 77777777-7777-7777-7777-777777777777 \
  --edge-segment 88888888-8888-8888-8888-888888888888 \
  --edge-key-generation aik-1 \
  --edge-cn bunker-01-aik \
  --edge-key-provider tpm2 \
  --edge-key-handle-out /var/lib/trstctl/edge-aik.keyref.json \
  --edge-csr-out /var/lib/trstctl/edge-aik.csr

The attestation authority issues an AIK certificate for that public key with an empty subject, TPM manufacturer/model/version directory-name SAN, digital signature use, and the TPM AIK extended key usage 2.23.133.8.3. Put the AIK leaf first and its chain through the pinned root in edge-aik-chain.pem.

With the edge CSR, both public handle files, and the AIK certificate chain on the host, have the same TPM certify the edge CA's persistent signing object:

trstctl-agent \
  --edge-attest \
  --edge-tenant 77777777-7777-7777-7777-777777777777 \
  --edge-segment 88888888-8888-8888-8888-888888888888 \
  --edge-key-provider tpm2 \
  --edge-csr-in /var/lib/trstctl/edge-ca.csr \
  --edge-ca-key-handle /var/lib/trstctl/edge-ca.keyref.json \
  --edge-aik-key-handle /var/lib/trstctl/edge-aik.keyref.json \
  --edge-aik-cert-chain /var/lib/trstctl/edge-aik-chain.pem \
  --edge-attestation-out /var/lib/trstctl/edge-attestation.json \
  --edge-parent-ca-id 99999999-9999-9999-9999-999999999999 \
  --edge-host bunker-01 \
  --edge-mint-ttl 24h \
  --edge-mint-request-out /var/lib/trstctl/edge-mint.json

Use --edge-tpm-path for a swtpm socket or a nondefault TPM device, and --edge-tpm-key-auth-file / --edge-aik-auth-file for distinct 0600 authorization files. The agent verifies the CSR against the edge TPM handle, the AIK certificate against its own TPM handle, and the resulting TPM2_Certify statement through the same parser used by the server. It writes the WebAuthn TPM credential as a 0600 file. It refuses a mismatch; there is no software attestation fallback. The mint request carries the CSR as base64 DER, attestation_credential_json as base64 of that JSON file, and key_provider: "tpm2". The control plane separately checks the installed root and binds the attested key digest to the CSR public-key digest. It also reads the attributes the TPM certified for that key: unless the TPM generated it (sensitiveDataOrigin) and will release it neither to another TPM (fixedTPM) nor under another parent (fixedParent), the key is not device-bound and the mint is refused. Keys the agent creates have all three. The certificate authority selected for minting must permit a subordinate CA. Review edge-mint.json and send it through the native CLI with a fresh idempotency key:

trstctl-cli --idempotency-key edge-bunker-01-generation-1 \
  edge delegations mint -f /var/lib/trstctl/edge-mint.json

The same JSON body can be sent to POST /api/v1/edge/delegations. An absent --edge-mint-request-out leaves only the attestation file; the agent never contacts the control plane from this offline host mode.

In the console, open Certificates → Certificate authorities → Authorities → Delegated edge CAs → Mint delegated edge CA. Upload or paste the complete host-generated edge-mint.json. The console checks that its segment is enabled for the chosen provider and shows the exact public CSR and TPM credential before submission. A lost response keeps the same reviewed request and idempotency key for retry. Success requires an independent read of the same delegation, key digest, status, and public certificate chain. Copy that chain from the read-only result into edge-ca.crt on the exact host; the TPM key never crosses the browser or control plane. The console also offers Reconcile host journal for the host's edge-journal.json, showing the exact certificates and counts for reconciled, already recorded, violated, and rejected leaves. Revoke delegated edge CA reviews the exact host, delegation ID, certificate serial, and reason before sending the request and reading the revoked state back. The host agent may write an empty host field in its journal; the console binds that journal to the operator-selected delegation host before sending it.

The delegated CA's initial and revoked status also enqueue signed parent-CA CRL publication in the same event transaction. Publication is retried by the outbox worker; an issued-ledger row alone does not mean relying clients can reject a revoked delegation. The issued event retains the complete public delegated and parent chain, so GET /api/v1/edge/delegations/{id} recovers exactly what the mint returned after a lost response or projection rebuild. For older events that retained only the delegated certificate, detail readback adds the current parent chain only after checking that its public certificate signed that exact delegated certificate. If the historical parent cannot be verified, readback refuses instead of presenting a different trust path as recovered.

After the control plane returns the delegated certificate chain:

trstctl-agent \
  --edge-issue \
  --edge-key-provider tpm2 \
  --edge-ca-cert /var/lib/trstctl/edge-ca.crt \
  --edge-ca-key-handle /var/lib/trstctl/edge-ca.keyref.json \
  --edge-issue-cn db.edge.example.test \
  --edge-cert-out /var/lib/trstctl/db.crt \
  --edge-leaf-key-out /var/lib/trstctl/db.key \
  --edge-journal /var/lib/trstctl/edge-journal.json

The agent verifies that the certificate public key equals the handle public key before asking the TPM to sign. A missing TPM, unknown handle, changed provider, or mismatched certificate refuses issuance. None triggers a software fallback.

To have the host send its journal itself, run the host agent with --edge-journal-upload and the same --edge-journal path:

trstctl-agent --name bunker-01 ... \
  --edge-journal /var/lib/trstctl/edge-journal.json \
  --edge-journal-upload

The agent sends the journal over its mutual-TLS agent channel when it connects, after a heartbeat that succeeds following failed ones (the host was offline), and when the journal grows while it is connected. The control plane reconciles each certificate against the delegation that issued it among the delegations minted for this host, where the host is the name in the agent's channel certificate, so --name must match the delegation's host. It records good leaves into the inventory and constraint breaches as violations, exactly as the console or CLI reconcile does, with the agent as the actor. Only a host-role agent may send a journal. The agent keeps edge-journal.json.uploaded beside the journal with the acknowledged prefix, so a reconnect sends only new entries; a failed upload is retried after the next heartbeat. The journal itself still only grows, and the CLI and console reconcile keep working.

PKCS#11 alternative

Set --edge-key-provider pkcs11 and configure --edge-pkcs11-module, --edge-pkcs11-token, and --edge-pkcs11-pin-file for both CSR creation and issuance. The shipping module creates persistent RSA-2048 private objects with CKA_SENSITIVE=true and CKA_EXTRACTABLE=false. The segment policy must explicitly include pkcs11 in allowed_key_providers.

The default static artifact has CGO disabled and therefore refuses this lane. Build the agent with CGO enabled on the token host so it can load the vendor's native PKCS#11 module; a static artifact never substitutes software custody.

The host TPM attestation still authenticates the host and binds the request to the CSR, but it cannot attest a different PKCS#11 object. Evidence therefore says host_attested_operator_claim. It does not claim that the TPM proved token custody. Independent token inspection is the deployment proof that the object is non-extractable.

Exportable software exception

Software custody needs two independent choices:

  1. The segment policy includes software in allowed_key_providers.
  2. Both CSR creation and issuance specify --edge-key-provider software --edge-allow-software-key.

The agent then uses --edge-key-out / --edge-ca-key for a mode-0600 PKCS#8 PEM file. The control plane records key_storage=file, key_exportable=true, and custody_assurance=host_attested_software_exception. The Sovereign/edge panel shows that warning. A host TPM proof authenticates the request but never changes the software key's exportable classification.

Segment policy shape

An enabled policy is still default-off unless it exists, pins at least one TPM attestation root, and carries DNS name constraints. Custody is an additional closed allowlist:

In the console, open Certificates → Certificate authorities → Authorities → Delegated edge CAs → Configure edge segment. An operator with issuers:write can select an existing declared segment ID; an operator who also has discovery:write can declare a segment by name and exact address or CIDR. Paste public PEM attestation roots, enter comma-separated permitted and excluded DNS subtrees, then review the entire public root bundle and provider allowlist before saving. A lost response leaves the reviewed request available for retry with the same idempotency keys. The resulting row shows the active policy; it does not prove that a host has attested or that a delegated CA was minted. Disable an existing segment through the same form to stop new mints.

For automation, declare the segment with POST /api/v1/discovery/segments or trstctl-cli discovery segments create -f segment.json, retain the returned UUID, then use PUT /api/v1/edge/segments/{segmentID} or trstctl-cli edge segments set <segmentID> -f policy.json. Read back with GET /api/v1/edge/segments or trstctl-cli edge segments list. Keep the attestation private key on the host or in its test fixture; the policy accepts only its public root certificate.

{
  "enabled": true,
  "attestation_roots_pem": ["-----BEGIN CERTIFICATE-----..."],
  "permitted_dns_domains": ["edge.example.test"],
  "excluded_dns_domains": ["blocked.edge.example.test"],
  "allowed_key_providers": ["tpm2"]
}

Omitting allowed_key_providers means exactly ["tpm2"]. Unknown providers, duplicate values, and unapproved PKCS#11/software mint requests fail closed.

Attested renewal

A segment's hosts can renew their delegation before it expires, without an operator carrying a new mint request, when the segment policy allows it:

{
  "enabled": true,
  "attestation_roots_pem": ["-----BEGIN CERTIFICATE-----..."],
  "permitted_dns_domains": ["edge.example.test"],
  "renewal_enabled": true,
  "renewal_window_days": 7
}

renewal_window_days is how long before expiry a renewal opens, 1 to 29 (0 or omitted means 7). In the console, the same Configure edge segment form has the renewal checkbox and window. Run the host agent with --edge-renew beside its usual TPM flags:

trstctl-agent --name bunker-01 ... \
  --edge-renew --edge-renew-before 168h \
  --edge-key-provider tpm2 \
  --edge-tenant <tenant-id> --edge-segment <segment-id> \
  --edge-ca-cert /var/lib/trstctl/edge-ca.crt \
  --edge-ca-key-handle /var/lib/trstctl/edge-ca.keyref.json \
  --edge-aik-key-handle /var/lib/trstctl/aik.keyref.json \
  --edge-aik-cert-chain /var/lib/trstctl/aik-chain.pem

When the certificate in --edge-ca-cert is within --edge-renew-before of expiry, the agent has the same TPM key sign a fresh CSR, certifies it with TPM2_Certify over the new challenge, and asks over its mutual-TLS channel. The control plane checks it exactly as it checks a mint, against the segment's current roots, constraints and provider allowlist. It also checks that the CSR is for the delegated CA's key, that the delegation belongs to this agent's host, and that the window is open. It then mints a delegation with the old one's lifetime, which names the old one in renewed_from (the old one shows renewed_by). The agent replaces --edge-ca-cert with the new chain, so --edge-issue moves to it. The old delegation stays valid until it expires. A refused renewal is logged and retried hourly; nothing is extended, so a host whose attestation keeps failing sees its delegation expire. An expired, revoked or already renewed delegation does not renew. Renewal attests with TPM2_Certify, so it serves the TPM2 lane; PKCS#11 and software delegations ask again through the attested mint.

Rendered live from github.com/ctlplne/trstctl — found a mistake? edit this page.