@lamalibre/create-lamaste 4.4.1 → 4.6.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 +234 -18
- 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/02-guides/static-sites.md +4 -0
- 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 +229 -24
- 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 +378 -34
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
- package/vendor/docs/04-api-reference/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +87 -47
- 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 +24 -6
- package/vendor/docs/06-reference/config-files.md +100 -202
- 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/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/index.d.ts +4 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +4 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.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 +10 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +49 -56
- 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 +332 -104
- 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/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +156 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/sites.js +15 -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 +186 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.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 +34 -5
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- 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 +226 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
- 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/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +546 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +95 -7
- package/vendor/serverd/src/lib/registry-credential.js +9 -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 +36 -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 +754 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- 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/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +2 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +109 -22
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +6 -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-roDHz3dM.js +0 -84
|
@@ -100,6 +100,9 @@ server {
|
|
|
100
100
|
proxy_set_header X-Real-IP $remote_addr;
|
|
101
101
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
102
102
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
103
|
+
|
|
104
|
+
# A relay assertion comes only from Gatekeeper, on a plugin's user surface
|
|
105
|
+
proxy_set_header X-Lamalibre-Relay-Assertion "";
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
# API paths with WebSocket upgrade support
|
|
@@ -118,6 +121,9 @@ server {
|
|
|
118
121
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
119
122
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
120
123
|
|
|
124
|
+
# A relay assertion comes only from Gatekeeper, on a plugin's user surface
|
|
125
|
+
proxy_set_header X-Lamalibre-Relay-Assertion "";
|
|
126
|
+
|
|
121
127
|
# WebSocket: only upgrade when client requests it
|
|
122
128
|
proxy_set_header Upgrade $http_upgrade;
|
|
123
129
|
proxy_set_header Connection $connection_upgrade;
|
|
@@ -132,6 +138,7 @@ Key characteristics:
|
|
|
132
138
|
- Includes the mTLS snippet for client certificate enforcement
|
|
133
139
|
- Error pages 495 (no cert) and 496 (bad cert) serve a static help page with import instructions
|
|
134
140
|
- Two `location` blocks: `/` for standard HTTP proxy, `/api` for WebSocket-capable proxy
|
|
141
|
+
- Every location that proxies to the panel clears a client's `X-Lamalibre-Relay-Assertion` (`proxy_set_header X-Lamalibre-Relay-Assertion "";`): only Gatekeeper's, on a plugin's user surface, may reach the panel. The panel cannot write this vhost; `create-lamaste`'s redeploy refreshes it when its text differs (from 4.5)
|
|
135
142
|
|
|
136
143
|
### 2. Domain Panel Vhost (`lamalibre-lamaste-panel-domain`)
|
|
137
144
|
|
|
@@ -304,6 +311,9 @@ All three also declare the same request headers inside their `location /` (`TUNN
|
|
|
304
311
|
proxy_set_header X-SSL-Client-DN "";
|
|
305
312
|
proxy_set_header X-SSL-Client-Serial "";
|
|
306
313
|
|
|
314
|
+
# Never Authelia's session cookie: the browser's other cookies only
|
|
315
|
+
proxy_set_header Cookie $lamaste_backend_cookie;
|
|
316
|
+
|
|
307
317
|
# WebSocket support
|
|
308
318
|
proxy_set_header Upgrade $http_upgrade;
|
|
309
319
|
proxy_set_header Connection "upgrade";
|
|
@@ -311,8 +321,9 @@ All three also declare the same request headers inside their `location /` (`TUNN
|
|
|
311
321
|
|
|
312
322
|
- **Per location, not per server.** nginx inherits `proxy_set_header` from the enclosing level only when a level declares none of its own. Every tunnel location declares some (`Upgrade`, `Remote-*`), so headers placed at server level never reached the app — it saw `Host: 127.0.0.1:<port>` and no client address. Declared in the location, `Host`, `X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto` now reach the app.
|
|
313
323
|
- **Client-certificate headers are always cleared.** The panel treats `X-SSL-Client-*` from nginx as proof of an mTLS client certificate. A tunnel vhost never passes client-supplied values through, whatever listens on its port.
|
|
324
|
+
- **Authelia's session cookie never reaches the backend.** `Cookie` is set from `$lamaste_backend_cookie`: the browser's cookies with every `authelia_session` pair removed — or no `Cookie` header at all when nothing else is left, or when the map could not strip it ([The backend cookie map](#the-backend-cookie-map)). The cookie is scoped to the base domain, so the browser sends it to every tunnel host; an application that received it could replay the visitor's session on every authenticated host and every plugin's user surface. The agent-panel vhost's `location /` sets it the same way.
|
|
314
325
|
- **Identity headers.** Public vhosts also clear `Remote-User`, `Remote-Groups`, `Remote-Name` and `Remote-Email` — a public tunnel has no authenticated user, and an app might trust those headers from an authenticated vhost. Authenticated and restricted vhosts set them from the Gatekeeper's answer, which replaces anything the client sent.
|
|
315
|
-
- **Relay assertions on plugin tunnels.** A plugin tunnel (a vhost with a `pathPrefix`) ends at the agent's lamaste-agentd, which takes the user only from a relay assertion — its port can be reached without nginx, so a `Remote-User` header proves nothing to it. Authenticated and restricted plugin vhosts forward the one Gatekeeper signs for the request (`auth_request_set $relay_assertion $upstream_http_x_lamalibre_relay_assertion;` and `proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;`); public plugin vhosts clear the header. App tunnel vhosts
|
|
326
|
+
- **Relay assertions on plugin tunnels.** A plugin tunnel (a vhost with a `pathPrefix`) ends at the agent's lamaste-agentd, which takes the user only from a relay assertion — its port can be reached without nginx, so a `Remote-User` header proves nothing to it. Authenticated and restricted plugin vhosts forward the one Gatekeeper signs for the request (`auth_request_set $relay_assertion $upstream_http_x_lamalibre_relay_assertion;` and `proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;`); public plugin vhosts clear the header. App tunnel vhosts clear it too: Gatekeeper signs one only for a plugin tunnel or the panel, never one a client sent. An app tunnel's backend gets the user only as the unsigned `Remote-*` headers — trustworthy as far as nginx goes, but anything that reaches its loopback port can send them as well. An app tunnel bound as a plugin's user surface also carries the [user-surface locations](#user-surface-locations). See [Security Model](../01-concepts/security-model.md#5c-the-agent-daemon-lamaste-agentd-and-relay-assertions).
|
|
316
327
|
|
|
317
328
|
A tunnel created before `maxBodySizeMb` existed has no value and its vhost has no directive, so nginx applies its built-in 1 MiB limit until a body limit is set on it (`PATCH /api/tunnels/:id` with `maxBodySizeMb`, or **Edit** in the panel). Changing the access mode or body limit re-renders the vhost in place through the write-with-rollback pattern below; a disabled tunnel's vhost stays disabled.
|
|
318
329
|
|
|
@@ -341,6 +352,8 @@ location / {
|
|
|
341
352
|
proxy_pass http://127.0.0.1:<port>;
|
|
342
353
|
proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;
|
|
343
354
|
# X-SSL-Client-* are still sent for agents older than 4.3; lamaste-agentd 4.3 removes them unread
|
|
355
|
+
# Never Authelia's session cookie
|
|
356
|
+
proxy_set_header Cookie $lamaste_backend_cookie;
|
|
344
357
|
# ...
|
|
345
358
|
}
|
|
346
359
|
```
|
|
@@ -397,6 +410,64 @@ Optional Authelia protection is controlled by the `autheliaProtected` flag on th
|
|
|
397
410
|
|
|
398
411
|
A custom-domain site may list `aliases` (e.g. `www.myblog.net`). They are served by a second server block that answers with a 301 to the primary host. One certificate lineage, named after the site's domain (`certbot certonly --nginx --cert-name <fqdn> -d <fqdn> -d <alias> ...`), covers the primary name and every alias, so both blocks point at the same `live/<fqdn>/` directory.
|
|
399
412
|
|
|
413
|
+
### Web-surface vhosts
|
|
414
|
+
|
|
415
|
+
A site a plugin owns for its [web surface](../04-api-reference/agent-hosted-plugins.md#web-surface) has a vhost of its own kind under the site name (`lamalibre-lamaste-site-<id>`), rendered by the panel from the plugin's state (`renderWebSurfaceVhost`) — never by the static-site generator. While the surface is served:
|
|
416
|
+
|
|
417
|
+
```nginx
|
|
418
|
+
server {
|
|
419
|
+
listen 443 ssl;
|
|
420
|
+
server_name piyasa.example.com;
|
|
421
|
+
ssl_certificate /etc/letsencrypt/live/piyasa.example.com/fullchain.pem;
|
|
422
|
+
ssl_certificate_key /etc/letsencrypt/live/piyasa.example.com/privkey.pem;
|
|
423
|
+
|
|
424
|
+
root /var/www/lamaste/apps/market-indicators/current;
|
|
425
|
+
index index.html;
|
|
426
|
+
|
|
427
|
+
# Every path of the shell: Gatekeeper first (the API's locations ask on their own)
|
|
428
|
+
auth_request /internal/lamaste/shell-authz;
|
|
429
|
+
error_page 401 =302 https://auth.example.com/?rd=https://piyasa.example.com$request_uri;
|
|
430
|
+
error_page 403 = /internal/lamaste/shell-authz;
|
|
431
|
+
|
|
432
|
+
location = /internal/lamaste/shell-authz {
|
|
433
|
+
internal;
|
|
434
|
+
auth_request off;
|
|
435
|
+
proxy_pass http://127.0.0.1:9294/authz/user-surface-shell/piyasa.example.com;
|
|
436
|
+
# ... as every Gatekeeper subrequest
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# /internal/lamaste/user-surface-authz, -401 and the /api/market-indicators/app
|
|
440
|
+
# locations — exactly the user-surface locations of a bound tunnel
|
|
441
|
+
|
|
442
|
+
# A script, style, font or image the current tree lacks: the previous
|
|
443
|
+
# tree's (a page still running the old shell), else 404
|
|
444
|
+
location ~* [.](?:js|mjs|css|woff2?|ttf|otf|png|jpe?g|gif|webp|avif|ico)$ {
|
|
445
|
+
if ($request_method !~ ^(GET|HEAD)$) { return 405; }
|
|
446
|
+
if ($http_service_worker) { return 404; }
|
|
447
|
+
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; ..." always;
|
|
448
|
+
# X-Content-Type-Options, X-Frame-Options DENY, Referrer-Policy, COOP, CORP, Cache-Control no-cache
|
|
449
|
+
types { text/javascript js mjs; text/css css; ... } # core WEB_TREE_CONTENT_TYPES
|
|
450
|
+
try_files $uri @previous;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# Everything else: the current tree's file, else the shell
|
|
454
|
+
location / {
|
|
455
|
+
# the same method check, worker refusal, headers and types
|
|
456
|
+
try_files $uri /index.html;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
location @previous {
|
|
460
|
+
# the same headers and types
|
|
461
|
+
root /var/www/lamaste/apps/market-indicators/previous;
|
|
462
|
+
try_files $uri =404;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
`auth_request` sits at server level so every location of the shell asks Gatekeeper — the API's locations name their own subrequest, and the internal ones turn it `off`. The API's prefix location is `^~`, so the subresource regex never takes an API path. The shell's headers are set in its locations only: the API's answers are the panel's to head. A document, an SVG or a manifest is never served from the previous tree, and a service worker's script (`Service-Worker: script`) is always `404` — a worker would outlive the tree that installed it. The `types` block names each extension's content type from core's list (`lamaste-priv` accepts a `types` block in a location — `media/type ext…` entries only, no `include`) rather than trust the host's `mime.types`, which differs between nginx versions. `current` and `previous` are links the panel switches with a rename (see [Serving the tree](../04-api-reference/agent-hosted-plugins.md#serving-the-tree)); nginx follows them per request. While the surface is not served, the vhost answers everything with `503` (`Cache-Control: no-store`, a worker's script `404`) — and, once the plugin is uninstalled, `Clear-Site-Data: "storage"`.
|
|
468
|
+
|
|
469
|
+
Moving a bound tunnel's host to a web surface hands the hostname over in **one reload**: the tunnel's vhost is unlinked, the site's written and linked, `nginx -t` run, nginx reloaded once; the tunnel's file stays installed, unlinked, so the tunnel can be enabled again once the site is deleted. On a failed test both are put back.
|
|
470
|
+
|
|
400
471
|
### 7. Public Tunnel Vhost (`lamalibre-lamaste-app-<name>`, access mode: public)
|
|
401
472
|
|
|
402
473
|
Created when a tunnel is configured with public access. Direct proxy with no authentication.
|
|
@@ -421,7 +492,8 @@ server {
|
|
|
421
492
|
proxy_set_header Remote-Email "";
|
|
422
493
|
|
|
423
494
|
# ... the tunnel proxy headers (section 5): Host, X-Real-IP,
|
|
424
|
-
# X-Forwarded-For/Proto, X-SSL-Client-* cleared,
|
|
495
|
+
# X-Forwarded-For/Proto, X-SSL-Client-* cleared,
|
|
496
|
+
# Cookie $lamaste_backend_cookie, WebSocket
|
|
425
497
|
|
|
426
498
|
proxy_pass http://127.0.0.1:<port>;
|
|
427
499
|
proxy_http_version 1.1;
|
|
@@ -485,7 +557,8 @@ server {
|
|
|
485
557
|
proxy_set_header Remote-Email $email;
|
|
486
558
|
|
|
487
559
|
# ... the tunnel proxy headers (section 5): Host, X-Real-IP,
|
|
488
|
-
# X-Forwarded-For/Proto, X-SSL-Client-* cleared,
|
|
560
|
+
# X-Forwarded-For/Proto, X-SSL-Client-* cleared,
|
|
561
|
+
# Cookie $lamaste_backend_cookie, WebSocket
|
|
489
562
|
|
|
490
563
|
proxy_pass http://127.0.0.1:<port>;
|
|
491
564
|
proxy_http_version 1.1;
|
|
@@ -502,7 +575,8 @@ Key characteristics:
|
|
|
502
575
|
|
|
503
576
|
- `auth_request` targets the Gatekeeper at `127.0.0.1:9294/authz/tunnel/<fqdn>` instead of Authelia directly
|
|
504
577
|
- The subrequest names the tunnel by the vhost's own hostname, written into the configuration — in its path, `X-Original-URL` and `Host` — never by the client's `Host` header (see [below](#authorization-subrequests-name-their-own-hostname))
|
|
505
|
-
- The Gatekeeper validates the Authelia session cookie and returns 200 (authenticated) or 401 (unauthenticated)
|
|
578
|
+
- The Gatekeeper validates the Authelia session cookie and returns 200 (authenticated) or 401 (unauthenticated) — the subrequest is the one place the browser's `Cookie` goes as it is (`$http_cookie`), and Gatekeeper sends Authelia the `authelia_session` pair alone; a request with two is a 401
|
|
579
|
+
- The backend gets the browser's other cookies (`$lamaste_backend_cookie`), never the session
|
|
506
580
|
- The upstream receives `Remote-*` headers only as set from the Gatekeeper's response; client-supplied values under those names are replaced
|
|
507
581
|
- 401 redirects to the Authelia login portal
|
|
508
582
|
|
|
@@ -510,7 +584,7 @@ Key characteristics:
|
|
|
510
584
|
|
|
511
585
|
### 9. Restricted Tunnel Vhost (`lamalibre-lamaste-app-<name>`, access mode: restricted)
|
|
512
586
|
|
|
513
|
-
Created when a tunnel requires both Authelia login and an explicit access grant. Only users with a matching grant in `
|
|
587
|
+
Created when a tunnel requires both Authelia login and an explicit access grant. Only users with a matching grant in Gatekeeper (`gatekeeper.db`) can access the tunnel.
|
|
514
588
|
|
|
515
589
|
```nginx
|
|
516
590
|
server {
|
|
@@ -560,7 +634,8 @@ server {
|
|
|
560
634
|
proxy_set_header Remote-Email $email;
|
|
561
635
|
|
|
562
636
|
# ... the tunnel proxy headers (section 5): Host, X-Real-IP,
|
|
563
|
-
# X-Forwarded-For/Proto, X-SSL-Client-* cleared,
|
|
637
|
+
# X-Forwarded-For/Proto, X-SSL-Client-* cleared,
|
|
638
|
+
# Cookie $lamaste_backend_cookie, WebSocket
|
|
564
639
|
|
|
565
640
|
proxy_pass http://127.0.0.1:<port>;
|
|
566
641
|
proxy_http_version 1.1;
|
|
@@ -582,6 +657,96 @@ Key differences from the authenticated vhost:
|
|
|
582
657
|
- `error_page 403 = /internal/lamaste/authz` replays the request to the Gatekeeper internal location. The `=` prefix causes nginx to use the response body and status code from the Gatekeeper, which serves an inline HTML access-request page with pre-filled message templates
|
|
583
658
|
- 401 (unauthenticated) still redirects to Authelia login as before
|
|
584
659
|
|
|
660
|
+
### User-surface locations
|
|
661
|
+
|
|
662
|
+
An `authenticated` or `restricted` app tunnel bound as the user surface of agent-hosted plugins ([Tunnels API](../04-api-reference/tunnels.md#user-surface-binding)) carries, for each plugin whose installed, approved version declares `identity` **and whose hosting agent is the agent carrying the tunnel**, two more locations — `= /api/<route>/app` and `/api/<route>/app/` — and two internal ones they share (`renderTunnelVhost` with `userSurface`, `userSurfaceLocations` in `nginx.js`). A bound route whose plugin does not declare identity yet, or is not hosted by the tunnel's agent (the tunnel moved or its agent revoked, the plugin hosted elsewhere or not at all), has no locations: its requests take `location /` down the tunnel. Moving the tunnel, revoking its agent and hosting or unhosting the plugin re-render the vhost. A public vhost never carries them (the generator refuses).
|
|
663
|
+
|
|
664
|
+
```nginx
|
|
665
|
+
# The user surface of agent-hosted plugins on this host: Gatekeeper
|
|
666
|
+
# vouches for the session's user to the panel, for one request each.
|
|
667
|
+
location = /internal/lamaste/user-surface-authz {
|
|
668
|
+
internal;
|
|
669
|
+
|
|
670
|
+
proxy_pass http://127.0.0.1:9294/authz/user-surface/app.example.com;
|
|
671
|
+
proxy_pass_request_body off;
|
|
672
|
+
|
|
673
|
+
proxy_set_header Content-Length "";
|
|
674
|
+
proxy_set_header Connection "";
|
|
675
|
+
proxy_set_header X-Original-Method $request_method;
|
|
676
|
+
proxy_set_header X-Original-URL https://app.example.com$request_uri;
|
|
677
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
678
|
+
# ... X-Forwarded-Host, -Proto, -Uri and -Method cleared ("")
|
|
679
|
+
proxy_set_header Host app.example.com;
|
|
680
|
+
proxy_set_header Cookie $http_cookie;
|
|
681
|
+
|
|
682
|
+
proxy_http_version 1.1;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
location = /internal/lamaste/user-surface-401 {
|
|
686
|
+
internal;
|
|
687
|
+
|
|
688
|
+
proxy_pass http://127.0.0.1:9294/authz/user-surface-unauthenticated;
|
|
689
|
+
proxy_pass_request_body off;
|
|
690
|
+
|
|
691
|
+
proxy_set_header Content-Length "";
|
|
692
|
+
proxy_set_header Cookie "";
|
|
693
|
+
proxy_set_header Authorization "";
|
|
694
|
+
# Gatekeeper's rate limit counts the client nginx saw, never one a client named
|
|
695
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
696
|
+
|
|
697
|
+
proxy_http_version 1.1;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
# The user surface of plugin "market-indicators"
|
|
701
|
+
location = /api/market-indicators/app { ... } # the same block as below
|
|
702
|
+
|
|
703
|
+
location ^~ /api/market-indicators/app/ {
|
|
704
|
+
auth_request /internal/lamaste/user-surface-authz;
|
|
705
|
+
auth_request_set $relay_assertion $upstream_http_x_lamalibre_relay_assertion;
|
|
706
|
+
|
|
707
|
+
# The user, vouched for by Gatekeeper to the panel for this one request
|
|
708
|
+
proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;
|
|
709
|
+
|
|
710
|
+
# Nothing else that any part of the panel trusts
|
|
711
|
+
proxy_set_header Cookie "";
|
|
712
|
+
proxy_set_header Authorization "";
|
|
713
|
+
proxy_set_header Remote-User "";
|
|
714
|
+
proxy_set_header Remote-Groups "";
|
|
715
|
+
proxy_set_header Remote-Name "";
|
|
716
|
+
proxy_set_header Remote-Email "";
|
|
717
|
+
proxy_set_header X-SSL-Client-Verify "";
|
|
718
|
+
proxy_set_header X-SSL-Client-DN "";
|
|
719
|
+
proxy_set_header X-SSL-Client-Serial "";
|
|
720
|
+
proxy_set_header X-Forwarded-Host "";
|
|
721
|
+
|
|
722
|
+
proxy_set_header Host $host;
|
|
723
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
724
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
725
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
726
|
+
proxy_set_header Connection "";
|
|
727
|
+
|
|
728
|
+
# No URI: the request target reaches the panel as the browser sent it
|
|
729
|
+
proxy_pass http://127.0.0.1:3100;
|
|
730
|
+
proxy_http_version 1.1;
|
|
731
|
+
|
|
732
|
+
client_max_body_size 10m; # the plugin's internal tunnel's maxBodySizeMb
|
|
733
|
+
# No response lives longer (the panel ends one then)
|
|
734
|
+
proxy_read_timeout 3600s;
|
|
735
|
+
proxy_send_timeout 3600s;
|
|
736
|
+
|
|
737
|
+
# No session: a JSON 401, never a redirect
|
|
738
|
+
error_page 401 = /internal/lamaste/user-surface-401;
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
- **Gatekeeper attests, the panel identifies.** The subrequest names the tunnel as the tunnel's own does; Gatekeeper answers with a single-use relay assertion for the panel (never cached) and no `Remote-*` headers. The location passes that assertion to the panel and **nothing else of the browser's credentials**: the panel serves other contexts on the same listener, so everything any of them trusts — the client-certificate headers, `Remote-*`, a desktop session's `Authorization`, an administrator's 2FA `Cookie`, `X-Forwarded-Host` — is cleared. `X-Forwarded-For` is the client's address alone: the panel trusts one loopback hop, and the user surface's rate limit keys on it.
|
|
743
|
+
- **`proxy_pass` names no URI**, so the panel receives `$request_uri` unchanged — the raw target the assertion is bound to and the panel compares byte for byte. A normalized path would differ from what Gatekeeper signed.
|
|
744
|
+
- **The body limit is the plugin's** (its internal tunnel's `maxBodySizeMb`), not the tunnel's. The timeouts are one hour (`USER_SURFACE_MAX_RESPONSE_SECONDS`), the longest a user-surface response lives: long enough for server-sent events, which the panel streams unbuffered, and no longer than the panel lets any response stay open — it ends one after an hour, and at once when its user loses access.
|
|
745
|
+
- **The panel's answer steers nothing.** It never carries `X-Accel-*` from an agent half (nginx obeys `X-Accel-Redirect` from 3100); the panel sets `X-Accel-Buffering: no` itself on an event stream. Every answer carries `Cross-Origin-Resource-Policy: same-origin`, `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox; default-src 'none'`, and no `Access-Control-*`.
|
|
746
|
+
- **`= /api/<route>/app`** and **`/api/<route>/app/`** are both needed: a prefix location `/api/<route>/app` would also match `/api/<route>/apps`, which belongs to the tunnel.
|
|
747
|
+
|
|
748
|
+
**A client's relay assertion is cleared wherever a browser reaches the panel.** The panel accepts `X-Lamalibre-Relay-Assertion` only on the user surface, and only from Gatekeeper; every location of the domain panel vhost, the auth vhost's panel locations, the agent-panel vhost's subrequest to the panel and the IP panel vhost set it to `""`, and so does every tunnel vhost that does not set Gatekeeper's (app tunnels, public vhosts). The panel's startup reconcile rewrites the domain panel and auth vhosts and every tunnel vhost; the IP panel vhost, which the installer writes and the panel cannot, is refreshed by `create-lamaste`'s redeploy (`Refreshing the IP panel vhost`).
|
|
749
|
+
|
|
585
750
|
### Authorization subrequests name their own hostname
|
|
586
751
|
|
|
587
752
|
Every authorization subrequest — to Gatekeeper from an `authenticated` or `restricted` tunnel vhost (app and plugin tunnels), to Authelia from a protected static site and from the auth portal's `/api/user-access/authorize` — names the vhost by its own hostname, written into the configuration: `X-Original-URL https://<fqdn>$request_uri`; for Gatekeeper also the path, `/authz/tunnel/<fqdn>`, and `Host <fqdn>`. A tunnel's 401 login redirect returns to `https://<fqdn>$request_uri`. The client's `X-Forwarded-Host`, `-Proto`, `-Uri` and `-Method` are cleared in these subrequests. Generated vhosts contain no `$http_host`.
|
|
@@ -592,6 +757,37 @@ Gatekeeper authorizes the tunnel named in the path, and refuses a check whose `X
|
|
|
592
757
|
|
|
593
758
|
Authelia evaluates the raw `$request_uri`, while nginx serves the normalized `$uri` (`/a/../b` is `/b`, `%2e` is `.`). Lamaste writes no path (`resources`) rules into Authelia's `access_control`, only hostname rules; a path rule added by hand would see a different path than nginx serves.
|
|
594
759
|
|
|
760
|
+
## The backend cookie map
|
|
761
|
+
|
|
762
|
+
Authelia's session cookie (`authelia_session`, the name core configures in `server/authelia.ts` and exports as `AUTHELIA_SESSION_COOKIE`) is scoped to the base domain, so a browser sends it to every host under it. No tunnel or agent backend may receive it: every location that proxies to one sends
|
|
763
|
+
|
|
764
|
+
```nginx
|
|
765
|
+
proxy_set_header Cookie $lamaste_backend_cookie;
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
and the variable comes from an http-level map that create-lamaste installs as `/etc/nginx/conf.d/lamalibre-lamaste-backend-cookie.conf` (root-owned, 0644; nginx's own `nginx.conf` includes `conf.d/*.conf` in `http {}`). Its text is core's `renderBackendCookieMap()`:
|
|
769
|
+
|
|
770
|
+
```nginx
|
|
771
|
+
map $http_cookie $lamaste_cookie_strip_1 {
|
|
772
|
+
default $http_cookie;
|
|
773
|
+
"~^[ \t]*authelia_session[ \t]*=[^;]*(?:;[ \t]*)?(.*)$" $1; # the first pair
|
|
774
|
+
"~^(.*?)[ \t]*;[ \t]*authelia_session[ \t]*=[^;]*(.*)$" $1$2; # a later pair
|
|
775
|
+
}
|
|
776
|
+
# ... $lamaste_cookie_strip_2 to _4 from the one before, alike
|
|
777
|
+
|
|
778
|
+
map $lamaste_cookie_strip_4 $lamaste_backend_cookie {
|
|
779
|
+
default $lamaste_cookie_strip_4;
|
|
780
|
+
"~(?:^|;)[ \t]*authelia_session[ \t]*=" ""; # one left: fail closed
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
- **One pair per round, four rounds.** A pair is the session cookie when its name — spaces and tabs around it trimmed — is exactly `authelia_session` (case-sensitive, as cookie names are and as Authelia reads them); `authelia_session_x`, `xauthelia_session` and a value containing `authelia_session=` are other cookies and stay. A browser holds one cookie per name, domain and path, so more than four is a request no browser of Authelia's makes — and a fifth pair empties the variable: the backend gets **no `Cookie` header at all**, never the session. nginx sends no header whose value is empty.
|
|
785
|
+
- **The rest of the cookies stay**, in order — an application keeps its own session and preferences.
|
|
786
|
+
- **Several `Cookie` lines** (HTTP/1.1, or HTTP/2's split cookies) are one `$http_cookie` to nginx, joined with `; `, and `proxy_set_header Cookie` replaces all of them.
|
|
787
|
+
- **Gatekeeper reads the same grammar** (core `autheliaSessionCookies`): it is the only party handed the browser's `Cookie` as it is (`proxy_set_header Cookie $http_cookie;` in the authorization subrequests), sends Authelia the one pair alone, and refuses a request with more than one.
|
|
788
|
+
- **Order of installation.** A vhost that names the variable fails `nginx -t` without the map and is rolled back, so the map is installed before the panel writes its first tunnel vhost — on a fresh install with nginx's own configuration, on a redeploy as its first step, before the running panel is stopped (a relay whose nginx refuses the map keeps the version it runs). The panel's startup reconcile then rewrites every tunnel and agent-panel vhost to use it.
|
|
789
|
+
- **Tested** by evaluating the rendered map's expressions as PCRE does (they use only constructs PCRE and JavaScript read alike) against cookie strings with the session first, in the middle, last, repeated, beside similar names and with any spacing, and — where an nginx binary is installed — by running nginx with the map and a backend that reports the `Cookie` it received (`packages/provisioners/server/test/backend-cookie.test.js`).
|
|
790
|
+
|
|
595
791
|
## nginx Cache Zone (`lamaste_authz`)
|
|
596
792
|
|
|
597
793
|
The Gatekeeper authorization responses are cached by nginx to reduce subrequest overhead. The cache zone is defined in a shared snippet:
|
|
@@ -714,12 +910,12 @@ nginx's master process runs as root and opens the files a configuration names
|
|
|
714
910
|
- its **name** is one the panel manages: `lamalibre-lamaste-panel-domain`, `-auth`, `-tunnel`, `-app-<subdomain>`, `-agent-panel-<label>` or `-site-<uuid>`. `lamalibre-lamaste-panel-ip` (written by the installer) may only be enabled or disabled — which is how panel 2FA turns the IP vhost off;
|
|
715
911
|
- every **directive** is on the allow-list for its context, with arguments that cannot name a file outside what Lamaste serves:
|
|
716
912
|
|
|
717
|
-
| Context | Allowed
|
|
718
|
-
| ---------- |
|
|
719
|
-
| top level | `server`, `map` (plain `key value;` entries, no `include`), `limit_req_zone`
|
|
720
|
-
| `server` | `listen 443` / `[::]:443` with optional `ssl` / `http2`; `server_name` (hostnames); `ssl_certificate(_key)` only `/etc/letsencrypt/live/<name>/{fullchain,privkey}.pem`; `include` only `/etc/nginx/snippets/lamalibre-lamaste-*.conf`; `root` only under `/var/www/lamaste/`; `ssl_protocols`, `ssl_ciphers`, `ssl_prefer_server_ciphers`, `client_max_body_size`, `index`, `add_header`, `proxy_set_header`, `error_page`, `return`, `rewrite`, `location` |
|
|
721
|
-
| `location` | `proxy_pass` only `http://127.0.0.1:<port>[/path]`; `root` / `alias` only under `/var/www/lamaste/`; `proxy_set_header`, `proxy_http_version`, `proxy_read_timeout`, `proxy_send_timeout`, `proxy_buffers`, `proxy_next_upstream`, `proxy_pass_request_body`, `auth_request`, `auth_request_set`, `limit_req`, `try_files
|
|
722
|
-
| `if` | `return`, `rewrite`
|
|
913
|
+
| Context | Allowed |
|
|
914
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
915
|
+
| top level | `server`, `map` (plain `key value;` entries, no `include`), `limit_req_zone` |
|
|
916
|
+
| `server` | `listen 443` / `[::]:443` with optional `ssl` / `http2`; `server_name` (hostnames); `ssl_certificate(_key)` only `/etc/letsencrypt/live/<name>/{fullchain,privkey}.pem`; `include` only `/etc/nginx/snippets/lamalibre-lamaste-*.conf`; `root` only under `/var/www/lamaste/`; `auth_request` only a URI of the server or `off`; `ssl_protocols`, `ssl_ciphers`, `ssl_prefer_server_ciphers`, `client_max_body_size`, `index`, `add_header`, `proxy_set_header`, `error_page`, `return`, `rewrite`, `location` (a path, or a named location `@name`) |
|
|
917
|
+
| `location` | `proxy_pass` only `http://127.0.0.1:<port>[/path]`; `root` / `alias` only under `/var/www/lamaste/`; `proxy_set_header`, `proxy_http_version`, `proxy_read_timeout`, `proxy_send_timeout`, `proxy_buffers`, `proxy_next_upstream`, `proxy_pass_request_body`, `auth_request`, `auth_request_set`, `limit_req`, `try_files` (the last argument may name a location, `@name`), `index`, `add_header`, `error_page`, `client_max_body_size`, `internal`, `return`, `rewrite`, `if`, nested `location` |
|
|
918
|
+
| `if` | `return`, `rewrite` |
|
|
723
919
|
|
|
724
920
|
Port 80 is not allowed: it belongs to the installer's HTTPS redirect and to certbot's HTTP-01 challenges. Backslashes, `${`, control characters and non-ASCII text outside comments are refused rather than interpreted, as is any directive not listed — including ones nginx would accept (`access_log`, `log_format`, `ssl_*` files elsewhere, `proxy_pass` to another host, ...). A refused `write` fails with `nginx site write failed for <name>: lamaste-priv: vhost line <n>: ...` and leaves the previous vhost in place.
|
|
725
921
|
|
|
@@ -782,6 +978,8 @@ The 24-hour timeout (`proxy_read_timeout 86400s`) is set on tunnel and app vhost
|
|
|
782
978
|
```
|
|
783
979
|
/etc/nginx/
|
|
784
980
|
├── nginx.conf ← Default nginx config (not modified by Lamaste)
|
|
981
|
+
├── conf.d/
|
|
982
|
+
│ └── lamalibre-lamaste-backend-cookie.conf ← http-level map: $lamaste_backend_cookie (installer, redeploy)
|
|
785
983
|
├── sites-available/
|
|
786
984
|
│ ├── lamalibre-lamaste-http-redirect ← :80 catch-all → HTTPS (created by installer)
|
|
787
985
|
│ ├── lamalibre-lamaste-panel-ip ← IP:9292 panel access (created by installer)
|
|
@@ -814,6 +1012,8 @@ All Lamaste-managed files are prefixed with `lamalibre-lamaste-` to distinguish
|
|
|
814
1012
|
| `lamalibre-lamaste-panel-ip` | Installation | Never (fallback access) |
|
|
815
1013
|
| `lamalibre-lamaste-http-redirect` | Installation, redeploy | Uninstall |
|
|
816
1014
|
|
|
1015
|
+
Beside them the installer writes the http-level map `conf.d/lamalibre-lamaste-backend-cookie.conf` — on a fresh install with nginx's configuration, on every redeploy as the first step ([The backend cookie map](#the-backend-cookie-map)); `lamaste-server uninstall` removes it after the sites that use it.
|
|
1016
|
+
|
|
817
1017
|
`lamalibre-lamaste-http-redirect` is the port-80 catch-all:
|
|
818
1018
|
|
|
819
1019
|
```nginx
|
|
@@ -848,8 +1048,8 @@ Without it nothing listens on port 80 and `http://` links to the relay's hostnam
|
|
|
848
1048
|
|
|
849
1049
|
A vhost is written when its tunnel or site is created or changed, so a generator change does not reach existing vhosts by itself. At startup, once onboarding is complete, the panel (`lib/vhost-reconcile.js`) renders installed vhosts again and rewrites each one whose text differs through the write-with-rollback pattern, on the certificate directory the installed file names (a wildcard certificate stays a wildcard certificate). A rewrite never links a vhost that is not linked now; a disabled tunnel's vhost is unlinked:
|
|
850
1050
|
|
|
851
|
-
- **Before it listens:** the auth portal (`lamalibre-lamaste-auth`) and every static site, from `sites.json` and the domain. Site changes take no lock, so this runs while none can arrive.
|
|
852
|
-
- **Once it listens**, under the tunnel lock: every agent-panel, app and plugin tunnel, from `tunnels.json` — the stored access mode (`restricted` when absent, as Gatekeeper reads it), the plugin route,
|
|
1051
|
+
- **Before it listens:** the domain panel vhost (`lamalibre-lamaste-panel-domain`), the auth portal (`lamalibre-lamaste-auth`) and every static site, from `sites.json` and the domain. Site changes take no lock, so this runs while none can arrive.
|
|
1052
|
+
- **Once it listens**, under the tunnel lock: every agent-panel, app and plugin tunnel, from `tunnels.json` — the stored access mode (`restricted` when absent, as Gatekeeper reads it), the plugin route, the body limit only when the tunnel has one, and the user-surface locations of the bound routes it serves now. The same reconcile, limited to the tunnels a route is bound to, runs whenever whether that route is served can change: the plugin's approval, update or revert, hosting and unhosting, uninstall. After listen because a rewritten agent-panel vhost asks the panel about every request.
|
|
853
1053
|
|
|
854
1054
|
That is how vhosts written before 4.3 start forwarding relay assertions (agent-panel and plugin tunnels) and start naming their own hostname to Gatekeeper and Authelia (see [above](#authorization-subrequests-name-their-own-hostname)). A rewrite never adds or removes authentication: a tunnel or site vhost whose `auth_request` disagrees with its state (a public vhost for a restricted tunnel, say) is logged and left alone until the tunnel or site is reconfigured — the agent-panel vhost, which gains the panel's `auth_request` beside its mTLS check, is the exception. Missing vhosts are logged (a site still waiting for its certificate has none yet); a failed rewrite restores the previous file and the others still run.
|
|
855
1055
|
|
|
@@ -857,16 +1057,19 @@ A tunnel vhost left as it was fails closed: Gatekeeper refuses its `/authz/check
|
|
|
857
1057
|
|
|
858
1058
|
## Key Files
|
|
859
1059
|
|
|
860
|
-
| File
|
|
861
|
-
|
|
|
862
|
-
| `packages/provisioners/server/src/tasks/nginx.js`
|
|
863
|
-
| `packages/provisioners/server/src/lib/http-redirect.js`
|
|
864
|
-
| `packages/server/
|
|
865
|
-
| `packages/
|
|
866
|
-
| `/etc/nginx/
|
|
867
|
-
|
|
|
868
|
-
|
|
|
869
|
-
| `/etc/nginx/
|
|
1060
|
+
| File | Role |
|
|
1061
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
1062
|
+
| `packages/provisioners/server/src/tasks/nginx.js` | Installer: self-signed cert, mTLS snippet, IP vhost, port-80 redirect |
|
|
1063
|
+
| `packages/provisioners/server/src/lib/http-redirect.js` | Installs/refreshes the port-80 redirect site (installer and redeploy) |
|
|
1064
|
+
| `packages/provisioners/server/src/lib/backend-cookie.js` | Installs/refreshes the backend cookie map (installer and redeploy), `nginx -t` with rollback |
|
|
1065
|
+
| `packages/core/lib/src/authelia-session.ts` | The session cookie's name, the map's text (`renderBackendCookieMap`), Gatekeeper's reader |
|
|
1066
|
+
| `/etc/nginx/conf.d/lamalibre-lamaste-backend-cookie.conf` | Defines `$lamaste_backend_cookie`: the browser's cookies without `authelia_session` |
|
|
1067
|
+
| `packages/server/daemon/src/lib/nginx.js` | Runtime: vhost generation, write-with-rollback, enable/disable/reload |
|
|
1068
|
+
| `packages/provisioners/server/scripts/lamaste-priv` | Root helper (`/usr/local/sbin/lamaste-priv`): site names, vhost allow-list, install/enable/remove |
|
|
1069
|
+
| `/etc/nginx/snippets/lamalibre-lamaste-mtls.conf` | Shared mTLS snippet |
|
|
1070
|
+
| `/etc/nginx/snippets/lamalibre-lamaste-authz-cache.conf` | Gatekeeper proxy_cache zone definition |
|
|
1071
|
+
| `/etc/nginx/sites-available/lamalibre-lamaste-*` | Vhost configuration files |
|
|
1072
|
+
| `/etc/nginx/sites-enabled/lamalibre-lamaste-*` | Symlinks to enabled vhosts |
|
|
870
1073
|
|
|
871
1074
|
## Design Decisions
|
|
872
1075
|
|
|
@@ -890,6 +1093,8 @@ nginx emits error 495 when no client certificate is provided and 496 when the ce
|
|
|
890
1093
|
|
|
891
1094
|
Chisel tunnel connections and some app WebSocket connections are long-lived. nginx's default `proxy_read_timeout` of 60 seconds would close these connections, causing tunnel drops. The 24-hour timeout is effectively "never timeout" for practical purposes, while still allowing nginx to reclaim resources from genuinely dead connections.
|
|
892
1095
|
|
|
1096
|
+
A plugin's user-surface locations are the exception: one hour, the longest the panel lets a user-surface response live. A response that outlived its user's access — an event stream left open after the user was disabled — is what the bound is there to stop; the panel also ends such a response within seconds of the change.
|
|
1097
|
+
|
|
893
1098
|
### Why a root helper for nginx operations?
|
|
894
1099
|
|
|
895
1100
|
The Panel Server runs as the `lamaste` user, not root. Writing to `/etc/nginx/sites-available/` and reloading nginx require root privileges. Instead of running the Panel Server as root, Lamaste allows the `lamaste` user a few fixed command lines (`nginx -t`, `systemctl reload nginx`, ...) and the root-owned `lamaste-priv` helper, which accepts only the panel's own site names and only vhosts built from the allow-listed directives above. Earlier versions used sudoers rules with wildcards (`mv /tmp/nginx-* /etc/nginx/sites-available/*`); a sudoers `*` also matches spaces, so such a rule accepted extra arguments, and even an exact rule would have let the panel install any nginx configuration — which nginx's root master process would act on. Certbot is reached only through the root-owned `/usr/local/sbin/lamaste-certbot` wrapper, which validates hostnames and email and runs certbot with a fixed argument vector. See [Security Model](../01-concepts/security-model.md#privilege-boundary).
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Lamaste has three main pieces:
|
|
8
8
|
|
|
9
|
-
1. **The Installer** (`npx @lamalibre/create-lamaste@4.
|
|
9
|
+
1. **The Installer** (`npx @lamalibre/create-lamaste@4.6.0`) — a one-shot script you run once on a fresh VPS. It sets up nginx, generates security certificates, and deploys a web-based control panel. After it finishes, you never SSH into the server again.
|
|
10
10
|
|
|
11
11
|
2. **The Panel** (browser UI + backend API) — a management interface protected by a client certificate. Through it, you configure your domain, create tunnels, manage users, and monitor services.
|
|
12
12
|
|
|
@@ -185,28 +185,25 @@ End User → nginx :443 → auth_request → 127.0.0.1:9294/authz/tunnel/<fqdn>
|
|
|
185
185
|
│ In-memory session │
|
|
186
186
|
│ cache (Map) │
|
|
187
187
|
│ │
|
|
188
|
-
│ State
|
|
189
|
-
│ -
|
|
190
|
-
│ - access-grants.json│
|
|
188
|
+
│ State: │
|
|
189
|
+
│ - gatekeeper.db │
|
|
191
190
|
│ - gatekeeper.json │
|
|
192
191
|
└─────────────────────┘
|
|
193
192
|
```
|
|
194
193
|
|
|
195
|
-
**Grant model:** The Gatekeeper uses a generic principal-to-resource grant model. A principal is an Authelia user or a Lamaste group. A resource is a tunnel subdomain or other protected endpoint. Grants are stored in `/etc/lamalibre/lamaste/
|
|
194
|
+
**Grant model:** The Gatekeeper uses a generic principal-to-resource grant model. A principal is an Authelia user or a Lamaste group. A resource is a tunnel subdomain or other protected endpoint. Grants are stored in Gatekeeper's SQLite database, `/etc/lamalibre/lamaste/gatekeeper.db`. A plugin grant whose `context` names `roles` is an agent-hosted plugin's entitlement (see [Agent-Hosted Plugins](../04-api-reference/agent-hosted-plugins.md#users-and-identity)).
|
|
196
195
|
|
|
197
|
-
**Lamaste groups:** Separate from Authelia groups. Stored in
|
|
196
|
+
**Lamaste groups:** Separate from Authelia groups. Stored in `gatekeeper.db`. Lamaste groups allow the admin to organize users for access control without modifying Authelia's `users.yml`.
|
|
198
197
|
|
|
199
198
|
**Two-layer caching:**
|
|
200
199
|
|
|
201
200
|
1. **nginx `proxy_cache`** — the installer defines the `lamaste_authz` cache zone (1m keys, 10m storage) and a location-level policy (key `$server_name|$cookie_authelia_session`, `200` cached for 10 seconds, `403` never), but the generated vhosts do not include it today (see [nginx Configuration](nginx-configuration.md#nginx-cache-zone-lamaste_authz)).
|
|
202
201
|
2. **In-memory session cache** — the Gatekeeper keeps validated Authelia sessions in a `Map` keyed by an HMAC of the session cookie, so a check within the TTL does not ask Authelia again. It caches the identity, not the decision: grants are checked on every request.
|
|
203
202
|
|
|
204
|
-
**State
|
|
203
|
+
**State:** In `/etc/lamalibre/lamaste/` with 0600 permissions and `lamaste:lamaste` ownership:
|
|
205
204
|
|
|
206
|
-
- `
|
|
207
|
-
- `
|
|
208
|
-
- `gatekeeper.json` — Settings (admin contact info, cache TTL, logging configuration)
|
|
209
|
-
- `access-request-log.json` — Optional log of denied access attempts (for admin review)
|
|
205
|
+
- `gatekeeper.db` — SQLite (WAL): Lamaste groups and members, access grants, the optional log of denied access attempts (for admin review); read on every check
|
|
206
|
+
- `gatekeeper.json` — Settings (admin contact info, cache TTL, logging configuration), atomic writes (tmp, fsync, rename)
|
|
210
207
|
|
|
211
208
|
**Service:** Runs as `lamalibre-lamaste-gatekeeper.service` (systemd), binding `127.0.0.1:9294`. nginx is the sole entry point — the Gatekeeper is never directly accessible from the internet.
|
|
212
209
|
|
|
@@ -321,7 +318,7 @@ lamaste/
|
|
|
321
318
|
│ │ ├── files.js ← Static site file operations (upload, delete, list)
|
|
322
319
|
│ │ └── app-error.js ← Operational error class (AppError)
|
|
323
320
|
│ │
|
|
324
|
-
│ ├── lamaste-server-ui/ ← React 18 + Vite + Tailwind SPA + shared admin components (@lamalibre/lamaste-server-ui@4.
|
|
321
|
+
│ ├── lamaste-server-ui/ ← React 18 + Vite + Tailwind SPA + shared admin components (@lamalibre/lamaste-server-ui@4.6.0)
|
|
325
322
|
│ │ └── src/
|
|
326
323
|
│ │ ├── index.js ← Package exports (pages, context, utilities)
|
|
327
324
|
│ │ ├── context/
|
|
@@ -337,7 +334,7 @@ lamaste/
|
|
|
337
334
|
│ │ │ └── formatters.js ← Byte, uptime, relative time formatters
|
|
338
335
|
│ │ └── pages/ ← Admin pages (Dashboard, Tunnels, Sites, Users, Certs, Services, Plugins, Tickets, Settings)
|
|
339
336
|
│ │
|
|
340
|
-
│ ├── lamaste-agent-ui/ ← Shared React agent UI (pages, context, components) (@lamalibre/lamaste-agent-ui@4.
|
|
337
|
+
│ ├── lamaste-agent-ui/ ← Shared React agent UI (pages, context, components) (@lamalibre/lamaste-agent-ui@4.6.0)
|
|
341
338
|
│ │
|
|
342
339
|
│ ├── lamaste-tickets/ ← Agent-to-agent authorization SDK (mTLS, undici)
|
|
343
340
|
│ │ └── src/
|
|
@@ -377,7 +374,7 @@ lamaste/
|
|
|
377
374
|
│ │ │ └── CompleteStep.jsx ← Credentials + TOTP QR + next steps
|
|
378
375
|
│ │ ├── management/ ← Legacy standalone implementations (kept for reference)
|
|
379
376
|
│ │ │ ├── Certificates.jsx ← Standalone certificates page (dead code)
|
|
380
|
-
│ │ │ ├── Dashboard.jsx ← Standalone dashboard (not imported — App.jsx uses @lamalibre/lamaste-server-ui@4.
|
|
377
|
+
│ │ │ ├── Dashboard.jsx ← Standalone dashboard (not imported — App.jsx uses @lamalibre/lamaste-server-ui@4.6.0)
|
|
381
378
|
│ │ │ ├── Plugins.jsx ← Standalone plugins page (dead code)
|
|
382
379
|
│ │ │ ├── Services.jsx ← Standalone services page (dead code)
|
|
383
380
|
│ │ │ ├── Settings.jsx ← Standalone settings page (dead code)
|
|
@@ -486,7 +483,7 @@ The Panel Server runs as the `lamaste` user with minimal privileges and cannot b
|
|
|
486
483
|
The full lifecycle of a Lamaste installation:
|
|
487
484
|
|
|
488
485
|
```
|
|
489
|
-
Phase 1: Installation (npx @lamalibre/create-lamaste@4.
|
|
486
|
+
Phase 1: Installation (npx @lamalibre/create-lamaste@4.6.0)
|
|
490
487
|
└─ OS hardening, Node.js, mTLS certs, nginx, panel deploy
|
|
491
488
|
└─ Result: Panel accessible at https://<ip>:9292
|
|
492
489
|
|
|
@@ -530,8 +527,7 @@ Phase 4: Recovery (if needed)
|
|
|
530
527
|
| `/etc/systemd/system/authelia.service` | Authelia systemd unit |
|
|
531
528
|
| `/etc/lamalibre/lamaste/plugins.json` | Plugin registry (installed plugins and status) |
|
|
532
529
|
| `/etc/lamalibre/lamaste/plugins/` | Per-plugin data directories |
|
|
533
|
-
| `/etc/lamalibre/lamaste/
|
|
534
|
-
| `/etc/lamalibre/lamaste/access-grants.json` | Generic access grants (principal → resource) |
|
|
530
|
+
| `/etc/lamalibre/lamaste/gatekeeper.db` | Gatekeeper: groups, access grants, denied-access log |
|
|
535
531
|
| `/etc/lamalibre/lamaste/gatekeeper.json` | Gatekeeper settings (cache TTL, admin contact, logging) |
|
|
536
532
|
| `/etc/lamalibre/lamaste/push-install-config.json` | Push install configuration and policies |
|
|
537
533
|
| `/etc/lamalibre/lamaste/push-install-sessions.json` | Push install session audit log |
|
|
@@ -10,9 +10,9 @@ The client has two modes. On first visit, it shows an onboarding wizard that wal
|
|
|
10
10
|
|
|
11
11
|
## Shared Panel Packages
|
|
12
12
|
|
|
13
|
-
The management-mode admin pages (Dashboard, Tunnels, Sites, Users, Certificates, Services, Plugins, Tickets, Logs, Settings) live in `@lamalibre/lamaste-server-ui@4.
|
|
13
|
+
The management-mode admin pages (Dashboard, Tunnels, Sites, Users, Certificates, Services, Plugins, Tickets, Logs, Settings) live in `@lamalibre/lamaste-server-ui@4.6.0`. This package provides both the browser SPA build and the shared admin components consumed by `lamaste-desktop` (Tauri app).
|
|
14
14
|
|
|
15
|
-
The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are extracted into a separate shared package: `@lamalibre/lamaste-agent-ui@4.
|
|
15
|
+
The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are extracted into a separate shared package: `@lamalibre/lamaste-agent-ui@4.6.0`. This package is consumed by `lamaste-desktop` for agent mode.
|
|
16
16
|
|
|
17
17
|
The shared package exposes an `AdminClientContext` abstraction that decouples admin UI pages from their data transport. Each host provides its own client implementation:
|
|
18
18
|
|
|
@@ -118,7 +118,7 @@ The shell renders the appropriate step component and shows a progress indicator
|
|
|
118
118
|
|
|
119
119
|
### Management Mode
|
|
120
120
|
|
|
121
|
-
React Router handles navigation within the `Layout` component (sidebar + content area). Management page components are imported from the shared `@lamalibre/lamaste-server-ui@4.
|
|
121
|
+
React Router handles navigation within the `Layout` component (sidebar + content area). Management page components are imported from the shared `@lamalibre/lamaste-server-ui@4.6.0` package and wrapped with the `AdminClientProvider` (which supplies the web-specific data client):
|
|
122
122
|
|
|
123
123
|
| Path | Component | Description |
|
|
124
124
|
| ------------------------ | ------------------- | ------------------------------------------------------------------- |
|
|
@@ -350,9 +350,9 @@ The app root wraps all content in five providers:
|
|
|
350
350
|
```
|
|
351
351
|
|
|
352
352
|
1. **QueryClientProvider** — react-query cache with global defaults (`retry: 1`, `refetchOnWindowFocus: false`)
|
|
353
|
-
2. **AdminClientProvider** — from `@lamalibre/lamaste-server-ui@4.
|
|
354
|
-
3. **ToastProvider** — from `@lamalibre/lamaste-server-ui@4.
|
|
355
|
-
4. **TwoFaProvider** — from `@lamalibre/lamaste-server-ui@4.
|
|
353
|
+
2. **AdminClientProvider** — from `@lamalibre/lamaste-server-ui@4.6.0`, supplies the host-specific data client (`webAdminClient` for browser fetch, Tauri `invoke` for desktop)
|
|
354
|
+
3. **ToastProvider** — from `@lamalibre/lamaste-server-ui@4.6.0`, notification toast system
|
|
355
|
+
4. **TwoFaProvider** — from `@lamalibre/lamaste-server-ui@4.6.0`, handles 2FA verification prompts when the panel has 2FA enabled
|
|
356
356
|
5. **BrowserRouter** — React Router for client-side navigation
|
|
357
357
|
|
|
358
358
|
## Documentation Viewer
|
|
@@ -453,7 +453,7 @@ See [nginx-configuration.md](./nginx-configuration.md) for detailed coverage.
|
|
|
453
453
|
|
|
454
454
|
### chisel-users.js — Chisel Credentials and Grants
|
|
455
455
|
|
|
456
|
-
- Thin shim over `@lamalibre/lamaste@4.
|
|
456
|
+
- Thin shim over `@lamalibre/lamaste@4.6.0/server`: per-agent credentials in `chisel-credentials.json` (0600)
|
|
457
457
|
- Renders the authfile `chisel-users` from the credentials and `tunnels.json`: each `agent-<label>` may bind exactly `^R:127\.0\.0\.1:<port>$` for its enabled tunnels
|
|
458
458
|
- Adds the `lamaste-no-grants` sentinel user (password in `chisel-sentinel`, 0600) so the authfile is never empty — Chisel disables authentication on an empty authfile
|
|
459
459
|
- Never grants a reserved port (3100, 9090, 9091, 9292, 9294) or a port claimed by two tunnels
|
|
@@ -495,6 +495,7 @@ If the authfile cannot be established, Chisel is **stopped and disabled** and th
|
|
|
495
495
|
- Writes YAML configuration (bcrypt cost 12, file-based users, TOTP, session cookies) directly, without sudo: `/etc/authelia` is `lamaste:lamaste-authelia 2770` (setgid), files are written as a temp file + fsync + rename (`configuration.yml` 0640, `users.yml` 0660, `.secrets.json` 0600)
|
|
496
496
|
- User CRUD: creates users with bcrypt-hashed passwords, reads/writes `users.yml`
|
|
497
497
|
- Stores a user's TOTP secret with `sudo lamaste-priv authelia-totp <username>` (secret on stdin), which runs Authelia's CLI as `lamaste-authelia` — the database is that account's file
|
|
498
|
+
- Deletes a removed user's second factors (`sudo lamaste-priv authelia-user purge-second-factors <username>`: TOTP configuration and WebAuthn credentials) and asks whether Authelia holds an OpenID Connect opaque identifier for a username (`… has-oidc-identifier <username>`, `yes`/`no`) before it gives the username to a new user — core `server/authelia-user-storage.ts`
|
|
498
499
|
- TOTP generation: `crypto.randomBytes(20)` → base32-encoded secret → `otpauth://` URI (parameterized `generateTotpSecret(username, { issuer })` — reused by panel 2FA via `lib/totp.js`)
|
|
499
500
|
- `base32Decode()` utility for decoding base32 TOTP secrets into raw bytes
|
|
500
501
|
- Manages service lifecycle
|
|
@@ -535,7 +536,7 @@ Agent-to-agent authorization with scopes, instances, tickets, and sessions. Prov
|
|
|
535
536
|
- **Periodic cleanup** — stale/dead instances, expired tickets, dead sessions
|
|
536
537
|
- **Concurrency** — promise-chain mutex with atomic file writes (temp → fsync → rename)
|
|
537
538
|
|
|
538
|
-
Client SDK: `@lamalibre/lamaste-tickets@4.
|
|
539
|
+
Client SDK: `@lamalibre/lamaste-tickets@4.6.0` (TypeScript, undici) provides `TicketClient`, `TicketInstanceManager` (source side), and `TicketSessionManager` (target side) for plugin integration.
|
|
539
540
|
|
|
540
541
|
State files: `/etc/lamalibre/lamaste/ticket-scopes.json` (scopes, instances, assignments) and `/etc/lamalibre/lamaste/tickets.json` (tickets, sessions).
|
|
541
542
|
|