Committable VPS artifacts for the native reverse-tunnel (no live-ops, no secrets):
- gen-device-ca.sh: EC P-256 self-signed device CA (CA:TRUE pathlen:0, keyCertSign+cRLSign),
openssl ca db + empty CRL, idempotent, --kind frp-client scaffolds the control-channel CA.
- issue-device-cert.sh: P-256 leaf, EKU=clientAuth only, 825d, emits .p12 (-legacy) + .pem/.key/.fullchain,
copy_extensions=none prevents CSR-injected serverAuth/CA:TRUE; CN traversal guard.
- revoke-device.sh: openssl ca revoke -> regenerate crl.pem (revocation enforced via -crl_check).
- frp/{frps.toml.example,frpc.example.toml}: control-channel mTLS+token, disableCustomTLSFirstByte=true.
- nginx/frp-mtls.conf: :8470 TLS-term, ssl_verify_client on + device-CA + ssl_crl, WS-upgrade proxy to :7080.
- nginx/stream-sni-additions.md: the two additive SNI map lines (merge-only, coexistence-safe).
Verified: gen->issue->openssl verify->revoke->CRL chain green in a tmp dir (OpenSSL 3.0.18).
Native mTLS Tunnel — per-host runbook (V7)
Bring one local host onto the tunnel so native clients (iOS / Android / desktop) can reach its base
app at https://<name>.terminal.yaojia.wang over the VPS, gated only by device-certificate mTLS.
See docs/PLAN_NATIVE_TUNNEL.md for the full design; this is the short operational checklist.
Trust model: A — single-owner fleet (all devices/hosts are the operator's). A shared device-CA means "the closed set of my devices"; there is no cross-tenant isolation — do not enroll distrusting third parties on this CA (that is Model B, out of scope for v1).
Files here
| File | Install target on the VPS | Purpose |
|---|---|---|
frps.toml.example |
/etc/relay/frp/frps.toml (0600) |
frps control :7000 + vhost :7080 |
frpc.example.toml |
on each LOCAL host (0600) | dials the VPS, exposes 127.0.0.1:3000 |
../nginx/frp-mtls.conf |
/etc/nginx/conf.d/frp-mtls.conf |
:8470 TLS-term + device-CA mTLS + WS proxy |
../nginx/stream-sni-additions.md |
(merge into the live stream map) | the two additive SNI routes |
One-time VPS setup (summarised — see PLAN V1–V5)
- frps (
frps.toml.example→/etc/relay/frp/frps.toml): setauth.token=openssl rand -hex 32. - frp-client CA (control-channel mTLS):
bash deploy/scripts/gen-device-ca.sh --kind frp-client. - device CA (data-path mTLS):
bash deploy/scripts/gen-device-ca.sh(KIND=device default). - LE wildcard
*.terminal.yaojia.wang(V3) →/etc/relay/frp-tls/{fullchain,privkey}.pem. - nginx
:8470(frp-mtls.conf) + the two stream SNI lines (stream-sni-additions.md), thennginx -t && systemctl reload nginx.
Onboard a host (the repeatable part)
- frp-client cert for this host (control mTLS), issued from the frp-client CA:
CA_DIR=/etc/relay/frp-client-ca bash deploy/scripts/issue-device-cert.sh <name> ./out-<name> # use out-<name>/<name>.cert.pem + <name>.key.pem for frpc.certFile/keyFile (ignore the .p12) - frpc.toml on the host from
frpc.example.toml: setsubdomain=<name>, the sharedauth.token, and the frp-client cert/key paths;chmod 600 frpc.toml. - Base app on the host — the tunnel is safe ONLY with these:
Var Value Why BIND_HOST127.0.0.1Mandatory. Default 0.0.0.0exposes an unauth'd shell on the LAN, bypassing mTLS.PORT3000frpc dials 127.0.0.1:3000.ALLOWED_ORIGINShttps://<name>.terminal.yaojia.wangbare host; :443also matches (normalized).IDLE_TTL≥ 86400walk-away survival. USE_TMUX1(*nix) /0(Windows)cross-restart PTY survival. - Device cert for whoever will connect (data-path mTLS), issued from the device CA:
P12_PASSWORD='...' bash deploy/scripts/issue-device-cert.sh <person-or-device> ./out-dev # deliver out-dev/<...>.p12 SECURELY: scp / AirDrop / Files — NEVER email. - Verify (M1): no-cert → TLS handshake failure;
curl --cert …/<dev>.cert.pem --key …/<dev>.key.pem -sI https://<name>.terminal.yaojia.wang/live-sessions→200.
Revoke a device
CA_DIR=/etc/relay/device-ca bash deploy/scripts/revoke-device.sh /path/to/<dev>.cert.pem
# copy the regenerated /etc/relay/device-ca/crl.pem to the VPS, then ON THE VPS:
nginx -t && nginx -s reload
The revoked cert is rejected fleet-wide the moment nginx reloads the CRL.
Trust-boundary note (Model A)
- The frp-client cert + token is a trusted secret: leaking a host's frp-client cert and the
shared token lets an attacker register a subdomain (subdomain-takeover / MITM). Keep both
chmod 600; deliver over scp, never email. Revoke a compromised host by rotating thefrp-client-catrust (re-issue the remaining hosts) — device certs are unaffected. - The device cert is the data-path gate. Under Model A any valid device cert can reach any
subdomain and can read
/live-sessions/:id/previewscrollback — acceptable because every device is yours. Do not extend this CA to third parties without moving to Model B (per-tenant CA). - P-256, never Ed25519 for both CAs (nginx
ssl_verify_client+ iOS client-cert compatibility).