Intermediate CA key rotation & rollover¶
Signing keys don't live forever. An intermediate ("issuing") CA key must be rotated periodically — as it nears the end of its own validity, to shorten key lifetimes as a matter of hygiene, or urgently in response to a suspected compromise. The hard requirement is continuity: rotating the key must not break the certificates already issued under the old key.
secsy-pki performs rotation as an HSM-backed, three-stage rollover with a dual-chain overlap window, so old and new chains both validate until the old key has drained.
Every signing operation in this workflow happens inside the key provider (the HSM via PKCS#11, or the software keystore in dev/CI). The new private key is generated on the device and never leaves it, exactly like the original CA keys (see HSM configuration and Key ceremony).
The rollover model¶
root CA (unchanged trust anchor)
│ signs both intermediates
┌─────────┴──────────┐
▼ ▼
old intermediate new intermediate ← same Subject DN, different key
(superseded) (active)
│ │
▼ ▼
leaves signed by leaves signed by
the old key the new key
└────────┬───────────┘
▼
both validate during the overlap window
(combined chain / AIA bundle carries both intermediates)
Three controlled stages:
-
Rotate. A fresh keypair is generated inside the provider. A new intermediate certificate is cross-signed under the same parent (root), carrying the same Subject DN as the old intermediate so it is a drop-in issuer — only the key (and its Subject Key Identifier) changes. The old CA is marked
superseded; it keeps validating the leaves it already signed. New issuance is automatically directed at the new key. -
Overlap. Both intermediate certificates are published together as a combined chain / bundle. A leaf signed by the old key chains through the old intermediate; a leaf signed by the new key chains through the new one. Relying parties pick the correct issuer by Authority Key Identifier, so a single bundle validates both.
-
Retire. Once no leaves signed by the old key remain valid (they have expired or been re-issued onto the new key), the old intermediate is revoked under its parent and the parent CRL/OCSP is refreshed, decommissioning the retired key. Retirement is refused while outstanding leaves remain unless explicitly forced (for emergency key-compromise response).
Why continuity holds¶
- The new intermediate keeps the same Subject DN, so it is a valid issuer for
the same
Issuerfield that appears in already-issued leaves. - Each leaf carries an Authority Key Identifier matching the Subject Key Identifier of the key that signed it. During overlap both intermediates are in the bundle, so a verifier deterministically selects the right one.
- The root is untouched — the trust anchor never changes, so nothing needs to be re-distributed to relying parties beyond the intermediate bundle.
CLI (secsy-ca)¶
Rotation is a ceremony-style operation, mirroring secsy-ca ceremony: when you
enroll operators it requires an M-of-N confirmation quorum and writes an
auditable transcript. All events are recorded in the tamper-evident audit log
(RBAC & audit).
| Command | Purpose |
|---|---|
rotate-intermediate |
Generate a new key, cross-sign a new intermediate under the parent, open the overlap window |
rotation-status |
Show a CA's rollover state (superseded/active, predecessor/successor, outstanding leaves, safe-to-retire) |
list-rotations |
List all CAs currently in a rollover lineage |
publish-chain |
Emit the combined overlap chain (AIA/bundle) for a CA |
retire-intermediate |
Retire a drained, superseded key: revoke it under the parent and refresh the parent CRL |
Rotate¶
# Simple rotation (no quorum); writes a transcript and the combined chain.
secsy-ca -config config.yaml rotate-intermediate \
-ca issuing-ca \
-transcript-out rotation.json \
-chain-out combined-chain.pem
Flags: -new-label (default derives an -rN generation suffix), -key-type
(default reuses the current key's type), -validity-days (default reuses the
old certificate's original span, always clamped to the parent's expiry).
Ceremony-style, 2-of-3 quorum (identical confirmation mechanics to
secsy-ca ceremony):
printf 'alice:phrase-a\nbob:phrase-b\n' | secsy-ca rotate-intermediate \
-ca issuing-ca -operators alice,bob,carol -quorum 2 -non-interactive
Inspect¶
secsy-ca rotation-status -ca issuing-ca # human-readable
secsy-ca rotation-status -ca issuing-ca -json # machine-readable
secsy-ca list-rotations # all CAs in a rollover lineage
Publish the bundle¶
Serve bundle.pem wherever relying parties fetch issuer certificates (AIA, a
static bundle, your ingress). During overlap it contains both intermediates
plus the parent chain; after retirement it drops the retired key.
Retire¶
# Refused while old-key leaves are still valid:
secsy-ca retire-intermediate -ca issuing-ca
# error: cannot retire ...: N leaf certificate(s) signed by the old key are
# still valid; safe to retire after <timestamp> ...
# After the old key has drained (leaves expired or re-issued):
secsy-ca retire-intermediate -ca issuing-ca \
-reason cessationOfOperation -crl-out root-crl.der
-force retires despite outstanding leaves — this intentionally breaks those
chains and is meant only for key-compromise response, where invalidating the old
key is the goal. retire-intermediate accepts the same -operators/-quorum
confirmation flags as rotation.
After retirement, publish the refreshed parent CRL (written to -crl-out, or
served live from /api/ca/{parent}/crl) so relying parties learn the old
intermediate is revoked.
HTTP API¶
A public, unauthenticated endpoint serves the combined bundle for relying parties (like CRL/OCSP):
Returns the active intermediate, any overlapping superseded siblings, and the
parent chain up to the root — the same bundle as secsy-ca publish-chain.
Rotation and retirement themselves are administrative, ceremony-style operations
and are driven through the secsy-ca CLI rather than the API.
Automatic rotation via the expiry monitor¶
The background expiry monitor can trigger rotation automatically as an intermediate nears expiry, so long-lived issuing keys roll over ahead of time without manual intervention. Retirement remains a deliberate, manual step.
monitor:
enabled: true
interval_hours: 12
warning_days: 30
rotate_intermediates: true # enable auto-rotation of intermediates
rotate_before_days: 45 # rotate when the intermediate cert has <= 45 days left
# (default: warning_days)
On each scan the monitor rotates every active intermediate whose own
certificate falls within rotate_before_days, opening the overlap window and
recording a ca.rotate audit event. New issuance immediately uses the fresh key;
old-key leaves keep validating through the published bundle until they drain, at
which point an operator retires the old key.
Environment override: set SECSY_MONITOR_ROTATE_INTERMEDIATES=true to enable
auto-rotation without editing the config file (the threshold still comes from
rotate_before_days).
Draining strategy¶
The old key is "drained" when it has no outstanding valid leaves. Two levers:
- Expiry. Short-lived leaves drain on their own.
rotation-statusshows the recordedretire_afterdeadline (the latestNotAfteramong the outstanding leaves at rotation time) as a hint. - Re-issuance. New issuance is automatically routed to the new key, so any
workload that re-requests a certificate (ACME renewal, SCEP/EST re-enrollment,
secsy-ca issueagainst the same CA reference) moves onto the new key.
Retirement is gated on a live outstanding-leaf check, not the stored hint —
so it is always safe: the old chain cannot break as long as you retire without
-force.
Rotation drill (SoftHSM)¶
scripts/rotation-drill.sh exercises the whole rollover end-to-end in an
isolated SoftHSM sandbox and asserts continuity with openssl verify:
./scripts/rotation-drill.sh # runs and cleans up
ROT_KEEP=1 ./scripts/rotation-drill.sh # keep the workspace for inspection
It provisions a fresh token, creates a root + intermediate, issues a leaf under the old key, rotates the intermediate, issues a leaf under the new key, and proves both leaves validate against the single combined overlap chain. It then shows premature retirement being refused, drains the old key, retires it, and verifies the root CRL lists the retired intermediate and that the freshly published chain no longer carries it.
Tests¶
server/internal/ca/rotation_test.go(build tagsqlite) — runs against both the software provider and SoftHSM (pkcs11). Proves an old-key leaf validates against the combined chain after rotation, that new issuance routes to the new key, controlled/forced retirement, and the monitor-facingAutoRotateDuetrigger (only near-expiry intermediates rotate).server/internal/monitor/runner_rotation_test.go— the monitor runner triggers rotation only when enabled, with the configured threshold, and audits it.
Run them with: