@lamalibre/create-lamaste 4.2.0 → 4.3.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.
Files changed (123) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +205 -2
  3. package/src/index.js +4 -4
  4. package/src/lib/node-version.js +114 -0
  5. package/src/lib/relay-services.js +15 -1
  6. package/src/lib/service-config.js +113 -0
  7. package/src/tasks/gatekeeper.js +8 -3
  8. package/src/tasks/node.js +34 -75
  9. package/src/tasks/redeploy.js +36 -0
  10. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  11. package/vendor/docs/00-introduction/quickstart.md +5 -5
  12. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  13. package/vendor/docs/01-concepts/authentication.md +6 -3
  14. package/vendor/docs/01-concepts/authorization.md +29 -17
  15. package/vendor/docs/01-concepts/certificates.md +3 -3
  16. package/vendor/docs/01-concepts/mtls.md +2 -2
  17. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
  18. package/vendor/docs/01-concepts/security-model.md +98 -34
  19. package/vendor/docs/01-concepts/tickets.md +2 -2
  20. package/vendor/docs/01-concepts/tunneling.md +4 -4
  21. package/vendor/docs/02-guides/agent-setup.md +34 -16
  22. package/vendor/docs/02-guides/certificate-management.md +2 -2
  23. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  24. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  25. package/vendor/docs/02-guides/installation.md +3 -3
  26. package/vendor/docs/02-guides/onboarding.md +2 -2
  27. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  28. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  29. package/vendor/docs/03-architecture/installer.md +4 -4
  30. package/vendor/docs/03-architecture/management-flow.md +1 -1
  31. package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
  32. package/vendor/docs/03-architecture/overview.md +8 -8
  33. package/vendor/docs/03-architecture/panel-client.md +6 -6
  34. package/vendor/docs/03-architecture/panel-server.md +2 -2
  35. package/vendor/docs/03-architecture/state-management.md +2 -2
  36. package/vendor/docs/03-architecture/system-overview.md +1 -1
  37. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
  38. package/vendor/docs/04-api-reference/app-host.md +360 -0
  39. package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
  40. package/vendor/docs/04-api-reference/certificates.md +95 -38
  41. package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
  42. package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
  43. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  44. package/vendor/docs/04-api-reference/overview.md +137 -111
  45. package/vendor/docs/04-api-reference/registry-and-oidc.md +7 -3
  46. package/vendor/docs/04-api-reference/tunnels.md +15 -13
  47. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  48. package/vendor/docs/05-operations/uninstalling.md +1 -1
  49. package/vendor/docs/05-operations/upgrades.md +25 -20
  50. package/vendor/docs/06-reference/config-files.md +190 -97
  51. package/vendor/docs/06-reference/glossary.md +3 -3
  52. package/vendor/docs/06-reference/installer-flags.md +13 -13
  53. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  54. package/vendor/docs/06-reference/troubleshooting.md +33 -8
  55. package/vendor/docs/_index.json +20 -0
  56. package/vendor/docs/gatekeeper-design.md +22 -19
  57. package/vendor/docs/identity-directory-design.md +57 -56
  58. package/vendor/docs/package.json +1 -1
  59. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  60. package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
  61. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  62. package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
  63. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  64. package/vendor/gatekeeper/dist/server/index.js +47 -8
  65. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  66. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
  68. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  69. package/vendor/gatekeeper/package.json +4 -3
  70. package/vendor/server/bin/lamaste-server.js +18 -1
  71. package/vendor/server/package.json +2 -2
  72. package/vendor/server/src/commands/audit.js +60 -0
  73. package/vendor/server/src/commands/plugins.js +963 -12
  74. package/vendor/server/src/commands/push-install.js +317 -0
  75. package/vendor/server/src/commands/registry-credential.js +2 -1
  76. package/vendor/server/src/commands/uninstall.js +28 -1
  77. package/vendor/server/src/ndjson.js +4 -2
  78. package/vendor/server/src/panel-api.js +32 -4
  79. package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
  80. package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +84 -0
  81. package/vendor/server-ui/dist/index.html +2 -2
  82. package/vendor/serverd/package.json +2 -2
  83. package/vendor/serverd/src/index.js +56 -4
  84. package/vendor/serverd/src/lib/actor.js +24 -0
  85. package/vendor/serverd/src/lib/agent-jobs.js +330 -0
  86. package/vendor/serverd/src/lib/app-error.js +10 -0
  87. package/vendor/serverd/src/lib/audit.js +145 -0
  88. package/vendor/serverd/src/lib/hook-effects.js +123 -0
  89. package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
  90. package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
  91. package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
  92. package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
  93. package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
  94. package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
  95. package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
  96. package/vendor/serverd/src/lib/mtls.js +89 -8
  97. package/vendor/serverd/src/lib/nginx.js +298 -44
  98. package/vendor/serverd/src/lib/notifications.js +107 -0
  99. package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
  100. package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
  101. package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
  102. package/vendor/serverd/src/lib/plugin-operations.js +295 -0
  103. package/vendor/serverd/src/lib/plugin-packages.js +249 -0
  104. package/vendor/serverd/src/lib/plugins.js +298 -68
  105. package/vendor/serverd/src/lib/push-install.js +88 -2
  106. package/vendor/serverd/src/lib/relay-hooks.js +100 -0
  107. package/vendor/serverd/src/lib/relay-signing.js +43 -0
  108. package/vendor/serverd/src/lib/request-hooks.js +79 -0
  109. package/vendor/serverd/src/lib/retained-data.js +99 -0
  110. package/vendor/serverd/src/lib/trust-proxy.js +34 -0
  111. package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
  112. package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
  113. package/vendor/serverd/src/middleware/twofa-session.js +11 -1
  114. package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
  115. package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
  116. package/vendor/serverd/src/routes/management/agents.js +262 -1
  117. package/vendor/serverd/src/routes/management/audit.js +81 -0
  118. package/vendor/serverd/src/routes/management/certs.js +1 -15
  119. package/vendor/serverd/src/routes/management/plugins.js +569 -54
  120. package/vendor/serverd/src/routes/management/tunnels.js +26 -0
  121. package/vendor/serverd/src/routes/management.js +4 -0
  122. package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
  123. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
@@ -312,10 +312,40 @@ All three also declare the same request headers inside their `location /` (`TUNN
312
312
  - **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
313
  - **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.
314
314
  - **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 are unchanged. See [Security Model](../01-concepts/security-model.md#5c-the-agent-daemon-lamaste-agentd-and-relay-assertions).
315
316
 
316
317
  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.
317
318
 
318
- Agent panel tunnels (`type: 'panel'`) use a separate mTLS vhost (`writeAgentPanelVhost()`) with no body limit setting.
319
+ Agent panel tunnels (`type: 'panel'`) use a separate mTLS vhost (`writeAgentPanelVhost()`) with no body limit setting. Every request on it is authorized by the panel, which signs the relay assertion the agent's lamaste-agentd requires:
320
+
321
+ ```nginx
322
+ # Authorization by the panel: refuses revoked and foreign certificates,
323
+ # otherwise answers with the relay assertion for this one request
324
+ location = /internal/lamaste/agent-panel-authz {
325
+ internal;
326
+ proxy_pass http://127.0.0.1:3100/api/agent-panel/authz/<label>;
327
+ proxy_pass_request_body off;
328
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
329
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
330
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
331
+ proxy_set_header X-Original-Method $request_method;
332
+ proxy_set_header X-Original-URI $request_uri;
333
+ proxy_set_header X-Forwarded-For $remote_addr;
334
+ # ... Cookie, Authorization and Remote-* cleared
335
+ }
336
+
337
+ location / {
338
+ if ($ssl_client_verify != SUCCESS) { return 496; }
339
+ auth_request /internal/lamaste/agent-panel-authz;
340
+ auth_request_set $relay_assertion $upstream_http_x_lamalibre_relay_assertion;
341
+ proxy_pass http://127.0.0.1:<port>;
342
+ proxy_set_header X-Lamalibre-Relay-Assertion $relay_assertion;
343
+ # X-SSL-Client-* are still sent for agents older than 4.3; lamaste-agentd 4.3 removes them unread
344
+ # ...
345
+ }
346
+ ```
347
+
348
+ `location /` has no rewrite: the daemon must receive exactly the `$request_uri` the assertion names. The panel admits the admin certificate and the agent's own (`403` otherwise, also when the agent has no enabled panel tunnel); the panel's 2FA does not apply to this subrequest.
319
349
 
320
350
  ### 6. Static Site Vhost (`lamalibre-lamaste-site-<id>`)
321
351
 
@@ -336,7 +366,8 @@ server {
336
366
  add_header X-Frame-Options SAMEORIGIN always;
337
367
  add_header X-Content-Type-Options nosniff always;
338
368
 
339
- # Optional Authelia forward auth (auth_request to 127.0.0.1:9091/api/authz/auth-request)
369
+ # Optional Authelia forward auth (auth_request to 127.0.0.1:9091/api/authz/auth-request,
370
+ # X-Original-URL https://blog.example.com$request_uri — the site's own hostname)
340
371
 
341
372
  location / {
342
373
  try_files $uri $uri/ =404; # Standard mode
@@ -362,7 +393,7 @@ Two serving modes based on the `spaMode` flag:
362
393
  - **Standard**: `try_files $uri $uri/ =404` — serves files as-is, 404 for missing paths
363
394
  - **SPA**: `try_files $uri $uri/ /index.html` — falls back to `index.html` for client-side routing
364
395
 
365
- Optional Authelia protection is controlled by the `autheliaProtected` flag on the site object.
396
+ Optional Authelia protection is controlled by the `autheliaProtected` flag on the site object. Authelia's rules for a site restricted to some users (`allowedUsers`) are keyed by the site's hostname, so the subrequest names that hostname, never the client's `Host` header (see [below](#authorization-subrequests-name-their-own-hostname)).
366
397
 
367
398
  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.
368
399
 
@@ -417,19 +448,21 @@ server {
417
448
  # ... standard SSL settings
418
449
  client_max_body_size 10m;
419
450
 
420
- # Gatekeeper authorization subrequest (handles Authelia validation + grant check)
451
+ # Gatekeeper authorization subrequest (handles Authelia validation + grant check).
452
+ # The tunnel is named in the path — never by the client's Host header.
421
453
  location /internal/lamaste/authz {
422
454
  internal;
423
455
 
424
- proxy_pass http://127.0.0.1:9294/authz/check;
456
+ proxy_pass http://127.0.0.1:9294/authz/tunnel/app.example.com;
425
457
  proxy_pass_request_body off;
426
458
 
427
459
  proxy_set_header Content-Length "";
428
460
  proxy_set_header Connection "";
429
461
  proxy_set_header X-Original-Method $request_method;
430
- proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
462
+ proxy_set_header X-Original-URL https://app.example.com$request_uri;
431
463
  proxy_set_header X-Forwarded-For $remote_addr;
432
- proxy_set_header Host $http_host;
464
+ # ... X-Forwarded-Host, -Proto, -Uri and -Method cleared ("")
465
+ proxy_set_header Host app.example.com;
433
466
  proxy_set_header Cookie $http_cookie;
434
467
 
435
468
  proxy_http_version 1.1;
@@ -459,15 +492,16 @@ server {
459
492
  proxy_read_timeout 86400s;
460
493
  proxy_send_timeout 86400s;
461
494
 
462
- # Authentication failure -> redirect to Authelia login
463
- error_page 401 =302 https://auth.<domain>/?rd=$scheme://$http_host$request_uri;
495
+ # Authentication failure -> redirect to Authelia login, back to this vhost
496
+ error_page 401 =302 https://auth.<domain>/?rd=https://app.example.com$request_uri;
464
497
  }
465
498
  }
466
499
  ```
467
500
 
468
501
  Key characteristics:
469
502
 
470
- - `auth_request` targets the Gatekeeper at `127.0.0.1:9294/authz/check` instead of Authelia directly
503
+ - `auth_request` targets the Gatekeeper at `127.0.0.1:9294/authz/tunnel/<fqdn>` instead of Authelia directly
504
+ - 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))
471
505
  - The Gatekeeper validates the Authelia session cookie and returns 200 (authenticated) or 401 (unauthenticated)
472
506
  - The upstream receives `Remote-*` headers only as set from the Gatekeeper's response; client-supplied values under those names are replaced
473
507
  - 401 redirects to the Authelia login portal
@@ -489,19 +523,21 @@ server {
489
523
  # ... standard SSL settings
490
524
  client_max_body_size 10m;
491
525
 
492
- # Gatekeeper authorization subrequest (handles Authelia validation + grant check)
526
+ # Gatekeeper authorization subrequest (handles Authelia validation + grant check).
527
+ # The tunnel is named in the path — never by the client's Host header.
493
528
  location /internal/lamaste/authz {
494
529
  internal;
495
530
 
496
- proxy_pass http://127.0.0.1:9294/authz/check;
531
+ proxy_pass http://127.0.0.1:9294/authz/tunnel/restricted-app.example.com;
497
532
  proxy_pass_request_body off;
498
533
 
499
534
  proxy_set_header Content-Length "";
500
535
  proxy_set_header Connection "";
501
536
  proxy_set_header X-Original-Method $request_method;
502
- proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
537
+ proxy_set_header X-Original-URL https://restricted-app.example.com$request_uri;
503
538
  proxy_set_header X-Forwarded-For $remote_addr;
504
- proxy_set_header Host $http_host;
539
+ # ... X-Forwarded-Host, -Proto, -Uri and -Method cleared ("")
540
+ proxy_set_header Host restricted-app.example.com;
505
541
  proxy_set_header Cookie $http_cookie;
506
542
 
507
543
  proxy_http_version 1.1;
@@ -531,8 +567,8 @@ server {
531
567
  proxy_read_timeout 86400s;
532
568
  proxy_send_timeout 86400s;
533
569
 
534
- # Authentication failure -> redirect to Authelia login
535
- error_page 401 =302 https://auth.<domain>/?rd=$scheme://$http_host$request_uri;
570
+ # Authentication failure -> redirect to Authelia login, back to this vhost
571
+ error_page 401 =302 https://auth.<domain>/?rd=https://restricted-app.example.com$request_uri;
536
572
 
537
573
  # Authorization failure -> Gatekeeper serves inline access-request page
538
574
  error_page 403 = /internal/lamaste/authz;
@@ -546,6 +582,16 @@ Key differences from the authenticated vhost:
546
582
  - `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
547
583
  - 401 (unauthenticated) still redirects to Authelia login as before
548
584
 
585
+ ### Authorization subrequests name their own hostname
586
+
587
+ 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`.
588
+
589
+ nginx selects the vhost from the request line when a request is in absolute form (`GET https://a.example.com/x HTTP/1.1`) and from `Host` otherwise, so a client can send the request line of one hostname with the `Host` header of another. Vhosts written before 4.3 built `X-Original-URL` and `Host` from `$http_host` — the client's `Host` header — so the subrequest asked about the other hostname while nginx served this one: an Authelia user allowed on any tunnel (every user, on an `authenticated` tunnel) reached any `restricted` tunnel, app or plugin, without a grant, and any Authelia user reached a static site restricted to other users. `$host` would not fix it — on the default server it is whatever `Host` the client sent. `$request_uri` is what follows the host in the request target — it starts with `/`, or with `?` for an absolute-form target without a path — so it cannot change the URL's host; the scheme is fixed because every such vhost listens on 443 with TLS only. The generators refuse a hostname that is not a plain DNS name, so nothing but a literal reaches these headers. For the auth portal the change is hardening: no rule Lamaste writes for Authelia is more permissive than the portal's own (the default, `two_factor`), so naming another hostname there gained nothing.
590
+
591
+ Gatekeeper authorizes the tunnel named in the path, and refuses a check whose `X-Original-URL` or `Host` names another hostname, or has no `Host` (401). It refuses `/authz/check` — what vhosts written before 4.3 call, with the client's `Host` in both headers — outright (403, logged as `vhost_predates_host_binding`): a tunnel vhost the panel has not rewritten is closed, not open to the users of other tunnels. The panel rewrites them at startup (see [Rewritten at Startup](#rewritten-at-startup)).
592
+
593
+ 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
+
549
595
  ## nginx Cache Zone (`lamaste_authz`)
550
596
 
551
597
  The Gatekeeper authorization responses are cached by nginx to reduce subrequest overhead. The cache zone is defined in a shared snippet:
@@ -562,12 +608,19 @@ proxy_cache_path /var/cache/nginx/authz levels=1:2 keys_zone=lamaste_authz:1m ma
562
608
  | `max_size` | `10m` | 10 MB maximum disk storage for cached responses |
563
609
  | `inactive` | `5m` | Evict entries not accessed within 5 minutes |
564
610
 
565
- **Cache key:** `$cookie_authelia_session$http_host` — ensures each user session gets its own cache entry per host, preventing authorization decisions from leaking across users or subdomains.
611
+ The installer also writes the location-level policy, `/etc/nginx/snippets/lamalibre-lamaste-authz-cache-loc.conf`:
612
+
613
+ ```nginx
614
+ proxy_cache lamaste_authz;
615
+ proxy_cache_key "$server_name|$cookie_authelia_session";
616
+ proxy_cache_valid 200 10s;
617
+ proxy_cache_valid 403 0;
618
+ proxy_cache_use_stale off;
619
+ ```
566
620
 
567
- **Cache TTLs (when used):**
621
+ **Cache key:** `$server_name|$cookie_authelia_session` — one entry per vhost and user session. `$server_name` is the vhost's own name, first and followed by a character no hostname contains, so no other vhost and session give the same key. `$http_host` (in the key before 4.3) is the client's `Host` header, which can name another tunnel than the vhost serving the request (see [above](#authorization-subrequests-name-their-own-hostname)), so a decision cached for that other tunnel would have been replayed. A relay installed before 4.3 keeps the old snippet until it is reinstalled; no vhost includes it.
568
622
 
569
- - `200` (authorized): 30 seconds — reduces subrequests during rapid page loads
570
- - `401` / `403` (denied): 10 seconds — allows quick re-check after login or grant changes
623
+ **Cache TTLs (when used):** `200` (authorized) for 10 seconds, matching the `Cache-Control: max-age=10` Gatekeeper sends; `403` never, so a new grant takes effect at once. Gatekeeper marks a plugin tunnel's answer `no-store` — it carries a single-use relay assertion.
571
624
 
572
625
  > **Note:** The cache zone is installed by the Gatekeeper installer task, but the generated vhosts (`buildGatekeeperVhost()`) do not currently include `proxy_cache` directives in the internal location block. Authorization caching currently relies on the Gatekeeper's in-memory session cache (30-second TTL). The nginx cache zone is available for future use if nginx-level caching is needed.
573
626
 
@@ -674,14 +727,15 @@ Adding a directive to a generator therefore means adding it to the allow-list in
674
727
 
675
728
  ## Forward Auth Block
676
729
 
677
- The Authelia forward auth integration uses nginx's `auth_request` module. Protected static sites call Authelia directly as shown below; `restricted` and `authenticated` tunnels send the same subrequest to Gatekeeper (`127.0.0.1:9294/authz/check`), which validates the Authelia session and, for restricted tunnels, the grant (sections 8 and 9). Here is the flow:
730
+ The Authelia forward auth integration uses nginx's `auth_request` module. Protected static sites call Authelia directly as shown below; `restricted` and `authenticated` tunnels send the same subrequest to Gatekeeper (`127.0.0.1:9294/authz/tunnel/<fqdn>`), which validates the Authelia session and, for restricted tunnels, the grant (sections 8 and 9). Here is the flow:
678
731
 
679
732
  ```
680
733
  1. User requests https://myapp.example.com/page
681
734
  │
682
735
  2. nginx sends internal subrequest to /internal/lamaste/authz
683
736
  │ ├── proxy_pass → http://127.0.0.1:9091/api/authz/auth-request
684
- │ ├── Sends X-Original-URL, X-Original-Method headers
737
+ │ ├── Sends X-Original-URL (https://myapp.example.com/page — the vhost's
738
+ │ │ own hostname, never the client's Host header), X-Original-Method
685
739
  │ └── Strips request body (Content-Length: "")
686
740
  │
687
741
  3. Authelia checks the session cookie
@@ -783,12 +837,23 @@ Without it nothing listens on port 80 and `http://` links to the relay's hostnam
783
837
 
784
838
  ### Created/Removed at Runtime
785
839
 
786
- | Vhost | Created | Removed |
787
- | ------------------------------ | ----------------- | ----------------------- |
788
- | `lamalibre-lamaste-app-<name>` | POST /api/tunnels | DELETE /api/tunnels/:id |
840
+ | Vhost | Created | Removed |
841
+ | ------------------------------- | ----------------- | ----------------------- |
842
+ | `lamalibre-lamaste-app-<name>` | POST /api/tunnels | DELETE /api/tunnels/:id |
843
+ | `lamalibre-lamaste-site-<uuid>` | POST /api/sites | DELETE /api/sites/:id |
789
844
 
790
845
  `PATCH /api/tunnels/:id` removes or restores the `sites-enabled` symlink when a tunnel is disabled or enabled (the file in `sites-available` is kept), and re-renders the vhost in place when `accessMode` or `maxBodySizeMb` changes. `PATCH /api/sites/:id` re-renders a site vhost when `spaMode`, `autheliaProtected` or `aliases` change.
791
- | `lamalibre-lamaste-site-<uuid>` | POST /api/sites | DELETE /api/sites/:id |
846
+
847
+ ### Rewritten at Startup
848
+
849
+ 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
+
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, and the body limit only when the tunnel has one. After listen because a rewritten agent-panel vhost asks the panel about every request.
853
+
854
+ 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
+
856
+ A tunnel vhost left as it was fails closed: Gatekeeper refuses its `/authz/check` subrequests, so the tunnel answers 403 until it is rewritten. A static site or auth portal vhost left as it was still names the client's `Host` to Authelia. After an upgrade, check the panel's startup log for `Left tunnel vhosts as they are`, `Left site vhosts as they are` and `Failed to rewrite … vhost`, fix what they name (a failing `nginx -t`, a missing certificate) and restart `lamalibre-lamaste-serverd`.
792
857
 
793
858
  ## Key Files
794
859
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  Lamaste has three main pieces:
8
8
 
9
- 1. **The Installer** (`npx @lamalibre/create-lamaste@4.2.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.
9
+ 1. **The Installer** (`npx @lamalibre/create-lamaste@4.3.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
 
@@ -176,7 +176,7 @@ The Gatekeeper is a dedicated Fastify service on `127.0.0.1:9294` that acts as a
176
176
  **Architecture:**
177
177
 
178
178
  ```
179
- End User → nginx :443 → auth_request → 127.0.0.1:9294/authz/check
179
+ End User → nginx :443 → auth_request → 127.0.0.1:9294/authz/tunnel/<fqdn>
180
180
  │
181
181
  ┌─────────┴──────────┐
182
182
  │ Gatekeeper │
@@ -198,8 +198,8 @@ End User → nginx :443 → auth_request → 127.0.0.1:9294/authz/check
198
198
 
199
199
  **Two-layer caching:**
200
200
 
201
- 1. **nginx `proxy_cache`** — the `lamaste_authz` cache zone (1m keys, 10m storage) caches authorization responses. Cache key is `$cookie_authelia_session$http_host`. Positive responses (200) are cached for 30 seconds, negative responses (401/403) for 10 seconds. This eliminates redundant auth_request subrequests for rapid page loads.
202
- 2. **In-memory session cache** — the Gatekeeper maintains a `Map` of recent authorization decisions keyed by session + host. This avoids re-reading state files on every check within the cache TTL.
201
+ 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
+ 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
203
 
204
204
  **State files:** All stored in `/etc/lamalibre/lamaste/` with 0600 permissions, `lamaste:lamaste` ownership, and atomic writes (tmp, fsync, rename):
205
205
 
@@ -321,7 +321,7 @@ lamaste/
321
321
  │ │ ├── files.js ← Static site file operations (upload, delete, list)
322
322
  │ │ └── app-error.js ← Operational error class (AppError)
323
323
  │ │
324
- │ ├── lamaste-server-ui/ ← React 18 + Vite + Tailwind SPA + shared admin components (@lamalibre/lamaste-server-ui@4.2.0)
324
+ │ ├── lamaste-server-ui/ ← React 18 + Vite + Tailwind SPA + shared admin components (@lamalibre/lamaste-server-ui@4.3.0)
325
325
  │ │ └── src/
326
326
  │ │ ├── index.js ← Package exports (pages, context, utilities)
327
327
  │ │ ├── context/
@@ -337,7 +337,7 @@ lamaste/
337
337
  │ │ │ └── formatters.js ← Byte, uptime, relative time formatters
338
338
  │ │ └── pages/ ← Admin pages (Dashboard, Tunnels, Sites, Users, Certs, Services, Plugins, Tickets, Settings)
339
339
  │ │
340
- │ ├── lamaste-agent-ui/ ← Shared React agent UI (pages, context, components) (@lamalibre/lamaste-agent-ui@4.2.0)
340
+ │ ├── lamaste-agent-ui/ ← Shared React agent UI (pages, context, components) (@lamalibre/lamaste-agent-ui@4.3.0)
341
341
  │ │
342
342
  │ ├── lamaste-tickets/ ← Agent-to-agent authorization SDK (mTLS, undici)
343
343
  │ │ └── src/
@@ -377,7 +377,7 @@ lamaste/
377
377
  │ │ │ └── CompleteStep.jsx ← Credentials + TOTP QR + next steps
378
378
  │ │ ├── management/ ← Legacy standalone implementations (kept for reference)
379
379
  │ │ │ ├── Certificates.jsx ← Standalone certificates page (dead code)
380
- │ │ │ ├── Dashboard.jsx ← Standalone dashboard (not imported — App.jsx uses @lamalibre/lamaste-server-ui@4.2.0)
380
+ │ │ │ ├── Dashboard.jsx ← Standalone dashboard (not imported — App.jsx uses @lamalibre/lamaste-server-ui@4.3.0)
381
381
  │ │ │ ├── Plugins.jsx ← Standalone plugins page (dead code)
382
382
  │ │ │ ├── Services.jsx ← Standalone services page (dead code)
383
383
  │ │ │ ├── Settings.jsx ← Standalone settings page (dead code)
@@ -486,7 +486,7 @@ The Panel Server runs as the `lamaste` user with minimal privileges and cannot b
486
486
  The full lifecycle of a Lamaste installation:
487
487
 
488
488
  ```
489
- Phase 1: Installation (npx @lamalibre/create-lamaste@4.2.0)
489
+ Phase 1: Installation (npx @lamalibre/create-lamaste@4.3.0)
490
490
  └─ OS hardening, Node.js, mTLS certs, nginx, panel deploy
491
491
  └─ Result: Panel accessible at https://<ip>:9292
492
492
 
@@ -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.2.0`. This package provides both the browser SPA build and the shared admin components consumed by `lamaste-desktop` (Tauri app).
13
+ The management-mode admin pages (Dashboard, Tunnels, Sites, Users, Certificates, Services, Plugins, Tickets, Logs, Settings) live in `@lamalibre/lamaste-server-ui@4.3.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.2.0`. This package is consumed by `lamaste-desktop` for agent mode.
15
+ The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are extracted into a separate shared package: `@lamalibre/lamaste-agent-ui@4.3.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.2.0` package and wrapped with the `AdminClientProvider` (which supplies the web-specific data client):
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.3.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.2.0`, supplies the host-specific data client (`webAdminClient` for browser fetch, Tauri `invoke` for desktop)
354
- 3. **ToastProvider** — from `@lamalibre/lamaste-server-ui@4.2.0`, notification toast system
355
- 4. **TwoFaProvider** — from `@lamalibre/lamaste-server-ui@4.2.0`, handles 2FA verification prompts when the panel has 2FA enabled
353
+ 2. **AdminClientProvider** — from `@lamalibre/lamaste-server-ui@4.3.0`, supplies the host-specific data client (`webAdminClient` for browser fetch, Tauri `invoke` for desktop)
354
+ 3. **ToastProvider** — from `@lamalibre/lamaste-server-ui@4.3.0`, notification toast system
355
+ 4. **TwoFaProvider** — from `@lamalibre/lamaste-server-ui@4.3.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.2.0/server`: per-agent credentials in `chisel-credentials.json` (0600)
456
+ - Thin shim over `@lamalibre/lamaste@4.3.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
@@ -535,7 +535,7 @@ Agent-to-agent authorization with scopes, instances, tickets, and sessions. Prov
535
535
  - **Periodic cleanup** — stale/dead instances, expired tickets, dead sessions
536
536
  - **Concurrency** — promise-chain mutex with atomic file writes (temp → fsync → rename)
537
537
 
538
- Client SDK: `@lamalibre/lamaste-tickets@4.2.0` (TypeScript, undici) provides `TicketClient`, `TicketInstanceManager` (source side), and `TicketSessionManager` (target side) for plugin integration.
538
+ Client SDK: `@lamalibre/lamaste-tickets@4.3.0` (TypeScript, undici) provides `TicketClient`, `TicketInstanceManager` (source side), and `TicketSessionManager` (target side) for plugin integration.
539
539
 
540
540
  State files: `/etc/lamalibre/lamaste/ticket-scopes.json` (scopes, instances, assignments) and `/etc/lamalibre/lamaste/tickets.json` (tickets, sessions).
541
541
 
@@ -331,7 +331,7 @@ Read returns `[]` for missing files (ENOENT), enabling the system to start with
331
331
  5. `DELETE /api/certs/agent/:label` (revocation) — the agent's app and plugin tunnels lose their `agentLabel` (unassigned), and its `agent-<label>` panel tunnel is removed
332
332
  6. At startup (background, fail closed): an ownerless `agent-<label>` panel tunnel is bound to the agent it names when that agent is active; other ownerless tunnels (from versions before tunnel ownership) are bound to the sole active agent, or left unassigned when there are several. Then the pinned Chisel binary, the unit and the authfile are re-established — and Chisel is stopped if they cannot be
333
333
 
334
- Every one of these workflows runs under the single tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.2.0/server`), with its validation inside the lock.
334
+ Every one of these workflows runs under the single tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.3.0/server`), with its validation inside the lock.
335
335
 
336
336
  ## Site State (`sites.json`)
337
337
 
@@ -486,7 +486,7 @@ The PKI directory belongs to `lamaste` because the panel is the CA (it signs age
486
486
 
487
487
  The Panel Server is a single-process Node.js application, but a single-threaded event loop does not serialize asynchronous workflows: two requests that each read `tunnels.json`, await a certificate or an nginx test, and then write it back would interleave, and both could pass the same uniqueness check. Serialization is therefore explicit:
488
488
 
489
- **Tunnel state** is guarded by one promise-chain lock, `withTunnelLock` in `@lamalibre/lamaste@4.2.0/server`'s `tunnels.ts`. Every tunnel workflow — create, delete, toggle, assign, reconfigure, the combined `PATCH /api/tunnels/:id`, releasing a revoked agent's tunnels, and startup reconciliation — takes it for its whole duration, and does its validation (subdomain and port uniqueness, reserved ports, agent checks, authorization against the current entry) inside it.
489
+ **Tunnel state** is guarded by one promise-chain lock, `withTunnelLock` in `@lamalibre/lamaste@4.3.0/server`'s `tunnels.ts`. Every tunnel workflow — create, delete, toggle, assign, reconfigure, the combined `PATCH /api/tunnels/:id`, releasing a revoked agent's tunnels, and startup reconciliation — takes it for its whole duration, and does its validation (subdomain and port uniqueness, reserved ports, agent checks, authorization against the current entry) inside it.
490
490
 
491
491
  **Chisel authfile renders** are serialized by a promise-chain mutex in `chisel-users.ts`, keyed by the credentials file, around every render (enroll, revoke, rotate, tunnel change). The daemon's `syncChisel()` renders after the tunnel state is written and restarts Chisel (`systemctl try-restart`) only when the render withdrew something.
492
492
 
@@ -58,7 +58,7 @@
58
58
  │ │ │ │
59
59
  │ │ Lamaste Desktop (Tauri v2) — dual mode │ │
60
60
  │ │ ├─ Agent mode: service discovery + tunnel management │ │
61
- │ │ │ (pages from @lamalibre/lamaste-agent-ui@4.2.0) │ │
61
+ │ │ │ (pages from @lamalibre/lamaste-agent-ui@4.3.0) │ │
62
62
  │ │ ├─ Server mode: full admin panel (via lamaste-server-ui) │ │
63
63
  │ │ ├─ Multi-server registry (~/.lamalibre/lamaste/servers.json) │ │
64
64
  │ │ ├─ Cloud provisioning (DigitalOcean via lamaste-cloud) │ │