@lamalibre/create-lamaste 4.0.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 (270) hide show
  1. package/LICENSE.md +118 -0
  2. package/README.md +114 -0
  3. package/bin/create-lamaste.js +15 -0
  4. package/package.json +54 -0
  5. package/scripts/bundle-vendor.js +110 -0
  6. package/scripts/lamaste-cert-info +62 -0
  7. package/scripts/lamaste-certbot +96 -0
  8. package/scripts/lamaste-priv +693 -0
  9. package/src/index.js +646 -0
  10. package/src/lib/accounts.js +86 -0
  11. package/src/lib/binaries.js +137 -0
  12. package/src/lib/cert-help-page.js +160 -0
  13. package/src/lib/env.js +132 -0
  14. package/src/lib/http-redirect.js +38 -0
  15. package/src/lib/ownership.js +231 -0
  16. package/src/lib/relay-services.js +103 -0
  17. package/src/lib/secrets.js +19 -0
  18. package/src/lib/service-config.js +311 -0
  19. package/src/lib/summary.js +131 -0
  20. package/src/lib/wrappers.js +63 -0
  21. package/src/tasks/gatekeeper.js +233 -0
  22. package/src/tasks/harden.js +293 -0
  23. package/src/tasks/mtls.js +193 -0
  24. package/src/tasks/nginx.js +265 -0
  25. package/src/tasks/node.js +110 -0
  26. package/src/tasks/panel.js +408 -0
  27. package/src/tasks/redeploy.js +438 -0
  28. package/vendor/docs/00-introduction/how-it-works.md +447 -0
  29. package/vendor/docs/00-introduction/quickstart.md +472 -0
  30. package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
  31. package/vendor/docs/01-concepts/authentication.md +464 -0
  32. package/vendor/docs/01-concepts/authorization.md +242 -0
  33. package/vendor/docs/01-concepts/certificates.md +452 -0
  34. package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
  35. package/vendor/docs/01-concepts/mtls.md +490 -0
  36. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
  37. package/vendor/docs/01-concepts/security-model.md +600 -0
  38. package/vendor/docs/01-concepts/tickets.md +414 -0
  39. package/vendor/docs/01-concepts/tunneling.md +367 -0
  40. package/vendor/docs/02-guides/agent-setup.md +194 -0
  41. package/vendor/docs/02-guides/certificate-management.md +341 -0
  42. package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
  43. package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
  44. package/vendor/docs/02-guides/disaster-recovery.md +464 -0
  45. package/vendor/docs/02-guides/first-tunnel.md +230 -0
  46. package/vendor/docs/02-guides/installation.md +393 -0
  47. package/vendor/docs/02-guides/managing-users.md +255 -0
  48. package/vendor/docs/02-guides/onboarding.md +244 -0
  49. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
  50. package/vendor/docs/02-guides/static-sites.md +364 -0
  51. package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
  52. package/vendor/docs/03-architecture/installer-flow.md +151 -0
  53. package/vendor/docs/03-architecture/installer.md +445 -0
  54. package/vendor/docs/03-architecture/management-flow.md +288 -0
  55. package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
  56. package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
  57. package/vendor/docs/03-architecture/overview.md +541 -0
  58. package/vendor/docs/03-architecture/panel-client.md +416 -0
  59. package/vendor/docs/03-architecture/panel-server.md +695 -0
  60. package/vendor/docs/03-architecture/state-management.md +529 -0
  61. package/vendor/docs/03-architecture/system-overview.md +167 -0
  62. package/vendor/docs/04-api-reference/certificates.md +1141 -0
  63. package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
  64. package/vendor/docs/04-api-reference/onboarding.md +473 -0
  65. package/vendor/docs/04-api-reference/overview.md +404 -0
  66. package/vendor/docs/04-api-reference/services.md +267 -0
  67. package/vendor/docs/04-api-reference/settings.md +343 -0
  68. package/vendor/docs/04-api-reference/sites.md +716 -0
  69. package/vendor/docs/04-api-reference/system.md +268 -0
  70. package/vendor/docs/04-api-reference/tickets.md +618 -0
  71. package/vendor/docs/04-api-reference/tunnels.md +417 -0
  72. package/vendor/docs/04-api-reference/users.md +333 -0
  73. package/vendor/docs/05-operations/backup-and-restore.md +342 -0
  74. package/vendor/docs/05-operations/monitoring.md +285 -0
  75. package/vendor/docs/05-operations/uninstalling.md +344 -0
  76. package/vendor/docs/05-operations/upgrades.md +270 -0
  77. package/vendor/docs/06-reference/config-files.md +1080 -0
  78. package/vendor/docs/06-reference/glossary.md +528 -0
  79. package/vendor/docs/06-reference/installer-flags.md +260 -0
  80. package/vendor/docs/06-reference/ports-and-services.md +244 -0
  81. package/vendor/docs/06-reference/troubleshooting.md +665 -0
  82. package/vendor/docs/_index.json +233 -0
  83. package/vendor/docs/gatekeeper-design.md +953 -0
  84. package/vendor/docs/package.json +15 -0
  85. package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
  86. package/vendor/gatekeeper/dist/boot.d.ts +1 -0
  87. package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
  88. package/vendor/gatekeeper/dist/boot.js +17 -0
  89. package/vendor/gatekeeper/dist/boot.js.map +1 -0
  90. package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
  91. package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
  92. package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
  93. package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
  94. package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
  95. package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
  96. package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
  97. package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
  98. package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
  99. package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
  100. package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
  101. package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
  102. package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
  103. package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
  104. package/vendor/gatekeeper/dist/cli/index.js +45 -0
  105. package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
  106. package/vendor/gatekeeper/dist/index.d.ts +9 -0
  107. package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
  108. package/vendor/gatekeeper/dist/index.js +13 -0
  109. package/vendor/gatekeeper/dist/index.js.map +1 -0
  110. package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
  111. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
  112. package/vendor/gatekeeper/dist/lib/authz.js +42 -0
  113. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
  114. package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
  115. package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
  116. package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
  117. package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
  118. package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
  119. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
  120. package/vendor/gatekeeper/dist/lib/constants.js +79 -0
  121. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
  122. package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
  123. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
  124. package/vendor/gatekeeper/dist/lib/grants.js +314 -0
  125. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
  126. package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
  127. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
  128. package/vendor/gatekeeper/dist/lib/groups.js +247 -0
  129. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
  130. package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
  131. package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
  132. package/vendor/gatekeeper/dist/lib/migration.js +79 -0
  133. package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
  134. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
  135. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
  136. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
  137. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
  138. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
  139. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
  140. package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
  141. package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
  142. package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
  143. package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
  144. package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
  145. package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
  146. package/vendor/gatekeeper/dist/lib/templates.js +151 -0
  147. package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
  148. package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
  149. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
  150. package/vendor/gatekeeper/dist/lib/types.js +2 -0
  151. package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
  152. package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
  153. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
  154. package/vendor/gatekeeper/dist/server/index.js +251 -0
  155. package/vendor/gatekeeper/dist/server/index.js.map +1 -0
  156. package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
  157. package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
  158. package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
  159. package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
  160. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
  161. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
  162. package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
  163. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
  164. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
  165. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
  166. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
  167. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
  168. package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
  169. package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
  170. package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
  171. package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
  172. package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
  173. package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
  174. package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
  175. package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
  176. package/vendor/gatekeeper/package.json +49 -0
  177. package/vendor/server/bin/lamaste-server.js +204 -0
  178. package/vendor/server/package.json +31 -0
  179. package/vendor/server/src/commands/certs.js +224 -0
  180. package/vendor/server/src/commands/chisel.js +132 -0
  181. package/vendor/server/src/commands/logs.js +49 -0
  182. package/vendor/server/src/commands/plugins.js +321 -0
  183. package/vendor/server/src/commands/reset-admin.js +478 -0
  184. package/vendor/server/src/commands/restart.js +94 -0
  185. package/vendor/server/src/commands/sites.js +257 -0
  186. package/vendor/server/src/commands/status.js +158 -0
  187. package/vendor/server/src/commands/tunnels.js +365 -0
  188. package/vendor/server/src/commands/uninstall.js +326 -0
  189. package/vendor/server/src/config.js +38 -0
  190. package/vendor/server/src/exec.js +26 -0
  191. package/vendor/server/src/logger.js +39 -0
  192. package/vendor/server/src/ndjson.js +56 -0
  193. package/vendor/server/src/panel-api.js +85 -0
  194. package/vendor/server/src/state.js +45 -0
  195. package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
  196. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
  197. package/vendor/server-ui/dist/index.html +13 -0
  198. package/vendor/serverd/package.json +44 -0
  199. package/vendor/serverd/src/boot.js +13 -0
  200. package/vendor/serverd/src/index.js +191 -0
  201. package/vendor/serverd/src/lib/app-error.js +14 -0
  202. package/vendor/serverd/src/lib/authelia.js +85 -0
  203. package/vendor/serverd/src/lib/certbot.js +74 -0
  204. package/vendor/serverd/src/lib/chisel-args.js +6 -0
  205. package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
  206. package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
  207. package/vendor/serverd/src/lib/chisel-users.js +109 -0
  208. package/vendor/serverd/src/lib/chisel.js +81 -0
  209. package/vendor/serverd/src/lib/config.js +190 -0
  210. package/vendor/serverd/src/lib/constants.js +8 -0
  211. package/vendor/serverd/src/lib/csr-signing.js +331 -0
  212. package/vendor/serverd/src/lib/enrollment.js +302 -0
  213. package/vendor/serverd/src/lib/files.js +19 -0
  214. package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
  215. package/vendor/serverd/src/lib/invite-page.js +281 -0
  216. package/vendor/serverd/src/lib/ip-utils.js +79 -0
  217. package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
  218. package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
  219. package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
  220. package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
  221. package/vendor/serverd/src/lib/mtls.js +795 -0
  222. package/vendor/serverd/src/lib/nginx.js +1090 -0
  223. package/vendor/serverd/src/lib/pki-paths.js +41 -0
  224. package/vendor/serverd/src/lib/pki-sign.js +103 -0
  225. package/vendor/serverd/src/lib/plugins.js +538 -0
  226. package/vendor/serverd/src/lib/push-install.js +390 -0
  227. package/vendor/serverd/src/lib/revocation.js +112 -0
  228. package/vendor/serverd/src/lib/services.js +131 -0
  229. package/vendor/serverd/src/lib/session.js +151 -0
  230. package/vendor/serverd/src/lib/state-db.js +130 -0
  231. package/vendor/serverd/src/lib/state.js +140 -0
  232. package/vendor/serverd/src/lib/storage.js +63 -0
  233. package/vendor/serverd/src/lib/system-stats.js +58 -0
  234. package/vendor/serverd/src/lib/tickets.js +1556 -0
  235. package/vendor/serverd/src/lib/totp.js +76 -0
  236. package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
  237. package/vendor/serverd/src/lib/user-access-session.js +142 -0
  238. package/vendor/serverd/src/lib/user-access.js +52 -0
  239. package/vendor/serverd/src/middleware/errors.js +63 -0
  240. package/vendor/serverd/src/middleware/mtls.js +120 -0
  241. package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
  242. package/vendor/serverd/src/middleware/role-guard.js +58 -0
  243. package/vendor/serverd/src/middleware/twofa-session.js +65 -0
  244. package/vendor/serverd/src/middleware/user-access-session.js +52 -0
  245. package/vendor/serverd/src/routes/enrollment.js +120 -0
  246. package/vendor/serverd/src/routes/health.js +22 -0
  247. package/vendor/serverd/src/routes/invite.js +163 -0
  248. package/vendor/serverd/src/routes/management/agents.js +182 -0
  249. package/vendor/serverd/src/routes/management/certs.js +911 -0
  250. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
  251. package/vendor/serverd/src/routes/management/identity.js +107 -0
  252. package/vendor/serverd/src/routes/management/invitations.js +166 -0
  253. package/vendor/serverd/src/routes/management/logs.js +218 -0
  254. package/vendor/serverd/src/routes/management/plugins.js +560 -0
  255. package/vendor/serverd/src/routes/management/services.js +63 -0
  256. package/vendor/serverd/src/routes/management/settings.js +325 -0
  257. package/vendor/serverd/src/routes/management/sites.js +522 -0
  258. package/vendor/serverd/src/routes/management/storage.js +192 -0
  259. package/vendor/serverd/src/routes/management/system.js +201 -0
  260. package/vendor/serverd/src/routes/management/tickets.js +457 -0
  261. package/vendor/serverd/src/routes/management/tunnels.js +709 -0
  262. package/vendor/serverd/src/routes/management/users.js +377 -0
  263. package/vendor/serverd/src/routes/management.js +36 -0
  264. package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
  265. package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
  266. package/vendor/serverd/src/routes/onboarding/index.js +18 -0
  267. package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
  268. package/vendor/serverd/src/routes/onboarding/status.js +12 -0
  269. package/vendor/serverd/src/routes/plugin-router.js +79 -0
  270. package/vendor/serverd/src/routes/user-access.js +389 -0
@@ -0,0 +1,953 @@
1
+ # Lamaste Gatekeeper — Design Document
2
+
3
+ ## Overview
4
+
5
+ `@lamalibre/lamaste-gatekeeper@4.0.0` is a standalone authorization package for Lamaste. It manages **groups** and **grants**, enforces tunnel access control via a lightweight Fastify service, and serves user-friendly "request access" pages when access is denied.
6
+
7
+ It follows the same architectural pattern as `@lamalibre/lamaste-tickets@4.0.0` — a solid library at the core, with a service, CLI, and desktop as consumers.
8
+
9
+ ---
10
+
11
+ ## Motivation
12
+
13
+ ### Problem
14
+
15
+ - App tunnels currently allow **all Authelia-authenticated users** — no per-user or per-group restrictions
16
+ - Plugin tunnel access uses a plugin-specific grant model (`user-plugin-access.json`) that doesn't generalize
17
+ - There is no group management — groups are free-form strings in Authelia's `users.yml` with no CRUD, no metadata, and every change requires an Authelia restart
18
+ - Access denial shows a generic Authelia 403 page with no context or actionable next steps
19
+
20
+ ### Solution
21
+
22
+ A two-layer authentication/authorization architecture:
23
+
24
+ 1. **Authelia** — authentication layer (who are you?)
25
+ 2. **Gatekeeper** — authorization layer (can you access this resource?)
26
+
27
+ When access is denied, users see a helpful page with pre-filled message templates to request access from their administrator.
28
+
29
+ ---
30
+
31
+ ## Dual Group System
32
+
33
+ Two separate, orthogonal group systems:
34
+
35
+ ### Authelia Groups (Identity Tier)
36
+
37
+ Fixed role tiers stored in `users.yml`. Determines _what kind of user you are_.
38
+
39
+ | Group | Purpose |
40
+ | ---------- | ------------------------------------------------------------------------- |
41
+ | `admins` | Full panel access (mTLS certificate holders) |
42
+ | `internal` | Power users — can install standalone plugins, serve resources via tunnels |
43
+ | `external` | Basic users — can access tunnels they've been granted access to |
44
+
45
+ - Managed in `users.yml`, changes require Authelia restart
46
+ - User creation UI uses hardcoded checkboxes: admin (disables others), internal (disables external), external
47
+ - Rarely change after initial user setup
48
+
49
+ ### Lamaste Groups (Access Control Tier)
50
+
51
+ Admin-created, arbitrary groups for fine-grained resource authorization. Determines _what resources you can access_.
52
+
53
+ | Example Group | Purpose |
54
+ | ------------- | -------------------------------------------------- |
55
+ | `developers` | Access to dev/staging tunnels |
56
+ | `design-team` | Access to design tool tunnels |
57
+ | `client-acme` | External client access to specific project tunnels |
58
+
59
+ - Full CRUD via gatekeeper library/CLI/API
60
+ - Stored in `/etc/lamalibre/lamaste/groups.json`
61
+ - No Authelia involvement — membership changes don't require any restart
62
+ - Used in grants: "group `client-acme` can access tunnel `staging-app`"
63
+
64
+ ---
65
+
66
+ ## Package Structure
67
+
68
+ ```
69
+ packages/lamaste-gatekeeper/
70
+ src/
71
+ lib/
72
+ groups.js # Group CRUD (atomic JSON writes, promise-chain mutex)
73
+ grants.js # Generic grant CRUD (atomic JSON writes, promise-chain mutex)
74
+ authz.js # Authorization check logic (resolves groups, evaluates grants)
75
+ templates.js # Access request message templates (email, Teams, Slack, WhatsApp)
76
+ constants.js # Principal types, resource types, access modes, hard caps
77
+ migration.js # Migrate user-plugin-access.json → access-grants.json
78
+ server/
79
+ index.js # Fastify service entry point (127.0.0.1:9294)
80
+ routes/
81
+ authz.js # GET /authz/check — nginx auth_request target
82
+ access-request.js # GET /access-request — denial page with message templates
83
+ groups.js # REST API for group management
84
+ grants.js # REST API for grant management
85
+ cli/
86
+ index.js # CLI entry point
87
+ commands/
88
+ groups.js # group create|list|show|delete|rename|add-member|remove-member
89
+ grants.js # grant create|list|show|revoke
90
+ access.js # access check <username> <resource> — diagnostic tool
91
+ index.js # Library exports (groups, grants, authz, templates, constants)
92
+ package.json
93
+ tsconfig.json
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Generic Grant Model
99
+
100
+ The grant system is resource-agnostic. It doesn't know what a "tunnel" or "plugin" is — it stores generic principal-to-resource bindings.
101
+
102
+ ### Schema
103
+
104
+ ```js
105
+ {
106
+ grantId: string, // UUID (crypto.randomUUID())
107
+
108
+ // WHO — the principal
109
+ principalType: 'user' | 'group',
110
+ principalId: string, // username or Lamaste group name
111
+
112
+ // WHAT — the resource
113
+ resourceType: string, // 'tunnel', 'plugin', or any future type
114
+ resourceId: string, // tunnel UUID, plugin package name, etc.
115
+
116
+ // TYPE-SPECIFIC CONTEXT
117
+ context: object, // { target: 'agent:myagent' } for plugins, {} for tunnels
118
+
119
+ // LIFECYCLE
120
+ used: boolean, // tracks single-use grants (local plugin enrollment)
121
+ createdAt: string, // ISO 8601
122
+ usedAt: string | null // ISO 8601, null if unused
123
+ }
124
+ ```
125
+
126
+ ### Why Generic?
127
+
128
+ - Adding a new resource type (e.g., `storage`, `site`) requires zero schema changes
129
+ - The grant system doesn't need to understand resource semantics
130
+ - Consumers validate resource-specific context at their layer
131
+ - Filter/query works uniformly across all resource types
132
+
133
+ ### Examples
134
+
135
+ **App tunnel grant (user):**
136
+
137
+ ```json
138
+ {
139
+ "grantId": "a1b2c3...",
140
+ "principalType": "user",
141
+ "principalId": "alice",
142
+ "resourceType": "tunnel",
143
+ "resourceId": "uuid-of-gitlab-tunnel",
144
+ "context": {},
145
+ "used": true,
146
+ "createdAt": "2026-04-04T10:00:00Z",
147
+ "usedAt": "2026-04-04T10:00:00Z"
148
+ }
149
+ ```
150
+
151
+ **App tunnel grant (group):**
152
+
153
+ ```json
154
+ {
155
+ "grantId": "d4e5f6...",
156
+ "principalType": "group",
157
+ "principalId": "developers",
158
+ "resourceType": "tunnel",
159
+ "resourceId": "uuid-of-gitlab-tunnel",
160
+ "context": {},
161
+ "used": true,
162
+ "createdAt": "2026-04-04T10:00:00Z",
163
+ "usedAt": "2026-04-04T10:00:00Z"
164
+ }
165
+ ```
166
+
167
+ **Plugin grant (agent-side):**
168
+
169
+ ```json
170
+ {
171
+ "grantId": "g7h8i9...",
172
+ "principalType": "user",
173
+ "principalId": "bob",
174
+ "resourceType": "plugin",
175
+ "resourceId": "@lamalibre/herd-server",
176
+ "context": { "target": "agent:myagent" },
177
+ "used": true,
178
+ "createdAt": "2026-04-04T10:00:00Z",
179
+ "usedAt": "2026-04-04T10:00:00Z"
180
+ }
181
+ ```
182
+
183
+ **Plugin grant (local/desktop):**
184
+
185
+ ```json
186
+ {
187
+ "grantId": "j0k1l2...",
188
+ "principalType": "user",
189
+ "principalId": "carol",
190
+ "resourceType": "plugin",
191
+ "resourceId": "@lamalibre/sync-server",
192
+ "context": { "target": "local" },
193
+ "used": false,
194
+ "createdAt": "2026-04-04T10:00:00Z",
195
+ "usedAt": null
196
+ }
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Library API
202
+
203
+ ### Groups
204
+
205
+ ```js
206
+ import { createGroup, listGroups, getGroup, updateGroup, deleteGroup,
207
+ addMembers, removeMembers, getGroupsForUser } from '@lamalibre/lamaste-gatekeeper@4.0.0';
208
+
209
+ createGroup(name, { description?, createdBy? })
210
+ // → { name, description, members: [], createdAt, createdBy }
211
+ // Validates: lowercase alphanumeric + hyphens, not 'admins'/'internal'/'external'
212
+ // Rejects duplicates
213
+
214
+ listGroups()
215
+ // → [{ name, description, members, createdAt, createdBy }]
216
+
217
+ getGroup(name)
218
+ // → group or null
219
+
220
+ updateGroup(name, { name?: newName, description? })
221
+ // Cannot rename to reserved names
222
+ // Renames update all grants referencing this group
223
+
224
+ deleteGroup(name)
225
+ // Cannot delete reserved Authelia groups
226
+ // Auto-revokes all grants referencing this group
227
+ // Returns { deletedGrants: number } for audit
228
+
229
+ addMembers(groupName, usernames)
230
+ // Idempotent — already-present members silently skipped
231
+ // Validates usernames exist in Authelia users.yml
232
+
233
+ removeMembers(groupName, usernames)
234
+ // Idempotent — already-absent members silently skipped
235
+
236
+ getGroupsForUser(username)
237
+ // → [groupName, ...] — scans all groups for membership
238
+ ```
239
+
240
+ ### Grants
241
+
242
+ ```js
243
+ import { createGrant, listGrants, getGrant, revokeGrant } from '@lamalibre/lamaste-gatekeeper@4.0.0';
244
+
245
+ createGrant({ principalType, principalId, resourceType, resourceId, context? })
246
+ // → grant object
247
+ // Validates principal exists (user in Authelia or group in groups.json)
248
+ // Validates no duplicate grant (same principal + resource + context)
249
+ // Tunnel/agent-side plugin grants: auto-consumed (used: true)
250
+ // Local plugin grants: created unused (used: false, consumed on enrollment)
251
+
252
+ listGrants(filter?)
253
+ // filter: { principalType?, principalId?, resourceType?, resourceId?, used? }
254
+ // → [grant, ...]
255
+ // No filter = all grants
256
+
257
+ getGrant(grantId)
258
+ // → grant or null
259
+
260
+ revokeGrant(grantId)
261
+ // Removes the grant
262
+ // Local plugin grants: only revocable if used === false
263
+ // All others: always revocable
264
+ // Returns revoked grant for audit
265
+ ```
266
+
267
+ ### Authorization
268
+
269
+ ```js
270
+ import { checkAccess } from '@lamalibre/lamaste-gatekeeper@4.0.0';
271
+
272
+ checkAccess(username, resourceType, resourceId);
273
+ // 1. Find direct user grants: principalType='user', principalId=username
274
+ // 2. Find user's Lamaste groups via getGroupsForUser(username)
275
+ // 3. Find group grants: principalType='group', principalId in user's groups
276
+ // 4. Any match → { allowed: true }
277
+ // 5. No match → { allowed: false, resource: { type, id }, templates: [...] }
278
+
279
+ // Templates include pre-filled messages with username and resource name
280
+ ```
281
+
282
+ ### Templates
283
+
284
+ ```js
285
+ import { getAccessRequestTemplates } from '@lamalibre/lamaste-gatekeeper@4.0.0';
286
+
287
+ getAccessRequestTemplates(username, resourceName, { adminContact?, domain? })
288
+ // → {
289
+ // email: { subject, body },
290
+ // slack: string,
291
+ // teams: string,
292
+ // whatsapp: string,
293
+ // generic: string
294
+ // }
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Performance: Caching Architecture
300
+
301
+ ### Current State (Authelia Only)
302
+
303
+ Today, **every single request** to a tunneled service triggers a localhost HTTP roundtrip from nginx to Authelia (`127.0.0.1:9091/api/authz/forward-auth`). For a page load with 1000 requests (HTML, CSS, JS, images, fonts, API calls), that's **1000 Authelia calls**. No caching exists.
304
+
305
+ Authelia handles this because it's a lightweight Go binary with in-memory session lookup (~1-2ms per call), but it's still 1000 localhost HTTP roundtrips per page load.
306
+
307
+ ### With Gatekeeper: Two-Layer Caching
308
+
309
+ Adding gatekeeper as the auth_request target doesn't increase overhead — it **reduces** it by introducing caching that benefits all tunnel types.
310
+
311
+ #### Layer 1 — nginx `proxy_cache` (eliminates most gatekeeper calls)
312
+
313
+ ```nginx
314
+ proxy_cache_path /var/cache/nginx/authz levels=1:2
315
+ keys_zone=authz_cache:1m max_size=10m inactive=5m;
316
+
317
+ location /internal/lamaste/authz {
318
+ internal;
319
+ proxy_pass http://127.0.0.1:9294/authz/check;
320
+ proxy_cache authz_cache;
321
+ proxy_cache_key "$cookie_authelia_session$http_host";
322
+ proxy_cache_valid 200 30s; # cache successful auth for 30s
323
+ proxy_cache_valid 403 10s; # cache denial for 10s (shorter — revocations take effect faster)
324
+ # Never cache 401 — user needs fresh login redirect
325
+ }
326
+ ```
327
+
328
+ Cache key is `session_cookie + hostname`:
329
+
330
+ - Same user, same tunnel = cached (correct)
331
+ - Different users = separate cache entries (correct)
332
+ - Different tunnels = separate cache entries (correct)
333
+
334
+ 1000 requests on page load → **1 hits gatekeeper, 999 served from nginx cache**.
335
+
336
+ #### Layer 2 — Gatekeeper in-memory session cache (eliminates most Authelia calls)
337
+
338
+ ```js
339
+ // In-memory LRU cache, keyed by Authelia session cookie hash
340
+ const sessionCache = new Map(); // cookie_hash → { username, groups, expiresAt }
341
+
342
+ async function validateAuthelia(cookie) {
343
+ const key = hash(cookie);
344
+ const cached = sessionCache.get(key);
345
+ if (cached && cached.expiresAt > Date.now()) return cached;
346
+
347
+ // Only hits Authelia on cache miss
348
+ const result = await forwardToAuthelia(cookie);
349
+ sessionCache.set(key, { ...result, expiresAt: Date.now() + 30_000 });
350
+ return result;
351
+ }
352
+ ```
353
+
354
+ Even without nginx cache (e.g., after a deploy), gatekeeper handles 1000 requests with:
355
+
356
+ - **1 Authelia HTTP call** (first request, then cached 30s)
357
+ - **1000 in-memory grant lookups** (microseconds each — small array scan)
358
+
359
+ #### Performance Comparison
360
+
361
+ | Scenario | Authelia calls | Gatekeeper calls | Added latency per request |
362
+ | --------------------------------------- | -------------- | ---------------- | ------------------------------------- |
363
+ | **Current (no gatekeeper)** | **1000** | N/A | ~1-2ms each |
364
+ | nginx cache warm | 0 | 0 | ~0.1ms (nginx cache lookup) |
365
+ | nginx cache cold, gatekeeper cache warm | 0 | 1 | ~1ms (localhost HTTP + memory lookup) |
366
+ | Both cold (first request of session) | 1 | 1 | ~5-10ms (one-time) |
367
+
368
+ **Net result:** Gatekeeper with caching turns 1000 Authelia calls into 1 (or 0 with warm cache). This is a performance improvement over the current setup, not a regression.
369
+
370
+ #### Universal Caching Benefit
371
+
372
+ The nginx `proxy_cache` layer applies to **all access modes**, not just restricted tunnels:
373
+
374
+ - `authenticated` tunnels: gatekeeper validates Authelia cookie, returns 200. Cached 30s.
375
+ - `restricted` tunnels: gatekeeper validates cookie + checks grants. Cached 30s.
376
+
377
+ Both modes benefit equally from caching. Current Authelia-only tunnels that migrate to `authenticated` mode get free caching they didn't have before.
378
+
379
+ #### Grant Revocation Latency
380
+
381
+ The tradeoff: a revoked grant takes up to 30 seconds to take effect (nginx cache TTL). This is acceptable for the use case (admin revokes access, user loses access within 30s). For immediate revocation, gatekeeper exposes `POST /api/cache/bust` — lamalibre-lamaste-serverd calls this after revoking a grant, which increments an internal version counter causing all cached responses to miss.
382
+
383
+ #### Memory Footprint
384
+
385
+ - Gatekeeper session cache: ~200 bytes per entry, 1000 concurrent users = ~200KB
386
+ - Grants/groups in memory: loaded via file watch, typically < 100KB
387
+ - nginx cache zone: 1MB key zone + 10MB data store
388
+ - **Total overhead: < 12MB** — trivial on the 512MB droplet (~245MB used by existing stack)
389
+
390
+ ---
391
+
392
+ ## Gatekeeper Service
393
+
394
+ ### Overview
395
+
396
+ Lightweight Fastify server on `127.0.0.1:9294`. Three responsibilities:
397
+
398
+ 1. **nginx auth_request target** — validate access for all tunnel types (authentication via Authelia + authorization via grants)
399
+ 2. **Access request page** — user-friendly denial page with message templates
400
+ 3. **Management REST API** — group and grant CRUD for CLI and desktop consumers
401
+
402
+ ### nginx Integration
403
+
404
+ For all non-mTLS tunnel types, nginx uses gatekeeper as the auth_request target. Gatekeeper handles both authentication (by forwarding to Authelia) and authorization (by checking grants):
405
+
406
+ ```nginx
407
+ # Restricted tunnel vhost — e.g., gitlab.example.com
408
+ server {
409
+ listen 443 ssl;
410
+ server_name gitlab.example.com;
411
+
412
+ # TLS (Let's Encrypt)
413
+ ssl_certificate /etc/letsencrypt/live/gitlab.example.com/fullchain.pem;
414
+ ssl_certificate_key /etc/letsencrypt/live/gitlab.example.com/privkey.pem;
415
+
416
+ # Gatekeeper auth (handles both authentication via Authelia + authorization via grants)
417
+ location /internal/lamaste/authz {
418
+ internal;
419
+ proxy_pass http://127.0.0.1:9294/authz/check;
420
+ proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
421
+ proxy_set_header X-Forwarded-For $remote_addr;
422
+ proxy_set_header Host $http_host;
423
+ proxy_pass_request_body off;
424
+ proxy_set_header Content-Length "";
425
+ # Forward cookies so gatekeeper can validate Authelia session
426
+ proxy_set_header Cookie $http_cookie;
427
+ }
428
+
429
+ location / {
430
+ auth_request /internal/lamaste/authz;
431
+ auth_request_set $remote_user $upstream_http_remote_user;
432
+ auth_request_set $remote_groups $upstream_http_remote_groups;
433
+ auth_request_set $remote_name $upstream_http_remote_name;
434
+ auth_request_set $remote_email $upstream_http_remote_email;
435
+
436
+ # Authentication failure → Authelia login
437
+ error_page 401 =302 https://auth.example.com/?rd=$scheme://$http_host$request_uri;
438
+
439
+ # Authorization failure → access request page
440
+ error_page 403 =302 https://auth.example.com/access-request?tunnel=$http_host;
441
+
442
+ # Set headers for backend
443
+ proxy_set_header Remote-User $remote_user;
444
+ proxy_set_header Remote-Groups $remote_groups;
445
+ proxy_set_header Remote-Name $remote_name;
446
+ proxy_set_header Remote-Email $remote_email;
447
+
448
+ # Proxy to tunnel backend
449
+ proxy_pass http://127.0.0.1:PORT;
450
+ proxy_http_version 1.1;
451
+ proxy_set_header Upgrade $http_upgrade;
452
+ proxy_set_header Connection "upgrade";
453
+ }
454
+ }
455
+ ```
456
+
457
+ ### Authz Check Endpoint
458
+
459
+ `GET /authz/check`
460
+
461
+ 1. Read `Cookie` and `X-Original-URL` headers from nginx
462
+ 2. Forward cookie to Authelia's verification endpoint (`http://127.0.0.1:9091/api/authz/forward-auth`)
463
+ 3. If Authelia returns 401 → return 401 (not authenticated)
464
+ 4. If Authelia returns 200 → extract `Remote-User`, `Remote-Groups`, `Remote-Name`, `Remote-Email` from Authelia response headers
465
+ 5. Parse hostname from `X-Original-URL` → look up tunnel
466
+ 6. Check tunnel's `accessMode`:
467
+ - `public` → return 200 (should not reach here, but safe fallback)
468
+ - `authenticated` → return 200 (Authelia passed = sufficient)
469
+ - `restricted` → call `checkAccess(username, 'tunnel', tunnelId)`
470
+ - Allowed → return 200 + forward Authelia headers
471
+ - Denied → return 403
472
+ 7. Response headers on 200: `Remote-User`, `Remote-Groups`, `Remote-Name`, `Remote-Email` (passed through from Authelia)
473
+
474
+ ### Access Request Page
475
+
476
+ `GET /access-request?tunnel=gitlab.example.com`
477
+
478
+ Serves an HTML page (server-rendered, no SPA dependency):
479
+
480
+ - User's Authelia cookie is valid (they passed authentication)
481
+ - Gatekeeper calls Authelia verify to get username
482
+ - Page shows:
483
+ - "You don't have access to **gitlab.example.com**"
484
+ - "Contact your administrator to request access"
485
+ - Admin contact info (from `/etc/lamalibre/lamaste/panel.json` or gatekeeper config)
486
+ - Pre-filled message templates with copy buttons:
487
+
488
+ ```
489
+ Email:
490
+ Subject: Access request for gitlab.example.com
491
+ Body: Hi, I'd like to request access to gitlab.example.com.
492
+ My username is alice. Thank you.
493
+
494
+ Slack / Teams:
495
+ Hi, could I get access to gitlab.example.com? My username is alice.
496
+
497
+ WhatsApp:
498
+ Hi, I need access to gitlab.example.com (username: alice). Thanks!
499
+ ```
500
+
501
+ - Styled with Lamaste's dark terminal aesthetic (inline CSS, no external deps)
502
+ - Copy button per template (vanilla JS, no framework)
503
+
504
+ ### Service REST API
505
+
506
+ For CLI and desktop consumption (binds 127.0.0.1 only):
507
+
508
+ **Groups:**
509
+
510
+ - `POST /api/groups` — create group
511
+ - `GET /api/groups` — list groups
512
+ - `GET /api/groups/:name` — get group with members
513
+ - `PATCH /api/groups/:name` — update group (rename, description)
514
+ - `DELETE /api/groups/:name` — delete group + auto-revoke grants
515
+ - `POST /api/groups/:name/members` — add members
516
+ - `DELETE /api/groups/:name/members/:username` — remove member
517
+
518
+ **Grants:**
519
+
520
+ - `POST /api/grants` — create grant
521
+ - `GET /api/grants` — list grants (query params for filtering)
522
+ - `GET /api/grants/:grantId` — get grant
523
+ - `DELETE /api/grants/:grantId` — revoke grant
524
+
525
+ **Diagnostic:**
526
+
527
+ - `GET /api/access/check?username=alice&resourceType=tunnel&resourceId=xxx` — test access
528
+
529
+ ### File Watching
530
+
531
+ Gatekeeper watches its state files for changes (using `fs.watch` with debounce):
532
+
533
+ - `/etc/lamalibre/lamaste/groups.json` — group membership changes
534
+ - `/etc/lamalibre/lamaste/access-grants.json` — grant changes
535
+ - `/etc/lamalibre/lamaste/tunnels.json` — tunnel access mode changes
536
+
537
+ On change: reload into memory. No restart needed. Panel-server writes, gatekeeper reads.
538
+
539
+ ### systemd Unit
540
+
541
+ ```ini
542
+ [Unit]
543
+ Description=Lamaste Gatekeeper — tunnel authorization service
544
+ After=network.target authelia.service
545
+
546
+ [Service]
547
+ ExecStart=/usr/bin/node /usr/lib/lamaste/gatekeeper/server/index.js
548
+ Restart=always
549
+ User=lamaste
550
+ Environment=NODE_ENV=production
551
+
552
+ [Install]
553
+ WantedBy=multi-user.target
554
+ ```
555
+
556
+ ---
557
+
558
+ ## Tunnel Access Model
559
+
560
+ ### Access Modes
561
+
562
+ Tunnel creation form offers four access levels:
563
+
564
+ | Mode | Authelia | Gatekeeper | nginx Template |
565
+ | --------------- | -------- | ------------ | ------------------------------------------------------ |
566
+ | `admin` | N/A | N/A | mTLS only (existing panel vhost pattern) |
567
+ | `public` | No auth | No auth | Direct proxy, no auth_request |
568
+ | `authenticated` | Yes | Pass-through | auth_request to gatekeeper, always 200 after Authelia |
569
+ | `restricted` | Yes | Grant check | auth_request to gatekeeper, 200 or 403 based on grants |
570
+
571
+ ### Tunnel Schema Changes
572
+
573
+ ```js
574
+ {
575
+ // Existing fields...
576
+ id, subdomain, port, type, description, fqdn, enabled, createdAt,
577
+ pluginName, agentLabel, pluginRoute,
578
+
579
+ // New field
580
+ accessMode: 'public' | 'authenticated' | 'restricted'
581
+ // Default: 'restricted' for new tunnels
582
+ }
583
+ ```
584
+
585
+ ### nginx Vhost Variants
586
+
587
+ Panel-server's `nginx.js` gains new vhost template functions:
588
+
589
+ - `writePublicVhost(subdomain, domain, port, certPath, { pathPrefix, maxBodySizeMb })` — no auth_request, direct proxy
590
+ - `writeAuthenticatedVhost(subdomain, domain, port, certPath, { pathPrefix, maxBodySizeMb })` — auth_request to gatekeeper, Authelia headers forwarded
591
+ - `writeRestrictedVhost(subdomain, domain, port, certPath, { pathPrefix, maxBodySizeMb })` — auth_request to gatekeeper, Authelia headers forwarded, 403 → access-request redirect
592
+
593
+ The former `writeAppVhost()` (direct Authelia forward auth) was replaced by `writeAuthenticatedVhost()` and has since been removed; every app tunnel vhost is rendered by one of the three writers above. `maxBodySizeMb` becomes the vhost's `client_max_body_size`.
594
+ Existing `writeAgentPanelVhost()` unchanged (mTLS, no gatekeeper involvement).
595
+
596
+ ### Default Behavior & Migration
597
+
598
+ - **New tunnels:** default to `restricted` (admin must explicitly grant access)
599
+ - **Existing app tunnels:** migrated to `authenticated` (preserves current behavior — all Authelia users can access)
600
+ - **Existing plugin tunnels:** continue using grant system (migrated to new grant model)
601
+
602
+ ---
603
+
604
+ ## Admin UI Changes
605
+
606
+ ### New "Groups" Page (lamaste-server-ui)
607
+
608
+ Sidebar entry under a new "Access" section.
609
+
610
+ **List view:**
611
+
612
+ - Table: name, description, member count, created date
613
+ - Create button → modal with name + description
614
+ - Row actions: edit, delete (with confirmation showing affected grant count)
615
+
616
+ **Detail view (click group name):**
617
+
618
+ - Group info (name, description, created by, created at)
619
+ - Members table with remove button
620
+ - "Add Members" button → modal with Authelia user dropdown (multi-select)
621
+
622
+ ### Updated "Users" Page
623
+
624
+ - User creation: Authelia group as radio buttons (Admin / Internal / External)
625
+ - Admin checked → Internal and External disabled
626
+ - Internal checked → External disabled
627
+ - External is default
628
+ - User invitation: same radio button pattern
629
+ - User list: show Authelia group badge (admin/internal/external)
630
+
631
+ ### Updated Tunnel Creation Form
632
+
633
+ ```
634
+ Subdomain: [____________]
635
+ Port: [____]
636
+ Type: [App ▼] (app | panel | plugin)
637
+
638
+ Access: (o) Restricted — specific users and groups
639
+ ( ) All Authelia Users
640
+ ( ) Public — no authentication
641
+
642
+ [When "Restricted" selected:]
643
+ Users: [alice] [x] [bob] [x] [+ Add]
644
+ Groups: [developers] [x] [+ Add]
645
+ ```
646
+
647
+ - Default selection: Restricted
648
+ - Adding users/groups creates grants via gatekeeper API
649
+ - Removing users/groups revokes grants
650
+ - Changing access mode updates tunnel and rewrites nginx vhost
651
+
652
+ ### Updated Tunnel List
653
+
654
+ - Access mode badge on each tunnel card (Public / Authenticated / Restricted)
655
+ - For restricted tunnels: show granted user/group count
656
+ - Click to expand: list of granted users and groups with revoke buttons
657
+
658
+ ### Grant Management (integrated, not standalone page)
659
+
660
+ Grants are managed in context:
661
+
662
+ - On tunnel detail: manage who can access this tunnel
663
+ - On group detail: see which resources this group can access
664
+ - On user access page: see which resources this user can access
665
+
666
+ No separate "Grants" page needed — grants are always viewed in the context of their principal or resource.
667
+
668
+ ---
669
+
670
+ ## CLI Commands
671
+
672
+ ```bash
673
+ # Group management
674
+ lamalibre-lamaste-gatekeeper group create <name> [--description "..."]
675
+ lamalibre-lamaste-gatekeeper group list
676
+ lamalibre-lamaste-gatekeeper group show <name>
677
+ lamalibre-lamaste-gatekeeper group rename <name> <new-name>
678
+ lamalibre-lamaste-gatekeeper group delete <name> [--force] # --force skips confirmation
679
+ lamalibre-lamaste-gatekeeper group add-member <group> <username> [<username>...]
680
+ lamalibre-lamaste-gatekeeper group remove-member <group> <username> [<username>...]
681
+
682
+ # Grant management
683
+ lamalibre-lamaste-gatekeeper grant create --user <username> --tunnel <tunnelId>
684
+ lamalibre-lamaste-gatekeeper grant create --group <groupname> --tunnel <tunnelId>
685
+ lamalibre-lamaste-gatekeeper grant create --user <username> --plugin <pluginName> --target agent:<label>
686
+ lamalibre-lamaste-gatekeeper grant list [--user <username>] [--group <groupname>] [--tunnel <tunnelId>]
687
+ lamalibre-lamaste-gatekeeper grant show <grantId>
688
+ lamalibre-lamaste-gatekeeper grant revoke <grantId>
689
+
690
+ # Diagnostic
691
+ lamalibre-lamaste-gatekeeper access check <username> tunnel <tunnelId>
692
+ lamalibre-lamaste-gatekeeper access check <username> plugin <pluginName>
693
+ ```
694
+
695
+ ---
696
+
697
+ ## Desktop Integration
698
+
699
+ ### Gatekeeper Menu (Sidebar Section)
700
+
701
+ A new top-level "Gatekeeper" section in the desktop app sidebar (visible when connected to a server). Provides full access to all gatekeeper functionality — groups, grants, access diagnostics, and configuration.
702
+
703
+ **Sidebar structure:**
704
+
705
+ ```
706
+ Gatekeeper
707
+ ├─ Dashboard # Overview: group count, grant count, recent activity
708
+ ├─ Groups # Full group CRUD + member management
709
+ ├─ Grants # Grant list, create, revoke (filterable by resource/principal)
710
+ ├─ Access Requests # Pending access request log (if request logging enabled)
711
+ └─ Settings # Gatekeeper configuration (admin contact, templates, cache TTL)
712
+ ```
713
+
714
+ ### Dashboard Page
715
+
716
+ At-a-glance overview of the access control system:
717
+
718
+ - **Stats cards:** total groups, total grants, total users with grants, active tunnels with restricted access
719
+ - **Recent activity:** last 10 grant creates/revokes with timestamps
720
+ - **Quick actions:** "Create Group", "Grant Access" buttons
721
+
722
+ ### Groups Page
723
+
724
+ Full CRUD for Lamaste access control groups.
725
+
726
+ **List view:**
727
+
728
+ - Table: name, description, member count, grant count (how many resources this group can access), created date
729
+ - Search/filter bar
730
+ - "Create Group" button → modal with name + description fields
731
+ - Row actions: edit (rename, description), manage members, delete (with confirmation showing affected grant count)
732
+
733
+ **Member management (inline expand or detail view):**
734
+
735
+ - Current members list with remove button per member
736
+ - "Add Members" button → dropdown of Authelia users (fetched from lamalibre-lamaste-serverd `/api/users`) with multi-select
737
+ - Shows which Authelia role tier each user belongs to (admin/internal/external badge)
738
+
739
+ ### Grants Page
740
+
741
+ Unified view of all grants across all resource types.
742
+
743
+ **List view:**
744
+
745
+ - Table: principal (user/group badge + name), resource (tunnel/plugin badge + name), context, status, created date
746
+ - Filter bar: by principal type, principal name, resource type, resource name
747
+ - "Create Grant" button → modal:
748
+ - Principal: radio (User / Group) + dropdown
749
+ - Resource: radio (Tunnel / Plugin) + dropdown (tunnels list / plugin list)
750
+ - Context: shown conditionally (target field for plugin grants)
751
+ - Row actions: revoke (with confirmation)
752
+
753
+ **Contextual views (also accessible from tunnel/group detail pages):**
754
+
755
+ - "Grants for this tunnel" — filtered grant list shown on tunnel detail
756
+ - "Grants for this group" — filtered grant list shown on group detail
757
+
758
+ ### Access Requests Page
759
+
760
+ Log of denied access attempts (if enabled in settings):
761
+
762
+ - Table: timestamp, username, requested resource (tunnel FQDN), status (pending/noted)
763
+ - Helps admins see who is trying to access what and proactively grant access
764
+ - Optional: gatekeeper logs denied attempts to `/etc/lamalibre/lamaste/access-request-log.json` (configurable, off by default, auto-rotated)
765
+
766
+ ### Settings Page
767
+
768
+ Gatekeeper configuration managed via the desktop app:
769
+
770
+ **Admin Contact Info** (used in access-request page templates):
771
+
772
+ - Admin email address
773
+ - Admin display name
774
+ - Optional: Slack channel, Teams channel
775
+ - Saved to `/etc/lamalibre/lamaste/gatekeeper.json`
776
+
777
+ **Message Templates:**
778
+
779
+ - Preview and customize the access-request message templates
780
+ - Per-channel toggle (email, Slack, Teams, WhatsApp)
781
+ - Template variables: `{{username}}`, `{{resource}}`, `{{adminName}}`, `{{adminEmail}}`
782
+
783
+ **Cache Configuration:**
784
+
785
+ - nginx cache TTL display (informational — actual value in nginx config)
786
+ - Gatekeeper session cache TTL (configurable, default 30s)
787
+ - "Bust Cache" button — immediately invalidates all cached auth decisions
788
+
789
+ **Access Request Logging:**
790
+
791
+ - Toggle on/off
792
+ - Retention period (default 30 days)
793
+ - "Clear Log" button
794
+
795
+ ### Tauri Commands
796
+
797
+ ```rust
798
+ // Groups
799
+ get_gatekeeper_groups(server_id) → Vec<Group>
800
+ create_gatekeeper_group(server_id, name, description) → Group
801
+ update_gatekeeper_group(server_id, name, updates) → Group
802
+ delete_gatekeeper_group(server_id, name) → DeleteResult { deleted_grants: u32 }
803
+ add_gatekeeper_group_members(server_id, group_name, usernames) → Group
804
+ remove_gatekeeper_group_members(server_id, group_name, usernames) → Group
805
+ get_gatekeeper_groups_for_user(server_id, username) → Vec<String>
806
+
807
+ // Grants
808
+ get_gatekeeper_grants(server_id, filter?) → Vec<Grant>
809
+ create_gatekeeper_grant(server_id, principal_type, principal_id, resource_type, resource_id, context?) → Grant
810
+ get_gatekeeper_grant(server_id, grant_id) → Grant
811
+ revoke_gatekeeper_grant(server_id, grant_id) → Grant
812
+
813
+ // Authorization diagnostic
814
+ check_gatekeeper_access(server_id, username, resource_type, resource_id) → AccessResult
815
+
816
+ // Settings
817
+ get_gatekeeper_settings(server_id) → GatekeeperSettings
818
+ update_gatekeeper_settings(server_id, settings) → GatekeeperSettings
819
+
820
+ // Cache
821
+ bust_gatekeeper_cache(server_id) → ()
822
+
823
+ // Access request log
824
+ get_access_request_log(server_id, limit?, offset?) → Vec<AccessRequestEntry>
825
+ clear_access_request_log(server_id) → ()
826
+ ```
827
+
828
+ **Communication:** Desktop calls gatekeeper's REST API via lamalibre-lamaste-serverd's SSH tunnel (for remote servers) or directly on `127.0.0.1:9294` (for local server installs). Uses `curl_panel`-style HTTP helper in `gatekeeper.rs` — same pattern as existing Tauri → lamalibre-lamaste-serverd communication but targeting gatekeeper's port.
829
+
830
+ ### React Pages (lamaste-server-ui)
831
+
832
+ Gatekeeper pages are implemented in `lamaste-server-ui` as shared components, following the same pattern as other admin pages:
833
+
834
+ ```
835
+ packages/lamaste-server-ui/src/pages/
836
+ GatekeeperDashboard.jsx
837
+ GatekeeperGroups.jsx
838
+ GatekeeperGrants.jsx
839
+ GatekeeperAccessRequests.jsx
840
+ GatekeeperSettings.jsx
841
+ ```
842
+
843
+ These use `useAdminClient()` context hook — desktop provides Tauri invoke implementation, web panel provides `apiFetch()` implementation. `AdminClientContext` interface extended with all gatekeeper methods.
844
+
845
+ ---
846
+
847
+ ## Migration
848
+
849
+ ### State File Migration
850
+
851
+ On first startup, gatekeeper checks for legacy files:
852
+
853
+ 1. **`/etc/lamalibre/lamaste/user-plugin-access.json`** → migrate to `/etc/lamalibre/lamaste/access-grants.json`
854
+ - Each legacy grant maps to new schema:
855
+ - `username` → `principalType: 'user'`, `principalId: username`
856
+ - `pluginName` → `resourceType: 'plugin'`, `resourceId: pluginName`
857
+ - `target` → `context: { target }`
858
+ - Legacy file renamed to `user-plugin-access.json.migrated` (backup)
859
+
860
+ 2. **Groups extracted from Authelia `users.yml`** → NOT migrated (Lamaste groups are a new concept, start empty)
861
+
862
+ ### Panel-Server Refactoring
863
+
864
+ - `lib/user-access.js` → delegates to gatekeeper library (import from `@lamalibre/lamaste-gatekeeper@4.0.0`)
865
+ - `lib/access-control-sync.js` → removed (gatekeeper handles authorization at nginx layer, no more Authelia config rewriting for per-user access rules)
866
+ - `routes/user-access.js` → simplified, delegates grant CRUD to gatekeeper
867
+ - Tunnel creation/update routes → call gatekeeper for access mode changes
868
+
869
+ ### Installer Updates (create-lamaste)
870
+
871
+ - Install `@lamalibre/lamaste-gatekeeper@4.0.0` package
872
+ - Create `/etc/lamalibre/lamaste/groups.json` (empty initial state)
873
+ - Create `/etc/lamalibre/lamaste/access-grants.json` (empty or migrated)
874
+ - Create `lamalibre-lamaste-gatekeeper.service` systemd unit
875
+ - Enable and start service
876
+ - Update nginx templates for new vhost variants
877
+
878
+ ---
879
+
880
+ ## Security
881
+
882
+ ### Boundaries
883
+
884
+ | Component | Binds to | Auth | Purpose |
885
+ | ------------------ | ---------------- | ---------------------- | ----------------------------- |
886
+ | Gatekeeper service | `127.0.0.1:9294` | None (localhost trust) | nginx auth_request + REST API |
887
+ | Panel-server | `127.0.0.1:9292` | mTLS | Admin operations |
888
+ | Authelia | `127.0.0.1:9091` | Session cookie | User authentication |
889
+
890
+ ### Attack Surface Analysis
891
+
892
+ - **Gatekeeper is not externally reachable** — binds `127.0.0.1`, nginx is the sole caller
893
+ - **No credential storage** — reads Authelia session via cookie forwarding to Authelia's own verify endpoint
894
+ - **No mTLS certs** — completely separate from the admin/agent certificate system
895
+ - **State files** — `0600` permissions, atomic writes (temp → fsync → rename)
896
+ - **DoS protection** — hard caps on groups (200), grants (1000), matching ticket system limits
897
+ - **Group name validation** — lowercase alphanumeric + hyphens, reserved names rejected
898
+ - **Grant uniqueness** — duplicate principal+resource+context rejected
899
+ - **Timing-safe checks** — not needed here (no secret comparison, just membership lookup)
900
+
901
+ ### What Gatekeeper Cannot Do
902
+
903
+ - Access lamalibre-lamaste-serverd API
904
+ - Read/write mTLS certificates
905
+ - Modify Authelia `users.yml`
906
+ - Restart any service
907
+ - Access any state file outside its own scope (`groups.json`, `access-grants.json`, `tunnels.json` read-only)
908
+
909
+ ---
910
+
911
+ ## Relationship to lamaste-tickets
912
+
913
+ Gatekeeper and tickets are separate packages that coexist. They solve different authorization problems:
914
+
915
+ | | **Tickets** | **Gatekeeper** |
916
+ | -------------- | ------------------------------------------------ | --------------------------------------- |
917
+ | **Who** | Agent (mTLS cert CN) | Human (Authelia username) |
918
+ | **Authorizes** | Agent-to-agent communication | User-to-resource access |
919
+ | **Identity** | mTLS certificate | Authelia session cookie |
920
+ | **Mechanism** | Issue token → validate → session with heartbeats | Check grant → allow/deny (stateless) |
921
+ | **Lifecycle** | Ephemeral (30s tokens, heartbeat sessions) | Persistent (grants live until revoked) |
922
+ | **Concepts** | Scopes, instances, transport negotiation | Groups, access modes, request templates |
923
+ | **nginx** | Not involved | auth_request target |
924
+
925
+ They share infrastructure patterns (atomic JSON writes, promise-chain mutex) but their domain logic is fundamentally different. Unifying them would create confused abstractions — every function would need to ask "am I in machine mode or human mode?"
926
+
927
+ - **Tickets** = machine-to-machine authorization protocol
928
+ - **Gatekeeper** = human-to-resource access control
929
+
930
+ ---
931
+
932
+ ## Affected Packages
933
+
934
+ | Package | Changes |
935
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
936
+ | **lamalibre-lamaste-gatekeeper** (NEW) | Full package — library, service, CLI |
937
+ | **lamalibre-lamaste-serverd** | Refactor user-access to delegate to gatekeeper lib, remove access-control-sync, new nginx vhost templates, tunnel schema gains accessMode |
938
+ | **lamaste-server-ui** | New Gatekeeper sidebar section (Dashboard, Groups, Grants, Access Requests, Settings), updated Users page (role radio buttons), updated tunnel creation form (access mode + user/group picker), tunnel list access badges |
939
+ | **create-lamaste** | Install gatekeeper package + service, state file creation, nginx template updates, nginx proxy_cache zone |
940
+ | **lamaste-agent** | Minimal — agent reports tunnel access mode in status |
941
+ | **lamaste-desktop** | New Gatekeeper sidebar section with full management UI, Tauri commands in `gatekeeper.rs`, React pages from admin-panel |
942
+ | **tests/e2e** | Group CRUD, grant CRUD, tunnel access modes, access-request page, caching behavior, migration |
943
+ | **tests/e2e-three-vm** | Multi-agent restricted tunnel access, group-based access across agents |
944
+
945
+ ---
946
+
947
+ ## Resolved Decisions
948
+
949
+ 1. **Access request page hosting**: **Inline on the tunnel's own FQDN.** Gatekeeper returns the full HTML page as the 403 response body — no redirect. nginx uses `error_page 403 = /internal/lamaste/authz` pattern where gatekeeper serves the "request access" page directly. The user stays on `gitlab.example.com` (the URL they tried to visit), which is the most natural UX. No extra subdomain, no extra DNS record, no extra TLS cert.
950
+
951
+ 2. **Grant auto-cleanup**: **90-day lazy cleanup for consumed grants**, consistent with the existing pattern. Configurable retention period via gatekeeper settings. Grants are small (JSON entries), but auto-cleanup prevents unbounded accumulation over years of operation.
952
+
953
+ 3. **Plugin tunnel migration**: **Hard cut-over.** Installer upgrade replaces all plugin tunnel vhosts with gatekeeper-based ones, migrates grants from `user-plugin-access.json` to `access-grants.json`, and removes the Authelia access control rules that `syncAllAccessControl()` previously wrote. Running two authorization systems in parallel would be more complex and error-prone than switching cleanly. The installer already handles nginx vhost rewrites during upgrades.