Skip to content

CA post-quantum migration

How we’ll transition the internal CA from today’s ECDSA P-384 root to a post-quantum (PQ) root, when the ecosystem is ready. This is a deferred workstream — not actionable today. It exists now as a road-map so the current classical CA’s design doesn’t accidentally box us out.

Pairs with internal-cert-authority.md (design + Phase 1-5 plan) and internal-ca-bootstrap.md (one-time bootstrap procedure for the classical root).

Status (2026-05)

Deferred. Today’s CA is ECDSA P-384 / SHA-384 — classically secure, not quantum-resistant. Both RSA and ECDSA are broken by Shor’s algorithm on a sufficient CRQC. No production-ready PQ X.509 issuance pipeline exists in our stack (step-ca + cert-manager + browsers/curl + OpenSSL).

Earliest plausible execution window: 2028-2030, gated on:

  • smallstep step-ca shipping native ML-DSA / SLH-DSA support (FIPS 204/205)
  • cert-manager landing PQ provisioner support in a stable release
  • OpenSSL 3.5+ widely deployed (it has PQ providers, but not enabled by default)
  • Operating-system trust stores accepting PQ root certs without manual workaround
  • The Go crypto/tls stack handling PQ in standard crypto/x509

Until those land, “do nothing different” is the correct call — both the threat (CRQC) and the production tooling (PQ-capable CAs + clients) are ~2030+ events.

Threat-model context

FactorThis CA
CRQC timeline (expert consensus 2024-26)10-30+ years
NIST federal PQC migration deadline~2035
Root validity10 years (expires ~2036)
Intermediate validity2 years — rotated regularly
Leaf validity90 days
Attacker modelInternal LAN only; not in the “nation-state harvest-now-decrypt-later” surface

The root’s 2036 expiry sits at the edge of “could matter,” but rotation is cheap relative to the threat: we already plan to rotate the intermediate every 2 years, and rotating the root via a successor + dual trust is a one-time playbook execution.

Migration architecture: dual-rooted CA

Two independent root CAs run side-by-side during the overlap. Fleet trust stores carry both roots; issuance flips from classical to PQ gradually as leaf certs rotate.

┌─────────────────────┐ ┌─────────────────────┐
│ Classical root │ │ PQ root │
│ (ECDSA P-384) │ │ (ML-DSA-65 etc) │
│ Generated 2026 │ │ Generated ~2028 │
│ Offline + escrow │ │ Offline + escrow │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Classical │ │ PQ │
│ intermediate │ │ intermediate │
│ (step-ca: heimdall) │ │ (step-ca: │
│ │ │ heimdall-pq) │
└──────────┬──────────┘ └──────────┬──────────┘
│ issues leaves │ issues leaves
▼ ▼
┌─────────────────────────────────────────────────┐
│ Fleet trust stores carry BOTH roots │
│ (one /usr/local/share/ca-certificates/ bundle) │
└─────────────────────────────────────────────────┘

The two CAs share nothing operationally — different roots, different intermediates, different step-ca pods. Failure of either doesn’t affect the other. cert-manager has one ClusterIssuer per CA; per-service Certificate resources pick.

Phased timeline

PhaseTrust storesIssuanceTriggered by
T+0 (today, 2026)Classical root onlyClassical CA only— current state
T+N1 (~2028)Both roots addedClassical CA still issues every leafstep-ca ships PQ support; we deploy heimdall-pq alongside heimdall; fleet trust update via ansible -t ca-trust
T+N2 (~2028-2030)Both rootsPQ CA issues new + renewed leaves; classical CA still issues existingcert-manager renewBefore drives natural rotation; new Certificate resources point at the PQ ClusterIssuer
T+N3 (~2030-2032)PQ root only (classical removed from trust)PQ CA onlyEvery leaf has rotated; classical intermediate has expired or been retired

Total transition window: 2-4 years overlap. Cleanup phase (T+N3) is optional — we can leave both roots in trust stores indefinitely with minimal cost; removing the classical root is purely hygiene.

Concrete changes from today’s IaC

The current commit (548cf2f infra: internal CA (heimdall/step-ca) Phase 1 IaC) already supports dual-root without modification. The migration is purely additive:

Trust stores (T+N1)

# ansible/group_vars/all.yaml — current state has one PEM block under
# ca_root_cert. Concatenate the PQ root cert as a second block:
ca_root_cert: |
-----BEGIN CERTIFICATE-----
... classical ECDSA P-384 root ...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
... PQ root ...
-----END CERTIFICATE-----

PEM is concatenation-friendly. update-ca-certificates reads multi-cert bundles natively. The init role’s Install internal CA cert into trust store task (ansible/roles/init/tasks/main.yaml:33) needs zero changes — it copies whatever PEM bytes are in ca_root_cert into /usr/local/share/ca-certificates/ and runs update-ca-certificates.

Apply with ansible-playbook playbooks/setup-{caddy,lamp,dns,docker, backup,package-proxies}-servers.yaml --tags ca-trust. This must run BEFORE any PQ-issued cert is served, so clients trust the new chain before they encounter it.

Second step-ca instance (T+N1)

Stand up a second IncusOS container parallel to heimdall, themed mimir (in Norse mythology Mimir keeps wisdom alongside Heimdall the gatekeeper — fits the naming pattern; pick whatever). Same host-class shape as heimdall — see internal-cert-authority.md “Why we moved out of k0s” for the VM-vs-k0s rationale that applies to both classical and PQ instances.

TF + inventory:

opentofu/step-ca.tf # add mimir alongside heimdall in the for_each map
ansible/inventory.yaml # add `mimir` to step_ca_servers group

Materials (mirrors the heimdall layout):

ansible/group_vars/step_ca_servers.yaml # add ca_intermediate_cert_pq (plaintext PEM)
ansible/host_vars/mimir.sops.yaml # new — SOPS-encrypted intermediate_ca_key
# + intermediate_ca_key_password (PQ algo)

The existing setup-step-ca-servers.yaml playbook targets the whole step_ca_servers group — adding mimir as a second member runs the same play against the new host. The play needs a small per-host conditional to pick ca_intermediate_cert vs ca_intermediate_cert_pq (or just per-host vars — simpler).

DNS (ansible/group_vars/dns_resolvers.yaml):

dns_service_aliases:
...
mimir: heimdall # OR a direct A record pointing at mimir's static IP

Since heimdall and mimir are non-Gateway VM hostnames, they go in dns_service_aliases / dns_zones (consumed by the Knot zone template on arceus) — NOT in ansible/vars/k0s-gateways.yaml (that file is for in-cluster Gateway A records).

Bootstrap runbook: internal-ca-bootstrap.md duplicates as internal-ca-bootstrap-pq.md with --kty set to whatever the PQ algorithm of choice is. The procedure is otherwise identical: offline root generation, signed intermediate, escrow.

cert-manager — two ClusterIssuers

Two ClusterIssuers in k0s/cert-manager/:

# heimdall-clusterissuer.yaml → ACME → https://heimdall.rrchnm.internal (existing)
# mimir-clusterissuer.yaml → ACME → https://mimir.rrchnm.internal (new)

Per-service Certificate resources reference one; flipping a service to PQ is a one-line edit (issuerRef.name: mimir).

Alternative: cross-signed intermediate

Instead of two parallel chains, one intermediate cert can be signed twice — once with each root key — producing two cert objects that share the same public key but verify against different roots. Clients trust whichever root matches their store; same intermediate serves both.

Trade-offs vs dual-CA:

Dual-CA (recommended)Cross-signed intermediate
step-ca instances21
Helm releases21
Failure isolationfull (one CA’s outage doesn’t affect the other)shared
Tooling maturitystandardbespoke; smallstep docs don’t make this trivial
Used historically bymost public-CA transitions (SHA-1→SHA-256, RSA→ECDSA)rare in practice
Operational simplicitymore components, simpler eachfewer components, each more complex

Cross-signed is more elegant on paper. In practice the operational tax of “two of everything” is small, and the failure isolation it buys is worth more than the extra Helm release costs. Stick with dual-CA unless cross-signing becomes well-supported tooling.

What today’s design preserves vs. forecloses

Preserves:

  • ca_root_cert as a multi-line YAML scalar — appending a second PEM is a two-line edit; no schema migration
  • Init role’s ca-trust task is algorithm-agnostic — handles N certs by construction
  • ansible/.sops.yaml regex covers intermediate_ca_key_password + intermediate_ca_key by name — same field names work for the new mimir.sops.yaml
  • setup-step-ca-servers.yaml targets the group — adding mimir as a second member is one inventory entry + per-host vars, no playbook rewrite
  • cert-manager design anticipates multiple ClusterIssuers from the start (heimdall + letsencrypt already coexist)

Forecloses: nothing. The classical CA can run indefinitely alongside a future PQ CA. Root rotation (replacing the classical root with a new classical one) and PQ migration are independent workstreams that compose cleanly.

Re-evaluation triggers

Watch for these and revisit the timeline:

  • step-ca PR / release ships PQ signature support — that’s the long-pole technical blocker. Smallstep have discussed it; no public ETA as of 2026-05.
  • NIST publishes hybrid X.509 profile — IETF LAMPS draft on hybrid cert formats stabilizes; current state is RFC-track but unstable.
  • A meaningful CRQC milestone — e.g., quantum machine factors a 2048-bit RSA modulus. As of 2026-05, the record is around 35-bit RSA factorization. A jump to ≥1024-bit would compress everyone’s timeline including ours.
  • Compliance / mandate — funding agency or institutional security policy requires PQ migration by a date. Federal contractors are watching the 2035 NIST deadline; a similar policy at GMU/RRCHNM scope would force action.
  • Public-CA precedent — Let’s Encrypt or another major public CA announces a PQ root or hybrid intermediate. Their tooling work typically backports to step-ca.

Decision log

  • Dual-root over cross-signed. Operational simplicity per-CA + full failure isolation outweigh the extra Helm release. Matches public-CA industry practice for algorithm transitions.
  • Defer until ecosystem-ready. Standing up a PQ CA today would mean none of our clients can validate its certs — pointless theatre. The classical CA’s design preserves the migration path without paying any cost today.
  • Both roots in trust stores BEFORE PQ issuance starts. Standard “trust before serve” sequencing — same principle as adding a new intermediate to cert-manager before flipping renewBefore.
  • Keep classical intermediate rotation cadence (2y) regardless. Independent of PQ work; reduces compromise blast radius for the classical CA. The two CAs rotate on their own schedules.
  • No PQ root key escrow plan committed yet. That’s a Phase-T+N1 runbook copy of internal-ca-bootstrap.md; draft it when the tooling lands, not before.