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
- Save the profile below as
/tmp/ca-expiry/profile.json — two intercepted routes, ca_validity: "120s".
nono proxy --profile /tmp/ca-expiry/profile.json --port 18080 --no-auth --verbose — note the printed trust-bundle path.
curl --proxy http://127.0.0.1:18080 --cacert <bundle> https://example.com/ → 200, interception working, leaf cached.
- Wait past 120s so the CA expires.
- Re-request
https://example.com/ (cached leaf) → fails, expired certificate.
- 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" }
}
}
What happened?
When the interception CA's
not_afterpasses, a running proxy stops being able to serve intercepted TLS, and stays that way for the rest of the process's life:not_after, so they expire the moment it does — the client rejects them withcertificate has expired.mint_leaffails withCA certificate has expired, no chain is resolved, and rustls aborts the handshake.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
/tmp/ca-expiry/profile.json— two intercepted routes,ca_validity: "120s".nono proxy --profile /tmp/ca-expiry/profile.json --port 18080 --no-auth --verbose— note the printed trust-bundle path.curl --proxy http://127.0.0.1:18080 --cacert <bundle> https://example.com/→ 200, interception working, leaf cached.https://example.com/(cached leaf) → fails, expired certificate.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
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" } } }