@lamalibre/create-lamaste 4.6.0 → 4.7.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 (149) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +20 -3
  3. package/scripts/lamaste-priv +22 -3
  4. package/scripts/pin-release.js +98 -0
  5. package/src/index.js +8 -1
  6. package/src/lib/env.js +19 -0
  7. package/src/lib/service-config.js +7 -0
  8. package/src/tasks/redeploy.js +8 -0
  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 +6 -2
  13. package/vendor/docs/01-concepts/authorization.md +4 -0
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
  16. package/vendor/docs/01-concepts/mtls.md +2 -2
  17. package/vendor/docs/01-concepts/security-model.md +28 -3
  18. package/vendor/docs/01-concepts/tickets.md +2 -2
  19. package/vendor/docs/01-concepts/tunneling.md +4 -4
  20. package/vendor/docs/02-guides/agent-setup.md +5 -5
  21. package/vendor/docs/02-guides/certificate-management.md +2 -2
  22. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  23. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  24. package/vendor/docs/02-guides/installation.md +3 -3
  25. package/vendor/docs/02-guides/managing-users.md +5 -11
  26. package/vendor/docs/02-guides/onboarding.md +2 -2
  27. package/vendor/docs/02-guides/second-domain.md +82 -0
  28. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  29. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  30. package/vendor/docs/03-architecture/installer.md +4 -4
  31. package/vendor/docs/03-architecture/management-flow.md +1 -1
  32. package/vendor/docs/03-architecture/nginx-configuration.md +15 -5
  33. package/vendor/docs/03-architecture/overview.md +5 -5
  34. package/vendor/docs/03-architecture/panel-client.md +6 -6
  35. package/vendor/docs/03-architecture/panel-server.md +2 -2
  36. package/vendor/docs/03-architecture/state-management.md +3 -3
  37. package/vendor/docs/03-architecture/system-overview.md +1 -1
  38. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +17 -13
  39. package/vendor/docs/04-api-reference/app-host.md +3 -3
  40. package/vendor/docs/04-api-reference/domains.md +192 -0
  41. package/vendor/docs/04-api-reference/gatekeeper.md +35 -7
  42. package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
  43. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  44. package/vendor/docs/04-api-reference/overview.md +9 -0
  45. package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
  46. package/vendor/docs/04-api-reference/sites.md +5 -0
  47. package/vendor/docs/04-api-reference/tunnels.md +15 -10
  48. package/vendor/docs/04-api-reference/users.md +41 -29
  49. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  50. package/vendor/docs/05-operations/uninstalling.md +1 -1
  51. package/vendor/docs/05-operations/upgrades.md +6 -6
  52. package/vendor/docs/06-reference/config-files.md +1 -1
  53. package/vendor/docs/06-reference/glossary.md +3 -3
  54. package/vendor/docs/06-reference/installer-flags.md +13 -13
  55. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  56. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  57. package/vendor/docs/_index.json +105 -19
  58. package/vendor/docs/external-members-design.md +816 -0
  59. package/vendor/docs/gatekeeper-design.md +8 -8
  60. package/vendor/docs/identity-directory-design.md +3 -3
  61. package/vendor/docs/multi-domain-relay-design.md +836 -0
  62. package/vendor/docs/package.json +1 -1
  63. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  64. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  65. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  66. package/vendor/gatekeeper/dist/lib/grants.js +20 -13
  67. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  68. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
  69. package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
  70. package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
  71. package/vendor/gatekeeper/dist/lib/secret.js +12 -0
  72. package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
  73. package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
  74. package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
  75. package/vendor/gatekeeper/dist/lib/templates.js +80 -14
  76. package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
  77. package/vendor/gatekeeper/dist/server/index.d.ts +5 -1
  78. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  79. package/vendor/gatekeeper/dist/server/index.js +28 -2
  80. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  81. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  82. package/vendor/gatekeeper/dist/server/routes/authz.js +225 -36
  83. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  84. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  85. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
  86. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  87. package/vendor/gatekeeper/package.json +2 -2
  88. package/vendor/server/bin/lamaste-server.js +7 -0
  89. package/vendor/server/package.json +2 -2
  90. package/vendor/server/src/commands/domains.js +214 -0
  91. package/vendor/server/src/commands/plugin-identity.js +2 -16
  92. package/vendor/server/src/commands/plugins.js +112 -8
  93. package/vendor/server/src/commands/registry-credential.js +10 -3
  94. package/vendor/server/src/commands/sites.js +3 -0
  95. package/vendor/server/src/commands/status.js +19 -0
  96. package/vendor/server/src/commands/tunnels.js +4 -1
  97. package/vendor/server/src/commands/users.js +8 -19
  98. package/vendor/server/src/domains.js +92 -0
  99. package/vendor/server/src/fail.js +21 -0
  100. package/vendor/server/src/oidc-client-grants.js +63 -0
  101. package/vendor/server/src/panel-api.js +15 -0
  102. package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
  103. package/vendor/server-ui/dist/assets/{index-BZwQ_aQc.css → index-Cb-ADsbB.css} +1 -1
  104. package/vendor/server-ui/dist/index.html +2 -2
  105. package/vendor/serverd/package.json +2 -2
  106. package/vendor/serverd/src/index.js +42 -16
  107. package/vendor/serverd/src/lib/app-error.js +24 -0
  108. package/vendor/serverd/src/lib/audience.js +12 -3
  109. package/vendor/serverd/src/lib/authelia.js +341 -35
  110. package/vendor/serverd/src/lib/directory.js +67 -52
  111. package/vendor/serverd/src/lib/domains.js +1792 -0
  112. package/vendor/serverd/src/lib/gatekeeper-client.js +20 -4
  113. package/vendor/serverd/src/lib/identity-sync.js +35 -0
  114. package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
  115. package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
  116. package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
  117. package/vendor/serverd/src/lib/nginx.js +318 -140
  118. package/vendor/serverd/src/lib/owned-groups.js +326 -0
  119. package/vendor/serverd/src/lib/plugin-domain.js +86 -0
  120. package/vendor/serverd/src/lib/plugin-lifecycle.js +1 -0
  121. package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
  122. package/vendor/serverd/src/lib/plugins.js +45 -5
  123. package/vendor/serverd/src/lib/registry-credential.js +39 -6
  124. package/vendor/serverd/src/lib/services.js +9 -1
  125. package/vendor/serverd/src/lib/state.js +11 -0
  126. package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
  127. package/vendor/serverd/src/lib/user-surface.js +29 -4
  128. package/vendor/serverd/src/lib/users-lock.js +23 -0
  129. package/vendor/serverd/src/lib/vhost-reconcile.js +149 -41
  130. package/vendor/serverd/src/lib/web-surface.js +49 -32
  131. package/vendor/serverd/src/middleware/user-access-session.js +11 -4
  132. package/vendor/serverd/src/routes/invite.js +15 -5
  133. package/vendor/serverd/src/routes/management/domains.js +154 -0
  134. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
  135. package/vendor/serverd/src/routes/management/identity.js +21 -7
  136. package/vendor/serverd/src/routes/management/invitations.js +33 -4
  137. package/vendor/serverd/src/routes/management/plugins.js +4 -2
  138. package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
  139. package/vendor/serverd/src/routes/management/sites.js +50 -11
  140. package/vendor/serverd/src/routes/management/tunnels.js +29 -5
  141. package/vendor/serverd/src/routes/management/users.js +184 -64
  142. package/vendor/serverd/src/routes/management.js +2 -0
  143. package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
  144. package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
  145. package/vendor/serverd/src/routes/user-access.js +13 -1
  146. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
  147. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
  148. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
  149. package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +0 -84
@@ -319,6 +319,15 @@ There are two exceptions:
319
319
  | PUT | `/api/users/:username` | Management | Update user |
320
320
  | DELETE | `/api/users/:username` | Management | Delete user |
321
321
  | POST | `/api/users/:username/reset-totp` | Management | Reset TOTP secret |
322
+ | GET | `/api/domains` | Management | List domains |
323
+ | POST | `/api/domains` | Management | Add a domain |
324
+ | POST | `/api/domains/:name/verify-dns` | Management | Check a domain's DNS |
325
+ | POST | `/api/domains/:name/activate` | Management | Activate a domain |
326
+ | DELETE | `/api/domains/:name` | Management | Remove a domain |
327
+ | GET | `/api/domains/:name/members` | Management | List a domain's members |
328
+ | PUT | `/api/domains/:name/members/:user` | Management | Add a member |
329
+ | DELETE | `/api/domains/:name/members/:user` | Management | End a membership |
330
+ | PUT | `/api/plugins/:name/domain` | Management | Move a plugin to a domain |
322
331
  | GET | `/api/certs` | Management | List certificates |
323
332
  | GET | `/api/certs/auto-renew-status` | Management | Auto-renew timer status |
324
333
  | POST | `/api/certs/:domain/renew` | Management | Force-renew certificate |
@@ -37,19 +37,21 @@ Plugins use the same Authelia to let people and programs log in. A plugin's mani
37
37
  }
38
38
  ```
39
39
 
40
- | Field | Default | Meaning |
41
- | ------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
- | `id` | — | Client id. Must be the plugin's route (`feria` for `@lamalibre/feria-server`) or `<route>-<suffix>`; one plugin cannot claim another's client |
43
- | `name` | `<plugin> (<id>)` | Shown on Authelia's consent screen |
44
- | `grantTypes` | — | Any of `urn:ietf:params:oauth:grant-type:device_code`, `authorization_code`, `refresh_token`, `client_credentials`. `client_credentials` requires `confidential: true`; `authorization_code` requires `redirectUris`. A client whose only grant is `client_credentials` may not have `openid` or `offline_access` |
45
- | `scopes` | `["openid"]` | Scopes the client may request (lowercase letters and underscores) |
46
- | `audience` | `[]` | Audiences the client may request; `{domain}` becomes the relay domain. Clients must request an audience explicitly (`requested_audience_mode: explicit`) |
47
- | `redirectUris` | `[]` | For `authorization_code`; `{domain}` is expanded |
48
- | `accessTokenSignedResponseAlg` | `RS256` | `RS256` makes access tokens JWTs (RFC 9068) a resource server verifies against Authelia's JWKS; `none` keeps them opaque |
49
- | `accessTokenClaims` | `[]` | Claims copied into the JWT access token through a per-client claims policy: `groups` (needs the `groups` scope), `preferred_username` and `name` (`profile`), `email` (`email`). Without a policy Authelia puts these claims only in the UserInfo response |
50
- | `authorizationPolicy` | `two_factor` | `one_factor` or `two_factor` for the user-facing grants. A client with the `client_credentials` grant has no end user: Authelia requires `one_factor` there, so it defaults to `one_factor` and `two_factor` is rejected |
51
- | `requirePkce` | `false` | Require PKCE on authorization-code requests |
52
- | `confidential` | `false` | A confidential client authenticates to the token endpoint with a secret (`client_secret_basic`). Lamaste generates the secret |
40
+ | Field | Default | Meaning |
41
+ | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `id` | — | Client id. Must be the plugin's route (`feria` for `@lamalibre/feria-server`) or `<route>-<suffix>`; one plugin cannot claim another's client |
43
+ | `name` | `<plugin> (<id>)` | Shown on Authelia's consent screen |
44
+ | `grantTypes` | — | Any of `urn:ietf:params:oauth:grant-type:device_code`, `authorization_code`, `client_credentials` — never `refresh_token` (below). `client_credentials` requires `confidential: true`; `authorization_code` requires `redirectUris`. A client whose only grant is `client_credentials` may not have `openid` or `offline_access` |
45
+ | `scopes` | `["openid"]` | Scopes the client may request (lowercase letters and underscores) |
46
+ | `audience` | `[]` | Audiences the client may request; `{domain}` becomes the relay domain. Clients must request an audience explicitly (`requested_audience_mode: explicit`) |
47
+ | `redirectUris` | `[]` | For `authorization_code`; `{domain}` is expanded |
48
+ | `accessTokenSignedResponseAlg` | `RS256` | `RS256` makes access tokens JWTs (RFC 9068) a resource server verifies against Authelia's JWKS; `none` keeps them opaque |
49
+ | `accessTokenClaims` | `[]` | Claims copied into the JWT access token through a per-client claims policy: `groups` (needs the `groups` scope), `preferred_username` and `name` (`profile`), `email` (`email`). Without a policy Authelia puts these claims only in the UserInfo response |
50
+ | `authorizationPolicy` | `two_factor` | `one_factor` or `two_factor` for the user-facing grants. A client with the `client_credentials` grant has no end user: Authelia requires `one_factor` there, so it defaults to `one_factor` and `two_factor` is rejected |
51
+ | `requirePkce` | `false` | Require PKCE on authorization-code requests |
52
+ | `confidential` | `false` | A confidential client authenticates to the token endpoint with a secret (`client_secret_basic`). Lamaste generates the secret |
53
+
54
+ **No refresh tokens.** A manifest declaring the `refresh_token` grant, or the `offline` / `offline_access` scope, is refused at install and update: Authelia checks membership of the client's domain only when a user authorizes a client, so a removed member could keep refreshing. A plugin installed before is served without them (`validateInstalledManifest`).
53
55
 
54
56
  What the panel writes for a public client: `public: true`, `token_endpoint_auth_method: none`, `consent_mode: explicit`. For a confidential client: `client_secret` as a pbkdf2-sha512 digest (`$pbkdf2-sha512$310000$<salt>$<key>`), `token_endpoint_auth_method: client_secret_basic`, and no consent mode when the only grant is `client_credentials`. Any client with the `client_credentials` grant is rendered with `authorization_policy: one_factor` and may not carry the `openid`, `offline` or `offline_access` scopes — Authelia 4.39 refuses the configuration otherwise (`validator/identity_providers.go`). A client with `accessTokenClaims` gets `claims_policy: <id>` and a policy of the same name under `identity_providers.oidc.claims_policies` with `access_token: [...]`.
55
57
 
@@ -162,6 +164,7 @@ Admin only, 10 per minute. Verifies the whole chain against the registry, then s
162
164
 
163
165
  ```json
164
166
  { "registryUrl": "https://feria.example.com", "auth": { "kind": "oidc" } }
167
+ { "registryUrl": "https://feria.codelama.com.tr", "auth": { "kind": "oidc", "issuerDomain": "codelama.com.tr" } }
165
168
  { "registryUrl": "https://feria.example.com", "auth": { "kind": "token", "token": "feria_…" }, "scope": "@lamalibre" }
166
169
  ```
167
170
 
@@ -206,6 +209,16 @@ CLI: `lamaste-server registry-credential show | set --url <registry> (--oidc | -
206
209
 
207
210
  When a user loses a `feria-*` group, or is deleted, the panel asks the registry to revoke that user's tokens (`DELETE /-/feria/tokens?owner=user:<name>`) with the same credential. This is best effort — the user operation reports `warnings` rather than failing. The identity change feed planned for the identity directory (see [Identity directory design](../identity-directory-design.md)) will supersede this hook: plugins will subscribe to membership changes instead of the panel calling each registry.
208
211
 
212
+ ## Several domains: one issuer each
213
+
214
+ Authelia takes the issuer from the request's host, so every active domain is an issuer of its own: `https://auth.<domain>`. Every client is known to every issuer — Authelia has no per-host client list — but a token carries the issuer it was obtained at, and a relying party that trusts one issuer (Feria) refuses the others.
215
+
216
+ - **A plugin's clients belong to its domain** ([`PUT /api/plugins/:name/domain`](domains.md#put-apipluginsnamedomain); the primary until moved): `{domain}` in `audience` and `redirectUris` expands to it, and each user-facing client has an authorization policy of its own, `lamaste_client_<id>` under `identity_providers.oidc.authorization_policies` (`default_policy: deny`): the domain's members (group `lamaste:m:<key>`, the domain's random group key) with the client's factor, and its external members only with a grant on the client (groups `lamaste:x:<key>` and `lamaste:oidc@<id>`). Authelia reads a user's groups afresh on the authorization-code path but not on the device-code path, so a change that takes a device-code client away from someone restarts Authelia. Uninstalling a plugin revokes the grants on its clients. A user's registry tokens are revoked when none of the registry's own sign-in clients (user-facing clients whose audience is the registry) admits them any more. Authelia's `access_control` does not apply to OIDC authorization; these policies are how `feria login` admits only members — and the external members granted it. A pure `client_credentials` client (no user) stays `one_factor`.
217
+ - **Granting a client to external members**: a Gatekeeper grant with `resourceType: "oidc-client"` and the client id as `resourceId`, to a user or a Lamaste group (`POST /api/gatekeeper/grants`; the panel refuses a client no installed plugin declares, `unknown_oidc_client`, and a service client, `service_oidc_client`). `GET /api/plugins/:name/oidc-clients` lists each user-facing client's `grants` (`userFacing: true`; `grants: null` when Gatekeeper could not be read); the plugin card grants and revokes; CLI `lamaste-server plugins oidc-clients <name> grant|revoke <clientId> --user <u>|--group <g>`. A grant or a revocation is a `users.yml` change: it applies at the user's next authorization, without restarting Authelia. Members need no grant.
218
+ - **Token lifetime.** Access and ID tokens of user-facing clients live 15 minutes (a custom lifespan the clients name): what an authorization already issued survives a revocation at most that long. Registry tokens Feria minted from a login are revoked by the panel when the user no longer reaches the registry's sign-in.
219
+ - **The registry credential names its issuer**: `auth.issuerDomain` (an active domain; absent = the primary) is the domain whose issuer the panel's tokens carry (`X-Forwarded-Host: auth.<issuerDomain>`); the owner Feria records is then `ci:auth.<issuerDomain>/lamaste-server`. CLI: `lamaste-server registry-credential set --url <registry> --oidc --issuer-domain <domain>`. Losing what reaches the registry's sign-in — membership of one of the registry's domains, or, for an external member, the grant on its client — revokes a user's registry tokens.
220
+ - **Moving Feria to another domain**: move its plugin's domain, re-create its tunnel there, point Feria's `auth.json` at the new issuer and audience, and set the credential again with the new `issuerDomain`.
221
+
209
222
  ## Related
210
223
 
211
224
  - [Authentication (Authelia)](../01-concepts/authentication.md) — the OpenID Connect provider configuration
@@ -150,6 +150,7 @@ For a custom domain:
150
150
  | `aliases` | `string[]` | Custom sites only, optional, at most 10 hostnames | Hostnames that 301 to the site, e.g. `["www.myblog.net"]`. Each needs its own A record; one certificate covers them all |
151
151
  | `spaMode` | `boolean` | Optional, defaults to `false` | Serve `index.html` for all routes |
152
152
  | `autheliaProtected` | `boolean` | Optional, defaults to `false` | Require Authelia login |
153
+ | `domain` | `string` | Optional, managed sites only; an active domain (defaults to the primary) | A managed site's base domain: `<name>.<domain>` ([Domains](domains.md)) |
153
154
 
154
155
  **Name regex:**
155
156
 
@@ -620,6 +621,10 @@ curl -s --cert client.p12:password \
620
621
  | 400 | `{"error":"..."}` | Path traversal attempt or file not found |
621
622
  | 404 | `{"error":"Site not found"}` | No site with the given UUID |
622
623
 
624
+ ## Sites and domains
625
+
626
+ A managed site is `<name>.<domain>`; site **names** stay unique across the relay (agents' `allowedSites` and the CLI name sites by them), so a second `blog.` on another domain is a custom site there. A custom site's hostname names its own domain. An Authelia-protected site admits members of its domain only (its `allowedUsers` narrowed to them); a site whose hostname is under none of the relay's domains cannot be protected (400 `NOT_UNDER_DOMAIN` — no session cookie reaches it). A signed-in user Authelia refuses gets Gatekeeper's refusal page (`error_page 403` → `/authz/refusal/<fqdn>`), which names both possible reasons: not a member of the domain, or not one of the site's users. The relay's own hosts (`panel.`, `auth.`, `tunnel.`, `agent-*` under any domain) are never a custom site or an alias; a domain's apex and `www.` may be.
627
+
623
628
  ## Quick Reference
624
629
 
625
630
  | Method | Path | Description |
@@ -98,15 +98,16 @@ The carrying agent's sync timer picks the tunnel up within about 30 seconds and
98
98
  }
99
99
  ```
100
100
 
101
- | Field | Type | Validation | Description |
102
- | --------------- | --------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
103
- | `subdomain` | `string` | Lowercase alphanumeric + hyphens, max 63 chars, cannot start/end with hyphen | The subdomain to create |
104
- | `port` | `integer` | 1024 - 65535, not a [reserved port](#port) | Port the agent forwards |
105
- | `agentLabel` | `string` | An enrolled, non-revoked agent's label | **Required for admin certificates.** An agent certificate may omit it (defaults to itself) but may not name another agent |
106
- | `description` | `string` | Max 200 chars, optional (defaults to `""`) | Human-readable description |
107
- | `type` | `string` | `'app'` (default), `'panel'`, or `'plugin'` | `panel` requires the `panel:expose` capability; `plugin` is admin-only |
108
- | `accessMode` | `string` | `'restricted'` (default), `'authenticated'`, `'public'` | `authenticated` and `public` are admin-only |
109
- | `maxBodySizeMb` | `integer` | 1 - 10240 for admins, 1 - 100 for agents; optional (defaults to 10) | Written into the vhost as `client_max_body_size`; larger requests get HTTP 413. Raise it for apps that take big uploads |
101
+ | Field | Type | Validation | Description |
102
+ | --------------- | --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
103
+ | `subdomain` | `string` | Lowercase alphanumeric + hyphens, max 63 chars, cannot start/end with hyphen | The subdomain to create |
104
+ | `port` | `integer` | 1024 - 65535, not a [reserved port](#port) | Port the agent forwards |
105
+ | `agentLabel` | `string` | An enrolled, non-revoked agent's label | **Required for admin certificates.** An agent certificate may omit it (defaults to itself) but may not name another agent |
106
+ | `description` | `string` | Max 200 chars, optional (defaults to `""`) | Human-readable description |
107
+ | `type` | `string` | `'app'` (default), `'panel'`, or `'plugin'` | `panel` requires the `panel:expose` capability; `plugin` is admin-only |
108
+ | `accessMode` | `string` | `'restricted'` (default), `'authenticated'`, `'public'` | `authenticated` and `public` are admin-only |
109
+ | `maxBodySizeMb` | `integer` | 1 - 10240 for admins, 1 - 100 for agents; optional (defaults to 10) | Written into the vhost as `client_max_body_size`; larger requests get HTTP 413. Raise it for apps that take big uploads |
110
+ | `domain` | `string` | An active domain of the relay; optional (defaults to the primary); panel tunnels only on the primary | The base domain of the hostname: `<subdomain>.<domain>` ([Domains](domains.md)) |
110
111
 
111
112
  **Subdomain regex:**
112
113
 
@@ -318,7 +319,7 @@ curl -s --cert client.p12:password \
318
319
  | `chiselArgs` | `string[]` | Chisel client arguments: `client`, the server URL, and one `R:127.0.0.1:<port>:127.0.0.1:<port>` remote per tunnel. No `--tls-skip-verify` (the agent verifies the relay's certificate) and no credential — the agent supplies its own through the service environment, never argv |
319
320
  | `chiselCredentialIssuedAt` | `string \| null` | When the agent's current Chisel credential was issued — a timestamp, never the credential. An agent holding a credential with a different issue time (an administrator rotated it) fetches the current one from `GET /api/agents/me/chisel-credential` |
320
321
  | `tunnels` | `array` | The agent's enabled tunnels with port and subdomain |
321
- | `hostFactsHash` | `string \| null` | Agent certificates only: the SHA-256 of the [host facts](agent-hosted-plugins.md#host-facts) the relay stores for the agent, computed as the agent computes it over its own (`hostFactsHash` in `@lamalibre/lamaste@4.6.0`); `null` when it stores none. The agent's sync reports its facts again when this is not the hash of its own, so a record replaced since its last report — the agent enrolled again on its label, the relay restored from a backup — has them again after one sync, not a day. An agent behind a relay without the field reports as before |
322
+ | `hostFactsHash` | `string \| null` | Agent certificates only: the SHA-256 of the [host facts](agent-hosted-plugins.md#host-facts) the relay stores for the agent, computed as the agent computes it over its own (`hostFactsHash` in `@lamalibre/lamaste@4.7.0`); `null` when it stores none. The agent's sync reports its facts again when this is not the hash of its own, so a record replaced since its last report — the agent enrolled again on its label, the relay restored from a backup — has them again after one sync, not a day. An agent behind a relay without the field reports as before |
322
323
 
323
324
  **Errors:**
324
325
 
@@ -407,6 +408,10 @@ Revoking an agent's certificate releases its tunnels: its app and plugin tunnels
407
408
  - Must be unique across all tunnels
408
409
  - Must not be a hostname a static site or site alias already serves (checked on tunnel creation; site creation and alias changes check the reverse)
409
410
 
411
+ ### Hostname
412
+
413
+ A tunnel's hostname is `<subdomain>.<domain>` and is unique as a whole: `feria.samplepark.tr` and `feria.codelama.com.tr` may both exist. Reserved labels (`panel`, `auth`, `tunnel`, `www`, `mail`, `ftp`, `api`, and `agent-*` except an agent's own panel tunnel) are reserved on every domain. `agent-config` lists each carried tunnel with its `fqdn`; agents' chisel needs only the port and the primary's `tunnel.` endpoint, so a tunnel on another domain changes nothing on the agent. The vhost is `lamalibre-lamaste-app-<fqdn>` (`lamalibre-lamaste-agent-panel-<fqdn>` for a panel tunnel).
414
+
410
415
  ### Port
411
416
 
412
417
  - Must be an integer
@@ -19,10 +19,10 @@ If onboarding is not complete, all endpoints return `503 Service Unavailable`.
19
19
  ## Important Behavior
20
20
 
21
21
  - **Password hashing:** Passwords are hashed with bcrypt (not argon2id — argon2id uses ~93MB per hash and causes OOM kills on a 512MB droplet).
22
- - **Authelia restart:** After every user change that modifies `users.yml` (create, update, delete), Authelia is restarted via `systemctl restart authelia` to pick up the changes. If the restart fails, the operation still succeeds — the failure is logged and listed in the response's `warnings`. Note: TOTP reset does **not** restart Authelia — it writes directly to Authelia's SQLite database via the `authelia storage user totp generate` CLI command, which takes effect immediately.
22
+ - **No Authelia restart:** Authelia watches `users.yml` (`authentication_backend.file.watch: true`) and re-reads a user's details at each of their requests, so a user change (create, update, disable, delete) applies without a restart and without ending anyone else's session: a disabled or deleted user's session ends at their next request. Authelia skips a reload within 0.5 s of its previous one, so the panel writes the file once more 0.6 s after its last write — the change is always picked up. TOTP reset writes directly to Authelia's SQLite database via the `authelia storage user totp generate` CLI command, which takes effect immediately.
23
23
  - **Atomic writes:** The `users.yml` file is written atomically (write to temp file, then rename) to prevent Authelia from reading a partially written file.
24
- - **One lock for every user write:** creating, updating, disabling and deleting users, and accepting an invitation, are read-modify-writes of `users.yml` under one lock in the panel. Under the same lock the panel brings the directory in line with the file **before** Authelia restarts — so a disabled user is refused by plugins at once — [offboards](#offboarding) every user that stopped being one, then restarts Authelia, then recomputes the plugins' audiences. Concurrent user changes therefore run one after the other.
25
- - **A change must stick:** Authelia's own password reset rewrites `users.yml` from what Authelia read when it started, so a change written just before its restart can be lost. After the restart the panel reads `users.yml` again and checks that it holds the change (the fields set, the user created or gone); when it does not, the change is made once more (and Authelia restarted again). When it still does not, the answer carries `"ok": false`, `"persisted": false` and a `persist` warning — never a silent success.
24
+ - **One lock for every user write:** creating, updating, disabling and deleting users, and accepting an invitation, are read-modify-writes of `users.yml` under one lock in the panel. Under the same lock the panel brings the directory in line with the file — so a disabled user is refused by plugins at once — [offboards](#offboarding) every user that stopped being one, renders who may reach each domain, drops Gatekeeper's cached sessions, then recomputes the plugins' audiences. Concurrent user changes therefore run one after the other.
25
+ - **A change must stick:** Authelia's own password reset rewrites `users.yml` from what it holds in memory, so a change written in the instant before can be lost. The panel reads `users.yml` again and checks that it holds the change (the fields set, the user created or gone); when it does not, the change is made once more. When it still does not, the answer carries `"ok": false`, `"persisted": false` and a `persist` warning — never a silent success.
26
26
  - **The directory follows `users.yml`, read only:** at start, after onboarding and every minute the panel reconciles the directory with the file without writing it (Authelia's own password reset rewrites it; an operator may edit it): an unknown username gets a new `userId`, a changed display name, email or `disabled` is adopted, and a username gone from the file — seen missing in two reads, so a half-written file never tombstones anyone — is removed as `deleted-outside`. A user the reconcile sees disabled or removed is offboarded exactly as one disabled or deleted through the API. Every change is audited (`directory.user.created`, `.updated`, `.removed`) by `userId` alone; audit entries never name a user.
27
27
  - **Audit:** besides the directory's own entries, the users API records `user.password.set` (a password set through `PUT`), `user.groups.changed` (`added`, `removed` group names) and `user.totp.reset` — each by `userId` alone.
28
28
  - **Last user protection:** You cannot delete the last remaining user. At least one user must always exist.
@@ -75,10 +75,11 @@ curl -s --cert client.p12:password \
75
75
  | `username` | `string` | Unique login identifier |
76
76
  | `displayname` | `string` | Human-readable display name |
77
77
  | `email` | `string` | User's email address |
78
- | `groups` | `string[]` | Authelia group memberships |
78
+ | `groups` | `string[]` | Authelia group memberships an administrator assigned — never the groups Lamaste owns ([Domains](#domains)) |
79
79
  | `disabled` | `boolean` | Authelia's `disabled: true` — the user cannot sign in |
80
80
  | `userId` | `string \| null` | The user's directory id; `null` until the directory has seen the user (within a minute of an edit of `users.yml` made outside the panel) |
81
81
  | `status` | `string` | `active` or `disabled` |
82
+ | `domains` | `object[]` | Domain memberships, `{ name, kind }` — `kind` `member` or `external` ([Domains](#domains)) |
82
83
 
83
84
  Users deleted from `users.yml` are not listed here; the [directory](#get-api-identity-directory) keeps their tombstones.
84
85
 
@@ -106,13 +107,14 @@ Creates a new Authelia user. The password is hashed with bcrypt before being sto
106
107
  }
107
108
  ```
108
109
 
109
- | Field | Type | Validation | Description |
110
- | ------------- | ---------- | ------------------------------------------------------------------------------------------- | ------------------------------------------- |
111
- | `username` | `string` | 2-32 chars, lowercase alphanumeric + underscores + hyphens, starting with a letter or digit | Login identifier |
112
- | `displayname` | `string` | 1-100 chars | Display name |
113
- | `email` | `string` | Valid email format | Email address |
114
- | `password` | `string` | 8-128 chars | Plain-text password (hashed before storage) |
115
- | `groups` | `string[]` | Optional, defaults to `[]` | Authelia group memberships |
110
+ | Field | Type | Validation | Description |
111
+ | ------------- | ---------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
112
+ | `username` | `string` | 2-32 chars, lowercase alphanumeric + underscores + hyphens, starting with a letter or digit | Login identifier |
113
+ | `displayname` | `string` | 1-100 chars | Display name |
114
+ | `email` | `string` | Valid email format | Email address |
115
+ | `password` | `string` | 8-128 chars | Plain-text password (hashed before storage) |
116
+ | `groups` | `string[]` | Optional, defaults to `[]` | Authelia group memberships |
117
+ | `domains` | array | Names or `{ name, kind }`, at most 50; required once the relay has two (400 `domains_required`) | Memberships ([Domains](#domains)); with one domain, a member of it unless named `external` |
116
118
 
117
119
  **Username regex** (the same rule applies to invitations — a leading `-` would read as a flag to Authelia's CLI, which the panel runs per username for TOTP secrets, second factors and OpenID Connect identities):
118
120
 
@@ -145,7 +147,7 @@ curl -s --cert client.p12:password \
145
147
  }
146
148
  ```
147
149
 
148
- A new user gets a new `userId`, even when an earlier, deleted user had the same username. Before the new user is created ([Reused usernames](#reused-usernames)): the username is refused when Authelia holds an OpenID Connect identity for it; and when a deleted user held it, Gatekeeper drops everything it holds under the username (grants, group memberships) and Authelia the second factors (TOTP, WebAuthn) — always, not only when a cleanup was pending — so a reused username inherits no grant, group, role or second factor. `warnings` (`[{ step, message }]`) lists what went wrong after `users.yml` was written — the directory not updated, Authelia not restarted; a change that did not stick answers `200` with `"ok": false`, `"persisted": false`.
150
+ A new user gets a new `userId`, even when an earlier, deleted user had the same username. Before the new user is created ([Reused usernames](#reused-usernames)): the username is refused when Authelia holds an OpenID Connect identity for it; and when a deleted user held it, Gatekeeper drops everything it holds under the username (grants, group memberships) and Authelia the second factors (TOTP, WebAuthn) — always, not only when a cleanup was pending — so a reused username inherits no grant, group, role or second factor. `warnings` (`[{ step, message }]`) lists what went wrong after `users.yml` was written — the directory not updated, domain access not rendered; a change that did not stick answers `200` with `"ok": false`, `"persisted": false`.
149
151
 
150
152
  **Errors:**
151
153
 
@@ -179,13 +181,14 @@ If a new password is provided, it is hashed with bcrypt before storage.
179
181
  }
180
182
  ```
181
183
 
182
- | Field | Type | Validation | Description |
183
- | ------------- | ---------- | --------------------- | --------------------------------------------------------------------------- |
184
- | `displayname` | `string` | 1-100 chars, optional | New display name |
185
- | `email` | `string` | Valid email, optional | New email address |
186
- | `password` | `string` | 8-128 chars, optional | New password (hashed before storage) |
187
- | `groups` | `string[]` | Optional | New group memberships (replaces entire list) |
188
- | `disabled` | `boolean` | Optional | `true` disables the user, `false` enables them ([below](#disabling-a-user)) |
184
+ | Field | Type | Validation | Description |
185
+ | ------------- | ---------- | --------------------- | --------------------------------------------------------------------------------- |
186
+ | `displayname` | `string` | 1-100 chars, optional | New display name |
187
+ | `email` | `string` | Valid email, optional | New email address |
188
+ | `password` | `string` | 8-128 chars, optional | New password (hashed before storage) |
189
+ | `groups` | `string[]` | Optional | New group memberships (replaces entire list; Lamaste's own groups are kept) |
190
+ | `domains` | array | Optional | Memberships afterwards, exactly — names or `{ name, kind }` ([Domains](#domains)) |
191
+ | `disabled` | `boolean` | Optional | `true` disables the user, `false` enables them ([below](#disabling-a-user)) |
189
192
 
190
193
  All fields are optional, but at least one must be provided (validated by a Zod `.refine()`).
191
194
 
@@ -214,11 +217,11 @@ curl -s --cert client.p12:password \
214
217
  }
215
218
  ```
216
219
 
217
- `warnings` lists what did not happen after the write (the directory, Authelia's restart, the steps of [offboarding](#offboarding)); `"ok": false` with `"persisted": false` and a `persist` warning says the change did not stick ([above](#important-behavior)). A change of `groups` revokes the user's registry (Feria) tokens, whose scopes were granted from `feria-*` groups at login.
220
+ `warnings` lists what did not happen after the write (the directory, rendering domain access, the steps of [offboarding](#offboarding)); `"ok": false` with `"persisted": false` and a `persist` warning says the change did not stick ([above](#important-behavior)). A change of `groups` revokes the user's registry (Feria) tokens, whose scopes were granted from `feria-*` groups at login.
218
221
 
219
222
  #### Disabling a user
220
223
 
221
- `{ "disabled": true }` is the offboarding action. It sets Authelia's `disabled: true` — the user can no longer sign in — and, before Authelia restarts:
224
+ `{ "disabled": true }` is the offboarding action. It sets Authelia's `disabled: true` — the user can no longer sign in, and their session ends at their next request — and:
222
225
 
223
226
  - the directory marks the user `disabled`, so every plugin's [user surface](agent-hosted-plugins.md#the-user-surface) refuses them at once (`403`);
224
227
  - every plugin audience that holds them shows them `disabled`, and their agent halves are told;
@@ -256,7 +259,7 @@ Desktop sessions are bound to the `userId` as well as the username: one is valid
256
259
 
257
260
  ### `DELETE /api/users/:username`
258
261
 
259
- Deletes an Authelia user: the user is removed from `users.yml`, the directory turns their row into a **tombstone** (`removed`, reason `deleted`, keeping the `userId`, the username and the last display name and email, so plugins' history stays readable), and Authelia restarts. With `?erase=true` the tombstone is [erased](#erasing-a-removed-user) at once as well.
262
+ Deletes an Authelia user: the user is removed from `users.yml`, the directory turns their row into a **tombstone** (`removed`, reason `deleted`, keeping the `userId`, the username and the last display name and email, so plugins' history stays readable); their session ends at their next request. With `?erase=true` the tombstone is [erased](#erasing-a-removed-user) at once as well.
260
263
 
261
264
  You cannot delete the last remaining user. This safety check prevents accidentally locking out all users from tunneled applications.
262
265
 
@@ -344,6 +347,14 @@ The `totpUri` follows the [Key Uri Format](https://github.com/google/google-auth
344
347
  | 500 | `{"error":"Failed to read user database"}` | Cannot read `users.yml` |
345
348
  | 500 | `{"error":"Failed to write TOTP configuration"}` | Cannot write TOTP secret to Authelia's SQLite database |
346
349
 
350
+ ### Domains
351
+
352
+ Every user listed by `GET /api/users` carries `domains`: their memberships, `{ name, kind }` — `kind` `member` or `external` (empty until the directory has seen them). `PUT /api/users/:username` takes `domains` too — the memberships afterwards, exactly: the difference is applied in one change, each membership audited (409 `user_not_in_directory` until the directory has seen the user, 400 `unknown_domain`, 400 `conflicting_membership` for one domain named twice with two kinds); one membership at a time is the [Domains API](domains.md) (`PUT`/`DELETE /api/domains/:name/members/:user`, by `userId` or username). Wherever a request names memberships, a domain name is a `member` and `{ "name": "…", "kind": "external" }` an external member. On a relay with two domains or more, `POST /api/users` needs a non-empty `domains` (400 `domains_required`). `POST /api/invitations` takes `domains` exactly as `POST /api/users` does, and `GET /api/invitations` lists each invitation's `domains` as `{ name, kind }`; the invitation link is on the sign-in page of the first domain it names (`https://auth.<domain>/invite/<token>`). Disabling a user keeps their memberships (they are let in nowhere while disabled); deleting ends them.
353
+
354
+ **External members** reach only what names them — `restricted` hosts that grant them, static sites that list them, OIDC clients granted to them — and **hold no Authelia groups**: a user who is an external member of any domain cannot be given a group (409 `external_member_groups`), a user with groups cannot be made external (409), and a new user or an invitation asking for both is refused (400 `external_member_groups`).
355
+
356
+ **Groups Lamaste owns.** Who may reach what is written into each user's `groups` in `users.yml` as groups Lamaste owns — `lamaste:m:<key>`, `lamaste:x:<key>` (a random key per domain), `lamaste:s:<site id>`, hostnames (external members' granted hosts), `lamaste:oidc@<client id>` ([Domains API](domains.md#where-membership-is-enforced)). The users and identity APIs never show them and an edit keeps them; an administrator assigns only names of letters, digits, `-` and `_` (400 `reserved_group` for a name with `.`, `:` or `@` the user does not hold already).
357
+
347
358
  ## The User Directory
348
359
 
349
360
  The directory lives in the panel's `state.db` (migration `0013`): one row per user it ever knew, never deleted, and the directory's own id (`directoryId`, a UUID v4 made with the store), which tells a plugin's agent half when the store it follows was replaced. Admin only.
@@ -475,13 +486,14 @@ Returned by list and mutation endpoints (sensitive fields excluded):
475
486
 
476
487
  ### Validation Summary
477
488
 
478
- | Field | Rules |
479
- | ------------- | --------------------------------------------- |
480
- | `username` | 2-32 chars, `/^[a-z0-9][a-z0-9_-]*$/`, unique |
481
- | `displayname` | 1-100 chars |
482
- | `email` | Valid email format |
483
- | `password` | 8-128 chars |
484
- | `groups` | Array of strings, optional |
489
+ | Field | Rules |
490
+ | ------------- | ----------------------------------------------------------------------------------- |
491
+ | `username` | 2-32 chars, `/^[a-z0-9][a-z0-9_-]*$/`, unique |
492
+ | `displayname` | 1-100 chars |
493
+ | `email` | Valid email format |
494
+ | `password` | 8-128 chars |
495
+ | `groups` | Array of strings, optional; `[A-Za-z0-9_-]{1,64}` each; none for an external member |
496
+ | `domains` | Names or `{ name, kind }`, at most 50 |
485
497
 
486
498
  ### curl Cheat Sheet
487
499
 
@@ -199,7 +199,7 @@ sudo chown lamaste-authelia:lamaste-authelia /etc/authelia/db.sqlite3* /etc/auth
199
199
  sudo chmod 600 /etc/authelia/db.sqlite3* /etc/authelia/notifications.txt
200
200
  ```
201
201
 
202
- A backup taken on a version before the privilege boundary holds root-owned Authelia files and older unit files. After restoring one, re-run `npx @lamalibre/create-lamaste@4.6.0 --yes`: the redeploy rewrites the `chisel` and `authelia` units and converts the ownership of `/etc/authelia`, the Chisel key and the PKI directory (see [Upgrades](upgrades.md)).
202
+ A backup taken on a version before the privilege boundary holds root-owned Authelia files and older unit files. After restoring one, re-run `npx @lamalibre/create-lamaste@4.7.0 --yes`: the redeploy rewrites the `chisel` and `authelia` units and converts the ownership of `/etc/authelia`, the Chisel key and the PKI directory (see [Upgrades](upgrades.md)).
203
203
 
204
204
  The panel re-applies the authfile's group and mode itself on every start, so a Chisel that fails to read it before the panel is up is started again by the panel's reconciliation.
205
205
 
@@ -29,7 +29,7 @@ It stops and disables the panel, gatekeeper, Chisel and Authelia; removes their
29
29
  The manual guide is also available from the CLI:
30
30
 
31
31
  ```bash
32
- npx @lamalibre/create-lamaste@4.6.0 --uninstall
32
+ npx @lamalibre/create-lamaste@4.7.0 --uninstall
33
33
  ```
34
34
 
35
35
  This prints the same steps listed below.
@@ -27,7 +27,7 @@ Coming from 3.x, upgrade the **agents first**, as 4.0 required: agents from befo
27
27
  On each agent machine, after the server:
28
28
 
29
29
  ```bash
30
- sudo npm install -g @lamalibre/lamaste-agent@4.6.0
30
+ sudo npm install -g @lamalibre/lamaste-agent@4.7.0
31
31
  lamaste-agent update
32
32
  ```
33
33
 
@@ -50,7 +50,7 @@ A machine that hosts app plugins' agent halves needs the root-owned install show
50
50
 
51
51
  - **`state.db` migration `0013`.** On its first start the panel creates the user directory (a `directoryId`, a `userId` per user, tombstones), the plugin audiences, the per-plugin feed state and the erasure records, and adds `plugins.identity`. It then reads `users.yml` — without writing it — and gives every existing user a `userId`. Back up `state.db` before upgrading, as before any upgrade: from now on it holds the mapping from usernames to user ids ([Backup and Restore](backup-and-restore.md#backup-considerations)).
52
52
  - **The redeploy now touches the IP panel vhost.** Until 4.4 a redeploy left every panel vhost alone. From 4.5 it refreshes `lamalibre-lamaste-panel-ip` — the vhost the installer writes and the panel cannot — when its text differs from the current generator's: written, checked with `nginx -t`, put back on failure, and nginx reloaded; its `sites-enabled` link (which panel 2FA owns) is left as it is. A failure is reported and skipped (`Refreshing the IP panel vhost — Skipped`): the panel keeps working on the vhost it had, which still passes a client's `X-Lamalibre-Relay-Assertion` through to the panel (which accepts only a valid, single-use one Gatekeeper signed) — fix what `nginx -t` names and redeploy. The panel itself rewrites the domain panel, auth portal and tunnel vhosts at start, as since 4.3; each now clears a client's `X-Lamalibre-Relay-Assertion`.
53
- - **No agent upgrade is needed**, and no agent half must be rebuilt. An agent half built on `@lamalibre/lamaste@4.6.0` 4.4 keeps working and **never receives a user caller**: the panel sends one only after the half has answered the identity feed with protocol 1 for its installed version. A plugin gets users by shipping a version that declares `identity` with a half built on 4.5 — an update that waits for approval — then binding its user surface to a tunnel.
53
+ - **No agent upgrade is needed**, and no agent half must be rebuilt. An agent half built on `@lamalibre/lamaste@4.7.0` 4.4 keeps working and **never receives a user caller**: the panel sends one only after the half has answered the identity feed with protocol 1 for its installed version. A plugin gets users by shipping a version that declares `identity` with a half built on 4.5 — an update that waits for approval — then binding its user surface to a tunnel.
54
54
  - **What changes for existing setups.** The admin forwarder no longer reaches an agent half's `/admin/app` or anything under it (on the panel `/api/<route>/app*` is the user surface). The hook preview's `inactiveRoutes` is now `userSurfaceRoutes`. Every write of `users.yml` the panel makes — creating, updating, deleting users, accepting invitations — now runs under one lock, one after the other. Deleting a user keeps a tombstone in the directory and clears the username's grants and group memberships in Gatekeeper. A registry credential must carry the `read` scope as well as `admin` (a `PUT /api/registry-credential` with an admin-only identity is refused).
55
55
 
56
56
  ### Updating Lamaste (Panel Server and Client)
@@ -102,7 +102,7 @@ exit
102
102
 
103
103
  ### Updating Chisel
104
104
 
105
- Chisel is the tunnel server binary at `/usr/local/bin/chisel`. Lamaste pins it: server and agents run Chisel 1.12.0 (`CHISEL_RELEASE` in `@lamalibre/lamaste@4.6.0`), downloaded from the fixed release URL and verified against a pinned SHA-256 before it is unpacked. **Do not replace the binary by hand.** On the server, `create-lamaste` installs it (as root) and replaces a binary that reports any other version; the panel cannot install binaries, and only logs an error at startup when the installed Chisel is not the pinned release (it then restarts Chisel on every authfile change). Each agent's sync replaces its own copy.
105
+ Chisel is the tunnel server binary at `/usr/local/bin/chisel`. Lamaste pins it: server and agents run Chisel 1.12.0 (`CHISEL_RELEASE` in `@lamalibre/lamaste@4.7.0`), downloaded from the fixed release URL and verified against a pinned SHA-256 before it is unpacked. **Do not replace the binary by hand.** On the server, `create-lamaste` installs it (as root) and replaces a binary that reports any other version; the panel cannot install binaries, and only logs an error at startup when the installed Chisel is not the pinned release (it then restarts Chisel on every authfile change). Each agent's sync replaces its own copy.
106
106
 
107
107
  A new Chisel version therefore arrives with a Lamaste release that changes the pin: re-run the installer, which installs it and restarts Chisel. To check what is running:
108
108
 
@@ -116,7 +116,7 @@ Tunnel clients reconnect on their own after the brief interruption (they retry a
116
116
 
117
117
  ### Updating Authelia
118
118
 
119
- Authelia is the authentication server binary at `/usr/local/bin/authelia`. Lamaste pins it too: Authelia 4.39.28 (`AUTHELIA_RELEASE` in `@lamalibre/lamaste@4.6.0`), downloaded by `create-lamaste` from its fixed release URL and verified against a pinned SHA-256 per architecture. A new Authelia version arrives with a Lamaste release that changes the pin — re-run the installer. It stops Authelia while it replaces the binary and starts it again.
119
+ Authelia is the authentication server binary at `/usr/local/bin/authelia`. Lamaste pins it too: Authelia 4.39.28 (`AUTHELIA_RELEASE` in `@lamalibre/lamaste@4.7.0`), downloaded by `create-lamaste` from its fixed release URL and verified against a pinned SHA-256 per architecture. A new Authelia version arrives with a Lamaste release that changes the pin — re-run the installer. It stops Authelia while it replaces the binary and starts it again.
120
120
 
121
121
  An installed Authelia **newer** than the pin is kept, never downgraded: its database may already use a schema the pinned release cannot read, and Authelia refuses to start on a database newer than itself. If you install a newer release by hand, stop the service, replace the binary as root with a `root:root 0755` file, and start it again; check the [Authelia changelog](https://github.com/authelia/authelia/blob/master/CHANGELOG.md) first, since the configuration format may change between versions.
122
122
 
@@ -255,7 +255,7 @@ The panel server, CLI, client, docs and gatekeeper are always redeployed (files
255
255
 
256
256
  ### Version Pinning
257
257
 
258
- Chisel and Authelia are pinned by version and SHA-256 in `packages/core/lib/src/constants.ts` (`CHISEL_RELEASE`, `AUTHELIA_RELEASE`); changing a pin means updating the version and every digest there. The downloads are verified by `downloadVerifiedChisel` / `downloadVerifiedAuthelia` (exported from `@lamalibre/lamaste@4.6.0`) before anything is unpacked; the server installer uses them in `packages/provisioners/server/src/lib/binaries.js`. The configuration the panel writes needs Authelia 4.38 or later.
258
+ Chisel and Authelia are pinned by version and SHA-256 in `packages/core/lib/src/constants.ts` (`CHISEL_RELEASE`, `AUTHELIA_RELEASE`); changing a pin means updating the version and every digest there. The downloads are verified by `downloadVerifiedChisel` / `downloadVerifiedAuthelia` (exported from `@lamalibre/lamaste@4.7.0`) before anything is unpacked; the server installer uses them in `packages/provisioners/server/src/lib/binaries.js`. The configuration the panel writes needs Authelia 4.38 or later.
259
259
 
260
260
  ### Automated Updates
261
261
 
@@ -277,7 +277,7 @@ sudo dpkg-reconfigure -plow unattended-upgrades
277
277
  | Panel server | `/opt/lamalibre/lamaste/lamaste-serverd/` | Re-run `npx @lamalibre/create-lamaste@latest` |
278
278
  | Panel client | `/opt/lamalibre/lamaste/lamaste-server-ui/` | Re-run `npx @lamalibre/create-lamaste@latest` |
279
279
  | Chisel | `/usr/local/bin/chisel` | Pinned; installed by `create-lamaste` |
280
- | Agents | `sudo npm install -g @lamalibre/lamaste-agent@4.6.0` | Upgrade after the server, then `lamaste-agent update` |
280
+ | Agents | `sudo npm install -g @lamalibre/lamaste-agent@4.7.0` | Upgrade after the server, then `lamaste-agent update` |
281
281
  | Authelia | `/usr/local/bin/authelia` | Pinned; installed by `create-lamaste` (newer is kept) |
282
282
  | nginx | System package | `sudo apt-get update && sudo apt-get upgrade` |
283
283
  | certbot | System package | `sudo apt-get update && sudo apt-get upgrade` |
@@ -822,7 +822,7 @@ Service files use per-agent names:
822
822
  - macOS: `com.lamalibre.lamaste.chisel-<label>` (plist label), `~/Library/LaunchAgents/com.lamalibre.lamaste.chisel-<label>.plist` — mode 0600, because it carries `AUTH` in `EnvironmentVariables`
823
823
  - Linux: `lamalibre-lamaste-chisel-<label>` (unit name), `~/.config/systemd/user/lamalibre-lamaste-chisel-<label>.service` — a systemd **user** unit with `EnvironmentFile=` pointing at `chisel.env`; enable lingering (`sudo loginctl enable-linger <user>`) for it to start at boot without a login
824
824
 
825
- Both run `chisel client --max-retry-interval 30s https://tunnel.<domain>:443 R:127.0.0.1:<port>:127.0.0.1:<port> ...` with TLS verification and no credential in argv. They are generated by `@lamalibre/lamaste@4.6.0/agent` (`chisel-service.ts`) and rewritten by `lamaste-agent setup`, `update` and `sync` — do not edit them by hand. An agent carrying no tunnels has its service unloaded (Chisel cannot run without a remote).
825
+ Both run `chisel client --max-retry-interval 30s https://tunnel.<domain>:443 R:127.0.0.1:<port>:127.0.0.1:<port> ...` with TLS verification and no credential in argv. They are generated by `@lamalibre/lamaste@4.7.0/agent` (`chisel-service.ts`) and rewritten by `lamaste-agent setup`, `update` and `sync` — do not edit them by hand. An agent carrying no tunnels has its service unloaded (Chisel cannot run without a remote).
826
826
 
827
827
  The sync timer, which runs `<node> <installed lamaste-agent> sync --label <label> --quiet` (`<node>` is a stable path such as `/opt/homebrew/bin/node` when one on `PATH` resolves to the running binary, so a Node.js upgrade does not break it):
828
828
 
@@ -24,7 +24,7 @@ If you are reading documentation and hit an unfamiliar term, this is the place t
24
24
 
25
25
  **Where it appears:**
26
26
 
27
- - Enrolled from the panel (**Certificates → Enrollment Token**) and `lamaste-agent setup` (installed globally with `npm install -g @lamalibre/lamaste-agent@4.6.0`)
27
+ - Enrolled from the panel (**Certificates → Enrollment Token**) and `lamaste-agent setup` (installed globally with `npm install -g @lamalibre/lamaste-agent@4.7.0`)
28
28
  - Agent registry: `/etc/lamalibre/lamaste/agents.json` on the server; `~/.lamalibre/lamaste/agents/<label>/` on the agent
29
29
  - The **Agent** column and field on the panel's Tunnels page
30
30
 
@@ -242,7 +242,7 @@ See also: [mTLS](#mtls-mutual-tls), [CA](#ca-certificate-authority), [PKCS12](#p
242
242
 
243
243
  **Where it appears:**
244
244
 
245
- - Generated by `lamaste-agent setup` / `update` (implementation in `@lamalibre/lamaste@4.6.0/agent`)
245
+ - Generated by `lamaste-agent setup` / `update` (implementation in `@lamalibre/lamaste@4.7.0/agent`)
246
246
  - Installed to `~/Library/LaunchAgents/com.lamalibre.lamaste.chisel-<label>.plist` on the agent
247
247
  - Manages the Chisel client process lifecycle
248
248
 
@@ -373,7 +373,7 @@ See also: [Client Certificate](#client-certificate), [CA](#ca-certificate-author
373
373
 
374
374
  **Plain English:** A small scheduled job on every agent that checks with the panel every 30 seconds and adjusts the tunnel client when the agent's tunnels changed — so tunnels created, disabled or moved in the panel reach the agent without anyone running a command on it.
375
375
 
376
- **Technical:** Runs `lamaste-agent sync --label <label>`, which fetches `GET /api/tunnels/agent-config` and converges the agent's Chisel client with it (relay URL, credential, pinned binary, granted remotes, running or stopped). It exists because Chisel rejects a client's whole session when one requested remote is not granted: an agent must drop a revoked tunnel promptly or all its other tunnels go dark. Requires a global install of `@lamalibre/lamaste-agent@4.6.0`.
376
+ **Technical:** Runs `lamaste-agent sync --label <label>`, which fetches `GET /api/tunnels/agent-config` and converges the agent's Chisel client with it (relay URL, credential, pinned binary, granted remotes, running or stopped). It exists because Chisel rejects a client's whole session when one requested remote is not granted: an agent must drop a revoked tunnel promptly or all its other tunnels go dark. Requires a global install of `@lamalibre/lamaste-agent@4.7.0`.
377
377
 
378
378
  **Where it appears:**
379
379
 
@@ -1,11 +1,11 @@
1
1
  # Installer Flags
2
2
 
3
- > CLI flags, environment variables, and exit codes for `npx @lamalibre/create-lamaste@4.6.0`.
3
+ > CLI flags, environment variables, and exit codes for `npx @lamalibre/create-lamaste@4.7.0`.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
- npx @lamalibre/create-lamaste@4.6.0 [flags]
8
+ npx @lamalibre/create-lamaste@4.7.0 [flags]
9
9
  ```
10
10
 
11
11
  The installer runs as root on a fresh Ubuntu 24.04 server. It is completely non-interactive by default — all configuration happens later through the browser-based onboarding wizard.
@@ -26,7 +26,7 @@ The installer runs as root on a fresh Ubuntu 24.04 server. It is completely non-
26
26
  Prints a description of what the installer does, lists all flags, and exits with code 0. Does not modify anything on the system.
27
27
 
28
28
  ```bash
29
- npx @lamalibre/create-lamaste@4.6.0 --help
29
+ npx @lamalibre/create-lamaste@4.7.0 --help
30
30
  ```
31
31
 
32
32
  ### `--yes`, `-y`
@@ -34,7 +34,7 @@ npx @lamalibre/create-lamaste@4.6.0 --help
34
34
  Skips the interactive confirmation prompt that appears before installation begins. Useful for automated deployments or scripts where you cannot press Enter.
35
35
 
36
36
  ```bash
37
- npx @lamalibre/create-lamaste@4.6.0 --yes
37
+ npx @lamalibre/create-lamaste@4.7.0 --yes
38
38
  ```
39
39
 
40
40
  Without this flag, the installer displays a banner listing all changes it will make and waits for the user to press Enter or Ctrl+C.
@@ -51,7 +51,7 @@ Skips all OS hardening tasks:
51
51
  Everything else (Node.js, nginx, mTLS, panel deployment) still runs.
52
52
 
53
53
  ```bash
54
- npx @lamalibre/create-lamaste@4.6.0 --skip-harden
54
+ npx @lamalibre/create-lamaste@4.7.0 --skip-harden
55
55
  ```
56
56
 
57
57
  Use this if you manage your own firewall rules, already have fail2ban configured, or are running on a platform that handles security at a different layer.
@@ -61,7 +61,7 @@ Use this if you manage your own firewall rules, already have fail2ban configured
61
61
  Allows the installer to run on a machine with a private or non-routable IP address (e.g., `192.168.x.x`, `10.x.x.x`). By default, the installer requires a public IP.
62
62
 
63
63
  ```bash
64
- npx @lamalibre/create-lamaste@4.6.0 --dev
64
+ npx @lamalibre/create-lamaste@4.7.0 --dev
65
65
  ```
66
66
 
67
67
  This is useful for:
@@ -76,7 +76,7 @@ When `--dev` is active, the installer prints the detected IP with a note: `(dev
76
76
  Forces the installer to run the complete installation pipeline, ignoring skip guards that would normally detect an existing installation. This is useful when an installation is in an inconsistent state and you want to ensure all components are fully deployed.
77
77
 
78
78
  ```bash
79
- npx @lamalibre/create-lamaste@4.6.0 --force-full
79
+ npx @lamalibre/create-lamaste@4.7.0 --force-full
80
80
  ```
81
81
 
82
82
  Use this if:
@@ -90,7 +90,7 @@ Use this if:
90
90
  Prints a step-by-step guide for manually removing Lamaste from the server, then exits with code 0. Does not modify anything on the system.
91
91
 
92
92
  ```bash
93
- npx @lamalibre/create-lamaste@4.6.0 --uninstall
93
+ npx @lamalibre/create-lamaste@4.7.0 --uninstall
94
94
  ```
95
95
 
96
96
  The printed guide first points to the automated removal, `sudo lamaste-server uninstall`, then covers:
@@ -207,25 +207,25 @@ Components that are always redeployed on re-run:
207
207
 
208
208
  ```bash
209
209
  ssh root@203.0.113.42
210
- npx @lamalibre/create-lamaste@4.6.0
210
+ npx @lamalibre/create-lamaste@4.7.0
211
211
  ```
212
212
 
213
213
  **Automated install (no confirmation prompt):**
214
214
 
215
215
  ```bash
216
- npx @lamalibre/create-lamaste@4.6.0 --yes
216
+ npx @lamalibre/create-lamaste@4.7.0 --yes
217
217
  ```
218
218
 
219
219
  **Install without OS hardening:**
220
220
 
221
221
  ```bash
222
- npx @lamalibre/create-lamaste@4.6.0 --yes --skip-harden
222
+ npx @lamalibre/create-lamaste@4.7.0 --yes --skip-harden
223
223
  ```
224
224
 
225
225
  **Test in a local VM:**
226
226
 
227
227
  ```bash
228
- npx @lamalibre/create-lamaste@4.6.0 --dev
228
+ npx @lamalibre/create-lamaste@4.7.0 --dev
229
229
  ```
230
230
 
231
231
  **Update an existing installation:**
@@ -237,7 +237,7 @@ npx @lamalibre/create-lamaste@latest --yes
237
237
  **View the uninstall guide:**
238
238
 
239
239
  ```bash
240
- npx @lamalibre/create-lamaste@4.6.0 --uninstall
240
+ npx @lamalibre/create-lamaste@4.7.0 --uninstall
241
241
  ```
242
242
 
243
243
  ## Quick Reference
@@ -34,7 +34,7 @@ A tunnel's port is also a loopback port on the relay: its vhost proxies to `127.
34
34
  | 9292 | IP-based panel listener |
35
35
  | 9294 | Gatekeeper |
36
36
 
37
- A tunnel on one of them would publish that internal service on a public hostname. For the panel that is an administrator takeover, not a leak: the panel trusts the client-certificate headers nginx sets. `RESERVED_TUNNEL_PORTS` in `@lamalibre/lamaste@4.6.0` is enforced by the request schema, inside the tunnel workflow, and again when the Chisel grants are rendered. Tunnel ports must also be 1024–65535.
37
+ A tunnel on one of them would publish that internal service on a public hostname. For the panel that is an administrator takeover, not a leak: the panel trusts the client-certificate headers nginx sets. `RESERVED_TUNNEL_PORTS` in `@lamalibre/lamaste@4.7.0` is enforced by the request schema, inside the tunnel workflow, and again when the Chisel grants are rendered. Tunnel ports must also be 1024–65535.
38
38
 
39
39
  ## Systemd Units
40
40