@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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/tasks/mtls.js +2 -3
- package/vendor/panel-client/dist/docs/00-introduction/how-it-works.md +3 -2
- package/vendor/panel-client/dist/docs/00-introduction/quickstart.md +9 -6
- package/vendor/panel-client/dist/docs/00-introduction/what-is-portlama.md +4 -2
- package/vendor/panel-client/dist/docs/01-concepts/authentication.md +31 -19
- package/vendor/panel-client/dist/docs/01-concepts/dns-and-domains.md +8 -1
- package/vendor/panel-client/dist/docs/01-concepts/mtls.md +47 -7
- package/vendor/panel-client/dist/docs/01-concepts/nginx-reverse-proxy.md +89 -39
- package/vendor/panel-client/dist/docs/01-concepts/security-model.md +9 -8
- package/vendor/panel-client/dist/docs/02-guides/certificate-management.md +81 -11
- package/vendor/panel-client/dist/docs/02-guides/disaster-recovery.md +1 -1
- package/vendor/panel-client/dist/docs/02-guides/first-tunnel.md +3 -2
- package/vendor/panel-client/dist/docs/02-guides/installation.md +6 -5
- package/vendor/panel-client/dist/docs/02-guides/mac-client-setup.md +15 -12
- package/vendor/panel-client/dist/docs/02-guides/managing-users.md +8 -8
- package/vendor/panel-client/dist/docs/02-guides/static-sites.md +22 -17
- package/vendor/panel-client/dist/docs/03-architecture/e2e-three-vm-sequences.md +431 -0
- package/vendor/panel-client/dist/docs/03-architecture/installer-flow.md +141 -0
- package/vendor/panel-client/dist/docs/03-architecture/installer.md +14 -1
- package/vendor/panel-client/dist/docs/03-architecture/management-flow.md +266 -0
- package/vendor/panel-client/dist/docs/03-architecture/nginx-configuration.md +121 -32
- package/vendor/panel-client/dist/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/panel-client/dist/docs/03-architecture/overview.md +16 -9
- package/vendor/panel-client/dist/docs/03-architecture/panel-client.md +1 -1
- package/vendor/panel-client/dist/docs/03-architecture/panel-server.md +87 -45
- package/vendor/panel-client/dist/docs/03-architecture/state-management.md +7 -3
- package/vendor/panel-client/dist/docs/03-architecture/system-overview.md +158 -0
- package/vendor/panel-client/dist/docs/04-api-reference/certificates.md +282 -15
- package/vendor/panel-client/dist/docs/04-api-reference/overview.md +22 -7
- package/vendor/panel-client/dist/docs/04-api-reference/services.md +4 -0
- package/vendor/panel-client/dist/docs/04-api-reference/sites.md +75 -0
- package/vendor/panel-client/dist/docs/04-api-reference/system.md +10 -11
- package/vendor/panel-client/dist/docs/04-api-reference/tunnels.md +62 -2
- package/vendor/panel-client/dist/docs/04-api-reference/users.md +3 -3
- package/vendor/panel-client/dist/docs/05-operations/backup-and-restore.md +4 -1
- package/vendor/panel-client/dist/docs/05-operations/monitoring.md +1 -1
- package/vendor/panel-client/dist/docs/06-reference/config-files.md +17 -9
- package/vendor/panel-client/dist/docs/06-reference/glossary.md +2 -2
- package/vendor/panel-client/dist/docs/06-reference/installer-flags.md +19 -1
- package/vendor/panel-client/dist/docs/06-reference/troubleshooting.md +12 -12
- package/vendor/panel-client/dist/docs/_index.json +6 -1
- package/vendor/panel-server/src/index.js +3 -2
- package/vendor/panel-server/src/lib/invite-page.js +32 -7
- package/vendor/panel-server/src/routes/invite.js +13 -1
- package/vendor/panel-server/src/routes/management/invitations.js +3 -1
- 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
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
|
|
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
|
-
|
|
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:
|
|
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 ──────→
|
|
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
|
-
--
|
|
273
|
-
|
|
274
|
-
R: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:
|
|
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 :
|
|
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
|
|
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** |
|
|
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 `
|
|
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
|
|
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
|
|
41
|
-
5.
|
|
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
|
-
|
|
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 →
|
|
50
|
-
→
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
143
|
+
default_policy: two_factor
|
|
140
144
|
|
|
141
145
|
session:
|
|
142
146
|
name: portlama_session
|
|
143
147
|
secret: <random-64-byte-hex>
|
|
144
|
-
|
|
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
|
-
- **`
|
|
166
|
-
- **`default_policy:
|
|
167
|
-
- **`session.
|
|
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
|
|
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
|
-
|
|
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
|
|
298
|
+
The panel server's mTLS middleware checks these headers in order:
|
|
259
299
|
|
|
260
|
-
- `SUCCESS`
|
|
261
|
-
- `
|
|
262
|
-
- `
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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 |
|