Lifecycle & PQC — keeping credentials fresh, and ready for quantum
What it is
A certificate is not a "set it and forget it" object: it's issued, used, nears expiry and must be renewed, sometimes rotated (replaced early) or revoked (canceled), and eventually retired. Lifecycle automation is trstctl doing that work on a schedule. This page also covers two forward-looking concerns: crypto-agility (changing algorithms without rewriting the system) and PQC migration (moving your estate to post-quantum algorithms before quantum computers break today's keys).
The mental model: lifecycle is the superintendent who cuts a new key before the old one wears out; crypto-agility is a master key-cutting machine that can switch blank types instantly; PQC migration is the project to re-cut every key in the building to a new, quantum-resistant blank.
Why it exists
Expiry is the number-one cause of certificate outages, and it's entirely preventable: a machine that renews on schedule never lets a certificate lapse. Rotation limits the damage of a leak, since a short-lived credential is only useful briefly. The quantum transition is a multi-year migration that can't start until your cryptography is agile — able to add and swap algorithms in one place. trstctl was built crypto-agile from the first commit, so this migration is a contained change, not a rewrite.
How it works
Lifecycle automation (F6)
Revoking or retiring an identity stops future issuance and renewal attempts for
that identity. Idle queued jobs become cancelled; a running rotation becomes
cancelled once its related queued work has stopped. Completed deliveries and
revocation publication are preserved. This does not remove a certificate from a
listener or undo an external action already performed.
Cancellation derives from the retained identity transition event. Live worker and agent leases keep their result path until completion or expiry. A bounded maintenance sweep, agent claim, or startup reconciliation then cancels remaining idle work, even when scheduled renewal is disabled or a maintenance window is closed. Attempts, errors, issuance bindings, and audit history remain intact; replaying the event log does not re-arm canceled work. Canceling a renewal while keeping its identity active is not yet available.
Open an X.509 identity's detail to see the certificate from its last completed deployment or restore, including validity dates, certificate status, destination, and receipt time. The certificate link opens that exact leaf's revocation view. This is retained evidence, not a fresh connection to the listener: a subsequent replacement or an out-of-band change may serve a different certificate.
GET /api/v1/identities/{id}/deployment-evidence requires certs:read and reads
only the caller's tenant. It selects one completed deployment (delivered or
verified) or restore (rolled_back), ordered by receipt update time and ID.
Queued and failed attempts do not displace that evidence. Certificate metadata
is resolved by the receipt's exact fingerprint, including superseded and revoked
leaves. An absent receipt means no retained completion; a receipt without a
certificate means inventory metadata is unavailable. Neither case substitutes a
certificate with a matching name or owner. The response records its read time;
it does not represent an atomic history snapshot or proof for every destination.
The same read is available to scripts, including after identity retirement:
trstctl-cli identities deployment-evidence "$identity_id"
Check the returned receipt and certificate before treating a deployment as
complete. To prove current application access, also connect to the destination
with a native client using its normal authentication and certificate checks.
The lifecycle manager watches the inventory and acts on three signals, tenant-isolated at the database layer:
Renew from ARI, with an expiry fallback. For trstctl-issued X.509 identities, the scheduler evaluates the ACME Renewal Information (ARI) window and renews early once it opens, even while the certificate is still outside the fixed
renew_beforethreshold (default720h= 30 days), a safety fallback for certificates with no usable ARI span. Each renewal re-issues through the one issuance path with anIdempotency-Key, supersedes the old certificate in a single transaction, and emits immutable lifecycle/rotation evidence. Requester-held issuance retains the original authorized CSR through renewal; the requester keeps the private key. The deprecated server-generated path records its separate custody and key-generation event.A fallback longer than the certificate's entire lifetime must not cause renewal on every sweep. New endpoint leaves issued by the platform or a selected private CA retain the actual signing constructor's validity timestamp. When
renew_beforeis at least the interval from that timestamp to signed expiry, the scheduler waits for the ARI window. Thus a new 30-day leaf with a 30-day lead does not immediately renew. A 47-day leaf with a 30-day lead still becomes due around day 17; this repair does not shorten the configured lead or postpone that deadline. The automation plan uses the same decision as the scheduler.Revoke with propagation.
Revoke(certID, reason)is idempotent, updates the inventory, enqueues arevocation.publishto the outbox in the same transaction so a crash can't drop it, and emitscertificate.revoked.Alert before expiry. It finds certificates inside the
alert_beforewindow, enriches the alert with the owner and approver recipients, enqueues a notification, stampsalerted_atso it doesn't nag, and emitscertificate.expiring.Alert when a renewal attempt fails. A transition into
renewal_failedqueues one warning for the affected identity and owner. It preserves the standing deployment and points the operator to rotation history, retry status, and the listener's certificate and expiry. The warning does not imply the retry job has ended; an existing retry can still recover automatically.The CA calendar. CA authorities run on their own clock. A leaf that expires is a page; a root that expires is an outage across every leaf beneath it, and the fix — get a new anchor into every relying party — takes quarters, not an afternoon. So the same sweep also walks
ca_authorities.not_afteragainst year-scale bands (36, 24, 12, 6 and 3 months), alerting once per band an authority crosses into, with severity scaled to the runway: a planning signal beyond a year, a warning inside one, critical inside three months. It also flags the failure that hides — an authority with less life left than the validity its leaves are issued with, which silently truncates every new leaf while issuance keeps succeeding. Alerts carry the authority, the band, how many active certificates chain to it, and the date after which leaves stop getting full validity.
The CA calendar's exact contract. Bands are 36/24/12/6/3 months, evaluated against
an averaged Gregorian month; an authority sits in the tightest band it has crossed and
alerts only when it crosses into a tighter one, so a repeated sweep is silent and each
tightening re-fires. The band already notified is recorded on the authority and only ever
tightens, so a clock skew cannot re-fire an alert the operator already saw. Authorities
beyond 36 months, and authorities with no recorded not_after, raise nothing —
an unknown expiry is stated as unknown, not rendered as healthy. Alerts land on the
notification.ca_horizon outbox destination as ca.horizon or
ca.validity_compression and fan out through the same channels as expiry alerts. The
reference leaf validity is lifecycle.leaf_validity
(TRSTCTL_LIFECYCLE_LEAF_VALIDITY, default 2160h/90 days); it is a yardstick for
horizon reporting and caps nothing. GET /api/v1/ca/authorities carries a horizon
object per authority (band, months remaining, severity, renew-by, whether truncation has
started, and the leaf validity assumed), and GET /api/v1/certificates/health now
resolves beyond 90 days into 180-day, 1-year, 2-year and 3-year bands — the flat
"later" bucket is exactly what hid multi-year hierarchy expiry.
Status: served by the running binary. A leader-only background loop scans
tenant-scoped deployed X.509 identities, honoring lifecycle.renew_before and
lifecycle.alert_before (both parsed and validated at startup), and writes the normal
ca.renew and notification.expiry outbox intents rather than acting inline. It's
integration-tested against real PostgreSQL, NATS, the signer process, and a signed
webhook sink.
Renewal-failure alerts use notification.renewal_failure, kind
identity.renewal_failed, and certificate-lifecycle routing with owner/<id> and
identity/<id> selectors. The lifecycle event retains the exact alert and receiver
identity; its state projection and notification intent commit together. Worker and
restart retries reuse that intent. Repeated failed attempts while already in
renewal_failed do not create another warning; a new failure after recovery does.
Historical failures without a retained notification are not backfilled. Raw CA and
agent errors are excluded from the notification. Only failures recorded through
this lifecycle transition produce this warning; consult job evidence for failures
that have not reached a lifecycle outcome.
POST /api/v1/lifecycle/endpoint-bindings/preview is the effect-free review path.
The operator must choose one exact built-in, private, or external CA; the response
names that CA, key custody, destination revision, planned records, queued effects,
recovery, verification, and the fingerprint that binds execution to the review.
POST /api/v1/lifecycle/endpoint-bindings accepts that fingerprint and is the served
end-to-end path for automated enrollment, provisioning, renewal, and endpoint
binding. It creates the X.509 identity, pins the issuer, binds the route, and queues
issue/deploy intents through the outbox. Initial issuance and renewal both use the
pinned authority; a missing or unavailable authority fails instead of falling back.
The issuer builds the deploy payload while the key is still in memory, so the
connector delivers the certificate/key bundle without PEM bytes ever returning from
the API response.
See and operate the live renewal plan
The Machine identities → Lifecycle automation panel answers the everyday operator questions in one place:
- Is automatic renewal running, disabled, or waiting for a maintenance window?
- How many days before expiry does trstctl renew and alert?
- Which certificates are watched, due now, already in flight, or failed?
- Is lifecycle work waiting, processing, or failed in the durable outbox?
- Which actions are real, and where are the rotation, delivery, and rollback receipts?
The panel reads GET /api/v1/lifecycle/automation-plan; headless operators use the same
contract with trstctl lifecycle automation-plan. This is an effect-free preview: it
writes no row or event and contacts no CA, connector, or notification service. Its
tenant-scoped inventory includes public metadata only. Certificate bytes, private keys,
outbox payloads, and idempotency keys are not returned.
When an identity has successful delivery or rollback evidence, the scheduler and
plan use the most recently proved certificate's expiry and renewal window. A
completed restore moves that deadline back to the predecessor; queued or failed
restores do not change it. Issuance history may still call that predecessor
superseded; a completed restore proves it is serving again, so its earlier
deadline still applies. Revoked certificates are never made renewable by a
restore receipt. A replacement with the same owner and
DNS name cannot inherit its predecessor's longer lifetime. Identities without
delivery evidence retain the legacy owner-and-name inventory match; that match
alone does not prove which certificate a listener serves.
For a due or failed identity, Review renewal now or Review retry opens the normal
version-bound lifecycle transition preview before anything is queued. Execution then
appends the lifecycle event and the ca.renew outbox intent through the existing
idempotent mutation path. Verification comes from the resulting rotation run and
connector receipt, not from a green button state.
A failed agent attempt can leave the identity in renewal_failed while its
existing renewal job retries. Scheduled sweeps do not start another renewal while
that identity has pending or processing ca.renew or endpoint.renew work.
Manual renewal previews and execution return HTTP 409 with guidance to follow the
existing job. Once that work is terminal, a still-due identity can be renewed again.
Revocation remains available during recovery.
The control limits are intentional and visible:
- Pause is a maintenance-window configuration action. The current web request does not silently rewrite operator configuration.
- Resume is automatic when the next configured maintenance window opens.
- Cancel after enqueue is not offered. A worker may already have leased the command, so claiming it was canceled could allow an unseen issuance or deployment to finish.
- Rollback is available through Connectors only when a deployed predecessor and a connector-backed rollback procedure exist. The rotation run keeps the public rollback reference used to verify that recovery.
Crypto-agility (F16)
Crypto-agility is an architecture property, and in trstctl it's non-negotiable: all
cryptography goes through a single isolated path, or an automated build check fails.
An algorithm is a typed identifier; a signer is an opaque handle that signs without
revealing its key; a backend (software, HSM, KMS) is one interface. Adding or swapping an
algorithm is therefore a one-place change, and every backend must pass a conformance
harness (ConformBackend) that signs a probe, verifies it, and confirms a wrong message
and a tampered signature both fail.
Profile selection is served for classical RSA, ECDSA, and Ed25519 and, with PQC in the
core since 2026-09-20, for the post-quantum and hybrid labels.
Operators create profile versions with POST /api/v1/profiles or trstctl-cli profiles create -f profile.json; the API validates every allowed_key_algorithms value through
internal/crypto before emitting a profile.created event, and unknown labels fail
closed.
All post-quantum algorithms and issuance/signing paths — ML-DSA (FIPS 204), ML-KEM
(FIPS 203), SLH-DSA (FIPS 205), hybrid certificate/key types, and PQC signer-held keys —
are part of the core (internal/pqc, internal/pqcruntime) and attach in every build
through cmd/trstctl/attach_families.go. They plug into the crypto and signer
interfaces behind the AN-3 boundary. The core campaign tracker described next records
work and evidence; the fleet executor (internal/pqcmigration) attaches beside it.
Core migration campaigns
CBOM sees the problem, so campaign ownership lives beside CBOM in core. A Community
operator can create a tenant-scoped campaign over existing quantum-vulnerable or
out-of-policy CBOM findings, assign an owner, deadline, wave, and readiness criteria,
then record work performed manually or by any external tool. No license is required:
POST /api/v1/pqc/campaigns starts the campaign; the corresponding list/detail/update,
readiness, finding-disposition, close, and evidence routes serve the complete workflow.
The same operations are available under trstctl-cli pqc campaigns and on /posture.
The preferred UI/API entry point is the graph-bound action route,
POST /api/v1/graph/crypto-readiness/actions. It refuses a missing, foreign,
unlocated, or unattributed row, and stores the exact current readiness-row digest in
the immutable campaign-start event. Existing /api/v1/pqc/campaigns remains the
manual campaign API; those legacy/manual campaigns do not claim a graph binding.
Graph-bound campaigns refuse further mutation with 409 Conflict when a dependent
edge or attributed owner changes. This prevents old coordination evidence from being
applied to a newly different blast radius.
Campaign mutations emit immutable pqc.migration_campaign.* events. PostgreSQL
projections carry tenant_id and forced row-level security. Closure is rejected until
the readiness gate passes and every selected finding is marked remediated or
excepted with a SHA-256 evidence digest. The closure artifact is signed by the
persistent audit key and binds the tenant, campaign, frozen finding digests,
dispositions, evidence digests, and timestamps; its included public JWKS permits
offline verification.
The canonical read and evidence routes are GET /api/v1/graph/crypto-readiness and
GET /api/v1/graph/crypto-readiness/export. The latter packages the same ordered
rows, owners, actions, evidence references, recommendations, and coverage limitation
as CSV and NDJSON, with an audit-key JWS and public JWKS for offline verification.
Automated fleet execution is an optional executor, not a prerequisite for a useful
campaign: the tracker records work and evidence on its own, and the executor
(internal/pqcmigration, attached in every build) runs re-issuance with rollback when
an operator starts a migration.
PQC migration orchestration (F57)
Knowing where your weak crypto is (the CBOM) is half the
battle; the other half is fixing it without a manual project. PQC migration is served
in every build (internal/pqcmigration): the orchestrator
consumes the CBOM read model, finds quantum-vulnerable certificate-key assets, and queues
re-issuance through the outbox toward the licensed target.
The migration API attaches POST /api/v1/pqc/migrations and
POST /api/v1/pqc/migrations/{run_id}/rollback through the attach seam, so those routes
are not part of the static OpenAPI golden; trstctl-cli pqc migrations start|rollback
drives them, while the pqc campaigns commands record operator work and never call them. Completion and rollback
project through the event log into crypto_assets, so posture dashboards and
migration_progress stay derived from replayable state, not hand-edited tables.
Execution status: served in every build through internal/pqcmigration, for CBOM
certificate-key assets through ACME hybrid transition re-issuance with rollback. The core
also exposes CBOM posture, profile selection, and migration campaign
tracking/proof, but not PQC algorithms, issuance, or automated fleet execution.
Offline operator rehearsal
Run the licensed rehearsal from the repository root after the pinned Go modules, container images, OpenSSL interop image, and bundled PostgreSQL artifact have been supplied locally:
make pqc-operator-lab
The command runs the three exact shipped-binary definition-of-done proofs: pure
ML-DSA EST enrollment with stock OpenSSL, the classical/PQ SPIFFE SVID pair, and
CBOM migration plus rollback. It sets the Go module resolver offline and the
pinned interop runner uses --pull=never. The result is
dist/pqc-operator-lab-licensed.tar.gz.
Community operators use the same workflow with an edition selector:
make core-only pqc-operator-lab
That command builds and starts isolated trstctl_core control-plane and signer
binaries against private bundled PostgreSQL and embedded NATS data directories.
It proves that CBOM reading remains served and that /v1/editions reports PQC
execution as unavailable. It exits successfully without advertising or calling
the proprietary migration mutation.
Both archives contain a versioned manifest, one machine report and one public
transcript per stage, and SHA256SUMS. Archive construction rejects private-key
PEM, bearer credentials, trstctl API tokens, and non-empty password/secret/token
JSON fields. The command deletes only its own private runtime directory; the
receipt archive is the only retained output.
In the console
In the web console, the certificate inventory at /certificates is also a lifecycle
command center: expiry bands, a 47-day renewal-readiness simulator (does each certificate
renew inside the shrinking CA/Browser Forum maximum lifetime?), deployment receipts, and
a per-certificate renewal-history timeline. The crypto-agility work surfaces at
/posture as CBOM-backed algorithm posture, the complete core campaign/evidence
workflow, and the licensed executor when attached. See The web console.
Use it
Lifecycle thresholds are configuration today:
{
"lifecycle": {
"renew_before": "720h",
"alert_before": "336h"
}
}
Both are shipped defaults (30 / 14 days). renew_before is the fallback window before
expiry when trstctl re-issues absent an earlier ARI window; alert_before is when it
warns. See Configuration and Operations for
the full set and running behavior. PQC posture is visible in the
CBOM via GET /api/v1/cbom/assets; core campaigns are
served under /api/v1/pqc/campaigns, while automated fleet execution attaches only
from proprietary EE.
Pitfalls & limits
- ARI-driven renewal covers trstctl-issued deployed X.509 identities. Rows discovered from an outside CA stay visible for expiry/risk, but renewing them needs an issuer or connector path that can actually replace that external certificate.
- Unknown issuance time remains unknown. Older records, external CA responses and
protocol issuance paths that do not retain the constructor timestamp still use the
existing fixed fallback alongside ARI. Discovery time and the backdated X.509
notBeforeare not substituted for issuance time. An older local leaf can therefore renew once before its new endpoint successor gains this protection; external successors without the timestamp can still repeat. This change covers the served identity scheduler and its plan, not the separate fixed-thresholdlifecycle.Manager.RenewExpiringlibrary method. - PQC execution is core scope since 2026-09-20: PQC algorithms, automated fleet
execution and the standalone
pqc campaignsAPI/CLI/UI all attach in every build. - Former PQC end-to-end residuals are served in every build.
Stock OpenSSL 3.5 enrolls and verifies a pure ML-DSA-65 subject leaf over EST; the
stock SPIFFE Workload API receives a two-entry classical + ML-DSA-65 response; and CBOM
TLS findings roll out TLS 1.3 plus
X25519MLKEM768through a posture-capable connector with receiver readback and exact rollback (the shipped proof uses Envoy) — a tested client/connector boundary, not a claim about every legacy client. Hybrid-to-pure replacement of an already deployed leaf stays gated by succession and evidence-based retirement; direct pure enrollment is served. - SLH-DSA signatures are large — the conservative choice for long-lived roots, not high-volume leaf issuance; pick the algorithm per profile.
Reference
- Config:
lifecycle.renew_before(default720h),lifecycle.alert_before(default336h; Go duration strings);TRSTCTL_LIFECYCLE_RENEW_BEFORE,TRSTCTL_LIFECYCLE_ALERT_BEFORE. - Lifecycle ops:
RenewExpiring,Rotate,Revoke,AlertExpiring. - Events:
certificate.renewed,certificate.revoked,certificate.expiring;pqc.migration_campaign.started,pqc.migration_campaign.updated,pqc.migration_campaign.finding_dispositioned,pqc.migration_campaign.closed;licensed_crypto.migration.started,licensed_crypto.migration.asset_completed,licensed_crypto.migration.rollback_completed,protocol.issued. - CBOM migration feed:
POST /api/v1/cbom/scansrecordscbom.asset.observed;GET /api/v1/cbom/assetsreturns crypto posture, licensed migration targets, andmigration_progress. - PQC migration API: proprietary EE attaches
POST /api/v1/pqc/migrations(CBOM certificate-key assets) andPOST /api/v1/pqc/migrations/{run_id}/rollback. - Core PQC campaign API:
/api/v1/pqc/campaignsplus detail, update, readiness, finding disposition, close, and signed-evidence export routes. - PQC algorithms: proprietary EE scope: ML-DSA (FIPS 204), ML-KEM (FIPS 203), SLH-DSA (FIPS 205), and hybrid algorithms. See the post-quantum section of Current limitations.
See also
Issuance & certificate authorities · Observability & risk (the CBOM source) · Configuration · Operations & resilience · glossary: rotation, revocation, PQC, CBOM
Covers: F6, F16, F57