This is the first post in a series about PKI.Next, a Certificate Authority built from scratch in Rust. The series covers the architecture, the cryptographic decisions, and the operational features that make a CA trustworthy enough to replace systems that have been running for two decades.

Why Build a New CA?

I have spent years working with Dogtag PKI, Red Hat’s Java-based Certificate Authority that has been in production since the mid-2000s. Dogtag works. It issues certificates, it generates CRLs, it handles OCSP, and it has passed Common Criteria evaluations. But it carries twenty years of accumulated decisions that are increasingly difficult to change:

  • Java serialization in the request pipeline, tightly coupling internal message formats to JDK versions
  • JSS (Java Security Services) wrapping Mozilla NSS, creating a two-layer abstraction over PKCS#11 that makes debugging HSM issues genuinely painful
  • Tomcat deployment, requiring a full Java application server for what is fundamentally a signing service
  • XML-heavy configuration spread across dozens of profile definitions and CS.cfg entries
  • No native container story — Dogtag assumes systemd, NFS-shared pki-tomcat directories, and persistent local state

None of these are bugs. They are the natural consequences of building a CA in 2004 and maintaining it through fifteen years of changing requirements. But they make it hard to answer the questions that matter today: How do you run a CA in Kubernetes? How do you add post-quantum algorithms without rewriting the crypto layer? How do you get from zero to issuing certificates in under a minute?

PKI.Next is an attempt to answer those questions with a clean sheet.

Why Rust

The language choice was not about performance benchmarks. A CA is not a high-throughput system — even large deployments issue thousands of certificates per day, not millions per second. The choice was about three properties that matter specifically for PKI infrastructure:

Memory safety without garbage collection. A CA holds signing keys in memory. A use-after-free or buffer overflow in a CA is not a crash — it is a key compromise. Rust’s ownership model eliminates these classes of bugs at compile time, without the unpredictable pause times of a garbage collector that would complicate HSM token sessions.

Zero-cost abstractions over unsafe operations. Cryptographic operations are inherently unsafe at the FFI boundary — you are calling into C libraries (OpenSSL, aws-lc, PKCS#11 modules) that use raw pointers and manual memory management. Rust lets you wrap these in safe abstractions with no runtime overhead. The Signer trait in PKI.Next looks the same whether the backing implementation is a software key, a FIPS-validated library, or a hardware security module.

Compile-time feature selection. Rust’s feature flag system lets you build the same codebase with different cryptographic backends. cargo build --features fips links against aws-lc-rs for FIPS 140-3 validated crypto. cargo build --features pkcs11 enables hardware security module support. The default build uses ring for fast development. These are not runtime configuration options that could be misconfigured in production — the binary physically cannot use the wrong backend.

The Crate Architecture

PKI.Next is a Cargo workspace of 23 crates. The dependency graph is intentionally layered to enforce separation of concerns:

graph TD
    subgraph "Foundation"
        types["pki-types
shared data structures"] crypto["pki-crypto
signing, cert building"] store["pki-store
persistence traits + PG"] lint["pki-lint
certificate validation"] end subgraph "CA Engine" ca["pki-ca
issuance, revocation, CRL"] ocsp["pki-ocsp
OCSP response building"] end subgraph "Server Infrastructure" common["pki-server-common
middleware, HA, audit"] server["pki-server
CA API + workers"] ra["pki-ra-client
RA → CA communication"] end subgraph "Protocol Servers" est["pki-est-server
RFC 7030"] acme["pki-acme-server
RFC 8555"] coap["pki-coap-server
RFC 9148"] spire["pki-spire-server
SPIFFE/SPIRE"] vault["pki-vault-server
key escrow"] dogtag["pki-dogtag-compat
Dogtag API proxy"] end subgraph "CLI" cli["pki-cli
rs-pki binary"] end types --> crypto types --> store crypto --> ca store --> ca lint --> ca crypto --> ocsp ca --> server ocsp --> server common --> server store --> common ra --> est ra --> acme ra --> coap ra --> spire ra --> vault ra --> dogtag ra --> cli
Click to expand

The key design constraint is that protocol servers never touch the CA’s signing key. They communicate with the CA through pki-ra-client, which makes mTLS-authenticated API calls. This is the Registration Authority (RA) pattern from RFC 4210, applied to every enrollment protocol:

sequenceDiagram
    participant Client as Client Device
    participant PS as Protocol Server
(EST/ACME/CoAP) participant CA as CA API Server participant HSM as Signing Key
(Software/PKCS#11) Client->>PS: Protocol-specific request
(EST SimpleEnroll, ACME Order, etc.) PS->>PS: Parse & validate protocol PS->>CA: POST /v1/ca/requests
(CSR + profile + metadata) CA->>CA: Policy check & profile enforcement CA->>HSM: Sign certificate HSM-->>CA: Signed certificate CA-->>PS: Certificate response PS-->>Client: Protocol-specific response
(PKCS#7, PEM, CBOR, etc.)
Click to expand

This means you can deploy the EST server in a DMZ, the ACME server on a public endpoint, and the CoAP server on an IoT gateway — all issuing certificates from the same CA — without exposing the CA’s signing key to any of them. If a protocol server is compromised, the attacker can submit requests but cannot sign certificates.

Seven Binaries from One Crate

The pki-server crate compiles into seven distinct binaries, each running a single responsibility:

BinaryPortPurpose
pki-ca-api8443REST API for certificate operations
pki-ocsp-responder8444OCSP status queries
pki-crl-worker9090 (health)Periodic CRL generation
pki-ocsp-presigner9090 (health)Pre-signs OCSP responses to Redis
pki-expiration-monitor9090 (health)Certificate expiry notifications
pki-migrateDatabase schema migrations
pki-server8443Monolith mode (all of the above)

The monolith binary is the same code with all workers running as Tokio tasks in a single process. It exists for development and small deployments where running six containers is overkill. The microservice binaries exist for production deployments where you want to scale, restart, and monitor each component independently.

This is not a matter of opinion or architecture astronautics. In practice:

  • The CRL worker needs to run exactly once (leader-elected) while the CA API scales horizontally
  • The OCSP presigner is CPU-bound (signing responses) while the OCSP responder is I/O-bound (serving from Redis)
  • The expiration monitor runs on a slow timer and should not compete for resources with request-serving components

The Signing Abstraction

The central abstraction in pki-crypto is the Signer trait:

pub trait Signer: Send + Sync {
    fn sign(&self, data: &[u8]) -> Result<Vec<u8>>;
    fn algorithm(&self) -> &SigningAlgorithm;
    fn public_key_der(&self) -> &[u8];
}

Three methods. Every signing operation in the system — certificate issuance, CRL signing, OCSP response signing — goes through this trait. The implementations are:

ImplementationBackendAlgorithmsUse Case
SoftwareSignerringECDSA, RSA, Ed25519Development, non-FIPS
FipsSoftwareSigneraws-lc-rsECDSA, RSAFIPS 140-3 production
Pkcs11SignercryptokiECDSA, RSA, Ed25519, ML-DSAHSM-backed production

The CA engine does not know which implementation it is using. The startup code selects the signer based on configuration:

[ca]
signing_key = "/etc/pki/keys/ca-key.pem"    # Software signer
hsm_enabled = false

# OR

[ca]
hsm_enabled = true
pkcs11_module = "/usr/lib/libkryoptic.so"    # PKCS#11 signer
pkcs11_slot = 0
key_label = "ca-signing-key"

This is where Rust’s type system pays dividends. The Signer trait is object-safe, so the CA engine stores Arc<dyn Signer> — a reference-counted pointer to whatever implementation was selected at startup. There is no if hsm { ... } else { ... } sprinkled through the certificate issuance code. The signing backend is decided once, at process startup, and the rest of the system is oblivious.

What 49,000 Lines Gets You

The complete feature set, as of this writing:

Certificate Lifecycle

  • CSR submission, agent approval/rejection, bulk operations
  • 26 built-in certificate profiles (TLS server, client auth, subordinate CA, OCSP signing, S/MIME, PKINIT, router, VPN, Wi-Fi, code signing, and more)
  • Certificate hold and unrevoke (RFC 5280 certificateHold reason)
  • Re-enrollment with new key pairs

Revocation

  • Full CRL, delta CRL, and sharded CRL generation
  • OCSP responder with Redis-backed pre-signed response cache
  • Bulk revocation API

Cryptography

  • ECDSA P-256/P-384, RSA-4096, Ed25519
  • ML-DSA-44/65/87 (FIPS 204 post-quantum signatures)
  • FIPS 140-3 mode via aws-lc-rs
  • PKCS#11 HSM support (Kryoptic soft-token for testing, hardware HSMs for production)

Enrollment Protocols

  • EST (RFC 7030) — enterprise device enrollment
  • ACME (RFC 8555) — automated certificate management with MPIC
  • CoAP/DTLS (RFC 9148) — constrained IoT devices
  • SPIFFE/SPIRE — Kubernetes workload identity
  • Dogtag compatibility proxy — drop-in replacement for FreeIPA

Operations

  • HMAC-chained audit logs (Common Criteria FAU_STG.2 tamper evidence)
  • Leader election for HA worker deployments (Redis or PostgreSQL advisory locks)
  • React/PatternFly 6 dashboard with 47 pages
  • CLI tool (rs-pki) with 16 command groups
  • Container-native: 12 container image targets from a single Containerfile
  • Health checks on every binary

Security

  • Role-based access control with exclusive role constraints
  • mTLS everywhere (inter-service, CLI, protocol servers)
  • Access banners, session management, system recovery
  • Certificate linting against CA/Browser Forum Baseline Requirements

The Dashboard

PKI is traditionally a CLI-and-config-file domain. Dogtag has a web UI, but it is a Struts-era JSP application that requires significant expertise to navigate. PKI.Next ships a modern React dashboard built on Red Hat’s PatternFly 6 design system:

graph LR
    subgraph "Dashboard Pages"
        overview["Overview
stats, charts, activity"] certs["Certificates
list, detail, download"] requests["Requests
queue, approve, reject"] profiles["Profiles
create, edit, manage"] crl["CRL Management
generate, shards, delta"] audit["Audit Log
search, filter, verify chain"] users["User Management
RBAC, roles, sessions"] servers["Protocol Servers
register, configure, deploy"] trust["Trust Hierarchy
CA chain visualization"] end subgraph "Backend" api["CA REST API
Axum / Rust"] end overview --> api certs --> api requests --> api profiles --> api crl --> api audit --> api users --> api servers --> api trust --> api
Click to expand

The dashboard is not a separate application — it is served by the CA API binary as static assets, so there is no additional deployment step. The protocol server management pages deserve special mention: you can register an EST or ACME server, configure its enrollment profile, validate the configuration, trigger a health check, and generate deployment artifacts (Docker Compose, Quadlet systemd units, or Ansible playbooks) directly from the browser.

What Comes Next

The rest of this series digs into specific features:

Each post includes architecture diagrams, configuration examples, and the specific implementation decisions that make a Certificate Authority trustworthy.


The previous posts on this blog cover OCSP vs CRL sharding performance and event-driven certificate lifecycle management.