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++