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