Skip to content

Commit 809d43e

Browse files
authored
Merge pull request #270 from gosuda/feat/keyless-time
Feat/keyless time
2 parents fbb3c67 + 5d34737 commit 809d43e

23 files changed

Lines changed: 182 additions & 899 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ profile.cov
4242
/target
4343

4444
# Docker Compose Override
45-
docker-compose.override.yml
46-
docker-compose.override.yaml
45+
/docker-compose.override.yml
46+
/docker-compose.override.yaml
4747

4848
# Added by goreleaser init:
4949
dist/

cmd/portal-tunnel/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,8 @@ resource = "/"
394394

395395
## Install Behavior
396396

397-
- `install.sh` installs the downloaded binary as `portal`.
397+
- `install.sh` installs the downloaded binary as `portal` and adds the install
398+
directory to the user's shell profile when it is not already on `PATH`.
398399
- `install.ps1` installs `portal.exe` for the current Windows user and updates
399400
the user `PATH`.
400401
- The installer does not write a config file.
@@ -429,8 +430,8 @@ resource = "/"
429430
are dropped from the active set after their retry budget is exhausted.
430431
- Tenant TLS is provisioned automatically through the relay keyless signer. The
431432
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.
433+
- `portal expose` logs MITM self-probe failures by default. Use `--ban-mitm`
434+
when suspected TLS termination should ban the relay automatically.
434435
- When the local stream target is unreachable, the tunnel returns an HTTP 503
435436
page to browser-style clients.
436437
- `--tcp` requires the relay to have TCP port transport enabled and a valid

cmd/portal-tunnel/agent/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ func (t *managedTunnel) runOnce(ctx context.Context) error {
687687
if cfg.Discovery != nil {
688688
discovery = *cfg.Discovery
689689
}
690-
banMITM := true
690+
banMITM := false
691691
if cfg.BanMITM != nil {
692692
banMITM = *cfg.BanMITM
693693
}

cmd/portal-tunnel/installer/install.sh

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,40 @@ RELAY_URL="${RELAY_URL:-https://your-relay.example.com}"
3434

3535
is_local_https_url() {
3636
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+
URL_HOSTPORT="${URL_HOSTPORT%%\?*}"
44+
URL_HOSTPORT="${URL_HOSTPORT%%#*}"
45+
46+
case "$URL_HOSTPORT" in
47+
\[::1\]:*)
48+
URL_HOST="[::1]"
49+
URL_PORT="${URL_HOSTPORT#\[::1\]:}"
50+
;;
51+
\[::1\])
52+
URL_HOST="[::1]"
53+
URL_PORT=""
54+
;;
55+
*:*)
56+
URL_HOST="${URL_HOSTPORT%%:*}"
57+
URL_PORT="${URL_HOSTPORT#*:}"
58+
;;
59+
*)
60+
URL_HOST="$URL_HOSTPORT"
61+
URL_PORT=""
62+
;;
63+
esac
64+
65+
case "$URL_PORT" in
66+
*[!0-9]*) return 1 ;;
67+
esac
68+
69+
case "$URL_HOST" in
70+
localhost|127.0.0.1|\[::1\]|*.localhost)
3871
return 0
3972
;;
4073
esac
@@ -137,7 +170,33 @@ echo "Installed portal to $INSTALL_PATH" >&2
137170
case ":$PATH:" in
138171
*":$INSTALL_DIR:"*) ;;
139172
*)
140-
echo "Warning: $INSTALL_DIR is not on PATH. Add it before running 'portal expose 3000'." >&2
173+
PROFILE_PATH=""
174+
if [ -n "${HOME:-}" ]; then
175+
SHELL_NAME="${SHELL:-}"
176+
SHELL_NAME="${SHELL_NAME##*/}"
177+
case "$SHELL_NAME" in
178+
zsh) PROFILE_PATH="$HOME/.zshrc" ;;
179+
bash) PROFILE_PATH="$HOME/.bashrc" ;;
180+
*) PROFILE_PATH="$HOME/.profile" ;;
181+
esac
182+
fi
183+
184+
if [ -n "$PROFILE_PATH" ]; then
185+
mkdir -p "$(dirname "$PROFILE_PATH")" 2>/dev/null || true
186+
if [ ! -f "$PROFILE_PATH" ] || ! grep -F "$INSTALL_DIR" "$PROFILE_PATH" >/dev/null 2>&1; then
187+
{
188+
printf '\n# Added by Portal installer\n'
189+
printf 'export PATH="%s:$PATH"\n' "$INSTALL_DIR"
190+
} >> "$PROFILE_PATH" || PROFILE_PATH=""
191+
fi
192+
fi
193+
194+
if [ -n "$PROFILE_PATH" ]; then
195+
echo "Added $INSTALL_DIR to PATH in $PROFILE_PATH" >&2
196+
echo "Open a new shell or run: export PATH=\"$INSTALL_DIR:\$PATH\"" >&2
197+
else
198+
echo "Warning: $INSTALL_DIR is not on PATH. Add it before running 'portal expose 3000'." >&2
199+
fi
141200
;;
142201
esac
143202

cmd/portal-tunnel/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func runExposeCommand(args []string) error {
9090
utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
9191
utils.StringFlagEnv(fs, &flags.multiHopCSV, "multi-hop", "", "Ordered multi-hop relay API URLs, comma-separated", "MULTI_HOP")
9292
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")
9494
utils.StringFlagEnv(fs, &flags.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
9595
utils.StringFlagEnv(fs, &flags.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
9696
utils.StringFlag(fs, &flags.name, "name", "", "Public hostname prefix (single DNS label); auto-generated when omitted")

docs/src/routes/cli-reference/+page.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ If your relay publishes its own installer:
3232
curl -sSL https://portal.example.com/api/install.sh | bash
3333
```
3434

35-
The installer writes the `portal` binary only. It does not write a config file.
35+
The installer downloads the `portal` binary and adds it to your `PATH`. It does
36+
not create a config file.
3637

3738
## Command Overview
3839

@@ -88,7 +89,7 @@ not supported.
8889
| `--max-active-relays` | int | `3` | Maximum auto-selected relays to keep connected; explicit relays are always included |
8990
| `--multi-hop` | string | | Ordered multi-hop relay API URLs, comma-separated |
9091
| `--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 |
92+
| `--ban-mitm` | bool | `false` | Ban relay when the MITM self-probe detects TLS termination |
9293
| `--identity-path` | string | `identity.json` | Identity JSON file path; created automatically when missing |
9394
| `--identity-json` | string | | Identity JSON payload; overrides `--identity-path` contents and is persisted there when both are set |
9495
| `--name` | string | auto | Public hostname prefix, one DNS label |
@@ -246,10 +247,10 @@ Ask Portal to select one three-hop route:
246247
portal expose 3000 --multi-hop-depth 3
247248
```
248249

249-
Keep MITM probe failures warning-only:
250+
Ban relays on MITM probe detection:
250251

251252
```bash
252-
portal expose 3000 --ban-mitm=false
253+
portal expose 3000 --ban-mitm
253254
```
254255

255256
## `portal list`

docs/src/routes/concepts/+page.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ Portal runs a TLS passthrough self-probe after real stream traffic starts:
142142
4. Matching exporter values indicate passthrough for that sampled connection.
143143
5. A mismatch is treated as suspected relay-side TLS termination.
144144

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.
147147

148148
The probe is a detection signal, not a mathematical proof for every future
149149
connection. It raises the cost of relay-side termination while preserving the

docs/src/routes/configuration/+page.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ variables:
8080
| `PORT` | `8081` | int | Frontend API HTTP listen port |
8181
| `PORTAL_API_BASE_URL` | `https://portal:4017` | string | Relay API base URL used to compose frontend-owned state |
8282
| `LANDING_PAGE_ENABLED` | `false` | bool | Default landing page flag when no frontend state has been saved yet |
83-
| `PORTAL_FRONTEND_STATE_PATH` | `""` | string | Optional JSON file path for persisted frontend-owned state; bundled Compose stores this under `./.portal-certs/frontend-state/state.json` |
83+
| `PORTAL_FRONTEND_STATE_PATH` | `""` | string | Optional JSON file path for persisted frontend-owned state; a Compose deployment can store this under `./.portal-certs/frontend-state/state.json` |
8484
| `HEADLESS_SHELL_URL` | `""` | string | Headless Chrome CDP WebSocket URL; leave empty to disable generated thumbnails |
8585

8686
### Cloudflare
@@ -141,7 +141,7 @@ The `portal expose` subcommand accepts the following flags. Flags that read from
141141
| `--multi-hop` | `MULTI_HOP` | string | | Ordered multi-hop relay API URLs, comma-separated |
142142
| `--multi-hop-depth` | `MULTI_HOP_DEPTH` | int | `0` | Automatically select one multi-hop route with this hop count; 0 or 1 disables multi-hop |
143143
| `--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 |
145145

146146
### Identity
147147

docs/src/routes/deployment/+page.md

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Open only the public ports that match the topology:
104104

105105
| Port | Required | Purpose |
106106
|---|---|---|
107-
| `80/tcp` | optional | HTTP to HTTPS redirect in the bundled nginx example |
107+
| `80/tcp` | optional | HTTP to HTTPS redirect in a front nginx |
108108
| `443/tcp` | yes | Public nginx edge for dashboard, relay API path routing, and wildcard TCP passthrough |
109109
| `WIREGUARD_PORT/udp` | when `DISCOVERY=true` | Relay discovery WireGuard transport |
110110
| `SNI_PORT/udp` | when UDP transport is enabled | QUIC tunnel ingress |
@@ -121,33 +121,26 @@ Keep these ports private or loopback-only in the recommended topology:
121121

122122
Certificate files are also split by owner:
123123

124-
| Certificate | Default path in the example | Used by |
124+
| Certificate | Location | Used by |
125125
|---|---|---|
126-
| Browser-facing HTTPS certificate | `./certs/fullchain.pem`, `./certs/privkey.pem` | nginx public edge |
126+
| Browser-facing HTTPS certificate | Edge-specific certificate path | nginx instance that terminates `portal.example.com` |
127127
| Relay API and SNI certificate | `./.portal-certs/fullchain.pem`, `./.portal-certs/privatekey.pem` | `portal` unless managed ACME is configured |
128128

129-
Portal-managed ACME can manage the relay certificate and relay DNS records. The bundled nginx example still expects a browser-facing certificate in `./certs`; manage that with your normal edge certificate process.
129+
Portal-managed ACME can manage the relay certificate and relay DNS records. If a separate front nginx only TCP-passthroughs Portal hostnames, it does not need Portal certificate material; the Portal nginx or relay edge behind it still terminates the Portal root host.
130130

131131
## 3. Deploy the Recommended Stack
132132

133-
Start from the single-domain nginx example:
133+
Deploy the Portal services with your own Compose, systemd, or orchestration
134+
manifest. If another nginx already owns public `443/tcp`, use the nginx
135+
passthrough example only for SNI routing to the Portal nginx:
134136

135137
```bash
136-
mkdir -p portal-deploy
137-
cd portal-deploy
138-
139-
cp <repo>/docs/static/examples/nginx-proxy/docker-compose.yaml ./docker-compose.yaml
140138
cp <repo>/docs/static/examples/nginx-proxy/nginx.conf ./nginx.conf
141-
cp <repo>/docs/static/examples/nginx-proxy/.env.example ./.env
142-
cp <repo>/docs/static/examples/nginx-proxy/deploy_portal.sh ./deploy_portal.sh
143-
cp <repo>/docs/static/examples/nginx-proxy/watch_and_deploy.sh ./watch_and_deploy.sh
144-
cp <repo>/docs/static/examples/nginx-proxy/nginx_deploy.sh ./nginx_deploy.sh
145-
chmod +x deploy_portal.sh watch_and_deploy.sh nginx_deploy.sh
146139
```
147140

148-
Replace every `portal.example.com` in `nginx.conf` and `.env`.
149-
150-
For deployments with multiple additional services behind the same edge nginx, use `docs/static/examples/nginx-proxy-multi-service` instead. The same Portal routing rules apply.
141+
Replace `portal.example.com` and the `portal_nginx` upstream in `nginx.conf`.
142+
The example does not deploy Portal containers, write Portal `.env` files, or
143+
reload Portal services.
151144

152145
### Configure `.env`
153146

@@ -176,7 +169,10 @@ TRUSTED_PROXY_CIDRS=
176169
LANDING_PAGE_ENABLED=false
177170
```
178171

179-
`API_PORT` defaults to `4017`. If you change it, update the relay `proxy_pass` targets in the bundled `nginx.conf` to the same port. Keep `SNI_PORT=443` because this is the public SNI port advertised to tunnel clients. The single-domain Compose example maps the relay container's SNI listener to `127.0.0.1:4443` on the host so nginx can own public `443/tcp` and still pass wildcard TCP traffic to the relay. Do not open `4443/tcp` publicly; it is only a host-local upstream in that example.
172+
`API_PORT` defaults to `4017`. Keep `SNI_PORT=443` because this is the public
173+
SNI port advertised to tunnel clients. If a separate nginx sits in front of
174+
Portal's own nginx, it should TCP-passthrough `portal.example.com` and
175+
`*.portal.example.com` instead of proxying Portal API paths itself.
180176

181177
If the relay joins public discovery, set `BOOTSTRAPS` to at least one reachable relay URL and keep `WIREGUARD_PORT/udp` open.
182178

@@ -361,10 +357,36 @@ It owns:
361357
- `/ui/policy/*` composition, while relay-enforced policy changes are still forwarded to `portal`.
362358
- `/ui/service/status`, derived from relay state for quick-start UI checks.
363359
- `/ui/thumbnail/<hostname>`, when optional screenshot generation is enabled.
364-
- The landing-page flag persisted at `PORTAL_FRONTEND_STATE_PATH`; the bundled Compose files store it under `./.portal-certs/frontend-state/state.json`.
360+
- The landing-page flag persisted at `PORTAL_FRONTEND_STATE_PATH`; a Compose deployment can store it under `./.portal-certs/frontend-state/state.json`.
365361

366362
The Go relay remains the owner of authentication, policy enforcement, lease state, tunnel ingress, install scripts, discovery, and x402 facilitator paths.
367363

364+
### Custom Frontend
365+
366+
To attach your own dashboard frontend, replace only the `portal-frontend`
367+
service image. Keep the service name `portal-frontend` and serve plain HTTP on
368+
port `8080` so the existing nginx route for SPA paths can continue to point at
369+
`portal-frontend:8080`.
370+
371+
The custom frontend should use same-origin browser requests and leave these
372+
paths owned by the Portal services:
373+
374+
| Path | Owner |
375+
|---|---|
376+
| `/ui/*` | `portal-api` presentation API |
377+
| `/api/*` | `portal` relay API |
378+
| `/sdk/*`, `/discovery*`, `/v1/sign` | `portal` relay protocols |
379+
| `*.portal.example.com` | `portal` SNI listener |
380+
381+
```bash
382+
cp <repo>/docs/static/examples/custom-frontend/docker-compose.override.yaml ./docker-compose.override.yaml
383+
docker compose up -d portal-frontend
384+
```
385+
386+
If the automated release updater should also track your custom frontend image,
387+
set `IMAGES` to include that image in addition to the Portal release-track
388+
images.
389+
368390
### Thumbnail Screenshots
369391

370392
Generated thumbnails are optional and disabled by default. Without this feature, apps without a custom thumbnail simply use the default card background.
@@ -384,33 +406,30 @@ thumbnail captured hostname=myapp.portal.example.com size=36209
384406

385407
Disable the feature by removing `HEADLESS_SHELL_URL` and stopping the `headless-shell` container.
386408

387-
## 7. Auto-Update
409+
## 7. Automated Release Updates
388410

389411
Auto-update should follow a published release tag, not `latest`. The `latest`
390412
image tag tracks default-branch image builds, so using it can update production
391413
on a `main` merge before the GitHub Release and tunnel binaries are published.
392414

393-
The bundled Compose examples use the v2 release track directly. Auto-update must
394-
pull all production images from that same release track:
415+
Production deployments should follow the v2 release track directly.
416+
Auto-update must pull all production images from that same release track:
395417

396418
- `ghcr.io/gosuda/portal:2`
397419
- `ghcr.io/gosuda/portal-frontend:2`
398420
- `ghcr.io/gosuda/portal-api:2`
399421

400-
The bundled `deploy_portal.sh` pulls all Portal images together and reloads nginx after the services are updated:
422+
The auto-update example watches those image digests, pulls the changed official
423+
images, recreates the Portal services, and reloads the Portal nginx:
401424

402425
```bash
403-
#!/bin/bash
404-
set -e
405-
406-
docker compose pull portal portal-frontend portal-api
407-
docker compose up -d portal portal-frontend portal-api
408-
bash nginx_deploy.sh
426+
cp <repo>/docs/static/examples/auto-update/watch_and_deploy.sh ./watch_and_deploy.sh
427+
chmod +x watch_and_deploy.sh
409428
```
410429

411-
The bundled `watch_and_deploy.sh` reads the Portal images from Docker Compose,
412-
polls their remote digests, and runs the deploy script when any watched release
413-
tag changes.
430+
The example watcher reads Portal images from Docker Compose when available,
431+
falls back to the v2 image set, and updates `portal`, `portal-frontend`, and
432+
`portal-api` when any watched release tag changes.
414433

415434
Systemd example:
416435

@@ -430,7 +449,6 @@ ExecStart=/bin/bash <path-to-project>/watch_and_deploy.sh
430449
Restart=always
431450
RestartSec=10
432451
Environment=INTERVAL=60
433-
Environment=DEPLOY_SCRIPT=deploy_portal.sh
434452
435453
[Install]
436454
WantedBy=multi-user.target
@@ -461,7 +479,7 @@ Logs like `"\x16\x03\x01..." 400` mean a client sent HTTPS to the plain HTTP `po
461479

462480
### Relay Logs Show `tls: unknown certificate`
463481

464-
This usually means a browser or proxy hit the relay API certificate directly instead of the public nginx certificate, or an upstream proxy tried to verify the relay's internal certificate. In the bundled nginx example, public browsers verify nginx's certificate, while nginx proxies to the relay API over internal HTTPS.
482+
This usually means a browser or proxy hit the relay API certificate directly instead of the public nginx certificate, or an upstream proxy tried to verify the relay's internal certificate. In the recommended topology, public browsers verify the Portal HTTPS edge certificate, while the edge proxies to the relay API over internal HTTPS.
465483

466484
### Root Host Works but Wildcard Apps Fail
467485

docs/src/routes/portal-agent/+page.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Common fields:
170170
| `tcp` | Dedicated raw TCP port setting |
171171
| `multi_hop` | Explicit ordered multi-hop relay URLs |
172172
| `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 |
174174
| `description`, `tags`, `owner`, `thumbnail`, `hide` | Public relay metadata |
175175

176176
Constraints match `portal expose`:

0 commit comments

Comments
 (0)