Engineering · Post 1 of 3

Building a Post-Quantum TLS Gateway in Rust

X25519MLKEM768 with quinn, rustls, and aws-lc-rs.

~13 minute readScrutari Engineering

Every TLS handshake your gateway completes today is being recorded somewhere. Not by you, by an adversary whose business model is patient. They aren’t going to decrypt the traffic in 2026. They’re going to decrypt it in 2032, when a cryptographically relevant quantum computer is online and the X25519 key agreement that protected today’s handshake has become a homework exercise.

This is the “harvest now, decrypt later” threat, and unlike most security threats it has a clock you can read off NIST’s roadmap. The post-quantum migration is the rare deadline where waiting until the year of the deadline means you’re already a decade too late. Every recorded handshake from today is a hostage to whoever wins the qubit-count race.

We built Scrutari’s gateway on the assumption that this is a 2026 problem, not a 2030 one. The gateway is a multi-tenant IoT edge written in Rust that terminates TLS 1.3 over QUIC, performs the key exchange using the hybrid post-quantum group X25519MLKEM768, and forwards plaintext h3 requests to tenant backends after a per-tenant routing pass. Every component of the cryptographic stack, quinn for the QUIC transport, rustls 0.23 for the TLS state machine, the aws-lc-rs crypto provider for the KEM and AEAD primitives, was chosen because it speaks hybrid post-quantum today, in production, on the same exact code paths every browser shipped after Chrome 131 already exercises millions of times per second.

This post is the first in a three-part series walking through the architecture. It covers the gateway’s TLS layer end-to-end: the hybrid-KEX rationale, the Rust crate stack and why we pinned the versions we did, the four-way policy matrix that lets one binary serve everything from a strict-PQ IoT fleet to a FIPS-approved-mode federal deployment, and the code at the QUIC listener boundary that makes it real. Post 2 (“Measuring Sustained PQ-KEM Handshake Throughput”) walks through the load-test harness we built to validate the perf numbers. Post 3 (“Domain Cascade and Multi-Tenant Routing in a PQ Gateway”) covers the per-tenant routing layer that sits on top of all this.

If you’re an engineer evaluating a post-quantum migration plan for your own infrastructure, the goal here is to give you a concrete reference point, code, version pins, performance receipts, rather than another marketing-shaped “PQ is coming” overview. Concrete decisions and the trade-offs behind them; that’s what was missing from the post-quantum content we wished existed when we started.

The rest of this post is structured to be skimmable. If you only have five minutes, jump to section 4, that’s where the code lives. Sections 2 and 3 are the background and the rationale; sections 5–8 are the operational nuance.

2. Why X25519MLKEM768, and why hybrid at all

The case for hybrid key exchange is the case for not betting your security on a single unbroken assumption. Classical X25519 is the assumption that the elliptic-curve discrete log problem is hard for classical adversaries; ML-KEM-768 (formerly Kyber-768, now standardised as FIPS 203) is the assumption that learning-with-errors over module lattices is hard for quantum adversaries. A hybrid handshake derives the session key from both exchanges combined, which means an attacker has to break both primitives to recover the plaintext. If ML-KEM has a structural break discovered tomorrow, classical X25519 is still there to do its job; if a quantum computer breaks X25519 in 2032, ML-KEM is still there to do its job. The cost is one extra ~1 KB key share in the ClientHello and ServerHello, which is the cheapest insurance any cryptosystem will ever offer.

X25519MLKEM768 specifically, not X25519Kyber768Draft00, not BIKE, not HQC, because the IETF’s draft-ietf-tls-ecdhe-mlkem settled on this construction, and the entire production ecosystem aligned around it. Chrome shipped X25519MLKEM768 as the default key agreement in version 131 for desktop in late 2024 and rolled it to stable for all platforms shortly after. Cloudflare’s edge enables it on customer zones by default per their PQC support docs. The major TLS implementations, OpenSSL, BoringSSL, rustls, the JDK as of Java 27, converged on it. Picking anything else in 2026 means picking a hybrid your clients can’t speak.

ML-KEM-768 is NIST PQC security level 3, roughly equivalent to AES-192 against both classical and quantum adversaries. That’s the level the IETF chose as the default precisely because levels 1 and 5 (AES-128 and AES-256 equivalents respectively) are bracketed too tightly to it to be worth the differentiation cost in a hybrid construction where classical X25519 is doing half the work anyway. The IETF-registered codepoint is 0x11EC, combining X25519 ECDHE with FIPS 203 ML-KEM-768.

3. The Rust crate stack: quinn, rustls 0.23, aws-lc-rs

Three workspace-pinned crates do the heavy lifting. Each was chosen for a specific property the others couldn’t supply, and each is pinned at workspace level so that no transitive dependency upgrade in an unrelated crate can silently move the crypto surface underneath us.

quinn = "0.11.9". quinn is the only async-native QUIC stack in Rust mature enough to run in production. We use it for the entire transport, UDP socket management, packet framing, ack-eliciting and PADDING frames, connection migration, path validation, the works. quinn factors crypto out behind a crypto::ServerConfig trait, which lets us slot in rustls without quinn caring about TLS internals. Its release cadence has tracked the IETF QUIC v1 errata closely; 0.11 is the line that fully supports the rustls-0.23 crypto-provider API we need.

rustls = "0.23". rustls is the TLS 1.3 state machine. The 0.23 line introduced the pluggable CryptoProvider trait, the single most important change for a post-quantum migration, because it lets you bring your own KEM groups instead of being locked into whatever ring supports. The default aws_lc_rs::default_provider() includes X25519MLKEM768 as a supported group out of the box. Before 0.23, getting PQ into rustls meant either patching it or using the side-loaded rustls-post-quantum crate from Prossimo, workable but a heavier maintenance burden than a default provider you just configure.

aws-lc-rs as the crypto provider. AWS-LC is the cryptographic library underpinning many AWS services; aws-lc-rs is the Rust binding. We picked it over the default ring provider for three reasons. First, FIPS 140-3 module validation is on the AWS-LC roadmap on a serious schedule, ring has no FIPS path. For a gateway product that ends up in regulated verticals (financial services IoT, healthcare telemetry, federal contractors), having a FIPS-validated build option behind a feature flag is a deal-qualifier. Second, X25519MLKEM768 group support landed in aws-lc-rs ahead of most alternatives, which let us prototype on a stable release rather than a fork. Third, ARM (Graviton on AWS, M-series on developer laptops) hardware-acceleration paths through AWS-LC’s assembly-optimized primitives are measurably faster than the portable-Rust fallbacks in ring.

Workspace dependency pinning isn’t just version discipline, it’s the property we depend on for our load-test harness to be honest. The harness pulls the exact same quinn and rustls and aws-lc-rs versions the gateway does. A bug in the handshake path can’t surface as a gateway-side perf number because both sides share the implementation.

4. The QUIC listener at the code level

The TLS layer is structured around three pure decision functions and a thin top-level builder. The shape decouples policy (which groups, which cipher suites) from plumbing (rustls + quinn wiring), which means an auditor can read the security-relevant decisions in two short functions without having to chase config-builder fluency.

The first decision is which KEM groups the server advertises. This is one match statement over the CryptoPolicy enum:

/// Pure decision function: which `SupportedKxGroup`s the server
/// advertises for a given [`CryptoPolicy`].
///
/// Order is significant — rustls picks the first group present in
/// *both* the server policy and the client's `key_shares` extension.
///
/// # Matrix
///
/// | Policy                          | KEX groups                                         |
/// |---------------------------------|----------------------------------------------------|
/// | `HybridOnly`                    | `[X25519MLKEM768]`                                 |
/// | `HybridWithClassicalFallback`   | `[X25519MLKEM768, X25519, P-256, P-384]`           |
/// | `FipsAdvisory`                  | `[X25519MLKEM768]` (validated module, hybrid PQ)   |
/// | `FipsStrict`                    | `[P-384, P-256]` (no hybrid — see SP 800-227)      |
fn server_kx_groups(policy: CryptoPolicy) -> Vec<&'static dyn SupportedKxGroup> {
    match policy {
        // Hybrid only. IETF codepoint 0x11EC, combining X25519 ECDHE
        // with FIPS 203 ML-KEM-768.
        CryptoPolicy::HybridOnly | CryptoPolicy::FipsAdvisory => {
            vec![kx_group::X25519MLKEM768]
        }
        CryptoPolicy::HybridWithClassicalFallback => vec![
            // Hybrid first so a PQ-capable client always negotiates it.
            kx_group::X25519MLKEM768,
            // Classical fallbacks, in rustls's own preference order.
            kx_group::X25519,
            kx_group::SECP256R1,
            kx_group::SECP384R1,
        ],
        // FIPS approved mode: classical NIST curves only. No hybrid
        // (SP 800-227 hasn't ratified a combiner), no X25519 (not on
        // the FIPS approved list). P-384 first — approved-mode
        // guidance prefers ≥112-bit security strength as the default.
        CryptoPolicy::FipsStrict => vec![kx_group::SECP384R1, kx_group::SECP256R1],
    }
}

Three things to notice. First, the policy decision is one match arm per policy, the gateway’s posture toward PQ is a single point of configuration, not scattered across the codebase. Second, HybridOnly and FipsAdvisory collapse into the same match arm; we’ll explain that decision in section 5. Third, the comments name the IETF codepoint (0x11EC) and the NIST publication (SP 800-227) that govern the choices, exactly the kind of citation an auditor or a NIST CMVP reviewer will be looking for.

The second decision is which TLS 1.3 cipher suites the QUIC listener advertises. QUIC has a wrinkle that TCP doesn’t:

/// Pure decision function: which TLS 1.3 cipher suites the QUIC
/// edge listener advertises. Always includes
/// `TLS13_AES_128_GCM_SHA256` because RFC 9001 § 4.1 names it as
/// the mandatory-to-implement AEAD for Initial-packet protection.
///
/// # Matrix
///
/// | Policy            | QUIC cipher suites                                         |
/// |-------------------|------------------------------------------------------------|
/// | `HybridOnly`      | `[AES_256_GCM, AES_128_GCM, CHACHA20_POLY1305]`            |
/// | `HybridWith…`     | `[AES_256_GCM, AES_128_GCM, CHACHA20_POLY1305]`            |
/// | `FipsAdvisory`    | `[AES_256_GCM, AES_128_GCM, CHACHA20_POLY1305]`            |
/// | `FipsStrict`      | `[AES_256_GCM, AES_128_GCM]` (no ChaCha20-Poly1305)        |
fn server_cipher_suites_quic(policy: CryptoPolicy) -> Vec<CipherSuite> {
    match policy {
        CryptoPolicy::FipsStrict => vec![
            CipherSuite::TLS13_AES_256_GCM_SHA384,
            CipherSuite::TLS13_AES_128_GCM_SHA256,
        ],
        _ => vec![
            CipherSuite::TLS13_AES_256_GCM_SHA384,
            CipherSuite::TLS13_AES_128_GCM_SHA256,
            CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
        ],
    }
}

TLS_AES_128_GCM_SHA256 must appear in every QUIC cipher-suite list because RFC 9001 § 4.1 names it as the mandatory-to-implement AEAD for QUIC Initial-packet protection. Without it, quinn::crypto::rustls::QuicServerConfig::try_from(rustls_config) rejects the config at boot with “no initial cipher suite found.” This is the kind of cross-RFC dependency you only discover by reading both specs side by side, AES-256-GCM-SHA384 is what application data ends up using on well-behaved clients (it’s listed first so server-preference negotiation lands there), but AES-128-GCM-SHA256 has to be in the list anyway, even on FipsStrict. The doc-comment names the spec section so the next reader doesn’t have to re-discover this.

The top-level public function wires those two decision functions together with the rustls ServerConfig builder and the quinn ALPN advertisement:

/// Builds a hybrid PQ TLS `ServerConfig` for the **QUIC** listener.
/// Same crypto hardening as `build_hybrid_tls_config_with_resolver`
/// (aws-lc-rs provider, TLS 1.3 only, policy-driven KEX + cipher
/// suites) — the only differences are at the *wire* layer:
///
/// 1. **ALPN advertises only `h3`.** RFC 9114 mandates `h3` as the
///    HTTP/3 ALPN identifier; a QUIC server MUST NOT advertise `h2`
///    or `http/1.1` because those are TCP-bound protocols.
/// 2. **No `acme-tls/1` ALPN.** RFC 8737 ACME-TLS-01 validation is
///    TCP-only by spec — Let's Encrypt's validator does not speak
///    QUIC. So the QUIC listener is wired straight to the BYOD
///    resolver, skipping the `AlpnAwareResolver` dispatcher
///    entirely. One less indirection on the QUIC hot path.
pub fn build_hybrid_quic_config_with_resolver(
    policy: CryptoPolicy,
    resolver: Arc<dyn ResolvesServerCert>,
) -> Result<ServerConfig> {
    // QUIC has its own provider — the cipher-suite list always
    // includes AES-128-GCM-SHA256 for RFC 9001 § 4.1 Initial-packet
    // protection regardless of policy.
    let provider = hardened_quic_provider(policy);

    info!(
        policy = policy.label(),
        "Building hardened TLS 1.3 config (QUIC / h3 path)"
    );

    let mut config = ServerConfig::builder_with_provider(Arc::new(provider))
        .with_protocol_versions(&[&TLS13])
        .context("Failed to set TLS 1.3 protocol version (QUIC)")?
        .with_no_client_auth()
        .with_cert_resolver(resolver);

    // Single ALPN. quinn's `QuicServerConfig::try_from(rustls_config)`
    // will reject a config that hasn't enabled QUIC keying, but the
    // `rustls/aws_lc_rs` feature already turns on the QUIC support
    // crate-wide; the only thing left for the caller is the ALPN
    // advertisement, which we do here.
    config.alpn_protocols = vec![b"h3".to_vec()];

    Ok(config)
}

The function is short because all the hard decisions live in the pure functions above it. The hardened_quic_provider(policy) helper is the one place that knows how to turn a CryptoPolicy into a fully-configured rustls::crypto::CryptoProvider; everything downstream is just calling rustls’s builder API correctly. Pinning TLS 1.3 exclusively is belt-and-braces (QUIC v1 already mandates 1.3), but it means a misconfigured downgrade fails closed instead of silently negotiating something weaker. The Result return propagates every configuration error up to the boot path; the workspace lint policy bans .unwrap() and .expect() outside a documented boot-only carve-out, enforced in CI.

This is the kind of code that’s worth unit-testing the matrix itself, not just the integration. We do, there’s a test that asserts HybridOnly produces exactly [X25519MLKEM768] and nothing else; another that asserts the QUIC provider includes TLS13_AES_128_GCM_SHA256 in every single policy variant (because the consequence of getting that wrong is a boot failure, not a runtime test failure); another that asserts FipsStrict excludes both X25519MLKEM768 and plain X25519, the two things a “performance optimisation” refactor might accidentally re-introduce. The tests are the only honest documentation of an invariant a code-comment can’t enforce.

How the handshake actually flows

End-to-end view of one inbound IoT connection from ClientHello through encrypted h3 stream to the tenant backend. The teal path is the only place X25519MLKEM768 bytes exist on the wire.

Hybrid post-quantum TLS handshake and h3 request flow through the Scrutari gatewayHorizontal data-flow diagram showing an IoT device's QUIC ClientHello advertising X25519MLKEM768 reaching the Scrutari gateway. The gateway's hybrid TLS termination layer (quinn 0.11 plus rustls 0.23 plus aws-lc-rs) derives a shared secret from both X25519 ECDHE and FIPS 203 ML-KEM-768, completes the TLS 1.3 handshake, terminates h3, runs the route resolver against the inbound SNI, then forwards a plaintext HTTP/1.1 or h2 request to the tenant backend.IoT devicefirmwareaws-lc-rsquinn 0.11ClientHelloX25519MLKEM768ServerHellohybrid KEM shareHybrid PQ TLS terminationquinn 0.11.9 (QUIC)rustls 0.23 (TLS 1.3)aws-lc-rs · X25519MLKEM768crypto_engine::tls_hybridh3 request(plaintext, TLS-decrypted)Route resolverSNI -> DomainDomain -> Route(detailed in Post 3)upstreamTenant backendHTTP/1.1 or h2Hybrid PQ handshakePlaintext h3 (post-TLS)
Once TLS terminates inside the gateway, the request is plaintext h3 routed to the tenant backend. The upstream hop itself is classical TLS by design (see “post-quantum at the edge, classical to upstream” in the original tls_hybrid.rs doc-comments).

5. Four policies, one resolver

The CryptoPolicy enum has four variants. The wire-level posture each one produces is the right thing to design around, not the policy name.

HybridOnly is the strict-PQ posture and the production default. The ClientHello and ServerHello both advertise exactly [X25519MLKEM768] as the supported groups. A connecting client that doesn’t speak the hybrid group fails the handshake, there’s no fallback negotiation surface. This is the right posture for a managed IoT fleet where every device ships with an aws-lc-rs (or otherwise PQ-capable) TLS stack baked into firmware. No downgrade path, no negotiation indirection, the strongest posture you can advertise.

HybridWithClassicalFallback is the brownfield migration posture. The ServerHello advertises the hybrid group first (so PQ-capable clients still negotiate it), but the gateway accepts clients that only support classical X25519, P-256, or P-384. This is what an operator selects during the cut-over window when a fleet is half-PQ and half-legacy. The operational signal it produces is the more valuable one, the gateway’s scrutari_quic_handshakes_total counter is broken out by the actual negotiated group, so you can watch the cut-over happen on a Grafana dashboard and retire the classical fallback once the legacy curve flatlines.

FipsAdvisory is the policy a customer selects when they need the FIPS-validated cryptographic module loaded, the aws-lc-rs FIPS feature flag, but they’re not claiming approved-mode operation. The wire posture is identical to HybridOnly: same [X25519MLKEM768] KEX list, same cipher suites including ChaCha20-Poly1305. The customer gets PQ protection plus the validated-module pedigree, which is exactly what a security-conscious enterprise wants when their compliance team is asking “is the crypto library validated?” but their threat model is still better served by hybrid PQ than by approved-mode-only.

FipsStrict is the approved-mode boundary. NIST SP 800-227 has not yet ratified a combiner construction that includes ML-KEM-768 in an FIPS-approved key-establishment path, and X25519 itself is not on the approved-curve list. So strict approved-mode operation excludes both, the KEX list is [P-384, P-256], the cipher-suite list excludes ChaCha20-Poly1305 (ChaCha20 and Poly1305 are not FIPS-approved primitives), and the gateway operates within the bounds an FIPS 140-3 module can attest to. This policy intentionally gives up the harvest-now-decrypt-later protection that hybrid PQ provides, the operator made a conscious trade-off in favour of approved-mode compliance, and we surface that trade-off explicitly in the operator-facing config docs.

The cert resolver, the Arc<dyn ResolvesServerCert> parameter to the function above, handles per-tenant cert selection independently of the policy. A single resolver instance serves every policy; the policy choice doesn’t influence which cert chain a connection sees. That decoupling is what lets us flip a single tenant from HybridWithClassicalFallback to HybridOnly without touching cert plumbing, and it’s the reason the policy decision can live in one match arm and not be threaded through the cert-resolution layer at all.

6. Observability at the handshake boundary

Cryptographic posture is only as good as the dashboard that tells you when it slips. Every connection that traverses the gateway emits at least three metric updates, a handshake counter, a handshake-duration histogram sample, and a client-classification counter, and every one of those is queryable via Prometheus.

The headline counter is scrutari_quic_handshakes_total{outcome=...}, one increment per attempted connection, broken out by success and failed. The single most useful PromQL on it is rate(scrutari_quic_handshakes_total{outcome="failed"}[5m]) / rate(scrutari_quic_handshakes_total[5m]), which gives you a failure ratio that pages cleanly above a threshold. Companion gauges (scrutari_quic_connections_active) and histograms (scrutari_quic_handshake_seconds, with HDR-style bucketing for accurate p99 / p99.9 readouts) round out the surface.

The interesting metric is scrutari_quic_clients_identified_total{class=...}. On every connection’s first h3 request, a heuristic classifier inspects the User-Agent and assigns one of browser (Mozilla-token UA), curl (curl-shaped CLI), iot_unknown (no-whitespace slash-versioned firmware identifier), or other (everything else). The classifier never inspects the request body, by architectural decision, the gateway treats tenant payload as opaque. What the classifier can tell us is the shape of who’s connecting, which is the right granularity for “is this fleet behaving the way we expect” questions without crossing the line into payload inspection. A sudden surge in class="browser" against a known-IoT tenant is a credential-leak alert. A sudden drop in class="iot_unknown" is a firmware-rollout problem.

The histograms use a lock-free hdr-style implementation (atomics on a fixed bucket array; no per-sample allocation, no mutex on the hot path) because the gateway’s load profile records many thousands of samples per second per worker. We’ll excerpt that in Post 2 alongside the load harness, the histograms and the harness are joined-at-the-hip components and the rationale reads better together.

7. What we deliberately left out

Three omissions are worth naming, because each is a decision a careful reader might reasonably push back on.

No post-quantum signatures. The server cert chain is classical X.509 with ECDSA P-256. The reason isn’t laziness; it’s that the post-quantum signature schemes NIST standardised, SLH-DSA, ML-DSA, FALCON, are either far too large for the QUIC handshake’s amplification budget (SLH-DSA-128f signatures are 17 KB), too slow to verify at the rate a gateway encounters them, or too immature in the certificate ecosystem to obtain a real cert chain for. The harvest-now threat targets the key agreement, not the signature, an attacker recording today’s traffic can’t retroactively forge the server’s identity by breaking the signature in 2032; they can only decrypt the recorded payload by breaking the KEM. PQ signatures are the right answer for future identity, not for current confidentiality, and the gateway adopts them when the cert ecosystem catches up.

No 0-RTT early-data on the strict-PQ profile. rustls supports 0-RTT and so does quinn, but the session-resumption ticket format for hybrid-PQ TLS is still moving in IETF working groups and we made the call that shipping a 0-RTT story we’d have to re-cut in six months wasn’t worth the latency win on reconnect. The classical-fallback policy supports 0-RTT today; the strict-PQ policy will when the spec stabilises.

P-521 in FipsStrict. P-521 is FIPS-approved and would be the ideal first-position group in approved-mode operation, but rustls 0.23 / aws-lc-rs doesn’t currently expose SECP521R1 as a SupportedKxGroup static. P-384 + P-256 is sufficient for approved-mode handshakes; P-521 is a missing-feature gap rather than a posture compromise, and the doc-comment marks the exact line where it slots in once upstream surfaces it.

8. Performance receipts and what’s next

Numbers grounded in the actual code above, from a calibration run against the dev gateway on a single laptop (local loopback, 8 vCPU). One thousand concurrent IoT-shaped QUIC connections held for sixty seconds of steady-state traffic, each pinging the gateway’s /health endpoint on a 5-second cadence:

Metricp50p95p99p99.9
Handshake duration2.8 ms3.8 ms5.6 ms16.7 ms
Initial RTT0.9 ms1.6 ms2.5 ms5.7 ms
Request duration (steady)2.7 ms4.6 ms5.5 ms7.3 ms

Zero connections dropped mid-scenario; zero retries exhausted; 12,669 of 12,669 requests reached the h3 layer. The full markdown report, including the run-config dump for reproducibility and the whole-run drill-down for cold-start diagnosis, is generated automatically by our load harness and committed to the repo on every canonical run.

These are local loopback calibration numbers, and we frame them honestly as that. There’s no network in this measurement; the p99 handshake duration is essentially the cost of aws-lc-rs evaluating X25519MLKEM768 twice (once on each side) plus the rustls state-machine overhead, with no real propagation delay or queueing or NIC contribution. They’re the floor, the cost the gateway adds on top of any real network. The next set of numbers we’ll publish, in Post 2 of this series, come from a distributed Azure benchmark, bench VM in the gateway VNet, real network latency between client and server, sustained 30-50k concurrent connections, that’ll tell you what the gateway costs in production-shaped conditions. Those numbers are what an SRE quotes when sizing a fleet.

Post 2 also walks through the load harness itself, the Rust crate that drove the numbers above, why the steady-window-scoped percentile is the only one worth quoting, how the hdrhistogram bucketing keeps the tail honest, and how the harness shares the gateway’s exact crypto stack so a harness-side bug can never masquerade as a gateway-side perf result. Post 3 covers the per-tenant routing layer, the Domain Cascade, that sits on top of the TLS gateway and turns it into a multi-tenant edge.

Ready to put a post-quantum gateway in front of your traffic?

Self-serve Starter is free with a card on file (up to 5 routes). Bring your own domain, point it at the gateway, and your traffic terminates on the same hybrid PQ stack documented above. Talk to us first if your migration has FIPS-strict or regulated-vertical constraints, we’ll size the right policy with you.

Engineering deep-dive series

Post 1 · You’re here

Building a Post-Quantum TLS Gateway in Rust

Post 2 · Coming next

Measuring Sustained PQ-KEM Handshake Throughput

Load harness internals + Azure bench-VM numbers.

Post 3 · Coming after

Domain Cascade and Multi-Tenant Routing

Per-tenant cert resolution + the route resolver.

Scrutari operates a post-quantum TLS gateway for IoT and edge deployments. The product is in private beta; the architecture is open for technical discussion.