@lamalibre/create-lamaste 4.4.0 → 4.5.0
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/package.json +2 -2
- package/scripts/lamaste-priv +200 -16
- package/src/index.js +1 -0
- package/src/lib/backend-cookie.js +65 -0
- package/src/lib/ip-panel-vhost.js +208 -0
- package/src/lib/service-config.js +5 -3
- package/src/tasks/nginx.js +18 -135
- package/src/tasks/redeploy.js +39 -1
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +1 -1
- package/vendor/docs/01-concepts/authorization.md +66 -48
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +70 -33
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +113 -25
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +3 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
- package/vendor/docs/03-architecture/overview.md +13 -17
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +3 -2
- package/vendor/docs/03-architecture/state-management.md +4 -8
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +292 -23
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +15 -13
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +6 -4
- package/vendor/docs/04-api-reference/tunnels.md +67 -29
- package/vendor/docs/04-api-reference/users.md +241 -45
- package/vendor/docs/05-operations/backup-and-restore.md +35 -26
- package/vendor/docs/05-operations/monitoring.md +39 -0
- package/vendor/docs/05-operations/uninstalling.md +2 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +83 -189
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +25 -26
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +10 -8
- package/vendor/docs/identity-directory-design.md +48 -14
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +6 -1
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.js +5 -1
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
- package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/principals.js +67 -0
- package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +22 -48
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +15 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-identity.js +335 -0
- package/vendor/server/src/commands/plugins.js +99 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/tunnels.js +102 -2
- package/vendor/server/src/commands/uninstall.js +3 -1
- package/vendor/server/src/commands/users.js +358 -0
- package/vendor/server/src/plugin-approval.js +107 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +25 -1
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/config.js +68 -43
- package/vendor/serverd/src/lib/directory.js +1113 -0
- package/vendor/serverd/src/lib/entitlements.js +517 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
- package/vendor/serverd/src/lib/identity-sync.js +905 -0
- package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
- package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +279 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
- package/vendor/serverd/src/lib/plugin-packages.js +30 -22
- package/vendor/serverd/src/lib/plugins.js +94 -15
- package/vendor/serverd/src/lib/registry-credential.js +46 -6
- package/vendor/serverd/src/lib/request-logging.js +19 -0
- package/vendor/serverd/src/lib/retained-data.js +50 -4
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
- package/vendor/serverd/src/lib/user-access-session.js +21 -11
- package/vendor/serverd/src/lib/user-caller.js +67 -0
- package/vendor/serverd/src/lib/user-surface.js +733 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
- package/vendor/serverd/src/middleware/user-access-session.js +9 -0
- package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
- package/vendor/serverd/src/routes/invite.js +53 -12
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
- package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
- package/vendor/serverd/src/routes/management/invitations.js +78 -68
- package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/tunnels.js +98 -2
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
- package/vendor/serverd/src/routes/user-access.js +17 -4
- package/vendor/serverd/src/routes/user-surface.js +69 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
- package/vendor/server-ui/dist/assets/index-CESyyK8V.js +0 -84
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { access, lstat, readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
BACKEND_COOKIE_VARIABLE,
|
|
6
|
+
RELAY_PANEL_PORT,
|
|
7
|
+
USER_SURFACE_MAX_RESPONSE_SECONDS,
|
|
8
|
+
isUserSurfaceRoute,
|
|
9
|
+
userSurfaceApiPath,
|
|
10
|
+
} from '@lamalibre/lamaste';
|
|
4
11
|
|
|
5
12
|
const SITES_AVAILABLE = '/etc/nginx/sites-available';
|
|
6
13
|
const SITES_ENABLED = '/etc/nginx/sites-enabled';
|
|
@@ -58,11 +65,41 @@ async function writeVhostFile(name, content) {
|
|
|
58
65
|
await siteAction('write', name, content);
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
/** Site name of the panel's domain vhost (`panel.<domain>`). */
|
|
69
|
+
export const PANEL_DOMAIN_VHOST = 'lamalibre-lamaste-panel-domain';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Cleared in every location that proxies a browser's request to the panel
|
|
73
|
+
* itself: a relay assertion reaches the panel only from a user-surface
|
|
74
|
+
* location of a tunnel vhost, which sets it from Gatekeeper's answer — never
|
|
75
|
+
* one a client sent.
|
|
76
|
+
*/
|
|
77
|
+
const PANEL_ASSERTION_CLEARED = `
|
|
78
|
+
# A relay assertion comes only from Gatekeeper, on a plugin's user surface
|
|
79
|
+
proxy_set_header X-Lamalibre-Relay-Assertion "";`;
|
|
80
|
+
|
|
61
81
|
/**
|
|
62
82
|
* Write the panel domain vhost with mTLS and Let's Encrypt certs.
|
|
63
83
|
*/
|
|
64
84
|
export async function writePanelVhost(domain) {
|
|
85
|
+
const { name, config } = renderPanelVhost(domain);
|
|
86
|
+
await writeVhostFile(name, config);
|
|
87
|
+
return path.join(SITES_AVAILABLE, name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render the panel domain vhost. Pure; the startup reconcile renders it on
|
|
92
|
+
* the certificate directory the installed vhost names.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} domain
|
|
95
|
+
* @param {string} [certPath] - certificate directory (default: the panel's own lineage)
|
|
96
|
+
* @returns {{ name: string, fqdn: string, config: string }}
|
|
97
|
+
*/
|
|
98
|
+
export function renderPanelVhost(domain, certPath) {
|
|
65
99
|
const fqdn = `panel.${domain}`;
|
|
100
|
+
assertLiteralHostname(fqdn);
|
|
101
|
+
let certDirClean = certPath || `/etc/letsencrypt/live/${fqdn}`;
|
|
102
|
+
while (certDirClean.endsWith('/')) certDirClean = certDirClean.slice(0, -1);
|
|
66
103
|
const config = `# Rate limit zone for public enrollment endpoint (5 requests/minute per IP)
|
|
67
104
|
limit_req_zone $binary_remote_addr zone=enroll_domain:1m rate=5r/m;
|
|
68
105
|
|
|
@@ -76,8 +113,8 @@ server {
|
|
|
76
113
|
listen 443 ssl;
|
|
77
114
|
server_name ${fqdn};
|
|
78
115
|
|
|
79
|
-
ssl_certificate
|
|
80
|
-
ssl_certificate_key
|
|
116
|
+
ssl_certificate ${certDirClean}/fullchain.pem;
|
|
117
|
+
ssl_certificate_key ${certDirClean}/privkey.pem;
|
|
81
118
|
|
|
82
119
|
# mTLS — same as IP-based access
|
|
83
120
|
include /etc/nginx/snippets/lamalibre-lamaste-mtls.conf;
|
|
@@ -103,7 +140,7 @@ server {
|
|
|
103
140
|
proxy_set_header Remote-User "";
|
|
104
141
|
proxy_set_header Remote-Groups "";
|
|
105
142
|
proxy_set_header Remote-Name "";
|
|
106
|
-
proxy_set_header Remote-Email ""
|
|
143
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
107
144
|
|
|
108
145
|
# Standard proxy headers
|
|
109
146
|
proxy_set_header Host $host;
|
|
@@ -126,7 +163,7 @@ server {
|
|
|
126
163
|
proxy_set_header Remote-User "";
|
|
127
164
|
proxy_set_header Remote-Groups "";
|
|
128
165
|
proxy_set_header Remote-Name "";
|
|
129
|
-
proxy_set_header Remote-Email ""
|
|
166
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
130
167
|
|
|
131
168
|
proxy_set_header Host $host;
|
|
132
169
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -146,7 +183,7 @@ server {
|
|
|
146
183
|
proxy_set_header Remote-User "";
|
|
147
184
|
proxy_set_header Remote-Groups "";
|
|
148
185
|
proxy_set_header Remote-Name "";
|
|
149
|
-
proxy_set_header Remote-Email ""
|
|
186
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
150
187
|
|
|
151
188
|
proxy_set_header Host $host;
|
|
152
189
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -167,7 +204,7 @@ server {
|
|
|
167
204
|
proxy_set_header Remote-User "";
|
|
168
205
|
proxy_set_header Remote-Groups "";
|
|
169
206
|
proxy_set_header Remote-Name "";
|
|
170
|
-
proxy_set_header Remote-Email ""
|
|
207
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
171
208
|
|
|
172
209
|
proxy_set_header Host $host;
|
|
173
210
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -187,7 +224,7 @@ server {
|
|
|
187
224
|
proxy_set_header Remote-User "";
|
|
188
225
|
proxy_set_header Remote-Groups "";
|
|
189
226
|
proxy_set_header Remote-Name "";
|
|
190
|
-
proxy_set_header Remote-Email ""
|
|
227
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
191
228
|
|
|
192
229
|
proxy_set_header Host $host;
|
|
193
230
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -207,7 +244,7 @@ server {
|
|
|
207
244
|
proxy_set_header Remote-User "";
|
|
208
245
|
proxy_set_header Remote-Groups "";
|
|
209
246
|
proxy_set_header Remote-Name "";
|
|
210
|
-
proxy_set_header Remote-Email ""
|
|
247
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
211
248
|
|
|
212
249
|
proxy_set_header Host $host;
|
|
213
250
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -232,7 +269,7 @@ server {
|
|
|
232
269
|
proxy_set_header Remote-User "";
|
|
233
270
|
proxy_set_header Remote-Groups "";
|
|
234
271
|
proxy_set_header Remote-Name "";
|
|
235
|
-
proxy_set_header Remote-Email ""
|
|
272
|
+
proxy_set_header Remote-Email "";${PANEL_ASSERTION_CLEARED}
|
|
236
273
|
|
|
237
274
|
# Standard proxy headers
|
|
238
275
|
proxy_set_header Host $host;
|
|
@@ -247,8 +284,7 @@ server {
|
|
|
247
284
|
}
|
|
248
285
|
`;
|
|
249
286
|
|
|
250
|
-
|
|
251
|
-
return path.join(SITES_AVAILABLE, 'lamalibre-lamaste-panel-domain');
|
|
287
|
+
return { name: PANEL_DOMAIN_VHOST, fqdn, config };
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
/**
|
|
@@ -351,7 +387,7 @@ server {
|
|
|
351
387
|
proxy_set_header Host $host;
|
|
352
388
|
proxy_set_header X-Real-IP $remote_addr;
|
|
353
389
|
proxy_set_header X-Forwarded-For $remote_addr;
|
|
354
|
-
proxy_set_header X-Forwarded-Proto $scheme
|
|
390
|
+
proxy_set_header X-Forwarded-Proto $scheme;${PANEL_ASSERTION_CLEARED}
|
|
355
391
|
}
|
|
356
392
|
|
|
357
393
|
# User access authorization — Authelia forward auth protects this endpoint
|
|
@@ -393,7 +429,7 @@ server {
|
|
|
393
429
|
proxy_set_header Host $host;
|
|
394
430
|
proxy_set_header X-Real-IP $remote_addr;
|
|
395
431
|
proxy_set_header X-Forwarded-For $remote_addr;
|
|
396
|
-
proxy_set_header X-Forwarded-Proto $scheme
|
|
432
|
+
proxy_set_header X-Forwarded-Proto $scheme;${PANEL_ASSERTION_CLEARED}
|
|
397
433
|
|
|
398
434
|
error_page 401 =302 $redirection_url;
|
|
399
435
|
}
|
|
@@ -446,6 +482,23 @@ export async function writeTunnelVhost(domain) {
|
|
|
446
482
|
return path.join(SITES_AVAILABLE, 'lamalibre-lamaste-tunnel');
|
|
447
483
|
}
|
|
448
484
|
|
|
485
|
+
/**
|
|
486
|
+
* The `Cookie` a tunnel or agent backend receives: the browser's cookies
|
|
487
|
+
* without Authelia's session cookie. Authelia scopes that cookie to the base
|
|
488
|
+
* domain, so the browser sends it to every tunnel host; a backend that got
|
|
489
|
+
* it — any application on any agent, or whatever holds a tunnel port on the
|
|
490
|
+
* relay — could replay it as the visitor on every authenticated host and
|
|
491
|
+
* every plugin's user surface. `$lamaste_backend_cookie` is defined by the
|
|
492
|
+
* http-level map create-lamaste installs (core `renderBackendCookieMap`,
|
|
493
|
+
* `/etc/nginx/conf.d/lamalibre-lamaste-backend-cookie.conf`): every
|
|
494
|
+
* `authelia_session` pair removed, or no `Cookie` at all when one could not
|
|
495
|
+
* be. A vhost written before the map exists fails `nginx -t` and is rolled
|
|
496
|
+
* back — never served with the cookie.
|
|
497
|
+
*/
|
|
498
|
+
const BACKEND_COOKIE_HEADER = `
|
|
499
|
+
# Never Authelia's session cookie: the browser's other cookies only
|
|
500
|
+
proxy_set_header Cookie ${BACKEND_COOKIE_VARIABLE};`;
|
|
501
|
+
|
|
449
502
|
/**
|
|
450
503
|
* Request headers every tunnel location sends upstream.
|
|
451
504
|
*
|
|
@@ -457,7 +510,8 @@ export async function writeTunnelVhost(domain) {
|
|
|
457
510
|
* The client-certificate headers are cleared. The panel trusts
|
|
458
511
|
* `X-SSL-Client-*` from nginx as proof of an mTLS client certificate; a
|
|
459
512
|
* tunnel vhost must never pass client-supplied values through, whatever
|
|
460
|
-
* listens on its port.
|
|
513
|
+
* listens on its port. Authelia's session cookie never reaches the backend
|
|
514
|
+
* ({@link BACKEND_COOKIE_HEADER}).
|
|
461
515
|
*/
|
|
462
516
|
const TUNNEL_PROXY_HEADERS = `
|
|
463
517
|
proxy_set_header Host $host;
|
|
@@ -469,6 +523,7 @@ const TUNNEL_PROXY_HEADERS = `
|
|
|
469
523
|
proxy_set_header X-SSL-Client-Verify "";
|
|
470
524
|
proxy_set_header X-SSL-Client-DN "";
|
|
471
525
|
proxy_set_header X-SSL-Client-Serial "";
|
|
526
|
+
${BACKEND_COOKIE_HEADER}
|
|
472
527
|
|
|
473
528
|
# WebSocket support
|
|
474
529
|
proxy_set_header Upgrade $http_upgrade;
|
|
@@ -485,10 +540,164 @@ const TUNNEL_PROXY_HEADERS = `
|
|
|
485
540
|
*/
|
|
486
541
|
function bodySizeDirective(maxBodySizeMb) {
|
|
487
542
|
if (maxBodySizeMb === undefined) return '';
|
|
543
|
+
assertBodySizeMb(maxBodySizeMb);
|
|
544
|
+
return `\n client_max_body_size ${maxBodySizeMb}m;\n`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** A body limit in MiB, as the tunnel workflow allows it — interpolated into nginx configuration. */
|
|
548
|
+
function assertBodySizeMb(maxBodySizeMb) {
|
|
488
549
|
if (!Number.isInteger(maxBodySizeMb) || maxBodySizeMb < 1 || maxBodySizeMb > 10240) {
|
|
489
550
|
throw new Error(`Invalid maxBodySizeMb for nginx vhost: ${maxBodySizeMb}`);
|
|
490
551
|
}
|
|
491
|
-
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Request headers a user-surface location sends the panel. The panel's
|
|
556
|
+
* user-surface context trusts nothing but the relay assertion — but the
|
|
557
|
+
* panel serves other contexts on the same listener, so everything a browser
|
|
558
|
+
* could send that any of them trusts is cleared: no user request can pass
|
|
559
|
+
* for a client-certificate holder (`X-SSL-Client-*`), an Authelia user
|
|
560
|
+
* (`Remote-*`), a desktop session (`Authorization`) or an administrator's
|
|
561
|
+
* 2FA session (`Cookie`), nor name another host to the panel
|
|
562
|
+
* (`X-Forwarded-Host`). `X-Forwarded-For` is the client's address alone:
|
|
563
|
+
* the panel trusts one loopback hop (`trust-proxy.js`), so that is its
|
|
564
|
+
* `request.ip` — what the user surface's rate limit keys on. `Connection`
|
|
565
|
+
* cleared: the panel keeps the upstream connection alive (HTTP/1.1), and
|
|
566
|
+
* the user surface carries no WebSocket.
|
|
567
|
+
*/
|
|
568
|
+
const USER_SURFACE_PROXY_HEADERS = `
|
|
569
|
+
# Nothing else that any part of the panel trusts
|
|
570
|
+
proxy_set_header Cookie "";
|
|
571
|
+
proxy_set_header Authorization "";
|
|
572
|
+
proxy_set_header Remote-User "";
|
|
573
|
+
proxy_set_header Remote-Groups "";
|
|
574
|
+
proxy_set_header Remote-Name "";
|
|
575
|
+
proxy_set_header Remote-Email "";
|
|
576
|
+
proxy_set_header X-SSL-Client-Verify "";
|
|
577
|
+
proxy_set_header X-SSL-Client-DN "";
|
|
578
|
+
proxy_set_header X-SSL-Client-Serial "";
|
|
579
|
+
proxy_set_header X-Forwarded-Host "";
|
|
580
|
+
|
|
581
|
+
proxy_set_header Host $host;
|
|
582
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
583
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
584
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
585
|
+
proxy_set_header Connection "";`;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The body of a tunnel vhost's Gatekeeper subrequest location,
|
|
589
|
+
* `/authz/<check>/<fqdn>`: the tunnel is named in the path, in
|
|
590
|
+
* `X-Original-URL` and in `Host` — never by the client's `Host` header (see
|
|
591
|
+
* {@link originalUrl}) — and the session cookie is passed on.
|
|
592
|
+
*
|
|
593
|
+
* @param {'tunnel'|'user-surface'} check
|
|
594
|
+
* @param {string} fqdn
|
|
595
|
+
* @returns {string}
|
|
596
|
+
*/
|
|
597
|
+
function gatekeeperSubrequest(check, fqdn) {
|
|
598
|
+
return `internal;
|
|
599
|
+
|
|
600
|
+
proxy_pass http://127.0.0.1:9294/authz/${check}/${fqdn};
|
|
601
|
+
proxy_pass_request_body off;
|
|
602
|
+
|
|
603
|
+
proxy_set_header Content-Length "";
|
|
604
|
+
proxy_set_header Connection "";
|
|
605
|
+
proxy_set_header X-Original-Method $request_method;
|
|
606
|
+
proxy_set_header X-Original-URL ${originalUrl(fqdn)};
|
|
607
|
+
proxy_set_header X-Forwarded-For $remote_addr;${AUTHZ_CLEARED_HEADERS}
|
|
608
|
+
proxy_set_header Host ${fqdn};
|
|
609
|
+
proxy_set_header Cookie $http_cookie;
|
|
610
|
+
|
|
611
|
+
proxy_http_version 1.1;`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The user-surface locations of a tunnel vhost — `/api/<route>/app` and
|
|
616
|
+
* everything under it for each plugin it serves (`opts.userSurface`), and
|
|
617
|
+
* the two internal locations they share:
|
|
618
|
+
*
|
|
619
|
+
* - `/internal/lamaste/user-surface-authz` asks Gatekeeper
|
|
620
|
+
* (`/authz/user-surface/<fqdn>`, the tunnel named in the path and in
|
|
621
|
+
* `X-Original-URL` and `Host`, as for the tunnel's own subrequest).
|
|
622
|
+
* Gatekeeper authenticates the Authelia session — and, on a restricted
|
|
623
|
+
* tunnel, checks its grant — then vouches for the user to the panel with
|
|
624
|
+
* a relay assertion bound to the method and the raw request target. Never
|
|
625
|
+
* cached: an assertion is single use.
|
|
626
|
+
* - `/internal/lamaste/user-surface-401` answers a request without a
|
|
627
|
+
* session with Gatekeeper's JSON 401 — an application's request is never
|
|
628
|
+
* redirected to the login page.
|
|
629
|
+
*
|
|
630
|
+
* Each route's locations pass the assertion, and nothing else of the
|
|
631
|
+
* browser's credentials ({@link USER_SURFACE_PROXY_HEADERS}), to the panel
|
|
632
|
+
* with `proxy_pass` naming no URI: the panel receives the request target
|
|
633
|
+
* exactly as the browser sent it — the target the assertion is bound to.
|
|
634
|
+
* The body limit is the plugin's (its internal tunnel's); the timeouts are
|
|
635
|
+
* the longest a user-surface response lives (`USER_SURFACE_MAX_RESPONSE_SECONDS`,
|
|
636
|
+
* one hour — the panel ends one still open then, and ends it at once when
|
|
637
|
+
* its user loses access), long enough for server-sent events, which the
|
|
638
|
+
* panel streams unbuffered.
|
|
639
|
+
*
|
|
640
|
+
* @param {string} fqdn
|
|
641
|
+
* @param {ReadonlyArray<{ route: string, maxBodySizeMb: number }>} userSurface
|
|
642
|
+
* @returns {string}
|
|
643
|
+
*/
|
|
644
|
+
function userSurfaceLocations(fqdn, userSurface) {
|
|
645
|
+
if (userSurface.length === 0) return '';
|
|
646
|
+
const routes = userSurface.map(({ route, maxBodySizeMb }) => {
|
|
647
|
+
if (!isUserSurfaceRoute(route)) {
|
|
648
|
+
throw new Error(`Invalid user-surface route for nginx vhost: ${route}`);
|
|
649
|
+
}
|
|
650
|
+
assertBodySizeMb(maxBodySizeMb);
|
|
651
|
+
const block = `{
|
|
652
|
+
auth_request /internal/lamaste/user-surface-authz;
|
|
653
|
+
auth_request_set $relay_assertion $upstream_http_x_lamalibre_relay_assertion;
|
|
654
|
+
|
|
655
|
+
# The user, vouched for by Gatekeeper to the panel for this one request
|
|
656
|
+
proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;
|
|
657
|
+
${USER_SURFACE_PROXY_HEADERS}
|
|
658
|
+
|
|
659
|
+
# No URI: the request target reaches the panel as the browser sent it
|
|
660
|
+
proxy_pass http://127.0.0.1:${RELAY_PANEL_PORT};
|
|
661
|
+
proxy_http_version 1.1;
|
|
662
|
+
|
|
663
|
+
client_max_body_size ${maxBodySizeMb}m;
|
|
664
|
+
# No response lives longer (the panel ends one then)
|
|
665
|
+
proxy_read_timeout ${USER_SURFACE_MAX_RESPONSE_SECONDS}s;
|
|
666
|
+
proxy_send_timeout ${USER_SURFACE_MAX_RESPONSE_SECONDS}s;
|
|
667
|
+
|
|
668
|
+
# No session: a JSON 401, never a redirect
|
|
669
|
+
error_page 401 = /internal/lamaste/user-surface-401;
|
|
670
|
+
}`;
|
|
671
|
+
const apiPath = userSurfaceApiPath(route);
|
|
672
|
+
return `
|
|
673
|
+
# The user surface of plugin "${route}"
|
|
674
|
+
location = ${apiPath} ${block}
|
|
675
|
+
|
|
676
|
+
location ${apiPath}/ ${block}
|
|
677
|
+
`;
|
|
678
|
+
});
|
|
679
|
+
return `
|
|
680
|
+
# The user surface of agent-hosted plugins on this host: Gatekeeper
|
|
681
|
+
# vouches for the session's user to the panel, for one request each.
|
|
682
|
+
location = /internal/lamaste/user-surface-authz {
|
|
683
|
+
${gatekeeperSubrequest('user-surface', fqdn)}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
location = /internal/lamaste/user-surface-401 {
|
|
687
|
+
internal;
|
|
688
|
+
|
|
689
|
+
proxy_pass http://127.0.0.1:9294/authz/user-surface-unauthenticated;
|
|
690
|
+
proxy_pass_request_body off;
|
|
691
|
+
|
|
692
|
+
proxy_set_header Content-Length "";
|
|
693
|
+
proxy_set_header Cookie "";
|
|
694
|
+
proxy_set_header Authorization "";
|
|
695
|
+
# Gatekeeper's rate limit counts the client nginx saw, never one a client named
|
|
696
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
697
|
+
|
|
698
|
+
proxy_http_version 1.1;
|
|
699
|
+
}
|
|
700
|
+
${routes.join('')}`;
|
|
492
701
|
}
|
|
493
702
|
|
|
494
703
|
/**
|
|
@@ -500,19 +709,30 @@ export async function writePublicVhost(
|
|
|
500
709
|
domain,
|
|
501
710
|
port,
|
|
502
711
|
certPath,
|
|
503
|
-
{ pathPrefix, maxBodySizeMb, enabled = true } = {},
|
|
712
|
+
{ pathPrefix, maxBodySizeMb, userSurface, enabled = true } = {},
|
|
504
713
|
) {
|
|
505
714
|
const { name, fqdn, config } = renderPublicVhost(subdomain, domain, port, certPath, {
|
|
506
715
|
pathPrefix,
|
|
507
716
|
maxBodySizeMb,
|
|
717
|
+
userSurface,
|
|
508
718
|
});
|
|
509
719
|
return safeWriteVhost(name, config, fqdn, { enabled });
|
|
510
720
|
}
|
|
511
721
|
|
|
512
|
-
function renderPublicVhost(
|
|
722
|
+
function renderPublicVhost(
|
|
723
|
+
subdomain,
|
|
724
|
+
domain,
|
|
725
|
+
port,
|
|
726
|
+
certPath,
|
|
727
|
+
{ pathPrefix, maxBodySizeMb, userSurface } = {},
|
|
728
|
+
) {
|
|
513
729
|
if (pathPrefix && !/^[a-z0-9][a-z0-9._-]*$/.test(pathPrefix)) {
|
|
514
730
|
throw new Error(`Invalid pathPrefix for nginx vhost: ${pathPrefix}`);
|
|
515
731
|
}
|
|
732
|
+
// A public host has no signed-in user for Gatekeeper to vouch for.
|
|
733
|
+
if (userSurface && userSurface.length > 0) {
|
|
734
|
+
throw new Error("A public tunnel vhost cannot serve a plugin's user surface");
|
|
735
|
+
}
|
|
516
736
|
|
|
517
737
|
const fqdn = `${subdomain}.${domain}`;
|
|
518
738
|
const certDir = certPath || `/etc/letsencrypt/live/${fqdn}`;
|
|
@@ -536,14 +756,11 @@ ${bodySizeDirective(maxBodySizeMb)}
|
|
|
536
756
|
proxy_set_header Remote-User "";
|
|
537
757
|
proxy_set_header Remote-Groups "";
|
|
538
758
|
proxy_set_header Remote-Name "";
|
|
539
|
-
proxy_set_header Remote-Email ""
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
#
|
|
543
|
-
|
|
544
|
-
proxy_set_header X-Lamalibre-Relay-Assertion "";`
|
|
545
|
-
: ''
|
|
546
|
-
}
|
|
759
|
+
proxy_set_header Remote-Email "";
|
|
760
|
+
|
|
761
|
+
# Nor a relay assertion: only Gatekeeper's, set on an authenticated
|
|
762
|
+
# vhost, may reach a backend (lamaste-agentd serves plugin routes)
|
|
763
|
+
proxy_set_header X-Lamalibre-Relay-Assertion "";
|
|
547
764
|
${TUNNEL_PROXY_HEADERS}
|
|
548
765
|
|
|
549
766
|
${pathPrefix ? `rewrite ^/?(.*)$ /${pathPrefix}/$1 break;\n ` : ''}proxy_pass http://127.0.0.1:${port};
|
|
@@ -568,11 +785,12 @@ export async function writeAuthenticatedVhost(
|
|
|
568
785
|
domain,
|
|
569
786
|
port,
|
|
570
787
|
certPath,
|
|
571
|
-
{ pathPrefix, maxBodySizeMb, enabled = true } = {},
|
|
788
|
+
{ pathPrefix, maxBodySizeMb, userSurface, enabled = true } = {},
|
|
572
789
|
) {
|
|
573
790
|
const { name, fqdn, config } = renderGatekeeperTunnelVhost(subdomain, domain, port, certPath, {
|
|
574
791
|
pathPrefix,
|
|
575
792
|
maxBodySizeMb,
|
|
793
|
+
userSurface,
|
|
576
794
|
restricted: false,
|
|
577
795
|
});
|
|
578
796
|
return safeWriteVhost(name, config, fqdn, { enabled });
|
|
@@ -588,11 +806,12 @@ export async function writeRestrictedVhost(
|
|
|
588
806
|
domain,
|
|
589
807
|
port,
|
|
590
808
|
certPath,
|
|
591
|
-
{ pathPrefix, maxBodySizeMb, enabled = true } = {},
|
|
809
|
+
{ pathPrefix, maxBodySizeMb, userSurface, enabled = true } = {},
|
|
592
810
|
) {
|
|
593
811
|
const { name, fqdn, config } = renderGatekeeperTunnelVhost(subdomain, domain, port, certPath, {
|
|
594
812
|
pathPrefix,
|
|
595
813
|
maxBodySizeMb,
|
|
814
|
+
userSurface,
|
|
596
815
|
restricted: true,
|
|
597
816
|
});
|
|
598
817
|
return safeWriteVhost(name, config, fqdn, { enabled });
|
|
@@ -603,7 +822,7 @@ function renderGatekeeperTunnelVhost(
|
|
|
603
822
|
domain,
|
|
604
823
|
port,
|
|
605
824
|
certPath,
|
|
606
|
-
{ pathPrefix, maxBodySizeMb, restricted },
|
|
825
|
+
{ pathPrefix, maxBodySizeMb, userSurface, restricted },
|
|
607
826
|
) {
|
|
608
827
|
if (pathPrefix && !/^[a-z0-9][a-z0-9._-]*$/.test(pathPrefix)) {
|
|
609
828
|
throw new Error(`Invalid pathPrefix for nginx vhost: ${pathPrefix}`);
|
|
@@ -617,6 +836,7 @@ function renderGatekeeperTunnelVhost(
|
|
|
617
836
|
const config = buildGatekeeperVhost(fqdn, certDirClean, port, domain, {
|
|
618
837
|
pathPrefix,
|
|
619
838
|
maxBodySizeMb,
|
|
839
|
+
userSurface,
|
|
620
840
|
restricted,
|
|
621
841
|
});
|
|
622
842
|
|
|
@@ -629,16 +849,27 @@ function renderGatekeeperTunnelVhost(
|
|
|
629
849
|
* the generators produce today (see vhost-reconcile.js).
|
|
630
850
|
*
|
|
631
851
|
* @param {'public'|'authenticated'|'restricted'} accessMode
|
|
852
|
+
* @param {string} subdomain
|
|
853
|
+
* @param {string} domain
|
|
854
|
+
* @param {number} port
|
|
855
|
+
* @param {string} [certPath]
|
|
856
|
+
* @param {{ pathPrefix?: string, maxBodySizeMb?: number, userSurface?: ReadonlyArray<{ route: string, maxBodySizeMb: number }> }} [opts]
|
|
857
|
+
* `userSurface`: the plugins' user surfaces the vhost serves (not on a public vhost)
|
|
632
858
|
* @returns {{ name: string, fqdn: string, config: string }}
|
|
633
859
|
*/
|
|
634
860
|
export function renderTunnelVhost(accessMode, subdomain, domain, port, certPath, opts = {}) {
|
|
635
|
-
const { pathPrefix, maxBodySizeMb } = opts;
|
|
861
|
+
const { pathPrefix, maxBodySizeMb, userSurface } = opts;
|
|
636
862
|
if (accessMode === 'public') {
|
|
637
|
-
return renderPublicVhost(subdomain, domain, port, certPath, {
|
|
863
|
+
return renderPublicVhost(subdomain, domain, port, certPath, {
|
|
864
|
+
pathPrefix,
|
|
865
|
+
maxBodySizeMb,
|
|
866
|
+
userSurface,
|
|
867
|
+
});
|
|
638
868
|
}
|
|
639
869
|
return renderGatekeeperTunnelVhost(subdomain, domain, port, certPath, {
|
|
640
870
|
pathPrefix,
|
|
641
871
|
maxBodySizeMb,
|
|
872
|
+
userSurface,
|
|
642
873
|
restricted: accessMode !== 'authenticated',
|
|
643
874
|
});
|
|
644
875
|
}
|
|
@@ -658,13 +889,18 @@ export function renderTunnelVhost(accessMode, subdomain, domain, port, certPath,
|
|
|
658
889
|
* the one Gatekeeper authorized. Gatekeeper now refuses `/authz/check`: a
|
|
659
890
|
* vhost the panel has not rewritten closes its tunnel instead of opening it.
|
|
660
891
|
* The login redirect names this vhost for the same reason.
|
|
892
|
+
*
|
|
893
|
+
* The backend gets Gatekeeper's relay assertion on a plugin tunnel, and no
|
|
894
|
+
* relay assertion at all on any other — a client's is cleared. An app
|
|
895
|
+
* tunnel bound as plugins' user surface also carries their locations
|
|
896
|
+
* ({@link userSurfaceLocations}).
|
|
661
897
|
*/
|
|
662
898
|
function buildGatekeeperVhost(
|
|
663
899
|
fqdn,
|
|
664
900
|
certDirClean,
|
|
665
901
|
port,
|
|
666
902
|
domain,
|
|
667
|
-
{ pathPrefix, maxBodySizeMb, restricted } = {},
|
|
903
|
+
{ pathPrefix, maxBodySizeMb, userSurface = [], restricted } = {},
|
|
668
904
|
) {
|
|
669
905
|
const url = originalUrl(fqdn);
|
|
670
906
|
return `server {
|
|
@@ -681,24 +917,11 @@ ${bodySizeDirective(maxBodySizeMb)}
|
|
|
681
917
|
# Gatekeeper authorization subrequest (handles Authelia validation + grant check).
|
|
682
918
|
# The tunnel is named in the path — never by the client's Host header.
|
|
683
919
|
location /internal/lamaste/authz {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
proxy_pass http://127.0.0.1:9294/authz/tunnel/${fqdn};
|
|
687
|
-
proxy_pass_request_body off;
|
|
688
|
-
|
|
689
|
-
proxy_set_header Content-Length "";
|
|
690
|
-
proxy_set_header Connection "";
|
|
691
|
-
proxy_set_header X-Original-Method $request_method;
|
|
692
|
-
proxy_set_header X-Original-URL ${url};
|
|
693
|
-
proxy_set_header X-Forwarded-For $remote_addr;${AUTHZ_CLEARED_HEADERS}
|
|
694
|
-
proxy_set_header Host ${fqdn};
|
|
695
|
-
proxy_set_header Cookie $http_cookie;
|
|
696
|
-
|
|
697
|
-
proxy_http_version 1.1;
|
|
920
|
+
${gatekeeperSubrequest('tunnel', fqdn)}
|
|
698
921
|
proxy_buffers 4 32k;
|
|
699
922
|
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
|
|
700
923
|
}
|
|
701
|
-
|
|
924
|
+
${userSurfaceLocations(fqdn, userSurface)}
|
|
702
925
|
location / {
|
|
703
926
|
auth_request /internal/lamaste/authz;
|
|
704
927
|
auth_request_set $user $upstream_http_remote_user;
|
|
@@ -721,7 +944,11 @@ ${bodySizeDirective(maxBodySizeMb)}
|
|
|
721
944
|
# The user, signed by Gatekeeper for this one request — lamaste-agentd,
|
|
722
945
|
# which serves plugin routes, trusts no Remote-* header without it
|
|
723
946
|
proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;`
|
|
724
|
-
:
|
|
947
|
+
: `
|
|
948
|
+
|
|
949
|
+
# No relay assertion reaches an app: Gatekeeper signs one only for a
|
|
950
|
+
# plugin tunnel or the panel, never one a client sent
|
|
951
|
+
proxy_set_header X-Lamalibre-Relay-Assertion "";`
|
|
725
952
|
}
|
|
726
953
|
${TUNNEL_PROXY_HEADERS}
|
|
727
954
|
|
|
@@ -1148,7 +1375,9 @@ export async function writeAgentPanelVhost(
|
|
|
1148
1375
|
* reached without going through nginx, so a header proves nothing to it.
|
|
1149
1376
|
*
|
|
1150
1377
|
* `X-SSL-Client-*` still go upstream for agents older than 4.3, which read
|
|
1151
|
-
* them; lamaste-agentd from 4.3 removes them unread.
|
|
1378
|
+
* them; lamaste-agentd from 4.3 removes them unread. Authelia's session
|
|
1379
|
+
* cookie never does ({@link BACKEND_COOKIE_HEADER}): the agent panel's host
|
|
1380
|
+
* is under the base domain like every other.
|
|
1152
1381
|
*
|
|
1153
1382
|
* @param {string} subdomain - `agent-<label>`
|
|
1154
1383
|
* @param {string} domain
|
|
@@ -1208,6 +1437,7 @@ export function renderAgentPanelVhost(subdomain, domain, port, certPath) {
|
|
|
1208
1437
|
proxy_set_header Remote-Groups "";
|
|
1209
1438
|
proxy_set_header Remote-Name "";
|
|
1210
1439
|
proxy_set_header Remote-Email "";
|
|
1440
|
+
proxy_set_header X-Lamalibre-Relay-Assertion "";
|
|
1211
1441
|
}
|
|
1212
1442
|
|
|
1213
1443
|
# Reject requests without valid client certificate
|
|
@@ -1236,6 +1466,7 @@ export function renderAgentPanelVhost(subdomain, domain, port, certPath) {
|
|
|
1236
1466
|
proxy_set_header Remote-Groups "";
|
|
1237
1467
|
proxy_set_header Remote-Name "";
|
|
1238
1468
|
proxy_set_header Remote-Email "";
|
|
1469
|
+
${BACKEND_COOKIE_HEADER}
|
|
1239
1470
|
|
|
1240
1471
|
# Standard proxy headers
|
|
1241
1472
|
proxy_set_header Host $host;
|