Skip to content

TLS interception dies when the proxy CA expires mid-session, with no way to recover without a restart #1681

Description

@kipz

What happened?

When the interception CA's not_after passes, a running proxy stops being able to serve intercepted TLS, and stays that way for the rest of the process's life:

  • Cached leaves are clamped to the CA's not_after, so they expire the moment it does — the client rejects them with certificate has expired.
  • A newly-seen SNI can't be minted at all: mint_leaf fails with CA certificate has expired, no chain is resolved, and rustls aborts the handshake.
  • The CA is only checked at startup, so nothing notices, and there's no path for a replacement CA to take effect in the running process. Every intercepted route is dead until the agent is restarted.

The handshake WARN also blames the client (Agent likely pins certs or carries a hard-coded trust list. Remove endpoint_rules / credential_key from the route…) when the cause is our own expired CA, which sends people the wrong way.

What did you expect to happen?

That the session recovers on its own — a fresh CA takes effect in the running proxy without a restart. How that happens is open; the missing piece is that today there is no mechanism for it at all.

Steps to reproduce

  1. Save the profile below as /tmp/ca-expiry/profile.json — two intercepted routes, ca_validity: "120s".
  2. nono proxy --profile /tmp/ca-expiry/profile.json --port 18080 --no-auth --verbose — note the printed trust-bundle path.
  3. curl --proxy http://127.0.0.1:18080 --cacert <bundle> https://example.com/ → 200, interception working, leaf cached.
  4. Wait past 120s so the CA expires.
  5. Re-request https://example.com/ (cached leaf) → fails, expired certificate.
  6. Request https://example.net/, not contacted before, so it forces a fresh mint → fails, no chain resolved.

ca_validity: "120s" only compresses the timeline; the same thing happens to a long-lived trusted CA whenever a session outlives its certificate.

Relevant output or logs

$ openssl x509 -in <bundle> -noout -dates   # last cert in the bundle
notBefore=Aug 19 13:05:08 2026 GMT
notAfter=Aug 19 13:07:08 2026 GMT

=== t0 example.com (first contact, mints a leaf) ===  at 13:05:59Z
http_code=200

=== t+135 example.com (leaf cached pre-expiry) ===    at 13:08:14Z
curl: (60) SSL certificate problem: certificate has expired

=== t+135 example.net (new host, forces a fresh mint) === at 13:08:15Z
curl: (35) LibreSSL/3.3.6: error:1404B419:SSL routines:ST_CONNECT:tlsv1 alert access denied

# proxy, --verbose
13:05:08 INFO TLS interception active for 2 route(s); trust bundle at .../intercept-ca.pem
13:05:59 INFO l7 proxy response mode=connect_intercept target="example.com" method="GET" path="/" status=200
13:08:15 WARN tls_intercept: handshake failed for example.com:443 — received fatal alert: CertificateExpired. Agent likely pins certs or carries a hard-coded trust list. Remove endpoint_rules / credential_key from the route to fall back to a transparent CONNECT tunnel.
13:08:15 WARN tls_intercept: failed to mint leaf for SNI 'example.net': Configuration error: CA certificate has expired; cannot mint leaf for 'example.net'
13:08:15 WARN tls_intercept: handshake failed for example.net:443 — unexpected error: no server certificate chain resolved. Agent likely pins certs or carries a hard-coded trust list. Remove endpoint_rules / credential_key from the route to fall back to a transparent CONNECT tunnel.

nono version

0.73.0

Operating system

macOS

OS version / distro

macOS 26.5.2 (arm64)

Profile (if relevant)

{
  "network": {
    "allow_domain": [
      { "domain": "example.com", "endpoints": [{ "method": "GET", "path": "/**" }] },
      { "domain": "example.net", "endpoints": [{ "method": "GET", "path": "/**" }] }
    ],
    "tls_intercept": { "ca_lifecycle": "session", "ca_validity": "120s" }
  }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions