Files
Yaojia Wang 5337281e85 feat(deploy): device client-CA + issuance/revocation + frp/nginx mTLS templates (V2/V7)
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).
2026-07-07 09:42:12 +02:00
..

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 V1V5)

  1. frps (frps.toml.example/etc/relay/frp/frps.toml): set auth.token = openssl rand -hex 32.
  2. frp-client CA (control-channel mTLS): bash deploy/scripts/gen-device-ca.sh --kind frp-client.
  3. device CA (data-path mTLS): bash deploy/scripts/gen-device-ca.sh (KIND=device default).
  4. LE wildcard *.terminal.yaojia.wang (V3) → /etc/relay/frp-tls/{fullchain,privkey}.pem.
  5. nginx :8470 (frp-mtls.conf) + the two stream SNI lines (stream-sni-additions.md), then nginx -t && systemctl reload nginx.

Onboard a host (the repeatable part)

  1. 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)
    
  2. frpc.toml on the host from frpc.example.toml: set subdomain=<name>, the shared auth.token, and the frp-client cert/key paths; chmod 600 frpc.toml.
  3. Base app on the host — the tunnel is safe ONLY with these:
    Var Value Why
    BIND_HOST 127.0.0.1 Mandatory. Default 0.0.0.0 exposes an unauth'd shell on the LAN, bypassing mTLS.
    PORT 3000 frpc dials 127.0.0.1:3000.
    ALLOWED_ORIGINS https://<name>.terminal.yaojia.wang bare host; :443 also matches (normalized).
    IDLE_TTL ≥ 86400 walk-away survival.
    USE_TMUX 1 (*nix) / 0 (Windows) cross-restart PTY survival.
  4. 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.
    
  5. Verify (M1): no-cert → TLS handshake failure; curl --cert …/<dev>.cert.pem --key …/<dev>.key.pem -sI https://<name>.terminal.yaojia.wang/live-sessions200.

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 the frp-client-ca trust (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/preview scrollback — 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).