@lamalibre/create-lamaste 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +200 -16
  3. package/src/index.js +1 -0
  4. package/src/lib/backend-cookie.js +65 -0
  5. package/src/lib/ip-panel-vhost.js +208 -0
  6. package/src/lib/service-config.js +5 -3
  7. package/src/tasks/nginx.js +18 -135
  8. package/src/tasks/redeploy.js +39 -1
  9. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  10. package/vendor/docs/00-introduction/quickstart.md +5 -5
  11. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  12. package/vendor/docs/01-concepts/authentication.md +1 -1
  13. package/vendor/docs/01-concepts/authorization.md +66 -48
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/mtls.md +2 -2
  16. package/vendor/docs/01-concepts/security-model.md +70 -33
  17. package/vendor/docs/01-concepts/tickets.md +2 -2
  18. package/vendor/docs/01-concepts/tunneling.md +4 -4
  19. package/vendor/docs/02-guides/agent-setup.md +5 -5
  20. package/vendor/docs/02-guides/certificate-management.md +2 -2
  21. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  22. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  23. package/vendor/docs/02-guides/installation.md +3 -3
  24. package/vendor/docs/02-guides/managing-users.md +113 -25
  25. package/vendor/docs/02-guides/onboarding.md +2 -2
  26. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  27. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  28. package/vendor/docs/03-architecture/installer.md +4 -4
  29. package/vendor/docs/03-architecture/management-flow.md +3 -1
  30. package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
  31. package/vendor/docs/03-architecture/overview.md +13 -17
  32. package/vendor/docs/03-architecture/panel-client.md +6 -6
  33. package/vendor/docs/03-architecture/panel-server.md +3 -2
  34. package/vendor/docs/03-architecture/state-management.md +4 -8
  35. package/vendor/docs/03-architecture/system-overview.md +1 -1
  36. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +292 -23
  37. package/vendor/docs/04-api-reference/app-host.md +3 -3
  38. package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
  39. package/vendor/docs/04-api-reference/lifecycle-hooks.md +15 -13
  40. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  41. package/vendor/docs/04-api-reference/registry-and-oidc.md +6 -4
  42. package/vendor/docs/04-api-reference/tunnels.md +67 -29
  43. package/vendor/docs/04-api-reference/users.md +241 -45
  44. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  45. package/vendor/docs/05-operations/monitoring.md +39 -0
  46. package/vendor/docs/05-operations/uninstalling.md +2 -1
  47. package/vendor/docs/05-operations/upgrades.md +15 -6
  48. package/vendor/docs/06-reference/config-files.md +83 -189
  49. package/vendor/docs/06-reference/glossary.md +3 -3
  50. package/vendor/docs/06-reference/installer-flags.md +13 -13
  51. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  52. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  53. package/vendor/docs/gatekeeper-design.md +10 -8
  54. package/vendor/docs/identity-directory-design.md +48 -14
  55. package/vendor/docs/package.json +1 -1
  56. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  57. package/vendor/gatekeeper/dist/index.d.ts +2 -1
  58. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  59. package/vendor/gatekeeper/dist/index.js +2 -0
  60. package/vendor/gatekeeper/dist/index.js.map +1 -1
  61. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  62. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  63. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  64. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  65. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  66. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  68. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  69. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  70. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  71. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  72. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  73. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
  74. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  75. package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
  76. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  77. package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
  78. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  79. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  80. package/vendor/gatekeeper/dist/server/index.js +22 -48
  81. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  82. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  83. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  84. package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
  85. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  86. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  87. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  88. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  89. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  90. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  91. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  92. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  93. package/vendor/gatekeeper/package.json +2 -2
  94. package/vendor/server/bin/lamaste-server.js +15 -1
  95. package/vendor/server/package.json +2 -2
  96. package/vendor/server/src/commands/plugin-identity.js +335 -0
  97. package/vendor/server/src/commands/plugins.js +99 -11
  98. package/vendor/server/src/commands/registry-credential.js +2 -1
  99. package/vendor/server/src/commands/tunnels.js +102 -2
  100. package/vendor/server/src/commands/uninstall.js +3 -1
  101. package/vendor/server/src/commands/users.js +358 -0
  102. package/vendor/server/src/plugin-approval.js +107 -5
  103. package/vendor/server/src/plugin-identity.js +382 -0
  104. package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
  105. package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +84 -0
  106. package/vendor/server-ui/dist/index.html +2 -2
  107. package/vendor/serverd/package.json +2 -2
  108. package/vendor/serverd/src/index.js +25 -1
  109. package/vendor/serverd/src/lib/audience.js +576 -0
  110. package/vendor/serverd/src/lib/authelia.js +78 -7
  111. package/vendor/serverd/src/lib/config.js +68 -43
  112. package/vendor/serverd/src/lib/directory.js +1113 -0
  113. package/vendor/serverd/src/lib/entitlements.js +517 -0
  114. package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
  115. package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
  116. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  117. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  118. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  119. package/vendor/serverd/src/lib/mtls.js +6 -2
  120. package/vendor/serverd/src/lib/nginx.js +279 -48
  121. package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
  122. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  123. package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
  124. package/vendor/serverd/src/lib/plugin-packages.js +30 -22
  125. package/vendor/serverd/src/lib/plugins.js +94 -15
  126. package/vendor/serverd/src/lib/registry-credential.js +46 -6
  127. package/vendor/serverd/src/lib/request-logging.js +19 -0
  128. package/vendor/serverd/src/lib/retained-data.js +50 -4
  129. package/vendor/serverd/src/lib/state.js +18 -0
  130. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  131. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  132. package/vendor/serverd/src/lib/user-caller.js +67 -0
  133. package/vendor/serverd/src/lib/user-surface.js +733 -0
  134. package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
  135. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  136. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  137. package/vendor/serverd/src/routes/invite.js +53 -12
  138. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  139. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  140. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  141. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  142. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  143. package/vendor/serverd/src/routes/management/tunnels.js +98 -2
  144. package/vendor/serverd/src/routes/management/users.js +290 -208
  145. package/vendor/serverd/src/routes/management.js +4 -0
  146. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  147. package/vendor/serverd/src/routes/user-access.js +17 -4
  148. package/vendor/serverd/src/routes/user-surface.js +69 -0
  149. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  150. package/vendor/server-ui/dist/assets/index-CESyyK8V.js +0 -84
@@ -114,7 +114,8 @@ Every tunnel is carried by exactly one agent (its `agentLabel`), and three thing
114
114
  - **The Chisel authfile.** Each `agent-<label>` Chisel user may reverse-bind exactly `^R:127\.0\.0\.1:<port>$` for the enabled tunnels it owns, and nothing else. A second enrolled machine cannot bind another agent's port and take over its hostname. A `lamaste-no-grants` sentinel user keeps the authfile non-empty, because Chisel disables authentication outright on an empty authfile. The authfile holds every agent's password, so it is `0640`, group `lamaste-chisel` — readable by the Chisel process (`User=nobody`, `Group=lamaste-chisel`) and by nobody else.
115
115
  - **Reserved ports.** A tunnel vhost proxies to `127.0.0.1:<port>` on the relay, so the ports of Lamaste's own services — 3100 (panel), 9090 (Chisel), 9091 (Authelia), 9292 (IP panel listener), 9294 (Gatekeeper) — can never be tunnel ports. A tunnel on 3100 or 9292 would otherwise publish the panel API, which trusts the client-certificate headers nginx sets: an administrator takeover. The rule is enforced by the request schema, inside the tunnel workflow, and again when the authfile grants are rendered; a port claimed by two tunnels is never granted either.
116
116
  - **TLS verification on the agent.** The agent's Chisel client connects to `https://tunnel.<domain>:443` and verifies its Let's Encrypt certificate — there is no `--tls-skip-verify`. A machine on the path cannot impersonate the relay to collect the agent's credential or read tunnelled traffic. The credential itself is delivered through a 0600 environment file (Linux) or 0600 plist (macOS), never on the command line. The agent only ever connects to `tunnel.<enrolled domain>` and refuses a panel that reports a different domain. Agents upgraded from a version that exposed the credential in process arguments and 0644 unit files rotate it once, automatically.
117
- - **Revocation releases tunnels.** Revoking an agent certificate removes its Chisel credential and detaches it from its tunnels (app tunnels become unassigned, its panel tunnel is removed), so a later enrollment reusing the label inherits nothing.
117
+ - **Revocation releases tunnels.** Revoking an agent certificate removes its Chisel credential and detaches it from its tunnels (app tunnels become unassigned, its panel tunnel is removed), so a later enrollment reusing the label inherits nothing. An app tunnel bound as a plugin's user surface keeps its binding but serves none of it from then on (5f), and its vhost loses those locations.
118
+ - **Authelia's session cookie never reaches a backend.** Authelia scopes its session cookie (`authelia_session`) to the base domain, so a browser sends it to every tunnel host. Every nginx location that proxies to a tunnel or agent backend — public, authenticated and restricted app tunnels, plugin tunnels, agent panels — sends the backend `Cookie: $lamaste_backend_cookie`: the browser's other cookies, with every `authelia_session` pair removed by an http-level map create-lamaste installs (`/etc/nginx/conf.d/lamalibre-lamaste-backend-cookie.conf`, rendered by core `renderBackendCookieMap` from the cookie name Authelia is configured with). The map strips up to four pairs and fails closed: a request carrying more gets no `Cookie` header at all. Without it, any application on any agent — or whatever took a tunnel's port on the relay — could harvest every visitor's session and replay it on every authenticated host and every plugin's user surface. See [nginx Configuration](../03-architecture/nginx-configuration.md#the-backend-cookie-map).
118
119
  - **Fail closed at startup.** On every start the panel rewrites the Chisel unit and authfile from its state in the background. If that fails, Chisel is **stopped and disabled** (so a reboot cannot start it either) rather than left running with an authfile from an older version that may grant more than it should; reconciliation retries with backoff until it succeeds, then re-enables and starts it.
119
120
 
120
121
  See [Tunneling](tunneling.md#ownership-and-grants) for the mechanics.
@@ -163,7 +164,7 @@ Agents hold only the public key. The daemon fetches it, with the label the relay
163
164
 
164
165
  The owner's token is as good as the owner: the desktop app hands it to curl on stdin, never in a process's arguments, which other local users can read.
165
166
 
166
- Headers that only claim a caller — `X-SSL-Client-*`, `Remote-*` and the assertion itself — are removed from every request before a route or plugin sees it; `Remote-*` are set again only from a verified user assertion, so a plugin reading identity headers (`@lamalibre/lamaste-identity@4.4.0`) reads the relay's word. Whether a request needs a caller follows the route the daemon matched, not the URL: every route does except the health check, the plugin bundles and the SPA's static files, and so does a path no route matches under an installed plugin's name (a plugin may answer it with its own not-found handler). The claim headers are removed from `rawHeaders` too.
167
+ Headers that only claim a caller — `X-SSL-Client-*`, `Remote-*` and the assertion itself — are removed from every request before a route or plugin sees it; `Remote-*` are set again only from a verified user assertion, so a plugin reading identity headers (`@lamalibre/lamaste-identity@4.5.0`) reads the relay's word. Whether a request needs a caller follows the route the daemon matched, not the URL: every route does except the health check, the plugin bundles and the SPA's static files, and so does a path no route matches under an installed plugin's name (a plugin may answer it with its own not-found handler). The claim headers are removed from `rawHeaders` too.
167
168
 
168
169
  **What remains.** The relay's loopback is still the relay's trust boundary: the panel itself trusts the `X-SSL-Client-*` headers nginx sends to `127.0.0.1:3100`, so a process on the relay that can forge those can obtain an assertion as well — what it could do to agents before 4.3. With panel 2FA on it reaches further into agents than into the panel: forged (or stolen) admin-certificate headers no longer pass the panel's own management routes, but the agent-panel authorization is exempt from 2FA (the browser on `agent-<label>.<domain>` carries no 2FA cookie), so they still mint admin assertions — full control of every agent with a panel tunnel. Agent panels are guarded by the client certificate alone, as before; authenticating nginx to the panel is the relay-wide fix. An assertion binds the agent, the port, the method and the target, not the request body: a process on the agent machine that holds the daemon's port while the daemon is down receives the relay's requests, and could replay one with another body if the daemon comes back within the assertion's 65 seconds.
169
170
 
@@ -187,13 +188,15 @@ An agent half Lamaste installs runs on its agent as a service of its own, so som
187
188
 
188
189
  An agent-hosted plugin's settings may hold secrets — a data provider's API key, a token for another service. The agent half uses them, so the agent half keeps them; the relay never does. See [Settings](../04-api-reference/agent-hosted-plugins.md#settings) for the contract.
189
190
 
190
- **Anything that takes a tunnel's loopback port gets nothing and serves nothing.** The internal tunnel ends on loopback ports that every process on the relay and on the agent can reach — and take: on the relay while the hosting agent is offline (chisel listens only while it is connected), on the agent while an agent half restarts. Up to 4.3, a process holding such a port received Lamaste's calls in clear and could answer them as the agent half: pass the health probe, take a settings change with its secrets, replay a caller assertion with another body, or serve the plugin's `panel.js` into the panel's own document. On the agent, systemd now holds the port of an agent half Lamaste installs (a socket unit beside its service, passed to it as descriptor 3), so the port stays bound while the agent half restarts, is updated or is stopped ([App Host](../04-api-reference/app-host.md)); an agent half installed by hand binds its port itself. Whoever holds a port, three mechanisms, all keyed with the hosting's installation secret, leave it nothing to receive and nothing to serve ([Authentication between the relay and the agent half](../04-api-reference/agent-hosted-plugins.md#authentication-between-the-relay-and-the-agent-half)):
191
+ **Anything that takes a tunnel's loopback port serves nothing and opens no secret.** The internal tunnel ends on loopback ports that every process on the relay and on the agent can reach — and take: on the relay while the hosting agent is offline (chisel listens only while it is connected), on the agent while an agent half restarts. Up to 4.3, a process holding such a port received Lamaste's calls in clear and could answer them as the agent half: pass the health probe, take a settings change with its secrets, replay a caller assertion with another body, or serve the plugin's `panel.js` into the panel's own document. On the agent, systemd now holds the port of an agent half Lamaste installs (a socket unit beside its service, passed to it as descriptor 3), so the port stays bound while the agent half restarts, is updated or is stopped ([App Host](../04-api-reference/app-host.md)); an agent half installed by hand binds its port itself. Whoever holds a port, three mechanisms, all keyed with the hosting's installation secret, leave it nothing to serve and no secret to open ([Authentication between the relay and the agent half](../04-api-reference/agent-hosted-plugins.md#authentication-between-the-relay-and-the-agent-half)):
191
192
 
192
193
  - **Body-bound assertions.** Lamaste's calls with a body sign the SHA-256 of the exact bytes (`bodySha256`); the agent half's hook and settings handlers require it, so an assertion lifted onto another body is refused (`wrong_body`).
193
194
  - **Authenticated answers.** Every answer the agent half gives a verified request carries `X-Lamalibre-Responder`, an HMAC with the secret that verified the request over its nonce, the status and — for Lamaste's own calls — the SHA-256 of the exact answer body. The relay believes nothing without it: a settings call or a hook call fails as `unauthenticated`, the health probe counts the plugin's application as down, and the forwarder answers `502` before a status, header or byte of the answer reaches the client. Every `/admin/*` response of an agent half must carry it; core's handlers, `createHealthHandler` and `responderHeaderFor` produce it.
194
195
  - **Sealed secrets.** Each secret of a settings change crosses the tunnel encrypted (AES-256-GCM, key derived with HKDF-SHA256 from the installation secret per plugin) and bound to the plugin, the call's nonce, the field and the revision; the agent half opens it with the secret that verified the call.
195
196
 
196
- Agent halves built against `@lamalibre/lamaste@4.4.0` 4.3 must be rebuilt: one that does not sign its answers is down to the relay.
197
+ Agent halves built against `@lamalibre/lamaste@4.5.0` 4.3 must be rebuilt: one that does not sign its answers is down to the relay.
198
+
199
+ **What a port's squatter still receives.** These mechanisms authenticate answers and seal secrets; they do not keep the relay from _sending_. A squatter cannot pass a single health probe (it holds no installation secret), so the health monitor marks the hosting down after three failed probes in a row, 10 s apart. From then on **neither forwarder sends anything to the port**: a user's request is answered `503` by the panel, and an administrator's or agent's request on the admin API is answered as a request into a dead tunnel is — the plugin's `tunnel:down` fallback, or `502` with the reason (`tunnel` or `app`) — without a connection being made. So a forwarded request — its method, path, query and body, and the caller assertion with an administrator, an agent, or a user's `userId`, username and roles — can reach a relay-loopback squatter only in the window before the hosting is marked down, about 30 seconds after the agent went away (and, when the agent comes back, a squatter that still holds the port is what the probes find: the hosting stays down). Lamaste's own calls do not go through the forwarders and keep reaching the port — the health probe has to, to see the agent half come back: their bodies are bound to their assertions and a settings change's secrets are sealed, so a squatter learns their non-secret content and nothing it can replay.
197
200
 
198
201
  **The secret's path.** An administrator types the secret into the panel's form — a masked text input, not a password input (browsers offer to save a password input's value in their password managers whatever its `autocomplete` says), whose value never enters the page's state or its query cache and is read once when the form submits — or gives it to `lamaste-server plugins configure` (a masked prompt, or a JSON object on stdin; `--set` refuses a secret, since a command line is readable by other local users, and says to rotate it) or to the desktop app; both clients hand the request body to curl on stdin. The panel's route takes the body off the request before anything else, checks it — with messages that never contain a value — and sends the change through the plugin's internal tunnel to the agent half, `PUT /admin/settings` with a caller assertion naming Lamaste itself, each secret sealed to that call. The agent half opens it, writes it to `secrets.json` in its data directory — `0600`, its own service account's, atomically, refusing a link in the file's place — and answers with its state, which names the secrets that are set and when, never their values. On the relay the secret existed in clear only in the memory of that request; nothing keeps a reference to it afterwards.
199
202
 
@@ -213,6 +216,36 @@ Agent halves built against `@lamalibre/lamaste@4.4.0` 4.3 must be rebuilt: one t
213
216
 
214
217
  **What remains.** The installation secret is held by the relay's panel (`plugin-hostings.json`, `0600`, `lamaste`), by the hosting **agent's account** (its plugin config, `0600`) and by the **agent half's own account** (`/etc/lamalibre/apps/<route>/plugin-config.json`, `0600`). Whoever holds one of them can sign as Lamaste and answer as the agent half. The agent account runs the tunnel's client, so it can route the tunnel to a listener of its own and open the sealed secrets of a change sent then; any holder can read and change the settings through the agent half's handler — though never read a secret back from it, since no answer carries one, and a change made that way is reported as made outside Lamaste. Relay-signed assertions with a key the agent half holds alone would be the next step; until then the agent account and the agent half's account are inside the secrets' trust boundary. The agent half — the plugin's own code — holds its secrets in memory and in its file and uses them as it likes; its account and unit (5d above) keep them from other accounts on the agent, but root on the agent can read them, and they stay with the agent half's retained data when it is removed without purge. On the relay, a secret is readable in clear for the duration of a change by whoever can read the panel process's memory (root, or the `lamaste` account itself). None of the processes that hold these secrets writes a core dump: the units of the panel, Gatekeeper, the hook runner and every agent half set `LimitCORE=0`.
215
218
 
219
+ #### 5f. Users of agent-hosted plugins: who attests what
220
+
221
+ An agent-hosted plugin that declares `identity` serves its users at `/api/<route>/app/*` on a tunnel host bound as its user surface, and learns who is calling from Lamaste — never from a header it has to take on faith ([Users and identity](../04-api-reference/agent-hosted-plugins.md#users-and-identity)). Three parties vouch, each for what it knows:
222
+
223
+ | Who | Attests | How |
224
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
225
+ | **Gatekeeper** | The Authelia **session**: this request comes from the user signed in as `<username>` (and, on a restricted tunnel, the user holds its grant) | A relay assertion for the panel — Ed25519 with the relay signing key, audience `@panel` (no agent label can be one), port 3100, the username alone, bound to the method and the raw request target, 60 s, single use, never cached |
226
+ | **The panel** | The **identity** applications see: the directory's `userId`, the username and the user's roles in this plugin — or a refusal (disabled, removed, not entitled) | The HMAC caller assertion keyed with the hosting's installation secret, caller `{ type: "user", userId, username, roles }`, on the request it forwards to the agent half's `/app/*` |
227
+ | **The agent half** | Its answer is its own | The responder signature over the request's nonce and the status — without it the panel sends the client a `502` |
228
+
229
+ Gatekeeper attests the session; the panel issues the identity. Gatekeeper never signs a role or an attribute, and the panel never believes a username it did not receive in a Gatekeeper assertion bound to that very request. nginx sends the panel the request target exactly as the browser sent it, so the target the assertion binds is the one the panel checks byte for byte; every server on the way applies one path rule — no `.` or `..` segment, no backslash, no percent-encoded `.`, `/`, `\` or `%` (a `%25` would let a server that decodes twice read `%252e%252e` as `..`), no escape that does not decode — so the path a check approves is the path the next server resolves. Every caller assertion, whoever the caller, names an origin-form path — one leading `/`, never `//`, no scheme or host — or is neither signed nor verified (`wrong_request`).
230
+
231
+ **Only the hosting agent's host.** A tunnel can be bound as a plugin's user surface only when the agent that carries it hosts the plugin, and its host serves the route only while that stays so: the tunnel moved to another agent, its agent revoked, or the plugin hosted elsewhere or not at all — the binding stays, but the vhost loses its locations (status `not_hosting_agent`), Gatekeeper refuses to vouch (it reads the hosting agent from the plugin's internal tunnel in `tunnels.json`) and the panel refuses a request for the route even with an assertion. A host whose sessions reach the plugin's users is never another agent's to serve — or, through its backend, to read.
232
+
233
+ **Gatekeeper and the session cookie.** Gatekeeper reads Authelia's session cookie alone, sends Authelia `authelia_session=<value>` and nothing of the browser's other cookies, and refuses a request carrying more than one `authelia_session` (`401`): another host under the domain can plant a second one with another path, and which one Authelia would read is not Gatekeeper's to guess. Its session cache is keyed by the cookie's value **and the tunnel host** — Authelia decides per host, so a session it accepted on one host is asked about again on another — and its rate limit counts per client address and tunnel host (nginx sets `X-Forwarded-For` to the address it accepted, and Gatekeeper trusts that header from `127.0.0.1` only), so one client cannot exhaust the others' authorizations. A refusal is logged with the tunnel and a reason, never the username.
234
+
235
+ **The `/app` boundary.** On the agent half, paths are a boundary between callers: a `user` caller is valid only under `/app` (read literally), and every other caller — an administrator's certificate, an agent, Lamaste itself — never for a path a router could take for `/app` (`/App`, `/%61pp`, `//app`, `/app.json`). Signing refuses either mistake and verifying answers `wrong_request`: an administrator's assertion cannot act on a user's routes, a user's never reaches `/admin`. On the panel, `/api/<route>/app*` are routes of the user surface's own context — no mTLS, no 2FA session, the assertion alone — which win over the admin forwarder's wildcard, so the admin forwarder no longer reaches an agent half's `/admin/app*`. Panel, auth, agent-panel and IP-panel vhosts clear a client's `X-Lamalibre-Relay-Assertion`, as do tunnel vhosts that do not set Gatekeeper's; a user-surface location also clears the browser's `Cookie`, `Authorization`, `Remote-*`, `X-SSL-Client-*` and `X-Forwarded-Host` before the panel sees the request.
236
+
237
+ **CSRF and cross-site reads.** The Authelia session cookie travels with requests other sites start, so the panel refuses any user-surface request whose `Sec-Fetch-Site` is present and neither `same-origin` nor `none` — `cross-site`, and `same-site`, which every other tunnel host is — whatever its method (`403`). A request that may change something — any method but `GET`, `HEAD` and `OPTIONS` — must also carry `Origin` equal to the bound host as the tunnel state names it (never the request's `Host`); otherwise `403`. A user-surface request without a session gets a JSON `401`, never a redirect, and the panel never passes a `Set-Cookie` back.
238
+
239
+ **The answers.** Every user-surface answer — the agent half's, a hook's, the panel's own refusals and errors — carries `Cross-Origin-Resource-Policy: same-origin`, `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox; default-src 'none'` (the application's own policy stays as a further one), and no `Access-Control-*` header — the panel's CORS is off for these routes — so an answer from a compromised application can neither be read by another origin nor run as a page of the bound host. No answer from any agent half, on the user surface or the admin API, passes `X-Accel-*` to nginx (which would obey `X-Accel-Redirect` from the panel's port); the forwarder sets `X-Accel-Buffering: no` itself on an event stream. No request reaches an agent half with a header that makes some frameworks take it for another method or path than the one the caller assertion signs (`X-HTTP-Method-Override`, `X-Original-URL`, `X-Forwarded-Prefix`, …), and no request hook may set one. The panel's own errors on a user surface are fixed texts per status, never an internal message.
240
+
241
+ **Open responses end with access.** A response that stays open — an event stream, a slow download — would otherwise outlive what admitted it. The panel keeps every user-surface response it is sending with its user and roles, and every 5 seconds re-resolves each through the directory: one whose user is disabled, removed, no longer entitled, holds other roles, or whose route the host no longer serves is ended at once, the request to the agent half with it. No user-surface response lives longer than an hour (`USER_SURFACE_MAX_RESPONSE_SECONDS`); nginx's timeouts on these locations are the same hour.
242
+
243
+ **Data minimisation.** A plugin always receives a user's `userId` and **username** — in the caller assertion of every request and in every member and last-known record of its feed — and of the other attributes only those its manifest declares (`displayName`, `email`); widening them waits for an administrator's approval that auto-approve never gives (`identity.changed`). Nothing identity is ever sent to a plugin that does not declare `identity`. A user who leaves keeps, on the plugin's side, only what it last received. The feed's batches travel **sealed**: their changes are encrypted (AES-256-GCM) with a key derived from the hosting's installation secret — domain-separated from the settings secrets' — and bound to the caller assertion's nonce and the batch envelope, so the chisel server and whatever holds a loopback end of the internal tunnel never see a member in clear. Audit entries name users by `userId` alone, and the feed's wire parsers never echo a received value. The panel and Gatekeeper log one metadata line per request — method, **route pattern** (never the URL or its query), status, duration, certificate role, and the client address only for certificate holders, never for a person — instead of Fastify's own request lines; the panel's log lines name users by `userId`; Gatekeeper's principal removal and access-log purge take the username in a request body, never a URL. `users.yml`'s content never reaches a log, even when it cannot be parsed. What nginx's access log, journald and Authelia's log keep, and for how long: [Monitoring](../05-operations/monitoring.md#logs-and-personal-data).
244
+
245
+ **Offboarding and erasure.** One offboarding function, keyed by `userId`, runs for every user who stops being one — disabled or deleted through the users API, or disabled or removed in `users.yml` outside it (adopted by the minute's reconcile): their desktop sessions end (an epoch keyed by `userId`; sessions are bound to the `userId`, so a username given to someone else revives none), their registry tokens are revoked, Gatekeeper drops its cached session verdicts, and for a removal their legacy plugin-access grants, Gatekeeper principal (every grant and group membership under the username) and Authelia second factors (TOTP configuration, WebAuthn credentials) go. Disabling is effective at once: the directory is written before Authelia restarts, so the user surface refuses them (`403`) before Authelia lets them out, and every audience shows them disabled; a disable that Authelia's own rewrite of `users.yml` undoes is made again, and reported when it does not stick. Deleting keeps a tombstone (the `userId` and the last known name and address, `removed`) so history stays readable. **A re-created username is a new user with a new `userId` and inherits no grant, group, role, second factor or session**: at every creation over a tombstone Gatekeeper drops everything under the username and Authelia its second factors (refused while either cannot), a username a deleted user held and nobody holds takes no grant or membership (`409 username_deleted`), and grants are given under the users lock so none races a deletion. The one exception Lamaste cannot remove is the **OpenID Connect identity**: Authelia's opaque identifier (`sub`) for a username cannot be deleted, so a username Authelia holds one for is never given to a new user (`409 username_has_oidc_identity`). **Erasing** scrubs the tombstone, the user's invitations, Gatekeeper's access-request log rows and Authelia's second factors, and rewrites the user's row to `remove/erased` in every plugin that ever had them — uninstalled plugins whose data an agent keeps and plugins that no longer declare identity included (a plugin's audience is its erasure ledger, dropped only when no agent keeps its data); its agent half scrubs its row and anonymises its own data in one transaction, never takes the user back afterwards (erasure is sticky in its projection), and the panel shows the erasure as pending until each plugin's data has it. Outside Lamaste's reach: Authelia's opaque identifier and authentication log, nginx's access logs, journald's older entries, and backups taken before ([Backup and Restore](../05-operations/backup-and-restore.md) on restoring a relay).
246
+
247
+ **What remains.** Plain `authenticated` and `restricted` app tunnels still pass the user to their backend as **unsigned** `Remote-User`, `Remote-Groups`, `Remote-Name` and `Remote-Email` headers set from Gatekeeper's answer: nginx replaces whatever the client sent, but the backend's port is reachable on the relay's and the agent's loopback by any local process, which can send any `Remote-*` it likes. Only plugin tunnels (a relay assertion for lamaste-agentd, 5c) and plugin user surfaces carry a signed identity; Gatekeeper-signed identity for plain tunnels is future work. A backend never receives the session cookie (5a), but it answers on a host under the relay's domain and can set cookies there — a second `authelia_session` with another path, which Gatekeeper refuses rather than guess. Until the hosting is marked down, a forwarded request — a user's or an administrator's — can reach a relay-loopback squatter (5e). The relay's loopback stays the relay's trust boundary: a process on the relay that can read the relay signing key can mint assertions for the panel, as it could for agents before.
248
+
216
249
  #### 6. TOTP 2FA for app access
217
250
 
218
251
  Visitors to your tunneled apps authenticate through Authelia with a password and a TOTP code from their phone. See [Authentication](authentication.md) for full details.
@@ -538,13 +571,15 @@ Server plugins run inside the panel (the `@lamalibre/` scope is their trust boun
538
571
 
539
572
  `/usr/local/sbin/lamaste-priv` is a root-owned (`0755`), zero-dependency Node.js program installed by `create-lamaste`. Content (vhost text, TOTP secrets) arrives on stdin, never on the command line, so it is not written to the sudo log. It reads nothing the `lamaste` user can write.
540
573
 
541
- | Operation | Does |
542
- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
543
- | `nginx-site write\|backup\|restore\|discard-backup\|enable\|disable\|remove <name>` | Manage `sites-available/<name>` (and its `.bak`) and its `sites-enabled` link. `write` validates the vhost on stdin first. |
544
- | `authelia-totp <username>` | Store the TOTP secret from stdin with Authelia's CLI, run as `lamaste-authelia` (the database is that account's file) |
545
- | `self-update <version>` | Start `npx @lamalibre/create-lamaste@<version> --yes` in a transient systemd unit two seconds later (`MAJOR.MINOR.PATCH` only) |
546
- | `hooks install <route> <sha256\|none>` | Write a plugin's approved relay hook module (stdin, checked against the digest) root-owned to `/opt/lamalibre/lamaste/hooks/<route>/`, enable its runner socket |
547
- | `hooks activate\|restart\|remove <route> [sha256]` | Point the runner back at the kept previous module; stop a stuck runner; disable it and delete its modules |
574
+ | Operation | Does |
575
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
576
+ | `nginx-site write\|backup\|restore\|discard-backup\|enable\|disable\|remove <name>` | Manage `sites-available/<name>` (and its `.bak`) and its `sites-enabled` link. `write` validates the vhost on stdin first. |
577
+ | `authelia-totp <username>` | Store the TOTP secret from stdin with Authelia's CLI, run as `lamaste-authelia` (the database is that account's file) |
578
+ | `authelia-user purge-second-factors <username>` | Delete the user's TOTP configuration and every WebAuthn credential with Authelia's CLI, as `lamaste-authelia` (nothing there is fine) |
579
+ | `authelia-user has-oidc-identifier <username>` | Print `yes`/`no`: whether Authelia holds an OpenID Connect opaque identifier for the username — the table exported as `lamaste-authelia` to a `/run` directory only it and root can enter, read, overwritten and removed |
580
+ | `self-update <version>` | Start `npx @lamalibre/create-lamaste@<version> --yes` in a transient systemd unit two seconds later (`MAJOR.MINOR.PATCH` only) |
581
+ | `hooks install <route> <sha256\|none>` | Write a plugin's approved relay hook module (stdin, checked against the digest) root-owned to `/opt/lamalibre/lamaste/hooks/<route>/`, enable its runner socket |
582
+ | `hooks activate\|restart\|remove <route> [sha256]` | Point the runner back at the kept previous module; stop a stuck runner; disable it and delete its modules |
548
583
 
549
584
  Site names are limited to the panel's own vhosts: `lamalibre-lamaste-panel-domain`, `-auth`, `-tunnel`, `-app-<subdomain>`, `-agent-panel-<label>` and `-site-<uuid>`. `lamalibre-lamaste-panel-ip` (written by `create-lamaste`) may only be enabled or disabled.
550
585
 
@@ -633,28 +668,30 @@ Configuration files that are read live by services (like Authelia's `users.yml`)
633
668
 
634
669
  ### Security layers
635
670
 
636
- | Layer | Technology | Blocks |
637
- | -------------------- | --------------------------------------- | ------------------------------------- |
638
- | Firewall | UFW | Connections to non-allowed ports |
639
- | Intrusion prevention | fail2ban | Repeated failed login attempts |
640
- | SSH hardening | sshd_config | Password-based SSH access |
641
- | TLS encryption | Let's Encrypt / self-signed | Traffic interception and tampering |
642
- | Admin auth | mTLS client certificates | Unauthorized admin access |
643
- | Admin 2FA (optional) | Built-in TOTP | Stolen certificate abuse |
644
- | Agent-to-agent auth | Tickets (time-limited, single-use) | Unauthorized cross-agent access |
645
- | Tunnel ownership | Chisel authfile per-agent grants | One agent hijacking another's host |
646
- | Reserved ports | Tunnel port validation | Publishing internal services |
647
- | Privileged commands | No-wildcard sudoers, `lamaste-priv` | Root code execution via extra flags |
648
- | Relay authenticity | Agent verifies `tunnel.<domain>` TLS | Relay impersonation, credential theft |
649
- | Panel authenticity | Pinned panel key, CA for its domain | Panel impersonation, credential theft |
650
- | Agent daemon auth | Relay assertions (Ed25519), owner token | Callers forged on an agent's loopback |
651
- | Agent root helper | Relay-signed app jobs, pinned key | Root actions the relay did not sign |
652
- | Agent half sandbox | Own account, hardened systemd unit | Plugin code as root |
653
- | Plugin secrets | Agent half only, through the tunnel | Plugin secrets at rest on the relay |
654
- | App auth | Authelia TOTP 2FA | Unauthorized app access |
655
- | Service isolation | `127.0.0.1` binding | Direct access to internal services |
656
- | File permissions | `chmod 600` | Unauthorized secret access |
657
- | Atomic writes | `mv` pattern | Partial file reads by services |
671
+ | Layer | Technology | Blocks |
672
+ | -------------------- | ------------------------------------------ | ------------------------------------- |
673
+ | Firewall | UFW | Connections to non-allowed ports |
674
+ | Intrusion prevention | fail2ban | Repeated failed login attempts |
675
+ | SSH hardening | sshd_config | Password-based SSH access |
676
+ | TLS encryption | Let's Encrypt / self-signed | Traffic interception and tampering |
677
+ | Admin auth | mTLS client certificates | Unauthorized admin access |
678
+ | Admin 2FA (optional) | Built-in TOTP | Stolen certificate abuse |
679
+ | Agent-to-agent auth | Tickets (time-limited, single-use) | Unauthorized cross-agent access |
680
+ | Tunnel ownership | Chisel authfile per-agent grants | One agent hijacking another's host |
681
+ | Reserved ports | Tunnel port validation | Publishing internal services |
682
+ | Privileged commands | No-wildcard sudoers, `lamaste-priv` | Root code execution via extra flags |
683
+ | Relay authenticity | Agent verifies `tunnel.<domain>` TLS | Relay impersonation, credential theft |
684
+ | Panel authenticity | Pinned panel key, CA for its domain | Panel impersonation, credential theft |
685
+ | Agent daemon auth | Relay assertions (Ed25519), owner token | Callers forged on an agent's loopback |
686
+ | Agent root helper | Relay-signed app jobs, pinned key | Root actions the relay did not sign |
687
+ | Agent half sandbox | Own account, hardened systemd unit | Plugin code as root |
688
+ | Plugin secrets | Agent half only, through the tunnel | Plugin secrets at rest on the relay |
689
+ | Plugin users | Gatekeeper attests, the panel identifies | Forged users on a plugin's `/app` |
690
+ | Session confinement | Backend cookie map, per-host session cache | Session theft by a tunnel backend |
691
+ | App auth | Authelia TOTP 2FA | Unauthorized app access |
692
+ | Service isolation | `127.0.0.1` binding | Direct access to internal services |
693
+ | File permissions | `chmod 600` | Unauthorized secret access |
694
+ | Atomic writes | `mv` pattern | Partial file reads by services |
658
695
 
659
696
  ### Firewall rules
660
697
 
@@ -264,9 +264,9 @@ The integration flow:
264
264
  }
265
265
  ```
266
266
 
267
- ### Client SDK (`@lamalibre/lamaste-tickets@4.4.0`)
267
+ ### Client SDK (`@lamalibre/lamaste-tickets@4.5.0`)
268
268
 
269
- The `@lamalibre/lamaste-tickets@4.4.0` package is a TypeScript SDK that provides the client-side ticket lifecycle for plugins and agents. It uses `undici` for HTTP with mTLS dispatcher support and has no other runtime dependencies.
269
+ The `@lamalibre/lamaste-tickets@4.5.0` package is a TypeScript SDK that provides the client-side ticket lifecycle for plugins and agents. It uses `undici` for HTTP with mTLS dispatcher support and has no other runtime dependencies.
270
270
 
271
271
  The SDK exports three main classes:
272
272
 
@@ -144,7 +144,7 @@ On the API side the same rule holds: `GET /api/tunnels/agent-config` returns onl
144
144
 
145
145
  ### Client configuration
146
146
 
147
- The Chisel client runs as a user-level service managed by `lamaste-agent` (and `lamaste-agentd`): a LaunchAgent plist on macOS, a systemd **user** unit on Linux. Both are generated by one implementation in `@lamalibre/lamaste@4.4.0/agent`. The client command looks like:
147
+ The Chisel client runs as a user-level service managed by `lamaste-agent` (and `lamaste-agentd`): a LaunchAgent plist on macOS, a systemd **user** unit on Linux. Both are generated by one implementation in `@lamalibre/lamaste@4.5.0/agent`. The client command looks like:
148
148
 
149
149
  ```bash
150
150
  chisel client \
@@ -208,7 +208,7 @@ location / {
208
208
 
209
209
  ### Concurrency and locking
210
210
 
211
- Every tunnel workflow — create, delete, toggle, assign, reconfigure, the combined `PATCH`, releasing a revoked agent's tunnels, and startup reconciliation — runs under one tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.4.0/server`'s `tunnels.ts`), with its validation inside the lock. Two concurrent requests therefore cannot both pass a uniqueness check for the same subdomain or port. Authfile renders are additionally serialized with a keyed promise-chain mutex in `chisel-users.ts`, so enrollments and credential rotations never interleave authfile writes with tunnel changes.
211
+ Every tunnel workflow — create, delete, toggle, assign, reconfigure, the combined `PATCH`, releasing a revoked agent's tunnels, and startup reconciliation — runs under one tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.5.0/server`'s `tunnels.ts`), with its validation inside the lock. Two concurrent requests therefore cannot both pass a uniqueness check for the same subdomain or port. Authfile renders are additionally serialized with a keyed promise-chain mutex in `chisel-users.ts`, so enrollments and credential rotations never interleave authfile writes with tunnel changes.
212
212
 
213
213
  ### Startup reconciliation
214
214
 
@@ -223,7 +223,7 @@ It **fails closed**: if the authfile cannot be established, Chisel is stopped **
223
223
 
224
224
  ### Installation
225
225
 
226
- On the server, Chisel is installed by `npx @lamalibre/create-lamaste@4.4.0` — as root, together with its hardened systemd unit (`User=nobody`, `Group=lamaste-chisel`, `NoNewPrivileges`, `ProtectSystem=strict`, empty `CapabilityBoundingSet`) — and kept at the pinned release by every redeploy. The panel never installs a binary or writes the unit; onboarding only checks the binary exists, creates the server key and authfile, and starts the service. Both the server installer and the agents download the pinned release (`CHISEL_RELEASE` in `@lamalibre/lamaste@4.4.0`'s `constants.ts`) from its fixed URL, with no GitHub API lookup and no "latest":
226
+ On the server, Chisel is installed by `npx @lamalibre/create-lamaste@4.5.0` — as root, together with its hardened systemd unit (`User=nobody`, `Group=lamaste-chisel`, `NoNewPrivileges`, `ProtectSystem=strict`, empty `CapabilityBoundingSet`) — and kept at the pinned release by every redeploy. The panel never installs a binary or writes the unit; onboarding only checks the binary exists, creates the server key and authfile, and starts the service. Both the server installer and the agents download the pinned release (`CHISEL_RELEASE` in `@lamalibre/lamaste@4.5.0`'s `constants.ts`) from its fixed URL, with no GitHub API lookup and no "latest":
227
227
 
228
228
  ```
229
229
  https://github.com/jpillora/chisel/releases/download/v1.12.0/chisel_1.12.0_<os>_<arch>.gz
@@ -320,7 +320,7 @@ sudo systemctl restart chisel
320
320
 
321
321
  ```bash
322
322
  # Install globally, then set up the agent (interactive)
323
- npm install -g @lamalibre/lamaste-agent@4.4.0
323
+ npm install -g @lamalibre/lamaste-agent@4.5.0
324
324
  lamaste-agent setup --label <label>
325
325
 
326
326
  # Apply tunnel changes now (the sync timer does it within 30 s anyway)
@@ -59,7 +59,7 @@ The token is single-use and expires after 10 minutes. `tunnels:read` is enough f
59
59
  Install the agent **globally** first:
60
60
 
61
61
  ```bash
62
- npm install -g @lamalibre/lamaste-agent@4.4.0
62
+ npm install -g @lamalibre/lamaste-agent@4.5.0
63
63
  ```
64
64
 
65
65
  A global install is required, not a convenience. Setup installs a sync timer (see [How tunnel changes reach the agent](#how-tunnel-changes-reach-the-agent)) that runs the installed `lamaste-agent` every 30 seconds for as long as the agent exists. An `npx` copy lives in npm's cache, which npm may clear at any time — the timer would silently stop. `setup` and `update` therefore refuse to run from `npx`, and they check this **before** an enrollment token is spent. For an installation npm does not manage, set `LAMALIBRE_LAMASTE_AGENT_CLI_PATH` to the absolute path of the `lamaste-agent` script the timer should run.
@@ -148,7 +148,7 @@ Each agent authenticates to the relay with its own chisel credential, stored in
148
148
  Upgrade agents **before** the server. Agents from before this version reject the new shape of the relay's Chisel arguments; a current agent works against an older server, keeping its credential until the server offers rotation.
149
149
 
150
150
  ```bash
151
- npm install -g @lamalibre/lamaste-agent@4.4.0
151
+ npm install -g @lamalibre/lamaste-agent@4.5.0
152
152
  lamaste-agent update
153
153
  ```
154
154
 
@@ -172,7 +172,7 @@ lamaste-agent uninstall --label <l> # stop and remove one agent, including its
172
172
  To let the panel install the agent halves of app plugins on this machine — their accounts, units and directories — enable the app host helper once, from a root-owned install:
173
173
 
174
174
  ```bash
175
- sudo npm install -g @lamalibre/lamaste-agent@4.4.0
175
+ sudo npm install -g @lamalibre/lamaste-agent@4.5.0
176
176
  sudo lamaste-agent apphost enable # the agent runs as the user who ran sudo
177
177
  lamaste-agent apphost status # helper, pinned relay keys, installed agent halves
178
178
  sudo lamaste-agent apphost pin # after the relay rotated its signing key
@@ -184,7 +184,7 @@ The agent reports it on its next sync, and the panel can then host plugins whose
184
184
 
185
185
  | Symptom | Cause and fix |
186
186
  | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
187
- | `setup` says the agent must be installed globally | It was run through `npx`. Run `npm install -g @lamalibre/lamaste-agent@4.4.0`, then `lamaste-agent setup ...` again. The enrollment token was not spent. |
187
+ | `setup` says the agent must be installed globally | It was run through `npx`. Run `npm install -g @lamalibre/lamaste-agent@4.5.0`, then `lamaste-agent setup ...` again. The enrollment token was not spent. |
188
188
  | Log shows a TLS / x509 error | `tunnel.<domain>` does not present a valid certificate for its name. Check the relay's certificate (`Certificates` page) and that the agent's clock is correct. |
189
189
  | Log shows authentication failed | The credential was rotated on the panel. The next sync picks it up; `lamaste-agent chisel refresh-credential` applies it now. |
190
190
  | Log shows `access denied` for a remote | The relay no longer grants this agent that port: the tunnel was disabled, deleted, or moved to another agent. The next sync drops it. If it persists, check `lamaste-agent status` — the sync timer may not be running. |
@@ -196,7 +196,7 @@ The agent reports it on its next sync, and the panel can then host plugins whose
196
196
  | New tunnel returns 502 | The agent has not synced yet (up to ~30 s), or nothing listens on `127.0.0.1:<port>` on the agent. Run `lamaste-agent update` to apply at once. |
197
197
  | Upload fails with 413 | The request is larger than the tunnel's body limit. Raise **Largest request body** on the tunnel (`maxBodySizeMb`). |
198
198
  | `uninstall` refuses: agent halves are installed | Agent halves run here for this agent. Unhost or uninstall those plugins on the panel (the next sync removes them), or `sudo lamaste-agent apphost disable --force` (data is kept). |
199
- | `apphost enable` says the install is not root-owned | Root installs the helper only from code no other user can change: `sudo npm install -g @lamalibre/lamaste-agent@4.4.0`, then `sudo lamaste-agent apphost enable` from that install. |
199
+ | `apphost enable` says the install is not root-owned | Root installs the helper only from code no other user can change: `sudo npm install -g @lamalibre/lamaste-agent@4.5.0`, then `sudo lamaste-agent apphost enable` from that install. |
200
200
  | `apphost enable` says the helper serves another user | App hosting serves one OS user per machine. Unhost or uninstall the plugins that user's agents host (`sudo lamaste-agent apphost status` lists their agent halves), then `sudo lamaste-agent apphost enable --user <name> --force`. |
201
201
  | `apphost pin` says a label is not an agent of the user | Root pins only labels the agent user's registry lists. Pin as the user the agent runs as (`--user <name>`; `--user root` for an agent running as root). |
202
202
  | Hosting refused: the helper trusts another relay key | The relay's signing key was replaced, or the agent enrolled again. Run `sudo lamaste-agent apphost pin`; compare the key id with the relay's ([App Host](../04-api-reference/app-host.md#pinning-the-relay-key)). |
@@ -196,7 +196,7 @@ The admin certificate (`CN=admin`) has unrestricted access to all panel endpoint
196
196
 
197
197
  ### Distribute to the Agent Machine
198
198
 
199
- Send the `.p12` file and password to the agent machine (macOS or Linux) securely, and install the agent there with `npm install -g @lamalibre/lamaste-agent@4.4.0` and run `lamaste-agent setup --label <label>` — or import it into the Desktop App. An enrollment token is preferable where possible, because the private key is then generated on the agent and never travels. See [Agent Setup](agent-setup.md) for details.
199
+ Send the `.p12` file and password to the agent machine (macOS or Linux) securely, and install the agent there with `npm install -g @lamalibre/lamaste-agent@4.5.0` and run `lamaste-agent setup --label <label>` — or import it into the Desktop App. An enrollment token is preferable where possible, because the private key is then generated on the agent and never travels. See [Agent Setup](agent-setup.md) for details.
200
200
 
201
201
  ### Update Capabilities
202
202
 
@@ -293,7 +293,7 @@ Revocation is immediate — the certificate serial is added to a revocation list
293
293
 
294
294
  ### mTLS Rotation Internals
295
295
 
296
- Admin certificate rotation lives only in `lamaste-server reset-admin` (`packages/server/cli/src/commands/reset-admin.js`), run as root on the server. The panel's `rotateClientCert()` in `lib/mtls.js` is a stub that returns `503` with that instruction; `@lamalibre/lamaste@4.4.0/server` no longer exports a rotation function. The PKI directory belongs to `lamaste`, so `reset-admin` runs every file operation inside it (`openssl genrsa`/`req`/`x509 -req`/`pkcs12`, `cp`, `chmod`) as `lamaste`, and writes `panel.json` and `revoked.json` as fresh files renamed into place — root never writes through a path the panel could have replaced with a symlink.
296
+ Admin certificate rotation lives only in `lamaste-server reset-admin` (`packages/server/cli/src/commands/reset-admin.js`), run as root on the server. The panel's `rotateClientCert()` in `lib/mtls.js` is a stub that returns `503` with that instruction; `@lamalibre/lamaste@4.5.0/server` no longer exports a rotation function. The PKI directory belongs to `lamaste`, so `reset-admin` runs every file operation inside it (`openssl genrsa`/`req`/`x509 -req`/`pkcs12`, `cp`, `chmod`) as `lamaste`, and writes `panel.json` and `revoked.json` as fresh files renamed into place — root never writes through a path the panel could have replaced with a symlink.
297
297
 
298
298
  The PKCS12 bundle uses legacy encryption settings (`-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1`) because newer OpenSSL defaults produce files that macOS Keychain Access cannot import.
299
299
 
@@ -14,7 +14,7 @@ Before you start, make sure you have:
14
14
 
15
15
  | Requirement | Details |
16
16
  | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | **Desktop app installed** | `npx @lamalibre/create-lamaste-desktop@4.4.0` ([setup guide](desktop-app-setup.md)) |
17
+ | **Desktop app installed** | `npx @lamalibre/create-lamaste-desktop@4.5.0` ([setup guide](desktop-app-setup.md)) |
18
18
  | **DigitalOcean account** | A free account at [cloud.digitalocean.com](https://cloud.digitalocean.com). **If you have other infrastructure on DO, create a dedicated team first** (see below) |
19
19
  | **Payment method** | A credit card or PayPal on file in DigitalOcean (required to create droplets) |
20
20
  | **Domain name** | Optional for initial setup — you can add a domain later through the panel |
@@ -221,7 +221,7 @@ The wizard shows up to 12 steps with live progress indicators. Each step shows a
221
221
  | **Waiting for boot** | Polls DigitalOcean until the droplet has a public IP and is ready (up to 5 minutes) |
222
222
  | **Setting up DNS records** | Creates A and wildcard A records in DigitalOcean DNS (only if a domain was selected in the Domain step) |
223
223
  | **Connecting via SSH** | Establishes an SSH connection using the temporary key |
224
- | **Installing Lamaste** | Runs `npx @lamalibre/create-lamaste@4.4.0` on the droplet (up to 10 minutes) |
224
+ | **Installing Lamaste** | Runs `npx @lamalibre/create-lamaste@4.5.0` on the droplet (up to 10 minutes) |
225
225
  | **Retrieving credentials** | Downloads the admin certificate (`.p12` file) and reads the panel's TLS key pin from the droplet |
226
226
  | **Enrolling admin certificate** | Imports the certificate into your OS credential store |
227
227
  | **Saving configuration** | Writes the server entry to `~/.lamalibre/lamaste/servers.json` |
@@ -315,7 +315,7 @@ If you already have a Lamaste server set up (via the manual SSH method), you can
315
315
 
316
316
  The app checks that the panel is reachable before adding it. The server is added with `active: false` — click **Set Active** on its card to start using it.
317
317
 
318
- > **Note:** Adding an existing server registers it in the app but does not configure authentication. You will still need to set up your agent certificate separately using `lamaste-agent setup` (after `npm install -g @lamalibre/lamaste-agent@4.4.0`) or by importing a certificate manually.
318
+ > **Note:** Adding an existing server registers it in the app but does not configure authentication. You will still need to set up your agent certificate separately using `lamaste-agent setup` (after `npm install -g @lamalibre/lamaste-agent@4.5.0`) or by importing a certificate manually.
319
319
 
320
320
  ---
321
321
 
@@ -26,7 +26,7 @@ The Lamaste Desktop app is a native application (built with Tauri) that wraps th
26
26
  Run the installer:
27
27
 
28
28
  ```bash
29
- npx @lamalibre/create-lamaste-desktop@4.4.0
29
+ npx @lamalibre/create-lamaste-desktop@4.5.0
30
30
  ```
31
31
 
32
32
  The installer:
@@ -56,11 +56,11 @@ After the app launches, it shows a setup screen with two options:
56
56
  Install the agent globally and run its setup command:
57
57
 
58
58
  ```bash
59
- npm install -g @lamalibre/lamaste-agent@4.4.0
59
+ npm install -g @lamalibre/lamaste-agent@4.5.0
60
60
  lamaste-agent setup --label my-server
61
61
  ```
62
62
 
63
- The agent must be a global install — setup installs a sync timer that runs it every 30 seconds to pick up tunnel changes, and refuses to run from `npx`. (When the app sets up an agent itself, it installs `@lamalibre/lamaste-agent@4.4.0` globally for you.) The `--label` flag names this agent connection (used for multi-agent support). This command connects to your VPS panel using the agent certificate and configures the local Chisel client. During setup, the client certificate and key are extracted from the P12 bundle for mTLS client authentication. Once setup completes, the app detects the configuration and switches to the main interface.
63
+ The agent must be a global install — setup installs a sync timer that runs it every 30 seconds to pick up tunnel changes, and refuses to run from `npx`. (When the app sets up an agent itself, it installs `@lamalibre/lamaste-agent@4.5.0` globally for you.) The `--label` flag names this agent connection (used for multi-agent support). This command connects to your VPS panel using the agent certificate and configures the local Chisel client. During setup, the client certificate and key are extracted from the P12 bundle for mTLS client authentication. Once setup completes, the app detects the configuration and switches to the main interface.
64
64
 
65
65
  **Option B: Create a new server**
66
66
 
@@ -72,7 +72,7 @@ The desktop app has two modes, toggled via a pill switch in the sidebar header.
72
72
 
73
73
  ### Agent Mode (default)
74
74
 
75
- Agent mode provides local agent management. The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are imported from the shared `@lamalibre/lamaste-agent-ui@4.4.0` package. The landing page is the **Agents** list, showing all configured agents with their connection status. Click an agent to drill into its management pages:
75
+ Agent mode provides local agent management. The agent-mode pages (Dashboard, Tunnels, Services, Plugins, Logs, Settings) are imported from the shared `@lamalibre/lamaste-agent-ui@4.5.0` package. The landing page is the **Agents** list, showing all configured agents with their connection status. Click an agent to drill into its management pages:
76
76
 
77
77
  - **Agents** (landing) — list of all configured agents with start/stop controls and status
78
78
  - **Dashboard** — per-agent connection status, Chisel version, start/stop/restart controls
@@ -84,7 +84,7 @@ Agent mode provides local agent management. The agent-mode pages (Dashboard, Tun
84
84
 
85
85
  ### Server Mode
86
86
 
87
- Server mode provides the full admin panel — the same management pages available in the browser-based panel UI. These pages are imported from the shared `@lamalibre/lamaste-server-ui@4.4.0` package:
87
+ Server mode provides the full admin panel — the same management pages available in the browser-based panel UI. These pages are imported from the shared `@lamalibre/lamaste-server-ui@4.5.0` package:
88
88
 
89
89
  - **Dashboard** — system stats (CPU, RAM, disk, uptime) + service health
90
90
  - **Tunnels** — tunnel CRUD, carrying agent, access mode, body limit
@@ -170,7 +170,7 @@ Stopping an agent's tunnels from the app is remembered: the agent's sync timer k
170
170
  Run the installer again to get the latest version:
171
171
 
172
172
  ```bash
173
- npx @lamalibre/create-lamaste-desktop@4.4.0
173
+ npx @lamalibre/create-lamaste-desktop@4.5.0
174
174
  ```
175
175
 
176
176
  If a new release is available, it downloads and installs the update. The previous version is replaced.
@@ -203,9 +203,9 @@ Deleting `~/.lamalibre/lamaste/` by hand instead would leave the agents' service
203
203
 
204
204
  | Action | Command / Location |
205
205
  | -------------------- | ------------------------------------------------------------------------------------ |
206
- | **Install** | `npx @lamalibre/create-lamaste-desktop@4.4.0` |
207
- | **Update** | `npx @lamalibre/create-lamaste-desktop@4.4.0` |
208
- | **Agent setup** | `npm install -g @lamalibre/lamaste-agent@4.4.0`, then `lamaste-agent setup --label <name>` |
206
+ | **Install** | `npx @lamalibre/create-lamaste-desktop@4.5.0` |
207
+ | **Update** | `npx @lamalibre/create-lamaste-desktop@4.5.0` |
208
+ | **Agent setup** | `npm install -g @lamalibre/lamaste-agent@4.5.0`, then `lamaste-agent setup --label <name>` |
209
209
  | **App location** | `/Applications/Lamaste.app` (macOS) |
210
210
  | **Agent registry** | `~/.lamalibre/lamaste/agents.json` |
211
211
  | **Per-agent data** | `~/.lamalibre/lamaste/agents/<label>/` |
@@ -216,7 +216,7 @@ Deleting `~/.lamalibre/lamaste/` by hand instead would leave the agents' service
216
216
  | **Chisel logs** | `~/.lamalibre/lamaste/agents/<label>/logs/chisel.log` |
217
217
  | **Cloud tokens** | OS credential store (`com.lamalibre.cloud`) |
218
218
  | **P12 passwords** | OS credential store (`com.lamalibre.lamaste.server`) |
219
- | **npm package** | `@lamalibre/create-lamaste-desktop@4.4.0` |
219
+ | **npm package** | `@lamalibre/create-lamaste-desktop@4.5.0` |
220
220
 
221
221
  ### Related Documentation
222
222
 
@@ -62,7 +62,7 @@ Execute the Lamaste installer:
62
62
 
63
63
  ```bash
64
64
  apt install -y npm
65
- npx @lamalibre/create-lamaste@4.4.0
65
+ npx @lamalibre/create-lamaste@4.5.0
66
66
  ```
67
67
 
68
68
  The installer displays a confirmation banner before making any changes:
@@ -90,7 +90,7 @@ The installer displays a confirmation banner before making any changes:
90
90
  Press Enter to continue or Ctrl+C to abort...
91
91
  ```
92
92
 
93
- Press **Enter** to proceed. To skip the confirmation prompt, use `npx @lamalibre/create-lamaste@4.4.0 --yes`.
93
+ Press **Enter** to proceed. To skip the confirmation prompt, use `npx @lamalibre/create-lamaste@4.5.0 --yes`.
94
94
 
95
95
  ### 4. What the Installer Does
96
96
 
@@ -380,7 +380,7 @@ The service includes security hardening: `ProtectHome=true`, `PrivateTmp=true`,
380
380
 
381
381
  | Item | Value |
382
382
  | ------------------------ | ------------------------------------------------------------ |
383
- | **Install command** | `npx @lamalibre/create-lamaste@4.4.0` |
383
+ | **Install command** | `npx @lamalibre/create-lamaste@4.5.0` |
384
384
  | **Minimum VPS** | Ubuntu 24.04, 512 MB RAM |
385
385
  | **Required ports** | 22 (SSH), 80 (HTTP/Let's Encrypt), 443 (HTTPS), 9292 (panel) |
386
386
  | **Certificate location** | `/etc/lamalibre/lamaste/pki/client.p12` |