`POST /renew` is authenticated by mTLS with the very leaf it renews, so once that leaf lapsed the host could never renew it and the tunnel stayed down until an operator re-paired by hand. Production hit exactly this: the Mac slept through its 8h renewal window, the 24h leaf expired, and the agent then logged `client certificate has expired; renew before dialling` 6380 times over 8 days without recovering. Three layers independently refused an expired leaf, so all three had to move: - agent `buildTlsOptions` gains an opt-in `expiredGraceMs`. Absent/0 keeps the historical fail-closed behaviour, and the TUNNEL dial never passes it — only the renew transport does. Past the window it throws the new `CertExpiredBeyondGraceError`. - agent rotator routes an already-expired leaf to a separate recovery endpoint and treats "beyond grace" as TERMINAL: report once via `onExhausted`, stop retrying, and name the fix (re-pair) instead of spamming warnings forever. - control-plane `assertPresentedCertTrusted` grants a bounded grace on `notAfter` only. Chain validation, SPIFFE identity, `notBefore`, and the registry `active`/account checks are all unchanged, so revocation still bites. - new `deploy/nginx/recover-mtls.conf` (:8472). The strict enroll vhost cannot host this: under `ssl_verify_client optional` nginx answers a bare 400 as soon as a presented cert fails verification, so an expired leaf never reaches the location — and the directive is server-level, not per-location. Grace defaults to 30 days on both ends and is configurable (`RECOVER_URL`, `expiredRenewGraceMs`). The honest trade is recorded in the code: a stale stolen leaf stays reusable for the window, which widens an existing exposure (an unexpired stolen leaf already renews indefinitely) rather than opening a new one. Verified: agent 296/296, control-plane 286/286, tsc clean on both.
2.7 KiB
Stream SNI additions (Native mTLS tunnel · V5)
Two SNI routes must be merged into the existing stream { map $ssl_preread_server_name … }
block on the VPS (/etc/nginx/stream-relay.conf). They add the frp control channel and the
*.terminal data path to the single shared :443 ssl_preread listener.
This is a MERGE, not a file to ship. Do not drop a full stream conf here — that would clobber the live one. Only the two
mapbody lines below are new.
The two additive lines
Add these inside the existing map body (exact match wins over wildcard, so order is not
load-bearing, but keep the exact frp.terminal line above the *.terminal wildcard for clarity):
frp.terminal.yaojia.wang 127.0.0.1:7000; # frps control channel (TLS passthrough)
*.terminal.yaojia.wang 127.0.0.1:8470; # nginx :8470 TLS-term + device-CA mTLS (frp-mtls.conf)
Later addition — expired-leaf recovery (recover-mtls.conf)
recover.terminal.yaojia.wang 127.0.0.1:8472; # expired-leaf re-issue (recover-mtls.conf)
Order IS load-bearing for this one: it must sit ABOVE the *.terminal.yaojia.wang wildcard, the
same way enroll.terminal.yaojia.wang does. hostnames gives exact matches priority over wildcards
in nginx's map, so a correctly-placed line wins regardless — but keep the exact-match lines grouped
above the wildcard so a future editor cannot mis-read the intent. No DNS work is needed: the zone is
already served by the wildcard record *.terminal.yaojia.wang → <vps>.
Coexistence warning (do NOT retype the existing lines)
The existing map body already contains — and MUST be preserved verbatim:
# ... existing hostnames directive ...
*.term.yaojia.wang 127.0.0.1:8443; # E2E browser relay — DO NOT RETYPE / REORDER
default 127.0.0.1:10443; # xray Reality — DO NOT RETYPE / REORDER
*.term.yaojia.wang(browser relay) and*.terminal.yaojia.wang(this tunnel) are different parent labels —termvsterminal.ssl_prereadmatches the full SNI, so they never collide.- Re-type nothing you did not author. Capture the current body first
(
nginx -T | sed -n '/map \$ssl_preread_server_name/,/}/p'), then append only the two lines above.
Deploy gate
cp /etc/nginx/stream-relay.conf{,.bak.$(date +%s)} # snapshot first
# ...edit: append the two lines into the map body...
nginx -t && systemctl reload nginx # NEVER reload on a failed -t
After reload, run the 4-zone SNI oracle (openssl s_client -servername … for
frp.terminal / <name>.terminal / <name>.term / default) to confirm all four routes still
resolve to the right backend (PLAN V5 / M1 #7).