@lamalibre/create-portlama 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/index.js +1 -1
  4. package/src/tasks/mtls.js +2 -3
  5. package/vendor/panel-client/dist/docs/00-introduction/how-it-works.md +3 -2
  6. package/vendor/panel-client/dist/docs/00-introduction/quickstart.md +9 -6
  7. package/vendor/panel-client/dist/docs/00-introduction/what-is-portlama.md +4 -2
  8. package/vendor/panel-client/dist/docs/01-concepts/authentication.md +31 -19
  9. package/vendor/panel-client/dist/docs/01-concepts/dns-and-domains.md +8 -1
  10. package/vendor/panel-client/dist/docs/01-concepts/mtls.md +47 -7
  11. package/vendor/panel-client/dist/docs/01-concepts/nginx-reverse-proxy.md +89 -39
  12. package/vendor/panel-client/dist/docs/01-concepts/security-model.md +9 -8
  13. package/vendor/panel-client/dist/docs/02-guides/certificate-management.md +81 -11
  14. package/vendor/panel-client/dist/docs/02-guides/disaster-recovery.md +1 -1
  15. package/vendor/panel-client/dist/docs/02-guides/first-tunnel.md +3 -2
  16. package/vendor/panel-client/dist/docs/02-guides/installation.md +6 -5
  17. package/vendor/panel-client/dist/docs/02-guides/mac-client-setup.md +15 -12
  18. package/vendor/panel-client/dist/docs/02-guides/managing-users.md +8 -8
  19. package/vendor/panel-client/dist/docs/02-guides/static-sites.md +22 -17
  20. package/vendor/panel-client/dist/docs/03-architecture/e2e-three-vm-sequences.md +431 -0
  21. package/vendor/panel-client/dist/docs/03-architecture/installer-flow.md +141 -0
  22. package/vendor/panel-client/dist/docs/03-architecture/installer.md +14 -1
  23. package/vendor/panel-client/dist/docs/03-architecture/management-flow.md +266 -0
  24. package/vendor/panel-client/dist/docs/03-architecture/nginx-configuration.md +121 -32
  25. package/vendor/panel-client/dist/docs/03-architecture/onboarding-flow.md +164 -0
  26. package/vendor/panel-client/dist/docs/03-architecture/overview.md +16 -9
  27. package/vendor/panel-client/dist/docs/03-architecture/panel-client.md +1 -1
  28. package/vendor/panel-client/dist/docs/03-architecture/panel-server.md +87 -45
  29. package/vendor/panel-client/dist/docs/03-architecture/state-management.md +7 -3
  30. package/vendor/panel-client/dist/docs/03-architecture/system-overview.md +158 -0
  31. package/vendor/panel-client/dist/docs/04-api-reference/certificates.md +282 -15
  32. package/vendor/panel-client/dist/docs/04-api-reference/overview.md +22 -7
  33. package/vendor/panel-client/dist/docs/04-api-reference/services.md +4 -0
  34. package/vendor/panel-client/dist/docs/04-api-reference/sites.md +75 -0
  35. package/vendor/panel-client/dist/docs/04-api-reference/system.md +10 -11
  36. package/vendor/panel-client/dist/docs/04-api-reference/tunnels.md +62 -2
  37. package/vendor/panel-client/dist/docs/04-api-reference/users.md +3 -3
  38. package/vendor/panel-client/dist/docs/05-operations/backup-and-restore.md +4 -1
  39. package/vendor/panel-client/dist/docs/05-operations/monitoring.md +1 -1
  40. package/vendor/panel-client/dist/docs/06-reference/config-files.md +17 -9
  41. package/vendor/panel-client/dist/docs/06-reference/glossary.md +2 -2
  42. package/vendor/panel-client/dist/docs/06-reference/installer-flags.md +19 -1
  43. package/vendor/panel-client/dist/docs/06-reference/troubleshooting.md +12 -12
  44. package/vendor/panel-client/dist/docs/_index.json +6 -1
  45. package/vendor/panel-server/src/index.js +3 -2
  46. package/vendor/panel-server/src/lib/invite-page.js +32 -7
  47. package/vendor/panel-server/src/routes/invite.js +13 -1
  48. package/vendor/panel-server/src/routes/management/invitations.js +3 -1
  49. package/vendor/panel-server/src/routes/onboarding/provision.js +4 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@ One-command setup for secure reverse tunnels with a management dashboard.
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- ssh root@<droplet-ip> "npx @lamalibre/create-portlama"
8
+ ssh root@<droplet-ip> "apt install -y npm && npx @lamalibre/create-portlama"
9
9
  ```
10
10
 
11
11
  The installer runs unattended on a fresh Ubuntu 24.04 droplet and provisions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamalibre/create-portlama",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "One-command setup for secure reverse tunnels with a management dashboard",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/index.js CHANGED
@@ -304,7 +304,7 @@ ${chalk.cyan.bold('│')} This will install Portlama on this machine.
304
304
  ${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
305
305
  ${chalk.cyan.bold('│')} The following changes will be made: ${chalk.cyan.bold('│')}
306
306
  ${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
307
- ${chalk.cyan.bold('│')} ${chalk.yellow('•')} Reset UFW firewall (allow ports 22, 443, 9292 only) ${chalk.cyan.bold('│')}
307
+ ${chalk.cyan.bold('│')} ${chalk.yellow('•')} Reset UFW firewall (allow ports 22, 80, 443, 9292) ${chalk.cyan.bold('│')}
308
308
  ${chalk.cyan.bold('│')} ${chalk.yellow('•')} Harden SSH (disable password authentication) ${chalk.cyan.bold('│')}
309
309
  ${chalk.cyan.bold('│')} ${chalk.yellow('•')} Install fail2ban, Node.js 20, nginx, certbot ${chalk.cyan.bold('│')}
310
310
  ${chalk.cyan.bold('│')} ${chalk.yellow('•')} Generate mTLS certificates for browser access ${chalk.cyan.bold('│')}
package/src/tasks/mtls.js CHANGED
@@ -163,9 +163,8 @@ export function mtlsTasks(ctx, task) {
163
163
  `${pkiDir}/client.crt`,
164
164
  '-certfile',
165
165
  `${pkiDir}/ca.crt`,
166
- '-passout',
167
- `pass:${password}`,
168
- ]);
166
+ '-passout', 'stdin',
167
+ ], { input: password });
169
168
 
170
169
  // Save password to file (no trailing newline)
171
170
  await writeFile(`${pkiDir}/.p12-password`, password, { mode: 0o600 });
@@ -50,6 +50,7 @@ Portlama has three phases of life: installation, onboarding, and daily operation
50
50
  You SSH into a fresh Ubuntu droplet and run one command:
51
51
 
52
52
  ```bash
53
+ apt install -y npm
53
54
  npx @lamalibre/create-portlama
54
55
  ```
55
56
 
@@ -284,7 +285,7 @@ Onboarding Wizard
284
285
  ├── Write domain-based nginx vhosts
285
286
  ├── Create systemd service units for Chisel + Authelia
286
287
  ├── Start all services
287
- └── Update panel.json → onboarding status: COMPLETE
288
+ └── Update panel.json → onboarding status: COMPLETED
288
289
  ```
289
290
 
290
291
  After provisioning, onboarding endpoints return `410 Gone` and management endpoints become available.
@@ -339,7 +340,7 @@ Layer 7: Least-privilege sudoers
339
340
  The panel operates in distinct states:
340
341
 
341
342
  ```
342
- FRESH ──────→ DOMAIN_SET ──────→ DNS_VERIFIED ──────→ PROVISIONING ──────→ COMPLETE
343
+ FRESH ──────→ DOMAIN_SET ──────→ DNS_READY ──────→ PROVISIONING ──────→ COMPLETED
343
344
  │ │ │ │ │
344
345
  │ Onboarding │ Onboarding │ Onboarding │ Onboarding │ Management
345
346
  │ endpoints │ endpoints │ endpoints │ endpoints │ endpoints
@@ -56,6 +56,7 @@ ssh root@203.0.113.42
56
56
  Once connected:
57
57
 
58
58
  ```bash
59
+ apt install -y npm
59
60
  npx @lamalibre/create-portlama
60
61
  ```
61
62
 
@@ -269,12 +270,12 @@ Alternatively, you can run the Chisel client manually on your Mac:
269
270
 
270
271
  ```bash
271
272
  chisel client \
272
- --auth user:password \
273
- wss://example.com \
274
- R:8001:localhost:8001
273
+ --tls-skip-verify \
274
+ https://tunnel.example.com:443 \
275
+ R:127.0.0.1:8001:127.0.0.1:8001
275
276
  ```
276
277
 
277
- The `R:8001:localhost:8001` part means: "Reverse-forward port 8001 on the server to port 8001 on my Mac."
278
+ The `R:127.0.0.1:8001:127.0.0.1:8001` part means: "Reverse-forward port 8001 on the server's localhost to port 8001 on my Mac."
278
279
 
279
280
  ---
280
281
 
@@ -335,6 +336,7 @@ npx @lamalibre/create-portlama [flags]
335
336
  | `--yes`, `-y` | Skip the confirmation prompt |
336
337
  | `--skip-harden` | Skip OS hardening (swap, UFW, fail2ban, SSH) |
337
338
  | `--dev` | Allow private/non-routable IP addresses |
339
+ | `--force-full` | Run full installation even on existing installs |
338
340
  | `--uninstall` | Print manual removal guide and exit |
339
341
 
340
342
  The `--dev` flag is useful for testing on local VMs that do not have public IP addresses.
@@ -364,12 +366,12 @@ For local development without a VPS:
364
366
  cd packages/panel-server
365
367
  CONFIG_FILE=../../dev/panel.json NODE_ENV=development node src/index.js
366
368
 
367
- # Terminal 2: Start panel frontend (Vite dev server, proxies /api to :9292)
369
+ # Terminal 2: Start panel frontend (Vite dev server, proxies /api to :3100)
368
370
  cd packages/panel-client
369
371
  npx vite
370
372
  ```
371
373
 
372
- In development mode (`NODE_ENV=development`), the panel server skips mTLS client certificate verification, so you can access it without importing a certificate.
374
+ In development mode (`NODE_ENV=development` or `NODE_ENV` unset), the panel server skips mTLS client certificate verification, so you can access it without importing a certificate.
373
375
 
374
376
  ### Troubleshooting
375
377
 
@@ -414,6 +416,7 @@ In development mode (`NODE_ENV=development`), the panel server skips mTLS client
414
416
  # 1. Create droplet (Ubuntu 24.04, 512MB, $4/mo)
415
417
  # 2. SSH in and install
416
418
  ssh root@203.0.113.42
419
+ apt install -y npm
417
420
  npx @lamalibre/create-portlama
418
421
 
419
422
  # 3. Download cert (from your local machine)
@@ -46,13 +46,15 @@ Portlama solves the problem of exposing local services to the internet securely.
46
46
 
47
47
  ## For Developers
48
48
 
49
- Portlama is a monorepo with three packages:
49
+ Portlama is a monorepo with five packages:
50
50
 
51
51
  | Package | Technology | Purpose |
52
52
  |---------|------------|---------|
53
53
  | `create-portlama` | Node.js ESM, Listr2, execa | Zero-prompt installer CLI |
54
54
  | `panel-server` | Fastify 5, Node.js ESM | REST API + WebSocket backend |
55
55
  | `panel-client` | React 18, Vite, Tailwind | Management UI (SPA) |
56
+ | `portlama-agent` | Node.js ESM | Mac tunnel agent CLI |
57
+ | `portlama-desktop` | Tauri v2 | Desktop agent (WIP) |
56
58
 
57
59
  **Architecture summary:**
58
60
 
@@ -86,4 +88,4 @@ The installer (`npx @lamalibre/create-portlama`) is completely non-interactive
86
88
  | **State storage** | JSON files (no database) |
87
89
  | **RAM usage** | ~245MB total (all services) |
88
90
  | **npm package** | `@lamalibre/create-portlama` |
89
- | **License** | MIT |
91
+ | **License** | Polyform Noncommercial 1.0.0 |
@@ -20,34 +20,37 @@ These two systems are completely independent. Admin certificate holders do not a
20
20
 
21
21
  | Person | Accesses | Authentication method |
22
22
  |--------|----------|----------------------|
23
- | You (the admin) | Management panel at `panel.example.com` or `https://IP:9292` | Client certificate (mTLS) |
23
+ | You (the admin) | Management panel at `https://<IP>:9292` | Client certificate (mTLS) |
24
24
  | Your users | Tunneled apps at `myapp.example.com` | Username + password + TOTP code |
25
25
 
26
26
  ### Managing users
27
27
 
28
28
  From the Users page in the management panel, you can:
29
29
 
30
- - **Create users** — set a username and password; Portlama generates a TOTP secret
30
+ - **Create users** — set a username, display name, email, and password
31
31
  - **Delete users** — remove a user (you cannot delete the last user)
32
- - **Reset TOTP** — generate a new TOTP secret if a user loses their authenticator
32
+ - **Reset TOTP** — generate a new TOTP secret if a user loses their authenticator (separate step via `POST /api/users/:username/reset-totp`)
33
33
  - **Update password** — change a user's password
34
34
 
35
35
  ### Creating a user
36
36
 
37
37
  1. Navigate to the Users page in the management panel
38
38
  2. Click "Add User"
39
- 3. Enter a username and password
40
- 4. Portlama creates the user and generates a TOTP secret
41
- 5. A QR code is displayedthe user scans this with their authenticator app
42
- 6. The user is now ready to log in
39
+ 3. Enter a username, display name, email, and password
40
+ 4. Portlama creates the user in `users.yml` and restarts Authelia
41
+ 5. Share the credentials with the user they enroll in TOTP on first login
43
42
 
44
43
  ### TOTP enrollment flow
45
44
 
46
- When you create a user, Portlama generates a TOTP secret and displays it as both a QR code and a text string. The user opens their authenticator app (Google Authenticator, Authy, 1Password, etc.) and scans the QR code. From that point, the app generates a fresh 6-digit code every 30 seconds.
45
+ TOTP enrollment is a separate step from user creation. There are two paths:
46
+
47
+ **First-login enrollment:** When a new user visits a Portlama-protected app for the first time, Authelia presents a QR code during their initial login. The user scans it with their authenticator app to complete enrollment.
48
+
49
+ **Admin-initiated reset:** If a user loses their authenticator, the admin clicks "Reset TOTP" on the Users page, which calls `POST /api/users/:username/reset-totp`. This writes the new TOTP secret to Authelia's SQLite database via the `authelia storage user totp generate` CLI command. A QR code is displayed for the user to scan.
47
50
 
48
51
  ```
49
- Admin creates user → Portlama generates TOTP secret
50
- QR code displayed in panel
52
+ Admin creates user → User visits protected app
53
+ Authelia prompts for TOTP enrollment on first login
51
54
  → User scans QR code with authenticator app
52
55
  → Authenticator generates 6-digit codes every 30 seconds
53
56
  ```
@@ -118,14 +121,15 @@ The full configuration is written during onboarding provisioning (`packages/pane
118
121
 
119
122
  ```yaml
120
123
  server:
121
- host: 127.0.0.1
122
- port: 9091
124
+ address: 'tcp://127.0.0.1:9091/'
123
125
 
124
126
  log:
125
127
  level: info
126
128
  file_path: /var/log/authelia/authelia.log
127
129
 
128
- jwt_secret: <random-64-byte-hex>
130
+ identity_validation:
131
+ reset_password:
132
+ jwt_secret: <random-64-byte-hex>
129
133
 
130
134
  authentication_backend:
131
135
  file:
@@ -136,15 +140,23 @@ authentication_backend:
136
140
  cost: 12
137
141
 
138
142
  access_control:
139
- default_policy: one_factor
143
+ default_policy: two_factor
140
144
 
141
145
  session:
142
146
  name: portlama_session
143
147
  secret: <random-64-byte-hex>
144
- domain: example.com
148
+ cookies:
149
+ - domain: example.com
150
+ authelia_url: https://auth.example.com
151
+ default_redirection_url: https://example.com
145
152
  expiration: 12h
146
153
  inactivity: 2h
147
154
 
155
+ regulation:
156
+ max_retries: 5
157
+ find_time: 2m
158
+ ban_time: 5m
159
+
148
160
  storage:
149
161
  encryption_key: <random-64-byte-hex>
150
162
  local:
@@ -162,9 +174,9 @@ totp:
162
174
 
163
175
  Key configuration choices:
164
176
 
165
- - **`host: 127.0.0.1`** — binds to localhost only; nginx handles public access
166
- - **`default_policy: one_factor`** — Authelia uses "one_factor" for TOTP-based authentication
167
- - **`session.domain`** — the base domain, so session cookies work across subdomains
177
+ - **`server.address: 'tcp://127.0.0.1:9091/'`** — binds to localhost only; nginx handles public access
178
+ - **`default_policy: two_factor`** — requires password + TOTP for all authenticated users
179
+ - **`session.cookies`** — array format (Authelia v4.38+) specifying the domain and Authelia URL for session cookies
168
180
  - **`notifier.filesystem`** — writes notifications to a file instead of sending email (suitable for small-scale use)
169
181
  - **`totp.period: 30`** — standard 30-second TOTP window
170
182
 
@@ -394,7 +406,7 @@ sudo systemctl restart authelia
394
406
  | Method | Path | Description |
395
407
  |--------|------|-------------|
396
408
  | GET | `/api/users` | List all users (without password hashes) |
397
- | POST | `/api/users` | Create user with password and TOTP |
409
+ | POST | `/api/users` | Create user with username, displayname, email, and password |
398
410
  | PUT | `/api/users/:username` | Update user password or display name |
399
411
  | DELETE | `/api/users/:username` | Delete user (not the last one) |
400
412
  | POST | `/api/users/:username/reset-totp` | Generate new TOTP secret |
@@ -128,7 +128,7 @@ When creating a tunnel, the subdomain must follow DNS naming rules:
128
128
  - Maximum 63 characters
129
129
  - Must be unique (no two tunnels can use the same subdomain)
130
130
 
131
- Reserved subdomains that cannot be used for tunnels:
131
+ Reserved subdomains that cannot be used for tunnels or static sites:
132
132
 
133
133
  | Subdomain | Reason |
134
134
  |-----------|--------|
@@ -136,6 +136,9 @@ Reserved subdomains that cannot be used for tunnels:
136
136
  | `auth` | Authelia portal |
137
137
  | `tunnel` | Chisel WebSocket endpoint |
138
138
  | `www` | Conventionally the main site |
139
+ | `mail` | Email subdomain |
140
+ | `ftp` | File transfer subdomain |
141
+ | `api` | API subdomain |
139
142
 
140
143
  ### DNS and TLS certificate issuance
141
144
 
@@ -271,6 +274,10 @@ Once set during onboarding, the domain is used throughout the system for constru
271
274
  | `panel` | Admin panel |
272
275
  | `auth` | Authelia login portal |
273
276
  | `tunnel` | Chisel WebSocket endpoint |
277
+ | `www` | Conventionally the main site |
278
+ | `mail` | Email subdomain |
279
+ | `ftp` | File transfer subdomain |
280
+ | `api` | API subdomain |
274
281
 
275
282
  ### Subdomain naming rules
276
283
 
@@ -224,6 +224,11 @@ This snippet is included in every nginx vhost that should require mTLS. The `ssl
224
224
  The panel vhost includes this snippet:
225
225
 
226
226
  ```nginx
227
+ map $http_upgrade $connection_upgrade {
228
+ default upgrade;
229
+ '' close;
230
+ }
231
+
227
232
  server {
228
233
  listen 9292 ssl;
229
234
  server_name _;
@@ -235,8 +240,42 @@ server {
235
240
 
236
241
  # Show help page when client cert is missing
237
242
  error_page 495 496 /cert-help.html;
238
-
239
- # ... proxy to panel-server
243
+ location = /cert-help.html {
244
+ root /opt/portlama/panel-client;
245
+ internal;
246
+ }
247
+
248
+ # Proxy to panel-server
249
+ location / {
250
+ proxy_pass http://127.0.0.1:3100;
251
+
252
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
253
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
254
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
255
+
256
+ proxy_set_header Host $host;
257
+ proxy_set_header X-Real-IP $remote_addr;
258
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
259
+ proxy_set_header X-Forwarded-Proto $scheme;
260
+ }
261
+
262
+ # API paths with WebSocket upgrade support
263
+ location /api {
264
+ proxy_pass http://127.0.0.1:3100;
265
+ proxy_http_version 1.1;
266
+
267
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
268
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
269
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
270
+
271
+ proxy_set_header Host $host;
272
+ proxy_set_header X-Real-IP $remote_addr;
273
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
274
+ proxy_set_header X-Forwarded-Proto $scheme;
275
+
276
+ proxy_set_header Upgrade $http_upgrade;
277
+ proxy_set_header Connection $connection_upgrade;
278
+ }
240
279
  }
241
280
  ```
242
281
 
@@ -253,15 +292,16 @@ After TLS negotiation succeeds, nginx forwards client certificate information to
253
292
  ```nginx
254
293
  proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
255
294
  proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
295
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
256
296
  ```
257
297
 
258
- The panel server's mTLS middleware checks `X-SSL-Client-Verify`:
298
+ The panel server's mTLS middleware checks these headers in order:
259
299
 
260
- - `SUCCESS` valid client certificate presented
261
- - `FAILED` certificate presented but invalid
262
- - `NONE` no certificate presented
300
+ 1. **`X-SSL-Client-Verify`** — must be `SUCCESS` (valid client cert); any other value (`FAILED`, `NONE`) results in a 403 Forbidden response.
301
+ 2. **`X-SSL-Client-Serial`** — checked against the revocation list (`revoked.json`); if the serial is revoked, the request is rejected with 403.
302
+ 3. **`X-SSL-Client-DN`** — the CN is parsed to determine the role. `CN=admin` grants full access; `CN=agent:<label>` grants capability-based access with permissions looked up from the agent registry.
263
303
 
264
- In production, any value other than `SUCCESS` results in a 403 Forbidden response. In development (`NODE_ENV=development`), the check is skipped.
304
+ In development (`NODE_ENV=development`), all checks are skipped.
265
305
 
266
306
  ### Certificate rotation
267
307
 
@@ -77,6 +77,11 @@ All Portlama vhost files are prefixed with `portlama-` to distinguish them from
77
77
  This vhost is created during installation and is the only vhost that exists before onboarding:
78
78
 
79
79
  ```nginx
80
+ map $http_upgrade $connection_upgrade {
81
+ default upgrade;
82
+ '' close;
83
+ }
84
+
80
85
  server {
81
86
  listen 9292 ssl;
82
87
  server_name _;
@@ -99,27 +104,41 @@ server {
99
104
  internal;
100
105
  }
101
106
 
102
- # Client cert headers forwarded to backend
103
- proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
104
- proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
105
-
106
- # Standard proxy headers
107
- proxy_set_header Host $host;
108
- proxy_set_header X-Real-IP $remote_addr;
109
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
110
- proxy_set_header X-Forwarded-Proto $scheme;
111
-
112
- # Static files and API
107
+ # Proxy to panel-server
113
108
  location / {
114
109
  proxy_pass http://127.0.0.1:3100;
110
+
111
+ # Client cert headers — set from nginx TLS variables, never passed through from client
112
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
113
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
114
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
115
+
116
+ # Standard proxy headers
117
+ proxy_set_header Host $host;
118
+ proxy_set_header X-Real-IP $remote_addr;
119
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
120
+ proxy_set_header X-Forwarded-Proto $scheme;
115
121
  }
116
122
 
117
- # WebSocket support for API paths
123
+ # API paths with WebSocket upgrade support
118
124
  location /api {
119
125
  proxy_pass http://127.0.0.1:3100;
120
126
  proxy_http_version 1.1;
127
+
128
+ # Client cert headers — set from nginx TLS variables, never passed through from client
129
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
130
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
131
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
132
+
133
+ # Standard proxy headers
134
+ proxy_set_header Host $host;
135
+ proxy_set_header X-Real-IP $remote_addr;
136
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
137
+ proxy_set_header X-Forwarded-Proto $scheme;
138
+
139
+ # WebSocket: only upgrade when client requests it
121
140
  proxy_set_header Upgrade $http_upgrade;
122
- proxy_set_header Connection "upgrade";
141
+ proxy_set_header Connection $connection_upgrade;
123
142
  }
124
143
  }
125
144
  ```
@@ -165,30 +184,45 @@ server {
165
184
  ssl_ciphers HIGH:!aNULL:!MD5;
166
185
  ssl_prefer_server_ciphers on;
167
186
 
168
- # Client cert headers
169
- proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
170
- proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
171
-
172
- # Standard proxy headers
173
- proxy_set_header Host $host;
174
- proxy_set_header X-Real-IP $remote_addr;
175
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
176
- proxy_set_header X-Forwarded-Proto $scheme;
177
-
178
187
  location / {
179
188
  proxy_pass http://127.0.0.1:3100;
189
+
190
+ # Client cert headers — set from nginx TLS variables, never passed through from client
191
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
192
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
193
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
194
+
195
+ # Standard proxy headers
196
+ proxy_set_header Host $host;
197
+ proxy_set_header X-Real-IP $remote_addr;
198
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
199
+ proxy_set_header X-Forwarded-Proto $scheme;
180
200
  }
181
201
 
202
+ # API paths with WebSocket upgrade support
182
203
  location /api {
183
204
  proxy_pass http://127.0.0.1:3100;
184
205
  proxy_http_version 1.1;
206
+
207
+ # Client cert headers — set from nginx TLS variables, never passed through from client
208
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
209
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
210
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
211
+
212
+ # Standard proxy headers
213
+ proxy_set_header Host $host;
214
+ proxy_set_header X-Real-IP $remote_addr;
215
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
216
+ proxy_set_header X-Forwarded-Proto $scheme;
217
+
218
+ # WebSocket: only upgrade when client requests it
185
219
  proxy_set_header Upgrade $http_upgrade;
186
- proxy_set_header Connection "upgrade";
220
+ proxy_set_header Connection $connection_upgrade;
187
221
  }
188
222
  }
189
223
  ```
190
224
 
191
- The difference from the IP vhost: port 443 instead of 9292, Let's Encrypt certificates instead of self-signed, and a specific `server_name` instead of catch-all.
225
+ The difference from the IP vhost: port 443 instead of 9292, Let's Encrypt certificates instead of self-signed, and a specific `server_name` instead of catch-all. Requires the same `map $http_upgrade $connection_upgrade` block to be present.
192
226
 
193
227
  ### App tunnel vhost with Authelia
194
228
 
@@ -211,26 +245,31 @@ server {
211
245
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
212
246
  proxy_set_header X-Forwarded-Proto $scheme;
213
247
 
214
- # Authelia forward authentication
215
- location /authelia {
248
+ # Authelia forward authentication (AuthRequest implementation for nginx)
249
+ location /internal/authelia/authz {
216
250
  internal;
217
- proxy_pass http://127.0.0.1:9091/api/verify?rd=https://auth.example.com/;
251
+
252
+ proxy_pass http://127.0.0.1:9091/api/authz/auth-request;
218
253
  proxy_pass_request_body off;
254
+
219
255
  proxy_set_header Content-Length "";
256
+ proxy_set_header Connection "";
257
+ proxy_set_header X-Original-Method $request_method;
220
258
  proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
221
- proxy_set_header X-Forwarded-Method $request_method;
222
- proxy_set_header X-Forwarded-Proto $scheme;
223
- proxy_set_header X-Forwarded-Host $http_host;
224
- proxy_set_header X-Forwarded-Uri $request_uri;
225
259
  proxy_set_header X-Forwarded-For $remote_addr;
260
+
261
+ proxy_http_version 1.1;
262
+ proxy_buffers 4 32k;
263
+ proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
226
264
  }
227
265
 
228
266
  location / {
229
- auth_request /authelia;
267
+ auth_request /internal/authelia/authz;
230
268
  auth_request_set $user $upstream_http_remote_user;
231
269
  auth_request_set $groups $upstream_http_remote_groups;
232
270
  auth_request_set $name $upstream_http_remote_name;
233
271
  auth_request_set $email $upstream_http_remote_email;
272
+ auth_request_set $redirection_url $upstream_http_location;
234
273
 
235
274
  proxy_set_header Remote-User $user;
236
275
  proxy_set_header Remote-Groups $groups;
@@ -248,11 +287,12 @@ server {
248
287
  proxy_send_timeout 86400s;
249
288
  }
250
289
 
251
- error_page 401 =302 https://auth.example.com/?rd=$scheme://$http_host$request_uri;
290
+ # Redirect unauthenticated requests to Authelia login portal
291
+ error_page 401 =302 $redirection_url;
252
292
  }
253
293
  ```
254
294
 
255
- The `location /authelia` block is marked `internal`, meaning it cannot be accessed directly by clients. It is only triggered by the `auth_request` directive in the main `location /` block.
295
+ The `location /internal/authelia/authz` block is marked `internal`, meaning it cannot be accessed directly by clients. It is only triggered by the `auth_request` directive in the main `location /` block.
256
296
 
257
297
  ### Tunnel (Chisel) vhost
258
298
 
@@ -290,12 +330,21 @@ WebSocket connections start as HTTP and then "upgrade" to the WebSocket protocol
290
330
  ```nginx
291
331
  proxy_http_version 1.1;
292
332
  proxy_set_header Upgrade $http_upgrade;
293
- proxy_set_header Connection "upgrade";
333
+ proxy_set_header Connection $connection_upgrade;
334
+ ```
335
+
336
+ The `$connection_upgrade` variable comes from a `map` block defined at the top of the IP panel vhost:
337
+
338
+ ```nginx
339
+ map $http_upgrade $connection_upgrade {
340
+ default upgrade;
341
+ '' close;
342
+ }
294
343
  ```
295
344
 
296
345
  - **`proxy_http_version 1.1`** — WebSocket requires HTTP/1.1 (not 1.0)
297
346
  - **`Upgrade`** — forwards the client's upgrade request to the backend
298
- - **`Connection "upgrade"`** — tells the backend to switch protocols
347
+ - **`Connection $connection_upgrade`** — set to `upgrade` when the client requests a WebSocket upgrade, or `close` for regular HTTP requests. This avoids keeping non-WebSocket connections open unnecessarily
299
348
 
300
349
  These headers appear in three places: the panel vhost (for live log streaming), the tunnel vhost (for Chisel), and app vhosts (for apps that use WebSockets).
301
350
 
@@ -310,12 +359,13 @@ Every vhost sets standard proxy headers so backend services know about the origi
310
359
  | `X-Forwarded-For` | `$proxy_add_x_forwarded_for` | Chain of proxy IPs |
311
360
  | `X-Forwarded-Proto` | `$scheme` | Original protocol (http or https) |
312
361
 
313
- For mTLS vhosts, two additional headers are set:
362
+ For mTLS vhosts, three additional headers are set:
314
363
 
315
364
  | Header | nginx variable | Purpose |
316
365
  |--------|---------------|---------|
317
366
  | `X-SSL-Client-Verify` | `$ssl_client_verify` | `SUCCESS`, `FAILED`, or `NONE` |
318
367
  | `X-SSL-Client-DN` | `$ssl_client_s_dn` | Client certificate subject DN |
368
+ | `X-SSL-Client-Serial` | `$ssl_client_serial` | Client certificate serial number |
319
369
 
320
370
  ### Safe write-with-rollback
321
371
 
@@ -472,7 +522,7 @@ ls /etc/nginx/sites-enabled/portlama-*
472
522
  | Directive | Purpose |
473
523
  |-----------|---------|
474
524
  | `ssl_verify_client on` | Require client certificate (mTLS) |
475
- | `auth_request /authelia` | Delegate auth to Authelia subrequest |
525
+ | `auth_request /internal/authelia/authz` | Delegate auth to Authelia subrequest |
476
526
  | `proxy_http_version 1.1` | Required for WebSocket upgrade |
477
527
  | `proxy_read_timeout 86400s` | Keep WebSocket connections alive (24h) |
478
528
  | `error_page 495 496` | Handle missing/invalid client cert |