@lamalibre/create-lamaste 4.4.1 → 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 (149) 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 +291 -22
  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 +14 -12
  40. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  41. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  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 +82 -188
  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/plugins.js +59 -4
  125. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  126. package/vendor/serverd/src/lib/request-logging.js +19 -0
  127. package/vendor/serverd/src/lib/retained-data.js +50 -4
  128. package/vendor/serverd/src/lib/state.js +18 -0
  129. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  130. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  131. package/vendor/serverd/src/lib/user-caller.js +67 -0
  132. package/vendor/serverd/src/lib/user-surface.js +733 -0
  133. package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
  134. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  135. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  136. package/vendor/serverd/src/routes/invite.js +53 -12
  137. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  138. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  139. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  140. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  141. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  142. package/vendor/serverd/src/routes/management/tunnels.js +98 -2
  143. package/vendor/serverd/src/routes/management/users.js +290 -208
  144. package/vendor/serverd/src/routes/management.js +4 -0
  145. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  146. package/vendor/serverd/src/routes/user-access.js +17 -4
  147. package/vendor/serverd/src/routes/user-surface.js +69 -0
  148. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  149. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -6,7 +6,9 @@
6
6
 
7
7
  When someone visits one of your tunneled apps (like `app.example.com`), they are prompted to log in through Authelia with a username, password, and a TOTP code from an authenticator app. The users API lets you create those login accounts, update their details, reset their TOTP secrets, and delete accounts you no longer need.
8
8
 
9
- These are not admin panel users — the panel itself is protected by mTLS client certificates with no login screen. These users are for the apps you expose through tunnels.
9
+ These are not admin panel users — the panel itself is protected by mTLS client certificates with no login screen. These users are for the apps you expose through tunnels, and for the agent-hosted plugins that declare users of their own ([Agent-Hosted Plugins](agent-hosted-plugins.md#users-and-identity)).
10
+
11
+ Authelia keys users by username, which anyone can reuse. The panel therefore keeps a **user directory** beside Authelia's `users.yml`: every user gets a stable `userId` (a UUID v4) the moment their username appears, plugins key their data on it, and the directory keeps the row for good. A user is `active`, `disabled` (cannot sign in — the one offboarding action) or `removed` (deleted). A deleted user leaves a **tombstone** — the `userId` and the last known name and address — so a plugin's history stays readable, until an administrator **erases** it. A username that comes back after a deletion is a new user with a new `userId`.
10
12
 
11
13
  ## Authentication
12
14
 
@@ -17,9 +19,14 @@ If onboarding is not complete, all endpoints return `503 Service Unavailable`.
17
19
  ## Important Behavior
18
20
 
19
21
  - **Password hashing:** Passwords are hashed with bcrypt (not argon2id — argon2id uses ~93MB per hash and causes OOM kills on a 512MB droplet).
20
- - **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 — a warning is logged but no error is returned to the client. 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
+ - **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.
21
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.
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
+ - **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.
22
28
  - **Last user protection:** You cannot delete the last remaining user. At least one user must always exist.
29
+ - **Last administrator protection:** You cannot disable the last enabled member of the `admins` group.
23
30
 
24
31
  ## Endpoints
25
32
 
@@ -45,24 +52,35 @@ curl -s --cert client.p12:password \
45
52
  "username": "admin",
46
53
  "displayname": "Admin User",
47
54
  "email": "admin@example.com",
48
- "groups": []
55
+ "groups": ["admins"],
56
+ "disabled": false,
57
+ "userId": "0b6c2f4e-8a1d-4f3b-9c55-1e2d3f4a5b6c",
58
+ "status": "active"
49
59
  },
50
60
  {
51
61
  "username": "alice",
52
62
  "displayname": "Alice Smith",
53
63
  "email": "alice@example.com",
54
- "groups": ["developers"]
64
+ "groups": ["developers"],
65
+ "disabled": true,
66
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
67
+ "status": "disabled"
55
68
  }
56
69
  ]
57
70
  }
58
71
  ```
59
72
 
60
- | Field | Type | Description |
61
- | ------------- | ---------- | --------------------------- |
62
- | `username` | `string` | Unique login identifier |
63
- | `displayname` | `string` | Human-readable display name |
64
- | `email` | `string` | User's email address |
65
- | `groups` | `string[]` | Authelia group memberships |
73
+ | Field | Type | Description |
74
+ | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `username` | `string` | Unique login identifier |
76
+ | `displayname` | `string` | Human-readable display name |
77
+ | `email` | `string` | User's email address |
78
+ | `groups` | `string[]` | Authelia group memberships |
79
+ | `disabled` | `boolean` | Authelia's `disabled: true` — the user cannot sign in |
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
+ | `status` | `string` | `active` or `disabled` |
82
+
83
+ Users deleted from `users.yml` are not listed here; the [directory](#get-api-identity-directory) keeps their tombstones.
66
84
 
67
85
  **Errors:**
68
86
 
@@ -96,7 +114,7 @@ Creates a new Authelia user. The password is hashed with bcrypt before being sto
96
114
  | `password` | `string` | 8-128 chars | Plain-text password (hashed before storage) |
97
115
  | `groups` | `string[]` | Optional, defaults to `[]` | Authelia group memberships |
98
116
 
99
- **Username regex** (the same rule applies to invitations — a leading `-` would read as a flag to Authelia's CLI when a TOTP secret is stored):
117
+ **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):
100
118
 
101
119
  ```
102
120
  ^[a-z0-9][a-z0-9_-]*$
@@ -119,20 +137,28 @@ curl -s --cert client.p12:password \
119
137
  "username": "alice",
120
138
  "displayname": "Alice Smith",
121
139
  "email": "alice@example.com",
122
- "groups": []
140
+ "groups": [],
141
+ "disabled": false,
142
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
143
+ "status": "active"
123
144
  }
124
145
  }
125
146
  ```
126
147
 
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`.
149
+
127
150
  **Errors:**
128
151
 
129
- | Status | Body | When |
130
- | ------ | ---------------------------------------------------------- | ------------------------------------------- |
131
- | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | Invalid username, email, or password length |
132
- | 409 | `{"error":"Username already exists"}` | A user with this username already exists |
133
- | 500 | `{"error":"Failed to read user database"}` | Cannot read `users.yml` |
134
- | 500 | `{"error":"Failed to hash password"}` | bcrypt hashing failed |
135
- | 500 | `{"error":"Failed to update user database"}` | Cannot write `users.yml` |
152
+ | Status | Body | When |
153
+ | ------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
154
+ | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | Invalid username, email, or password length |
155
+ | 409 | `{"error":"Username already exists"}` | A user with this username already exists |
156
+ | 409 | `{"error":"Authelia holds an OpenID Connect identity …","code":"username_has_oidc_identity"}` | Authelia holds an OpenID Connect opaque identifier for the username: every relying party would take the new user for the earlier one ([Reused usernames](#reused-usernames)) — choose another username |
157
+ | 503 | `{"error":"A removed user's grants …","code":"gatekeeper_unavailable"}` | An earlier user with this username left grants or groups in Gatekeeper, and Gatekeeper cannot remove them now |
158
+ | 503 | `{"error":"Authelia's storage could not …","code":"authelia_storage_unavailable"}` | Authelia's storage could not be checked for an OpenID Connect identity of the username, or an earlier user's second factors under it could not be deleted |
159
+ | 500 | `{"error":"Failed to read user database"}` | Cannot read `users.yml` |
160
+ | 500 | `{"error":"Failed to hash password"}` | bcrypt hashing failed |
161
+ | 500 | `{"error":"Failed to update user database"}` | Cannot write `users.yml` |
136
162
 
137
163
  ---
138
164
 
@@ -153,12 +179,13 @@ If a new password is provided, it is hashed with bcrypt before storage.
153
179
  }
154
180
  ```
155
181
 
156
- | Field | Type | Validation | Description |
157
- | ------------- | ---------- | --------------------- | -------------------------------------------- |
158
- | `displayname` | `string` | 1-100 chars, optional | New display name |
159
- | `email` | `string` | Valid email, optional | New email address |
160
- | `password` | `string` | 8-128 chars, optional | New password (hashed before storage) |
161
- | `groups` | `string[]` | Optional | New group memberships (replaces entire list) |
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)) |
162
189
 
163
190
  All fields are optional, but at least one must be provided (validated by a Zod `.refine()`).
164
191
 
@@ -179,47 +206,95 @@ curl -s --cert client.p12:password \
179
206
  "username": "alice",
180
207
  "displayname": "Alice J. Smith",
181
208
  "email": "alice.smith@example.com",
182
- "groups": ["developers", "admins"]
209
+ "groups": ["developers", "admins"],
210
+ "disabled": false,
211
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
212
+ "status": "active"
183
213
  }
184
214
  }
185
215
  ```
186
216
 
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.
218
+
219
+ #### Disabling a user
220
+
221
+ `{ "disabled": true }` is the offboarding action. It sets Authelia's `disabled: true` — the user can no longer sign in — and, before Authelia restarts:
222
+
223
+ - 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
+ - every plugin audience that holds them shows them `disabled`, and their agent halves are told;
225
+ - the user is [offboarded](#offboarding): their desktop app sessions end and their registry tokens are revoked (best effort — `warnings` on failure).
226
+
227
+ The user keeps their `userId`, grants and group memberships; `{ "disabled": false }` enables them again with everything as it was — except desktop sessions, which stay ended. A disabled user still counts as a user (they can be deleted). The last enabled member of the `admins` group cannot be disabled. A user disabled by editing `users.yml` is offboarded the same way when the next reconcile sees it.
228
+
229
+ #### Offboarding
230
+
231
+ One function, keyed by `userId`, for every way a user stops being one — disabled or deleted through this API, and disabled or gone from `users.yml` outside it (adopted by the minute's reconcile):
232
+
233
+ | Step | Disabled | Removed |
234
+ | ------------------------------------------------------------------------------------------------- | -------- | ------- |
235
+ | Desktop app sessions end: the user's session epoch (`panel.json` `userEpochs`, keyed by `userId`) | ✓ | ✓ |
236
+ | Registry (Feria) tokens revoked | ✓ | ✓ |
237
+ | Grants in the legacy `user-plugin-access.json` removed | | ✓ |
238
+ | Gatekeeper drops every grant and group membership under the username (pending until it can) | | ✓ |
239
+ | Authelia deletes the user's TOTP configuration and WebAuthn credentials | | ✓ |
240
+ | Gatekeeper's cached session verdicts dropped | ✓ | ✓ |
241
+
242
+ Desktop sessions are bound to the `userId` as well as the username: one is valid only while its user still holds the username and is enabled, so a username given to someone else never revives a session. Sessions issued before 4.5 carry no `userId` and are refused — desktop users sign in again once after the upgrade. An epoch older than a session's longest life (12 hours) can reject nothing and is dropped; `panel.json` names nobody.
243
+
187
244
  **Errors:**
188
245
 
189
- | Status | Body | When |
190
- | ------ | ---------------------------------------------------------- | ------------------------------------------------ |
191
- | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | No fields provided, or invalid field values |
192
- | 404 | `{"error":"User not found"}` | No user with this username |
193
- | 500 | `{"error":"Failed to read user database"}` | Cannot read `users.yml` |
194
- | 500 | `{"error":"Failed to hash password"}` | bcrypt hashing failed (when password is updated) |
195
- | 500 | `{"error":"Failed to update user database"}` | Cannot write `users.yml` |
246
+ | Status | Body | When |
247
+ | ------ | ------------------------------------------------------------------------------- | ------------------------------------------------ |
248
+ | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | No fields provided, or invalid field values |
249
+ | 400 | `{"error":"Cannot disable the last enabled administrator","code":"last_admin"}` | Disabling the last enabled member of `admins` |
250
+ | 404 | `{"error":"User not found"}` | No user with this username |
251
+ | 500 | `{"error":"Failed to read user database"}` | Cannot read `users.yml` |
252
+ | 500 | `{"error":"Failed to hash password"}` | bcrypt hashing failed (when password is updated) |
253
+ | 500 | `{"error":"Failed to update user database"}` | Cannot write `users.yml` |
196
254
 
197
255
  ---
198
256
 
199
257
  ### `DELETE /api/users/:username`
200
258
 
201
- Deletes an Authelia user. The user is removed from `users.yml` and Authelia is reloaded.
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.
202
260
 
203
261
  You cannot delete the last remaining user. This safety check prevents accidentally locking out all users from tunneled applications.
204
262
 
263
+ Then, never rolled back — what fails is reported in `warnings` — the user is [offboarded](#offboarding):
264
+
265
+ 1. Gatekeeper drops every grant naming the username (any resource type) and its membership of every Lamaste group, in one transaction ([Usernames and Gatekeeper](#usernames-and-gatekeeper)). When Gatekeeper cannot, the cleanup stays **pending** on the tombstone and is retried every minute; until it succeeds the username holds no role in any plugin and cannot be given to a new user.
266
+ 2. Authelia deletes the user's TOTP configuration and WebAuthn credentials. When it cannot, the username cannot be given to a new user until it can ([Reused usernames](#reused-usernames)).
267
+ 3. The user's registry (Feria) tokens are revoked, their grants in the legacy `user-plugin-access.json` removed, and their desktop app sessions ended.
268
+ 4. Every plugin that had the user as a member is sent `remove` with reason `deleted` and what it last received about them.
269
+
205
270
  **Request:**
206
271
 
207
- No request body.
272
+ No request body. Query: `erase` — `true` to erase the tombstone in the same call.
208
273
 
209
274
  ```bash
210
275
  curl -s --cert client.p12:password \
211
276
  -X DELETE \
212
- https://203.0.113.42:9292/api/users/alice | jq
277
+ "https://203.0.113.42:9292/api/users/alice?erase=true" | jq
213
278
  ```
214
279
 
215
280
  **Response (200):**
216
281
 
217
282
  ```json
218
283
  {
219
- "ok": true
284
+ "ok": true,
285
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
286
+ "principalCleared": true,
287
+ "erased": true
220
288
  }
221
289
  ```
222
290
 
291
+ | Field | Type | Description |
292
+ | ------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
293
+ | `userId` | `string \| null` | The tombstone's id; `null` when the directory held no row for the user (nothing to erase then — a warning) |
294
+ | `principalCleared` | `boolean` | Gatekeeper dropped the grants and memberships under the username; `false`: pending, retried every minute |
295
+ | `erased` | `boolean` | Only with `?erase=true`: whether the tombstone was erased (`false` with a warning — erase it later by id) |
296
+ | `warnings` | `array` | What did not happen: `{ step, message }` (or a sentence) |
297
+
223
298
  **Errors:**
224
299
 
225
300
  | Status | Body | When |
@@ -269,15 +344,118 @@ The `totpUri` follows the [Key Uri Format](https://github.com/google/google-auth
269
344
  | 500 | `{"error":"Failed to read user database"}` | Cannot read `users.yml` |
270
345
  | 500 | `{"error":"Failed to write TOTP configuration"}` | Cannot write TOTP secret to Authelia's SQLite database |
271
346
 
347
+ ## The User Directory
348
+
349
+ 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.
350
+
351
+ ### `GET /api/identity/directory`
352
+
353
+ Every user the directory knows — members by username, then the removed, then the erased — with who erased each erased one and the plugins that have not yet acknowledged an erasure.
354
+
355
+ ```json
356
+ {
357
+ "directoryId": "3b0e5c1a-7f2d-4c8e-9a1b-2c3d4e5f6a7b",
358
+ "users": [
359
+ {
360
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
361
+ "username": "alice",
362
+ "displayName": "Alice Smith",
363
+ "email": "alice@example.com",
364
+ "status": "removed",
365
+ "removedReason": "deleted",
366
+ "removedAt": "2026-10-22T08:00:00.000Z",
367
+ "erasedAt": null,
368
+ "principalClearedAt": "2026-10-22T08:00:00.120Z",
369
+ "version": 4,
370
+ "createdAt": "2026-10-01T10:00:00.000Z",
371
+ "updatedAt": "2026-10-22T08:00:00.000Z",
372
+ "erasure": null,
373
+ "pendingErasures": []
374
+ }
375
+ ]
376
+ }
377
+ ```
378
+
379
+ | Field | Description |
380
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
381
+ | `userId` | Stable id (UUID v4); never reused |
382
+ | `username`, `displayName`, `email` | As `users.yml` last had them; all `null` once erased |
383
+ | `status` | `active`, `disabled` or `removed` |
384
+ | `removedReason` | `deleted` (through Lamaste) or `deleted-outside` (gone from `users.yml` without it); `null` unless removed |
385
+ | `removedAt`, `erasedAt` | When the user was removed, and erased |
386
+ | `principalClearedAt` | When Gatekeeper dropped what it held under the removed user's username; `null` on a removed user: **cleanup pending** |
387
+ | `version` | 1 at creation, + 1 on every change |
388
+ | `erasure` | `{ requestedAt, requestedBy }` for an erased user, else `null` |
389
+ | `pendingErasures` | Names of the plugins that have not yet acknowledged this user's erasure — their hosting's agent half, or an agent that retains their data |
390
+ | `pendingErasuresInactive` | Among them, the plugins the feed does not reach now (uninstalled, or no longer declaring identity) whose data an agent keeps: the erasure reaches it when the plugin comes back, or goes with the data when it is purged |
391
+
392
+ ### Erasing a removed user
393
+
394
+ Deleting keeps the tombstone; **erasing** scrubs it. An erasure of a removed user, in this order — anything that fails before the scrub leaves the tombstone as it was, still naming the username a later attempt needs:
395
+
396
+ 1. Gatekeeper's principal cleanup, if it is still pending.
397
+ 2. Gatekeeper's **access-request log**: the user's rows logged before they were removed (and those still queued for the log) are deleted.
398
+ 3. Authelia's **second factors** for the username (TOTP, WebAuthn) are deleted, should their deletion at removal have failed.
399
+ 4. The user's **invitations** made before they were removed are deleted from `invitations.json`.
400
+ 5. The directory row is scrubbed to the bare `userId` (`username`, display name and email `NULL`, `erasedAt` set), who asked is recorded, and `directory.user.erased` is audited (`userId`, counts).
401
+ 6. The audiences are recomputed: **every** plugin that ever had the user as a member — including one uninstalled while an agent keeps its data, or one that no longer declares identity — has the user's row rewritten to `remove` with reason `erased` and no attributes. A plugin that takes the feed is sent it: its agent half scrubs its own row and anonymises what the plugin keeps in the same transaction, and its cursor passing the change is the acknowledgement. One that does not now replays it when it comes back. Until then the plugin is listed in the user's `pendingErasures` (marked in `pendingErasuresInactive` when the feed does not reach it now) and the user in the plugin's — [Agent-Hosted Plugins](agent-hosted-plugins.md#erasure). Delivery takes seconds while the hosting agent is online, and happens when it reconnects otherwise. An agent half never takes an erased user back: an erasure is sticky in its projection.
402
+
403
+ **Outside Lamaste's reach:** Authelia keeps the username's OpenID Connect **opaque identifier** (Authelia 4.39 cannot delete one — which is why the username is never given to anyone again while it exists) and its own **authentication log**; nginx access logs and journald keep what they logged before (see [Monitoring](../05-operations/monitoring.md#logs-and-personal-data) for their retention); backups taken before the erasure keep everything. Plugins replay erasures at their daily reconcile, which is how an erasure reaches an agent half's data restored from an older backup; restoring the relay itself is covered in [Backup and Restore](../05-operations/backup-and-restore.md).
404
+
405
+ ### `POST /api/identity/directory/:userId/erase`
406
+
407
+ Erases a removed user. 30 a minute.
408
+
409
+ **Response (200):**
410
+
411
+ ```json
412
+ {
413
+ "ok": true,
414
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
415
+ "erasedAt": "2026-10-22T08:05:00.000Z",
416
+ "invitationsPurged": 1,
417
+ "accessLogPurged": 3
418
+ }
419
+ ```
420
+
421
+ **Errors:**
422
+
423
+ | Status | `code` | When |
424
+ | ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
425
+ | 400 | | `userId` is not a UUID v4 |
426
+ | 404 | `user_not_found` | No user with this id |
427
+ | 409 | `user_not_removed` | The user is active or disabled — delete them first, or delete and erase in one step (`DELETE /api/users/:username?erase=true`) |
428
+ | 409 | `already_erased` | Erased already |
429
+ | 503 | `gatekeeper_unavailable` | Gatekeeper could not clear the principal or purge its log — nothing was erased; try again |
430
+ | 503 | `authelia_storage_unavailable` | Authelia's storage could not delete the second factors — nothing was erased; try again |
431
+
432
+ ### Usernames and Gatekeeper
433
+
434
+ Gatekeeper keys users by username too: the grants naming a user and their memberships of Lamaste groups would pass to anyone who later holds the username. So every removed user's **principal** is cleared in Gatekeeper — every grant naming the username, of any resource type, and its membership of every group, in one Gatekeeper transaction (`POST /api/principals/user/remove { username }` on Gatekeeper's own API, which only the panel calls; the username travels in the body, never in a URL) — when the user is removed (through Lamaste or outside it), every minute while that is pending (Gatekeeper was down), before an erasure, and **whenever a user is created under the username** — always then, pending or not: a restored `gatekeeper.db`, or a grant that raced the deletion, may have brought something back. Each cleanup is audited as `directory.user.principal-cleared` with the user ids.
435
+
436
+ A grant or a group membership given to a username through the panel (an entitlement, the Gatekeeper proxy) is given holding the users lock, so no deletion runs between the checks and the grant. A username a deleted user held and nobody holds now takes **nothing** — `409 username_deleted` (whatever it was given would pass to whoever is given the username next); a pending cleanup under a username is completed first (`503 gatekeeper_unavailable` while it cannot be). While a username's cleanup is pending it holds no role in any plugin. A username the directory never saw goes through: Gatekeeper may hold grants for a user still to come.
437
+
438
+ ### Reused usernames
439
+
440
+ A new user under a username an earlier user held inherits **nothing**: not their `userId` (a new one), not their grants, group memberships or plugin roles (Gatekeeper's principal cleared at creation), not their second factors (Authelia's TOTP configuration and WebAuthn credentials deleted at removal and again at creation — `503 authelia_storage_unavailable` while they cannot be), not their desktop sessions (bound to the `userId`).
441
+
442
+ One thing Lamaste cannot take away: the **OpenID Connect identity**. Authelia gives each user an opaque identifier (the `sub` claim relying parties see — plugins that sign users in through Lamaste's OpenID Connect provider, a registry) the first time they sign in to one, keyed by username, and Authelia 4.39 has no way to delete it. A new user under that username would be the old user to every relying party. So **a username Authelia holds an opaque identifier for is never given to a new user**: creating a user (or accepting an invitation) under it is refused with `409 username_has_oidc_identity` — choose another username. The panel checks with Authelia's own CLI (`lamaste-priv authelia-user has-oidc-identifier`, which answers yes or no and nothing else).
443
+
444
+ ---
445
+
272
446
  ## Quick Reference
273
447
 
274
- | Method | Path | Description |
275
- | ------ | --------------------------------- | -------------------------------------------------- |
276
- | GET | `/api/users` | List all users (alphabetical, no sensitive fields) |
277
- | POST | `/api/users` | Create user (bcrypt hash + Authelia reload) |
278
- | PUT | `/api/users/:username` | Update user fields (partial update) |
279
- | DELETE | `/api/users/:username` | Delete user (not the last one) |
280
- | POST | `/api/users/:username/reset-totp` | Generate new TOTP secret |
448
+ | Method | Path | Description |
449
+ | ------ | --------------------------------------- | ---------------------------------------------------------------------------------------- |
450
+ | GET | `/api/users` | List all users (alphabetical, no sensitive fields), with `userId` and `status` |
451
+ | POST | `/api/users` | Create user (bcrypt hash + Authelia reload); a username with an OIDC identity is refused |
452
+ | PUT | `/api/users/:username` | Update user fields (partial update); `disabled` disables/enables |
453
+ | DELETE | `/api/users/:username[?erase=true]` | Delete user (not the last one); a tombstone stays unless erased |
454
+ | POST | `/api/users/:username/reset-totp` | Generate new TOTP secret |
455
+ | GET | `/api/identity/directory` | Every user the directory knows, tombstones and erasures included |
456
+ | POST | `/api/identity/directory/:userId/erase` | Erase a removed user |
457
+
458
+ From the CLI (through the running panel, `--json` for NDJSON): `lamaste-server users list`, `users disable <username>`, `users enable <username>`, `users delete <username> [--erase]`, `users erase <userId>`, `users directory`. The panel's Users page and the desktop app (`AdminClient` `getUsers`, `disableUser`, `enableUser`, `deleteUser(username, { erase })`, `getUserDirectory`, `eraseDirectoryUser`) do the same — see [Managing Users](../02-guides/managing-users.md).
281
459
 
282
460
  ### User Object Shape
283
461
 
@@ -288,7 +466,10 @@ Returned by list and mutation endpoints (sensitive fields excluded):
288
466
  "username": "alice",
289
467
  "displayname": "Alice Smith",
290
468
  "email": "alice@example.com",
291
- "groups": ["developers"]
469
+ "groups": ["developers"],
470
+ "disabled": false,
471
+ "userId": "7d9e1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
472
+ "status": "active"
292
473
  }
293
474
  ```
294
475
 
@@ -326,6 +507,21 @@ curl -s --cert client.p12:password \
326
507
  -X DELETE \
327
508
  https://203.0.113.42:9292/api/users/bob | jq
328
509
 
510
+ # Disable a user (offboarding); {"disabled":false} enables them again
511
+ curl -s --cert client.p12:password \
512
+ -X PUT -H "Content-Type: application/json" \
513
+ -d '{"disabled":true}' \
514
+ https://203.0.113.42:9292/api/users/bob | jq
515
+
516
+ # Delete a user and erase what the directory keeps of them
517
+ curl -s --cert client.p12:password \
518
+ -X DELETE \
519
+ "https://203.0.113.42:9292/api/users/bob?erase=true" | jq
520
+
521
+ # The directory, tombstones included
522
+ curl -s --cert client.p12:password \
523
+ https://203.0.113.42:9292/api/identity/directory | jq
524
+
329
525
  # Reset TOTP
330
526
  curl -s --cert client.p12:password \
331
527
  -X POST \
@@ -18,24 +18,26 @@ Lamaste's state is spread across four directories. All of them matter.
18
18
 
19
19
  This is the most important directory. It contains:
20
20
 
21
- | File | Purpose |
22
- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
- | `panel.json` | Server configuration (IP, domain, email, onboarding status) |
24
- | `tunnels.json` | Tunnel definitions (subdomain, port, carrying agent, enabled state) |
25
- | `sites.json` | Static site definitions (including custom-domain aliases) |
26
- | `chisel-credentials.json` | Per-agent Chisel credentials. If lost, the panel mints a new credential for every active agent at its next start, and each agent's sync timer fetches it within about 30 seconds (the issue time changed) |
27
- | `chisel-users` | Chisel authfile — derived; rewritten from the two files above, with mode `0640` and group `lamaste-chisel`, at every panel start |
28
- | `chisel-sentinel` | Password of the no-grants sentinel Chisel user |
29
- | `invitations.json` | Pending user invitations |
30
- | `pki/revoked.json` | Revoked certificate tracking |
31
- | `pki/ca.key` | Certificate Authority private key |
32
- | `pki/ca.crt` | Certificate Authority certificate |
33
- | `pki/client.key` | Client certificate private key |
34
- | `pki/client.crt` | Client certificate |
35
- | `pki/client.p12` | PKCS12 bundle (imported into browsers) |
36
- | `pki/.p12-password` | Password for the PKCS12 bundle |
37
- | `pki/self-signed.pem` | Self-signed TLS cert for IP:9292 access |
38
- | `pki/self-signed-key.pem` | Self-signed TLS key for IP:9292 access |
21
+ | File | Purpose |
22
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
23
+ | `panel.json` | Server configuration (IP, domain, email, onboarding status) |
24
+ | `tunnels.json` | Tunnel definitions (subdomain, port, carrying agent, enabled state) |
25
+ | `sites.json` | Static site definitions (including custom-domain aliases) |
26
+ | `chisel-credentials.json` | Per-agent Chisel credentials. If lost, the panel mints a new credential for every active agent at its next start, and each agent's sync timer fetches it within about 30 seconds (the issue time changed) |
27
+ | `chisel-users` | Chisel authfile — derived; rewritten from the two files above, with mode `0640` and group `lamaste-chisel`, at every panel start |
28
+ | `chisel-sentinel` | Password of the no-grants sentinel Chisel user |
29
+ | `invitations.json` | Pending user invitations |
30
+ | `state.db` | The panel's SQLite store (WAL): plugins and their hostings' state, agents, audit log, notifications, settings cache — and the **user directory**: every user's `userId`, tombstones, the `directoryId`, plugin audiences and the identity feed's state |
31
+ | `gatekeeper.db` | Gatekeeper's SQLite store (WAL): Lamaste groups and members, grants (plugin entitlements included), the access-request log |
32
+ | `pki/revoked.json` | Revoked certificate tracking |
33
+ | `pki/ca.key` | Certificate Authority private key |
34
+ | `pki/ca.crt` | Certificate Authority certificate |
35
+ | `pki/client.key` | Client certificate private key |
36
+ | `pki/client.crt` | Client certificate |
37
+ | `pki/client.p12` | PKCS12 bundle (imported into browsers) |
38
+ | `pki/.p12-password` | Password for the PKCS12 bundle |
39
+ | `pki/self-signed.pem` | Self-signed TLS cert for IP:9292 access |
40
+ | `pki/self-signed-key.pem` | Self-signed TLS key for IP:9292 access |
39
41
 
40
42
  **If you lose the PKI files**, every browser that imported the client certificate will need a new one. This is the single most important directory to back up.
41
43
 
@@ -197,7 +199,7 @@ sudo chown lamaste-authelia:lamaste-authelia /etc/authelia/db.sqlite3* /etc/auth
197
199
  sudo chmod 600 /etc/authelia/db.sqlite3* /etc/authelia/notifications.txt
198
200
  ```
199
201
 
200
- 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.4.1 --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.5.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)).
201
203
 
202
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.
203
205
 
@@ -304,13 +306,15 @@ The existing client certificate (`.p12` file) will still work because the CA key
304
306
 
305
307
  All Lamaste state is stored as flat files with atomic writes:
306
308
 
307
- | File | Format | Write Pattern |
308
- | ----------------------------------------- | ------ | ------------------------------------------------------------- |
309
- | `/etc/lamalibre/lamaste/panel.json` | JSON | Write `.tmp` then `rename()` |
310
- | `/etc/lamalibre/lamaste/tunnels.json` | JSON | Write `.tmp`, `fsync()`, then `rename()` |
311
- | `/etc/lamalibre/lamaste/sites.json` | JSON | Write `.tmp`, `fsync()`, then `rename()` |
312
- | `/etc/lamalibre/lamaste/invitations.json` | JSON | Write `.tmp`, `fsync()`, then `rename()` |
313
- | `/etc/authelia/users.yml` | YAML | Temp file in `/etc/authelia`, `fsync()`, `rename()` (no sudo) |
309
+ | File | Format | Write Pattern |
310
+ | ----------------------------------------- | ------ | ------------------------------------------------------------------- |
311
+ | `/etc/lamalibre/lamaste/panel.json` | JSON | Write `.tmp` then `rename()` |
312
+ | `/etc/lamalibre/lamaste/tunnels.json` | JSON | Write `.tmp`, `fsync()`, then `rename()` |
313
+ | `/etc/lamalibre/lamaste/sites.json` | JSON | Write `.tmp`, `fsync()`, then `rename()` |
314
+ | `/etc/lamalibre/lamaste/invitations.json` | JSON | Write `.tmp`, `fsync()`, then `rename()` |
315
+ | `/etc/authelia/users.yml` | YAML | Temp file in `/etc/authelia`, `fsync()`, `rename()` (no sudo) |
316
+ | `/etc/lamalibre/lamaste/state.db` | SQLite | WAL, transactions; numbered migrations (`0013`: the user directory) |
317
+ | `/etc/lamalibre/lamaste/gatekeeper.db` | SQLite | WAL, transactions; numbered migrations |
314
318
 
315
319
  The atomic write pattern (write to temporary file, sync, rename) ensures that a crash during a write does not corrupt the primary file. The `rename()` system call is atomic on POSIX filesystems.
316
320
 
@@ -320,6 +324,11 @@ The atomic write pattern (write to temporary file, sync, rename) ensures that a
320
324
  - **`/etc/authelia/.secrets.json`** contains the JWT secret. If this changes, all active Authelia sessions are invalidated and users must re-authenticate.
321
325
  - **`/etc/authelia/db.sqlite3`** contains Authelia session data and TOTP registration states. It can be regenerated (users re-enroll TOTP), but this is disruptive.
322
326
  - **Let's Encrypt account credentials** in `/etc/letsencrypt/accounts/` are tied to the email address used during registration. If lost, certbot creates a new account on next run.
327
+ - **`state.db` holds the user directory.** Each user's `userId` — what agent-hosted plugins key their data on — exists only there; `users.yml` has none. **Losing `state.db` re-keys every user**: the panel rebuilds the directory from `users.yml` with new user ids and a new `directoryId`, and every plugin's agent half, seeing another directory, replays its audience from 0 — its old rows are marked removed and the same people arrive under new ids, so their history in the plugin is no longer joined to them. Tombstones and erasure records are lost too. Keep `state.db` in every backup.
328
+ - **Restoring an older `state.db`** keeps the `directoryId` but moves the audiences back: an agent half whose cursor is now ahead of the panel's head is sent a full replay, which brings it in line with the restored audiences (users created since the backup come back from `users.yml` with new ids).
329
+ - **Erasures survive a relay restore in the agent halves.** An erasure made after the backup is lost from the relay — the restored directory has the user's tombstone again, perhaps a live user, and its audiences may send the user as a member or with their last name. An agent half that applied the erasure never writes the user back: an erasure is **sticky** in its projection (a later `upsert` or `remove` for an erased user leaves the row the bare id, calls none of the plugin's hooks, and counts as a mismatch at the daily reconcile — `identity.reconcile.mismatches` in the audit log is how a restore shows up). The relay does not know the user was erased: **erase the user again** to record it (and to scrub what the restore brought back on the relay: the tombstone, invitations, Gatekeeper's access-request log). A restored `gatekeeper.db` may bring grants back under a deleted user's username; the panel clears them before the username is given to anyone. A restored Authelia database may bring a deleted user's second factors back; they are deleted before the username is given to anyone, too. Telling a restored relay from the live one — rotating the `directoryId` on restore so every agent half replays against the restored directory — is **not done yet** (future work); until then, after restoring `state.db`, erase again every user erased since the backup.
330
+ - **A plugin's audience is its erasure ledger.** Its rows stay when the plugin is uninstalled while an agent keeps its data, or when it stops declaring identity; they go only when no data of the plugin is left anywhere (uninstall with purge, retained data purged or forgotten). A backup of `state.db` therefore carries the erasures every such plugin still owes.
331
+ - **SQLite files** (`state.db`, `gatekeeper.db`) run in WAL mode: for a consistent copy, stop `lamalibre-lamaste-serverd` and `lamalibre-lamaste-gatekeeper` while the archive is made, and restore the `-wal` and `-shm` files with the database (or neither). Both are `0600`, owned by `lamaste`.
323
332
  - **Settings of agent-hosted plugins** are kept by each plugin's agent half on its hosting agent — `settings.json` and `secrets.json` in its data directory (`/var/lib/lamalibre/apps/<route>/` for an agent half Lamaste installs) — not on the relay. The relay's `state.db` holds a cache of the non-secret values only, which it reads again from the agent half whenever the settings are read while the hosting is up. A relay backup therefore restores no plugin secret: back them up with the agent half's data. An agent half reads its restored files when it starts ([Agent-Hosted Plugins](../04-api-reference/agent-hosted-plugins.md#settings)); since restored files carry another revision than the one Lamaste last wrote, the relay's next read records the restore as a change made outside Lamaste (`plugin.settings.changed-outside-lamaste`, with the names of the settings that differ) and warns the administrators — expected after a restore you made.
324
333
 
325
334
  ## Quick Reference
@@ -136,6 +136,45 @@ Each log line contains an ISO timestamp and the log message:
136
136
  - **authelia** logs show authentication attempts, including failed TOTP entries
137
137
  - **lamalibre-lamaste-serverd** logs show API requests and any server-side errors
138
138
 
139
+ ### Logs and Personal Data
140
+
141
+ What each log on the server keeps about people, and for how long — what an [erasure](../04-api-reference/users.md#erasing-a-removed-user) cannot reach:
142
+
143
+ | Log | What it keeps | Retention |
144
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
145
+ | Panel and Gatekeeper (journald) | One metadata line per request: method, the **route pattern** (`/api/users/:username` — never the URL, never a query), status, duration, the caller's certificate role. The client address only for certificate holders (administrators, agents) — never for a person. The panel's own lines name users by `userId` alone | journald's limits (below) |
146
+ | nginx access log (`/var/log/nginx/access.log`) | Every request's client address and full URL — an administrator's request names the user it changes (`/api/users/alice`), a desktop user's or a tunneled app's request is logged with its address | Ubuntu's logrotate: daily, 14 files (`/etc/logrotate.d/nginx`) |
147
+ | Authelia (`/var/log/authelia/authelia.log`) | Sign-in attempts with the username and the client address; Authelia also keeps an authentication log in its database | **Not rotated** by Lamaste: the file grows until you rotate it (below) |
148
+ | Other units (journald: chisel, the agents' sync…) | Connections by agent label; no user data | journald's limits (below) |
149
+
150
+ **journald** keeps entries until its size limit (by default 10% of the file system, at most 4 GiB) — on a small droplet that can be months. To keep at most two weeks:
151
+
152
+ ```bash
153
+ sudo mkdir -p /etc/systemd/journald.conf.d
154
+ printf '[Journal]\nMaxRetentionSec=14day\n' | sudo tee /etc/systemd/journald.conf.d/lamaste-retention.conf
155
+ sudo systemctl restart systemd-journald
156
+ sudo journalctl --vacuum-time=14d # drop what is older now
157
+ ```
158
+
159
+ **nginx**: shorten `rotate 14` in `/etc/logrotate.d/nginx` to keep fewer days, or `sudo truncate -s 0 /var/log/nginx/access.log.*` after an erasure that must reach them now.
160
+
161
+ **Authelia's log file**: rotate it, e.g. `/etc/logrotate.d/authelia`:
162
+
163
+ ```
164
+ /var/log/authelia/authelia.log {
165
+ daily
166
+ rotate 14
167
+ missingok
168
+ notifempty
169
+ compress
170
+ delaycompress
171
+ copytruncate
172
+ su lamaste-authelia lamaste-authelia
173
+ }
174
+ ```
175
+
176
+ Backups taken before an erasure keep everything they copied; age them out on the same schedule.
177
+
139
178
  ### What Metrics to Watch
140
179
 
141
180
  For day-to-day monitoring, these are the most important signals:
@@ -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.4.1 --uninstall
32
+ npx @lamalibre/create-lamaste@4.5.0 --uninstall
33
33
  ```
34
34
 
35
35
  This prints the same steps listed below.
@@ -295,6 +295,7 @@ For reference, here is a complete list of everything the installer and onboardin
295
295
  - `/etc/nginx/sites-available/lamalibre-lamaste-agent-panel-*`
296
296
  - Corresponding symlinks in `/etc/nginx/sites-enabled/`
297
297
  - `/etc/nginx/snippets/lamalibre-lamaste-mtls.conf`, `lamalibre-lamaste-authz-cache.conf`
298
+ - `/etc/nginx/conf.d/lamalibre-lamaste-backend-cookie.conf` (the map the tunnel vhosts take the backends' `Cookie` from — remove it after the vhosts that use it)
298
299
 
299
300
  **System configuration:**
300
301