feat: update MITM behavior in portal expose command and related documentation

rabbitprincess committed Jun 1, 2026 at 23:27 UTC 390cb5d78d302ff2b130259cf748efde974ae9ba
12 files changed +28 -15
cmd/portal-tunnel/README.md
+2 -2
@@ -429,8 +429,8 @@ resource = "/"
429 are dropped from the active set after their retry budget is exhausted.
430 - Tenant TLS is provisioned automatically through the relay keyless signer. The
431 SDK fetches the relay certificate chain and uses `/v1/sign` for remote signing.
432 -- `portal expose` enables MITM strict enforcement by default. Use
433 - `--ban-mitm=false` for warning-only behavior.
432 +- `portal expose` logs MITM self-probe failures by default. Use `--ban-mitm`
433 + when suspected TLS termination should ban the relay automatically.
434 - When the local stream target is unreachable, the tunnel returns an HTTP 503
435 page to browser-style clients.
436 - `--tcp` requires the relay to have TCP port transport enabled and a valid
cmd/portal-tunnel/agent/manager.go
+1 -1
@@ -687,7 +687,7 @@ func (t *managedTunnel) runOnce(ctx context.Context) error {
687 if cfg.Discovery != nil {
688 discovery = *cfg.Discovery
689 }
690 - banMITM := true
690 + banMITM := false
691 if cfg.BanMITM != nil {
692 banMITM = *cfg.BanMITM
693 }
cmd/portal-tunnel/installer/install.sh
+8 -1
@@ -34,7 +34,14 @@ RELAY_URL="${RELAY_URL:-https://your-relay.example.com}"
34
35 is_local_https_url() {
36 case "$1" in
37 - https://localhost|https://localhost:*|https://127.0.0.1|https://127.0.0.1:*|https://[::1]|https://[::1]:*|https://*.localhost|https://*.localhost:*)
37 + https://*) ;;
38 + *) return 1 ;;
39 + esac
40 +
41 + URL_HOSTPORT="${1#https://}"
42 + URL_HOSTPORT="${URL_HOSTPORT%%/*}"
43 + case "$URL_HOSTPORT" in
44 + localhost|localhost:*|127.0.0.1|127.0.0.1:*|\[::1\]|\[::1\]:*|*.localhost|*.localhost:*)
45 return 0
46 ;;
47 esac
cmd/portal-tunnel/main.go
+1 -1
@@ -90,7 +90,7 @@ func runExposeCommand(args []string) error {
90 utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
91 utils.StringFlagEnv(fs, &flags.multiHopCSV, "multi-hop", "", "Ordered multi-hop relay API URLs, comma-separated", "MULTI_HOP")
92 utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include bootstrap relays and discover additional relays")
93 - utils.BoolFlagEnv(fs, &flags.banMITM, "ban-mitm", true, "Ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
93 + utils.BoolFlagEnv(fs, &flags.banMITM, "ban-mitm", false, "Ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
94 utils.StringFlagEnv(fs, &flags.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
95 utils.StringFlagEnv(fs, &flags.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
96 utils.StringFlag(fs, &flags.name, "name", "", "Public hostname prefix (single DNS label); auto-generated when omitted")
docs/src/routes/cli-reference/+page.md
+3 -3
@@ -88,7 +88,7 @@ not supported.
88 | `--max-active-relays` | int | `3` | Maximum auto-selected relays to keep connected; explicit relays are always included |
89 | `--multi-hop` | string | | Ordered multi-hop relay API URLs, comma-separated |
90 | `--multi-hop-depth` | int | `0` | Automatically select one multi-hop route with this hop count; `0` or `1` disables multi-hop |
91 -| `--ban-mitm` | bool | `true` | Ban relay when the MITM self-probe detects TLS termination |
91 +| `--ban-mitm` | bool | `false` | Ban relay when the MITM self-probe detects TLS termination |
92 | `--identity-path` | string | `identity.json` | Identity JSON file path; created automatically when missing |
93 | `--identity-json` | string | | Identity JSON payload; overrides `--identity-path` contents and is persisted there when both are set |
94 | `--name` | string | auto | Public hostname prefix, one DNS label |
@@ -246,10 +246,10 @@ Ask Portal to select one three-hop route:
246 portal expose 3000 --multi-hop-depth 3
247 ```
248
249 -Keep MITM probe failures warning-only:
249 +Ban relays on MITM probe detection:
250
251 ```bash
252 -portal expose 3000 --ban-mitm=false
252 +portal expose 3000 --ban-mitm
253 ```
254
255 ## `portal list`
docs/src/routes/concepts/+page.md
+2 -2
@@ -142,8 +142,8 @@ Portal runs a TLS passthrough self-probe after real stream traffic starts:
142 4. Matching exporter values indicate passthrough for that sampled connection.
143 5. A mismatch is treated as suspected relay-side TLS termination.
144
145 -By default, `portal expose` bans a relay when the self-probe detects
146 -termination. Use `--ban-mitm=false` for warning-only behavior.
145 +By default, `portal expose` logs self-probe detections without banning the relay.
146 +Use `--ban-mitm` when suspected TLS termination should ban the relay.
147
148 The probe is a detection signal, not a mathematical proof for every future
149 connection. It raises the cost of relay-side termination while preserving the
docs/src/routes/configuration/+page.md
+1 -1
@@ -141,7 +141,7 @@ The `portal expose` subcommand accepts the following flags. Flags that read from
141 | `--multi-hop` | `MULTI_HOP` | string | | Ordered multi-hop relay API URLs, comma-separated |
142 | `--multi-hop-depth` | `MULTI_HOP_DEPTH` | int | `0` | Automatically select one multi-hop route with this hop count; 0 or 1 disables multi-hop |
143 | `--max-active-relays` | `MAX_ACTIVE_RELAYS` | int | `3` | Maximum auto-selected relays to keep connected; explicit relays are always included |
144 -| `--ban-mitm` | `BAN_MITM` | bool | `true` | Ban relay when the MITM self-probe detects TLS termination |
144 +| `--ban-mitm` | `BAN_MITM` | bool | `false` | Ban relay when the MITM self-probe detects TLS termination |
145
146 ### Identity
147
docs/src/routes/portal-agent/+page.md
+1 -1
@@ -170,7 +170,7 @@ Common fields:
170 | `tcp` | Dedicated raw TCP port setting |
171 | `multi_hop` | Explicit ordered multi-hop relay URLs |
172 | `multi_hop_depth` | Automatically choose one multi-hop route with this depth |
173 -| `ban_mitm` | Ban relays when the TLS self-probe detects termination |
173 +| `ban_mitm` | Ban relays when the TLS self-probe detects termination; defaults to warning-only |
174 | `description`, `tags`, `owner`, `thumbnail`, `hide` | Public relay metadata |
175
176 Constraints match `portal expose`:
docs/src/routes/security-model/+page.md
+1 -1
@@ -45,7 +45,7 @@ Legacy clients and raw TCP/UDP transports still use the legacy hostname registra
45
46 `portal expose` runs an asynchronous TLS passthrough self-probe after real tenant traffic starts. The SDK connects to its own public hostname, exports TLS keying material from the client side, recognizes the returning probe after SDK-side TLS termination, and compares exporter values.
47
48 -Matching exporter values mean the sampled connection preserved passthrough. A mismatch is treated as suspected relay-side TLS termination. By default, `portal expose` bans that relay; use `--ban-mitm=false` for warning-only behavior.
48 +Matching exporter values mean the sampled connection preserved passthrough. A mismatch is treated as suspected relay-side TLS termination and logged by default; use `--ban-mitm` when suspected TLS termination should ban the relay.
49
50 ## Relay Visibility
51
docs/static/examples/nginx-proxy-multi-service/nginx.conf
+1 -1
@@ -46,7 +46,7 @@ stream {
46 proxy_socket_keepalive on;
47 proxy_connect_timeout 10s;
48 proxy_buffer_size 16k;
49 - proxy_timeout 600s;
49 + proxy_timeout 86400s;
50 }
51 }
52
portal/keyless/signer.go
+1 -1
@@ -15,7 +15,7 @@ import (
15
16 const (
17 RelayKeyID = "relay-cert"
18 - defaultAllowedSkew = 30 * time.Second
18 + defaultAllowedSkew = 5 * time.Minute
19 )
20
21 type Signer struct {
sdk/listener.go
+6
@@ -494,6 +494,12 @@ func (l *listener) runReverseSessionLoop(ctx context.Context, tlsConfig *tls.Con
494 case errors.Is(err, context.Canceled), errors.Is(err, net.ErrClosed):
495 return nil
496 case claimed:
497 + log.Debug().
498 + Err(err).
499 + Str("relay_url", l.relayURL.String()).
500 + Str("address", l.identity.Address).
501 + Int("reverse_session_slot", sessionSlot).
502 + Msg("tenant tls handshake failed")
503 retries = 0
504 default:
505 retries++