feat: add Portal Agent documentation and enhance wallet and ENS sections

Kim committed May 14, 2026 at 20:42 UTC 5829586c7934d665f4d4d6385ddffec56ad7ef03
15 files changed +653 -55
README.md
+20
@@ -75,6 +75,24 @@ portal expose localhost:25565 --name minecraft --tcp
75 portal expose 3000 --multi-hop-depth 3
76 ```
77
78 +### Keep tunnels running with Portal Agent
79 +
80 +Use `portal agent run` when tunnels should keep running outside your terminal.
81 +It runs as a local OS service, keeps every tunnel in one TOML config alive, and
82 +provides a dashboard for relay and multi-hop management.
83 +
84 +```bash
85 +portal agent run --config config.toml
86 +portal agent dashboard --config config.toml
87 +portal agent restart
88 +portal agent stop
89 +
90 +# Foreground mode skips OS service installation.
91 +portal agent run --config config.toml --foreground
92 +```
93 +
94 +See [Portal Agent](docs/src/routes/portal-agent/+page.md) for the config format.
95 +
96 ### Run your own relay
97
98 ```bash
@@ -130,6 +148,8 @@ Tunnel clients include this registry by default. If you operate a public Portal
148
149 - [CLI Reference](cmd/portal-tunnel/README.md)
150 - [Concepts](docs/src/routes/concepts/+page.md)
151 +- [Portal Agent](docs/src/routes/portal-agent/+page.md)
152 +- [Wallet and ENS](docs/src/routes/wallet-and-ens/+page.md)
153 - [Security Model](docs/src/routes/security-model/+page.md)
154 - [Architecture](docs/src/routes/architecture/+page.md)
155 - [Deployment](docs/src/routes/deployment/+page.md)
cmd/portal-tunnel/README.md
+4
@@ -219,6 +219,10 @@ The agent service owns multiple tunnel definitions from one config file. The
219 local control API binds to loopback and is authenticated with a token stored in
220 the agent state directory.
221
222 +For the full agent workflow, control API, dashboard behavior, and wallet status
223 +auth details, see [Portal Agent](../../docs/src/routes/portal-agent/+page.md)
224 +and [Wallet and ENS](../../docs/src/routes/wallet-and-ens/+page.md).
225 +
226 Useful commands:
227
228 - `portal agent run` reads the platform default config path, installs or updates
docs/src/lib/nav.ts
+2 -1
@@ -56,8 +56,9 @@ export const guidesNavigation: NavSection[] = [
56 title: 'Guides',
57 items: [
58 { title: 'Self-Hosting', href: '/self-hosting' },
59 + { title: 'Portal Agent', href: '/portal-agent' },
60 { title: 'TCP/UDP Tunneling', href: '/tcp-udp-tunneling' },
60 - { title: 'SIWE Authentication', href: '/siwe-authentication' },
61 + { title: 'Wallet and ENS', href: '/wallet-and-ens' },
62 { title: 'Deployment', href: '/deployment' },
63 { title: 'Configuration', href: '/configuration' }
64 ]
docs/src/routes/api-reference/+page.md
+11 -2
@@ -65,6 +65,11 @@ Admin clients authenticate with a wallet signature:
65 5. Include the cookie in subsequent admin requests
66 6. Sessions expire after 24 hours
67
68 +The local agent has its own loopback wallet auth endpoints under
69 +`/v1/agent/auth/*`. Agent wallet sessions can read `/v1/agent/status`; mutating
70 +agent actions require the bearer token stored in the agent state directory. See
71 +[Portal Agent](/portal-agent) for the local control API.
72 +
73 ## Endpoint Summary
74
75 ### SDK Endpoints
@@ -109,7 +114,7 @@ Admin clients authenticate with a wallet signature:
114 | `GET` | `/healthz` | Health check | None |
115 | `GET` | `/discovery` | Relay discovery | None |
116 | `POST` | `/discovery/announce` | Relay discovery self-announce | Signed Descriptor |
112 -| `POST` | `/v1/sign` | Keyless TLS signing | None |
117 +| `POST` | `/v1/sign` | Keyless TLS signing | Access Token |
118 | `GET` | `/thumbnail/{hostname}` | Cached thumbnail screenshot | None |
119 | `GET` | `/tunnel/status` | Tunnel connection status | Access Token |
120
@@ -194,7 +199,11 @@ Submits this relay's signed descriptor to a bootstrap relay so registry-external
199
200 ### `POST /v1/sign`
201
197 -Keyless TLS signing endpoint. Used by the relay's keyless TLS infrastructure. Only available when the API server is configured with a TLS private key.
202 +Keyless TLS signing endpoint. Used by the SDK-side tenant TLS server during the
203 +default stream handshake. Requests must include a valid lease access token in
204 +the `X-Portal-Access-Token` header.
205 +
206 +Only available when the API server is configured with a TLS private key.
207
208 Returns `404 Not Found` if signing is not configured.
209
docs/src/routes/api-reference/sdk/+page.md
+22 -1
@@ -66,6 +66,20 @@ Get relay domain and protocol version information. Used by the SDK to verify rel
66 |-------|------|-------------|
67 | `protocol_version` | `string` | Protocol version (must match SDK version) |
68 | `release_version` | `string` | Relay software release version |
69 +| `ens` | `object` | ENS gasless status for this relay |
70 +
71 +`ens` fields:
72 +
73 +| Field | Type | Description |
74 +|-------|------|-------------|
75 +| `enabled` | `bool` | ENS gasless automation is enabled for a non-local relay domain |
76 +| `verified` | `bool` | DNSSEC is active according to Portal and the last ENS sync succeeded |
77 +| `provider` | `string` | DNS provider used for automation |
78 +| `address` | `string` | Base-domain ENS address, usually the relay identity address |
79 +| `dnssec_state` | `string` | Provider DNSSEC state |
80 +| `ds_record` | `string` | DS record that may need registrar publication |
81 +| `message` | `string` | Provider-specific DNSSEC guidance |
82 +| `last_error` | `string` | Last ENS/DNS sync error |
83
84 **Example:**
85
@@ -80,7 +94,14 @@ curl https://relay.example.com/sdk/domain
94 "ok": true,
95 "data": {
96 "protocol_version": "5",
83 - "release_version": "v2.1.5"
97 + "release_version": "v2.1.5",
98 + "ens": {
99 + "enabled": true,
100 + "verified": true,
101 + "provider": "cloudflare",
102 + "address": "0x1234567890abcdef1234567890abcdef12345678",
103 + "dnssec_state": "active"
104 + }
105 }
106 }
107 ```
docs/src/routes/cli-reference/+page.md
+18 -2
@@ -206,8 +206,22 @@ portal agent restart
206 | `portal agent restart` | Stop the current agent if present, install or update the service, and start it again |
207
208 The local control API binds only to loopback and uses a token in the agent state
209 -directory. See [Configuration Reference](/configuration#configtoml) for the
210 -`config.toml` format.
209 +directory. See [Portal Agent](/portal-agent) for the workflow and
210 +[Configuration Reference](/configuration#configtoml) for the `config.toml`
211 +format.
212 +
213 +Agent flags:
214 +
215 +| Command | Flag | Default | Description |
216 +|---------|------|---------|-------------|
217 +| `portal agent run` | `--config` | platform default | Agent TOML config path |
218 +| `portal agent run` | `--foreground` | `false` | Run in the current process without installing the OS service |
219 +| `portal agent run` | `--service` | `false` | Internal service entrypoint used by the installed OS service |
220 +| `portal agent dashboard` | `--config` | platform default | Config path used for display and state-dir discovery |
221 +| `portal agent dashboard` | `--state-dir` | config/default | Agent state directory to attach to |
222 +| `portal agent stop` | `--config` | platform default | Config path used to resolve state dir and service name |
223 +| `portal agent stop` | `--state-dir` | config/default | Agent state directory to stop |
224 +| `portal agent restart` | `--config` | platform default | Config path used to reinstall and restart the service |
225
226 ## `portal update`
227
@@ -253,6 +267,8 @@ Prints the installed version string and exits.
267 ## Next Steps
268
269 - [Getting Started](/getting-started): run your first tunnel
270 +- [Portal Agent](/portal-agent): run durable multi-tunnel services
271 +- [Wallet and ENS](/wallet-and-ens): understand wallet auth and ENS gasless DNS
272 - [Concepts](/concepts): understand the relay and transport model
273 - [TCP and UDP Tunneling](/tcp-udp-tunneling): raw TCP and UDP setup
274 - [Deployment](/deployment): run your own relay server
docs/src/routes/concepts/+page.md
+5
@@ -160,6 +160,10 @@ datagram authentication.
160
161 Reusing the same identity path keeps the same tunnel identity across runs.
162
163 +Browser wallet login is separate from tunnel registration. Wallet sessions are
164 +used for relay admin access and optional local agent status access. See
165 +[Wallet and ENS](/wallet-and-ens) for the distinction.
166 +
167 ## Domain Boundary
168
169 The default stream path prevents the relay from safely injecting `robots.txt`,
@@ -170,5 +174,6 @@ separate wildcard tunnel domain instead of a brand or docs domain.
174 ## Next Steps
175
176 - [Getting Started](/getting-started): run your first tunnel
177 +- [Portal Agent](/portal-agent): run durable tunnel configs
178 - [CLI Reference](/cli-reference): command and flag details
179 - [Architecture](/architecture): protocol-level design notes
docs/src/routes/configuration/+page.md
+10 -1
@@ -198,7 +198,11 @@ Agent fields:
198 | `state_dir` | Platform default state directory | Stores the local control endpoint token and runtime state |
199 | `control_addr` | `127.0.0.1:4018` | Loopback-only local control API address |
200 | `service_name` | `portal-agent` | OS service name |
201 -| `allowed_wallets` | empty | Wallet addresses allowed to sign in to the local agent UI; empty allows any wallet on the loopback UI |
201 +| `allowed_wallets` | empty | Wallet addresses allowed to read local agent status through wallet auth; empty allows any wallet on the loopback auth endpoint |
202 +
203 +The local agent dashboard and mutating control API calls use the bearer token in
204 +the agent state directory. Wallet-authenticated agent requests are read-only and
205 +can only read `/v1/agent/status`.
206
207 Tunnel fields mirror `portal expose` flags:
208
@@ -212,9 +216,12 @@ Tunnel fields mirror `portal expose` flags:
216 | `multi_hop` | string array | Ordered multi-hop relay path |
217 | `multi_hop_depth` | int | Automatically select one multi-hop route with this depth |
218 | `identity_path` | string | Tunnel identity JSON file path. When omitted, one tunnel uses the platform default `identity.json`; multiple tunnels use `<state-dir>/<tunnel-id>/identity.json` |
219 +| `identity_json` | string | Identity JSON payload; overrides `identity_path` contents and is persisted there when both are set |
220 | `udp`, `udp_addr`, `tcp` | bool/string | UDP and raw TCP relay options |
221 | `description`, `tags`, `owner`, `thumbnail`, `hide` | mixed | Lease metadata shown by relays |
222
223 +For a task-oriented walkthrough, see [Portal Agent](/portal-agent).
224 +
225 ### `identity.json`
226
227 Stores the secp256k1 identity used to sign tunnel sessions and relay descriptors. `portal expose` treats `--identity-path` as a direct JSON file path. `relay-server` treats `IDENTITY_PATH` as a state directory and stores this file at `IDENTITY_PATH/identity.json`.
@@ -245,6 +252,8 @@ Set `ACME_DNS_PROVIDER` (or `--acme-dns-provider`) to one of the values below to
252
253 When this variable is empty the relay server falls back to manually supplied `fullchain.pem` and `privatekey.pem` files in `IDENTITY_PATH`.
254
255 +For ENS gasless behavior and wallet authentication details, see [Wallet and ENS](/wallet-and-ens).
256 +
257 ### Cloudflare (`cloudflare`)
258
259 | Variable | Required | Description |
docs/src/routes/deployment/+page.md
+3
@@ -58,6 +58,9 @@ Set `ACME_DNS_PROVIDER` to one of:
58 - `gcloud`
59 - `route53`
60
61 +For a focused explanation of wallet auth and ENS gasless DNS behavior, see
62 +[Wallet and ENS](/wallet-and-ens).
63 +
64 ### 3.2 Cloudflare setup
65
66 #### Add domain to Cloudflare
docs/src/routes/getting-started/+page.md
+1
@@ -134,6 +134,7 @@ verifies its SHA256 checksum, and replaces the current executable.
134 ## Next Steps
135
136 - [Concepts](/concepts): understand Portal's relay and transport model
137 +- [Portal Agent](/portal-agent): keep multiple tunnels running from config
138 - [CLI Reference](/cli-reference): complete command and flag documentation
139 - [TCP and UDP Tunneling](/tcp-udp-tunneling): raw TCP and UDP examples
140 - [Deployment](/deployment): run your own public relay
docs/src/routes/portal-agent/+page.md new
+264
@@ -0,0 +1,264 @@
1 +---
2 +title: Portal Agent
3 +description: Run durable multi-tunnel Portal services from a local config file.
4 +---
5 +
6 +# Portal Agent
7 +
8 +`portal agent` is the long-lived version of `portal expose`. It runs one local
9 +agent process, reads a TOML config file, and keeps every declared tunnel
10 +registered with the selected relays.
11 +
12 +Use the agent when tunnels should survive terminal closes, login sessions, or
13 +manual restarts. Use `portal expose` for one-off development sessions.
14 +
15 +## What The Agent Owns
16 +
17 +The agent owns:
18 +
19 +- one `config.toml`
20 +- one local loopback control API
21 +- one OS service when run in managed mode
22 +- one or more tunnel runtimes declared under `[[tunnels]]`
23 +- tunnel identities stored under the agent state directory unless overridden
24 +
25 +Each tunnel still uses the normal Portal SDK path internally: it registers a
26 +lease, opens reverse sessions, renews the lease, and proxies traffic to the
27 +configured local target.
28 +
29 +## Create A Config
30 +
31 +`portal agent run` requires an existing config file. The installer does not
32 +create one.
33 +
34 +Default config paths:
35 +
36 +| OS | Config path |
37 +|----|-------------|
38 +| Linux user | `$XDG_CONFIG_HOME/portal-tunnel/agent/config.toml` or `~/.config/portal-tunnel/agent/config.toml` |
39 +| Linux root | `/etc/portal-tunnel/agent/config.toml` |
40 +| macOS user | `~/Library/Application Support/Portal Tunnel/Agent/config.toml` |
41 +| macOS root | `/Library/Application Support/Portal Tunnel/Agent/config.toml` |
42 +| Windows | `%ProgramData%\Portal Tunnel\Agent\config.toml` |
43 +
44 +Minimal config:
45 +
46 +```toml
47 +[agent]
48 +control_addr = "127.0.0.1:4018"
49 +service_name = "portal-agent"
50 +
51 +[[tunnels]]
52 +id = "web"
53 +name = "myapp"
54 +target = "127.0.0.1:3000"
55 +relays = ["https://portal.example.com"]
56 +discovery = false
57 +description = "Managed web tunnel"
58 +tags = ["web"]
59 +```
60 +
61 +Routed HTTP config:
62 +
63 +```toml
64 +[agent]
65 +control_addr = "127.0.0.1:4018"
66 +service_name = "portal-agent"
67 +
68 +[[tunnels]]
69 +id = "frontend"
70 +name = "myapp"
71 +relays = ["https://portal.example.com"]
72 +discovery = false
73 +
74 +[[tunnels.http_routes]]
75 +prefix = "/api"
76 +upstream = "http://127.0.0.1:3001"
77 +
78 +[[tunnels.http_routes]]
79 +prefix = "/"
80 +upstream = "http://127.0.0.1:5173"
81 +```
82 +
83 +Relative paths in the config are resolved from the config file directory.
84 +
85 +## Run The Agent
86 +
87 +Run as a managed OS service:
88 +
89 +```bash
90 +portal agent run
91 +```
92 +
93 +Run in the current terminal:
94 +
95 +```bash
96 +portal agent run --config config.toml --foreground
97 +```
98 +
99 +Open the local dashboard:
100 +
101 +```bash
102 +portal agent dashboard
103 +```
104 +
105 +Restart or stop:
106 +
107 +```bash
108 +portal agent restart
109 +portal agent stop
110 +```
111 +
112 +`portal agent run`, `stop`, and `restart` load the config so they can find the
113 +state directory and service name. `portal agent dashboard` can attach with only
114 +the default state directory or an explicit `--state-dir`.
115 +
116 +`portal agent run --service` is the internal service entrypoint installed by
117 +`portal agent run`. Operators normally do not run it directly.
118 +
119 +## Dashboard
120 +
121 +The dashboard is a local terminal UI. It polls agent status every two seconds
122 +and edits the same TOML config file that the service uses.
123 +
124 +Dashboard panes:
125 +
126 +| Pane | Purpose |
127 +|------|---------|
128 +| Tunnels | Add, select, and delete simple target tunnels |
129 +| Settings | Edit max active relays and public metadata |
130 +| Relays | Connect or disconnect relays for the selected tunnel |
131 +| Multi-hop | Build and apply an ordered multi-hop route |
132 +
133 +Keyboard controls:
134 +
135 +| Key | Action |
136 +|-----|--------|
137 +| `left` / `right` | Switch panes |
138 +| `up` / `down` | Move within the active pane |
139 +| `enter` | Apply the active action |
140 +| `delete` | Delete the selected tunnel or disconnect the selected relay |
141 +| `c` | Connect the selected relay in the Relays pane |
142 +| `d` | Disconnect the selected relay in the Relays pane |
143 +| `o` | Open the selected public tunnel URL |
144 +| `a` | Add the selected relay as a multi-hop hop |
145 +| `p` | Apply a drafted multi-hop route |
146 +| `esc` | Cancel input or return to the Tunnels pane |
147 +| `ctrl+c` | Exit the dashboard |
148 +
149 +The Add Tunnel action accepts `name port`, for example `myapp 3000`. It creates
150 +a simple loopback target tunnel. Advanced options such as `http_routes`, UDP,
151 +TCP, custom identity JSON, or explicit multi-hop defaults should be edited in
152 +`config.toml`.
153 +
154 +## Tunnel Config Fields
155 +
156 +Common fields:
157 +
158 +| Field | Description |
159 +|-------|-------------|
160 +| `id` | Stable local tunnel ID used by the dashboard and control API |
161 +| `name` | Public lease name, used as the subdomain label |
162 +| `target` | Local TCP target, equivalent to `portal expose <target>` |
163 +| `http_routes` | Routed HTTP mappings; cannot be combined with `target` or `udp` |
164 +| `relays` | Explicit relay API URLs |
165 +| `discovery` | Include registry and relay discovery expansion |
166 +| `max_active_relays` | Maximum auto-selected relays kept connected |
167 +| `identity_path` | Tunnel identity JSON path |
168 +| `identity_json` | Identity JSON payload; persisted to `identity_path` when both are set |
169 +| `udp`, `udp_addr` | UDP transport settings |
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 |
174 +| `description`, `tags`, `owner`, `thumbnail`, `hide` | Public relay metadata |
175 +
176 +Constraints match `portal expose`:
177 +
178 +- `target` cannot be combined with `http_routes`.
179 +- `http_routes` cannot be combined with `udp`.
180 +- `multi_hop` requires at least two relay URLs.
181 +- `multi_hop` cannot be combined with `multi_hop_depth`.
182 +- Multi-hop currently supports only the default stream transport, not UDP or raw
183 + TCP port mode.
184 +
185 +## Identity Layout
186 +
187 +If `identity_path` is omitted:
188 +
189 +- a single tunnel uses `<state_dir>/identity.json`
190 +- multiple tunnels use `<state_dir>/<tunnel-id>/identity.json`
191 +
192 +Reusing an identity keeps the same tunnel address and lease identity across
193 +restarts. Use separate identity paths when two tunnels should have separate
194 +lease identities.
195 +
196 +## Local Control API
197 +
198 +The agent writes this file while running:
199 +
200 +```text
201 +<state_dir>/agent-endpoint.json
202 +```
203 +
204 +It contains the loopback control address and a random bearer token. CLI commands
205 +read this file and send `Authorization: Bearer <token>` to the local control
206 +API.
207 +
208 +The agent refuses non-loopback `control_addr` values. Use `127.0.0.1`,
209 +`localhost`, or another loopback address.
210 +
211 +Control endpoints:
212 +
213 +| Method | Path | Auth | Purpose |
214 +|--------|------|------|---------|
215 +| `GET` | `/v1/agent/status` | Bearer token or wallet session | Read agent and tunnel status |
216 +| `POST` | `/v1/agent/shutdown` | Bearer token | Ask the agent to stop |
217 +| `POST` | `/v1/agent/tunnels` | Bearer token | Add a simple target tunnel |
218 +| `PATCH` | `/v1/agent/tunnels/{id}` | Bearer token | Update metadata or max active relays |
219 +| `DELETE` | `/v1/agent/tunnels/{id}` | Bearer token | Delete a tunnel |
220 +| `POST` | `/v1/agent/tunnels/{id}/relays` | Bearer token | Connect a relay |
221 +| `DELETE` | `/v1/agent/tunnels/{id}/relays` | Bearer token | Disconnect a relay |
222 +| `POST` | `/v1/agent/tunnels/{id}/multi-hop` | Bearer token | Apply a multi-hop route |
223 +| `DELETE` | `/v1/agent/tunnels/{id}/multi-hop` | Bearer token | Clear multi-hop routing |
224 +
225 +Wallet auth endpoints also exist under `/v1/agent/auth/*`. Wallet-authenticated
226 +requests are read-only and can only call `/v1/agent/status`; mutating operations
227 +use the local bearer token from the state directory.
228 +
229 +## Agent Wallet Access
230 +
231 +Set `agent.allowed_wallets` to restrict wallet-authenticated status access:
232 +
233 +```toml
234 +[agent]
235 +allowed_wallets = ["0x1234567890abcdef1234567890abcdef12345678"]
236 +```
237 +
238 +When `allowed_wallets` is empty, any wallet can sign in to the loopback agent
239 +auth endpoint. This does not grant mutation rights; the bearer token still owns
240 +config and tunnel changes.
241 +
242 +## Troubleshooting
243 +
244 +If the dashboard says the agent is unavailable, start it explicitly:
245 +
246 +```bash
247 +portal agent run --config config.toml
248 +```
249 +
250 +If the OS service manager is unavailable:
251 +
252 +```bash
253 +portal agent run --config config.toml --foreground
254 +```
255 +
256 +If a tunnel is stuck in `error`, check the selected tunnel row in the dashboard.
257 +Common causes are an invalid local target, a relay URL that cannot be reached, a
258 +transport disabled on the relay, or an invalid multi-hop route.
259 +
260 +## Next Steps
261 +
262 +- [Configuration Reference](/configuration#configtoml): every agent config field
263 +- [Wallet and ENS](/wallet-and-ens): wallet auth and ENS gasless behavior
264 +- [CLI Reference](/cli-reference): command flags and examples
docs/src/routes/prerequisites/+page.md
+16 -10
@@ -5,22 +5,25 @@ description: System requirements and prerequisites for running Portal tunnel.
5
6 # Prerequisites
7
8 -Before installing Portal, make sure your environment meets the following requirements.
8 +Before installing Portal, make sure your environment meets the following
9 +requirements.
10
11 ## System Requirements
12
13 | Requirement | Minimum |
14 |-------------|---------|
15 | OS | Linux (amd64/arm64), macOS (amd64/arm64), Windows (amd64) |
15 -| Network | Outbound TCP access (no inbound ports needed) |
16 -| Disk | ~10 MB for the binary |
16 +| Network | Outbound TCP access for tunnel clients |
17 +| Disk | About 10 MB for the binary |
18
19 ## For Tunnel Users
20
20 -- A local service running on a TCP port (e.g., a web server on `localhost:3000`)
21 +- A local service running on a TCP port, for example a web server on
22 + `localhost:3000`
23 - Internet connectivity to reach a relay server
24
23 -No accounts, API keys, or billing setup required.
25 +No accounts, API keys, billing setup, inbound firewall rules, or browser wallet
26 +are required for normal tunnel use.
27
28 ## For Relay Operators
29
@@ -28,14 +31,17 @@ If you plan to run your own relay server:
31
32 - A server with a public IP address
33 - A domain name with DNS pointing to the server
31 -- TLS certificate (auto-provisioned via ACME/Let's Encrypt, or manually provided)
32 -- Ports 443 (HTTPS) and optionally 80 (HTTP redirect) open
34 +- TLS certificate material, either managed through ACME or manually provided
35 +- Open inbound `443/tcp` and `4017/tcp`
36 +- Optional UDP and raw TCP transport port ranges
37
38 ## Optional
39
36 -- **ENS name** — for SIWE-based identity and portable naming
37 -- **Ethereum wallet** — for signing SIWE authentication messages
40 +- Ethereum wallet for relay admin login or optional local agent status access
41 +- DNS provider account for relay-managed ACME, ECH DNS records, and optional ENS
42 + gasless DNS import
43
44 ## Next Steps
45
41 -- [Getting Started](/getting-started) — install Portal and create your first tunnel
46 +- [Getting Started](/getting-started): install Portal and create your first tunnel
47 +- [Wallet and ENS](/wallet-and-ens): understand wallet auth and ENS gasless DNS
docs/src/routes/security-model/+page.md
+4
@@ -64,7 +64,11 @@ Raw TCP and UDP port transports do not add tenant TLS. Use application-level enc
64
65 Registration uses a SIWE challenge signed by the SDK's secp256k1 identity key. The relay then issues a lease-scoped ES256K access token used by renew, unregister, reverse connect, and QUIC datagram authentication.
66
67 +Browser wallet login is a separate admin/status mechanism. It does not replace
68 +the local tunnel identity used for lease registration.
69 +
70 ## Next Steps
71
72 - [Architecture](/architecture) - deep dive into Portal's internal design
73 +- [Wallet and ENS](/wallet-and-ens) - wallet auth and ENS gasless DNS import
74 - [Self-Hosting](/self-hosting) - run your own relay server
docs/src/routes/siwe-authentication/+page.md
+35 -38
@@ -1,63 +1,60 @@
1 ---
2 title: SIWE Authentication
3 -description: Use Sign-In with Ethereum (SIWE) and ENS for portable tunnel identity.
3 +description: How Portal uses SIWE for tunnel registration and wallet sessions.
4 ---
5
6 # SIWE Authentication
7
8 -Portal supports **Sign-In with Ethereum (SIWE)** for proving ownership of tunnel names without centralized accounts or API keys.
8 +Portal uses Sign-In with Ethereum (SIWE) in two places:
9
10 -## Overview
10 +- tunnel registration, signed automatically by the local tunnel identity
11 +- browser wallet sessions for relay admin and optional local agent status access
12
12 -SIWE allows you to:
13 +For the full operational guide, see [Wallet and ENS](/wallet-and-ens).
14
14 -- **Claim a tunnel name** by signing a message with your Ethereum wallet
15 -- **Prove ownership** without passwords, tokens, or a central auth server
16 -- **Use ENS names** for human-readable, portable identity (e.g., `alice.eth`)
15 +## Tunnel Registration
16
18 -## How It Works
17 +`portal expose` and `portal agent` create or load a local secp256k1 identity
18 +from `identity.json`. During registration, the relay returns a SIWE challenge
19 +with statement `Register a portal lease`; the tunnel signs it with the local
20 +identity private key and receives a lease access token.
21
20 -1. You choose a tunnel name (or use your ENS name)
21 -2. Portal generates a SIWE message containing the tunnel name and relay domain
22 -3. You sign the message with your Ethereum wallet (e.g., MetaMask, hardware wallet)
23 -4. The signed message is sent to the relay server
24 -5. The relay verifies the signature on-chain and grants the tunnel name
22 +This flow is automatic. It does not require a browser wallet.
23
26 -```
27 -Wallet --> Sign SIWE message --> Portal CLI --> Relay server
28 - |
29 - Verify signature
30 - |
31 - Grant tunnel name
24 +```bash
25 +portal expose 3000 --name myapp
26 ```
27
34 -## ENS Integration
28 +There is no `--auth siwe` flag. SIWE is part of the normal registration
29 +protocol.
30
36 -If you own an ENS name, you can use it directly as your tunnel name:
31 +## Wallet Sessions
32
38 -- `alice.eth` becomes your portable identity across relays
39 -- No registration or DNS configuration needed
40 -- Works with any relay in the public registry
33 +The relay admin UI uses browser wallet login:
34
42 -## Configuration
35 +1. request `/admin/auth/challenge`
36 +2. sign the returned SIWE message with the connected wallet
37 +3. submit `/admin/auth/login`
38 +4. use the resulting `portal_admin` session cookie
39
44 -```bash
45 -# Use SIWE authentication with a specific tunnel name
46 -portal-tunnel --auth siwe --name my-tunnel localhost:3000
40 +The relay identity address is allowed by default. Add more admin wallets with
41 +`ADMIN_WALLETS`.
42
48 -# Use your ENS name
49 -portal-tunnel --auth siwe --name alice.eth localhost:3000
50 -```
43 +The local agent also exposes `/v1/agent/auth/*` wallet endpoints. Agent wallet
44 +sessions can read `/v1/agent/status`; tunnel mutations still require the local
45 +bearer token stored in the agent state directory.
46
52 -## Without SIWE
47 +## ENS
48
54 -SIWE is optional. Without it:
49 +Portal does not use ENS names as tunnel names. Tunnel names are single DNS
50 +labels such as `myapp`.
51
56 -- Tunnel names are assigned on a first-come, first-served basis
57 -- No ownership guarantee — anyone can claim an unused name
58 -- Suitable for temporary or throwaway tunnels
52 +Relay operators can optionally enable ENS gasless DNS import. In that mode,
53 +Portal manages DNSSEC and `ENS1 ...` TXT records for the relay domain and lease
54 +hostnames so ENS-aware clients can resolve them to Portal identity addresses.
55
56 ## Next Steps
57
62 -- [Security Model](/security-model) — understand Portal's encryption and trust model
63 -- [Configuration](/configuration) — full configuration reference
58 +- [Wallet and ENS](/wallet-and-ens): detailed wallet and ENS behavior
59 +- [Security Model](/security-model): encryption and identity boundaries
60 +- [Configuration](/configuration): full configuration reference
docs/src/routes/wallet-and-ens/+page.md new
+238
@@ -0,0 +1,238 @@
1 +---
2 +title: Wallet and ENS
3 +description: How Portal uses local identities, wallet login, SIWE, and ENS gasless DNS import.
4 +---
5 +
6 +# Wallet and ENS
7 +
8 +Portal uses Ethereum-style signatures in several different places. They are
9 +related, but they do not all mean "connect a browser wallet".
10 +
11 +## Identity Surfaces
12 +
13 +| Surface | Key material | Purpose |
14 +|---------|--------------|---------|
15 +| Tunnel identity | Local `identity.json` secp256k1 private key | Signs SIWE lease registration challenges |
16 +| Relay identity | Relay `IDENTITY_PATH/identity.json` secp256k1 private key | Signs relay descriptors, admin default wallet, lease access tokens, and ENS base-domain address |
17 +| Relay admin wallet | Browser wallet address allowlist | Signs in to `/admin` with a SIWE wallet session |
18 +| Agent wallet | Optional browser wallet allowlist | Reads loopback agent status through `/v1/agent/status` |
19 +| ENS gasless DNS | DNSSEC plus `ENS1 ...` TXT records | Lets ENS-aware clients resolve the relay domain and lease hostnames to Portal identities |
20 +
21 +## Tunnel SIWE Registration
22 +
23 +Tunnel registration always uses a SIWE challenge internally:
24 +
25 +1. The tunnel creates or loads a local identity from `identity.json`.
26 +2. The tunnel asks the relay for `/sdk/register/challenge`.
27 +3. The relay returns a SIWE message with statement `Register a portal lease`.
28 +4. The tunnel signs that message with the local identity private key using
29 + Ethereum `personal_sign` semantics.
30 +5. The relay verifies the signature and returns a lease-scoped access token.
31 +6. The access token is used for renew, unregister, reverse connect, keyless
32 + signing access, and UDP backhaul authentication.
33 +
34 +This does not require MetaMask or a user wallet. It is accountless identity
35 +proof based on the local tunnel key.
36 +
37 +There is no `--auth siwe` flag. The current CLI command is:
38 +
39 +```bash
40 +portal expose 3000 --name myapp
41 +```
42 +
43 +Use a stable identity path when the lease identity must survive working
44 +directory changes:
45 +
46 +```bash
47 +portal expose 3000 \
48 + --name myapp \
49 + --identity-path ~/.config/portal/myapp.identity.json
50 +```
51 +
52 +The public lease name is a single DNS label such as `myapp`. It is not an ENS
53 +name such as `alice.eth`.
54 +
55 +## Relay Admin Wallet Login
56 +
57 +The relay admin UI uses browser wallet login. The relay creates a SIWE challenge
58 +for the connected wallet and sets a `portal_admin` session cookie after the
59 +signature verifies.
60 +
61 +Allowed admin wallets:
62 +
63 +- the relay identity address is always allowed
64 +- additional wallets come from `ADMIN_WALLETS`
65 +
66 +Example:
67 +
68 +```bash
69 +ADMIN_WALLETS=0x1234567890abcdef1234567890abcdef12345678,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd
70 +```
71 +
72 +To find the relay identity address:
73 +
74 +```bash
75 +jq -r .address .portal-certs/identity.json
76 +```
77 +
78 +Admin wallet flow:
79 +
80 +1. `POST /admin/auth/challenge` with `{ "address": "0x..." }`.
81 +2. Sign the returned `siwe_message` in the browser wallet.
82 +3. `POST /admin/auth/login` with the challenge id, exact SIWE message, and
83 + signature.
84 +4. The relay sets an HttpOnly, Secure, SameSite=Strict session cookie.
85 +5. Admin endpoints require that session cookie.
86 +
87 +Challenges expire after two minutes. Sessions expire after 24 hours.
88 +
89 +## Agent Wallet Login
90 +
91 +The local agent also exposes SIWE wallet auth endpoints:
92 +
93 +```text
94 +/v1/agent/auth/challenge
95 +/v1/agent/auth/login
96 +/v1/agent/auth/logout
97 +/v1/agent/auth/status
98 +```
99 +
100 +Agent wallet access is intentionally narrow:
101 +
102 +- `agent.allowed_wallets` restricts which wallet addresses can sign in.
103 +- when `allowed_wallets` is empty, any wallet can sign in to the loopback auth
104 + endpoint.
105 +- wallet-authenticated requests can read `/v1/agent/status`.
106 +- config mutation, tunnel changes, relay changes, shutdown, and multi-hop edits
107 + still require the bearer token in `<state_dir>/agent-endpoint.json`.
108 +
109 +Example:
110 +
111 +```toml
112 +[agent]
113 +allowed_wallets = ["0x1234567890abcdef1234567890abcdef12345678"]
114 +```
115 +
116 +See [Portal Agent](/portal-agent) for the control API details.
117 +
118 +## ENS Gasless DNS Import
119 +
120 +ENS gasless DNS import is optional relay-side DNS automation. It is separate
121 +from tunnel registration and admin wallet login.
122 +
123 +When enabled, Portal uses the configured DNS provider to:
124 +
125 +- enable or inspect DNSSEC for the relay base domain
126 +- publish `ENS1 ...` TXT records for the base domain
127 +- publish `ENS1 ...` TXT records for lease hostnames
128 +- keep A records for lease hostnames in sync with the relay public IPv4
129 +- remove lease hostname records when leases unregister or expire
130 +
131 +Portal writes TXT values in this shape:
132 +
133 +```text
134 +ENS1 0x238A8F792dFA6033814B18618aD4100654aeef01 <address>
135 +```
136 +
137 +The base-domain address is the relay identity address. Lease hostname addresses
138 +come from the tunnel identity that registered each lease.
139 +
140 +ENS gasless automation does not perform an onchain ENS claim transaction. It
141 +only prepares DNSSEC-backed DNS records for ENS-aware clients.
142 +
143 +## Enable ENS Gasless
144 +
145 +Requirements:
146 +
147 +- public relay domain, not `localhost`
148 +- `ACME_DNS_PROVIDER=cloudflare`, `gcloud`, or `route53`
149 +- provider credentials with DNS write access
150 +- `ENS_GASLESS_ENABLED=true`
151 +- DNSSEC active at the parent zone
152 +
153 +Example:
154 +
155 +```bash
156 +PORTAL_URL=https://portal.example.com
157 +IDENTITY_PATH=/portal-certs
158 +ACME_DNS_PROVIDER=cloudflare
159 +CLOUDFLARE_TOKEN=cf_xxxxxxxxxxxxxxxxx
160 +ENS_GASLESS_ENABLED=true
161 +```
162 +
163 +The same provider is used for ACME DNS-01, managed A records, ECH HTTPS records,
164 +DNSSEC, and ENS TXT records. If manual `fullchain.pem` and `privatekey.pem`
165 +already exist under `IDENTITY_PATH`, Portal keeps using those certificate files
166 +and still uses the provider for ENS/DNS automation.
167 +
168 +## DNSSEC And Registrar State
169 +
170 +DNSSEC has two sides:
171 +
172 +- the DNS provider signs the hosted zone
173 +- the registrar publishes the DS record at the parent zone
174 +
175 +Portal can automate provider-side setup for supported providers. It cannot
176 +always publish the registrar-side DS record. If `/sdk/domain` reports a pending
177 +DNSSEC state and a `ds_record`, copy that DS record into the registrar's DNSSEC
178 +settings and wait for propagation.
179 +
180 +## Check ENS Status
181 +
182 +The relay exposes ENS status through `/sdk/domain`:
183 +
184 +```bash
185 +curl https://portal.example.com/sdk/domain
186 +```
187 +
188 +Relevant response fields:
189 +
190 +| Field | Meaning |
191 +|-------|---------|
192 +| `ens.enabled` | ENS gasless automation is enabled for a non-local relay domain |
193 +| `ens.verified` | Portal considers DNSSEC active and the last sync successful |
194 +| `ens.provider` | DNS provider used for automation |
195 +| `ens.address` | Base-domain ENS address, usually the relay identity address |
196 +| `ens.dnssec_state` | Provider DNSSEC state |
197 +| `ens.ds_record` | DS record that may need registrar publication |
198 +| `ens.message` | Provider-specific DNSSEC guidance |
199 +| `ens.last_error` | Last ENS/DNS sync error |
200 +
201 +The relay frontend shows an `ENS verified` badge when `ens.verified` is true.
202 +
203 +DNS checks:
204 +
205 +```bash
206 +dig +short DS portal.example.com
207 +dig +short TXT portal.example.com
208 +dig +short TXT myapp.portal.example.com
209 +```
210 +
211 +Expected TXT records start with `ENS1`.
212 +
213 +## Troubleshooting
214 +
215 +`ENS_GASLESS_ENABLED=true` fails at startup:
216 +
217 +- set `ACME_DNS_PROVIDER`
218 +- provide the provider credentials
219 +- use a public `PORTAL_URL`, not localhost
220 +
221 +`ens.verified` stays false:
222 +
223 +- publish the DS record at the registrar
224 +- wait for DNSSEC propagation
225 +- check `ens.last_error` from `/sdk/domain`
226 +- confirm the provider token can edit DNS records
227 +
228 +A lease hostname has no ENS TXT record:
229 +
230 +- confirm the tunnel is registered and not expired
231 +- confirm the hostname is under the relay base domain
232 +- check relay logs for `ensure ens gasless txt` or provider errors
233 +
234 +## Next Steps
235 +
236 +- [Deployment](/deployment#35-optional-ens-gasless-automation): production setup
237 +- [Security Model](/security-model): identity and TLS trust boundaries
238 +- [Portal Agent](/portal-agent): local durable tunnel management