@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,695 @@
1
+ # Panel Server Architecture
2
+
3
+ > The `lamalibre-lamaste-serverd` daemon is a Fastify 5 REST API that manages all Lamaste operations at runtime, from onboarding through tunnel management, running as a non-root systemd service on the VPS.
4
+
5
+ ## In Plain English
6
+
7
+ The panel server is the brain of Lamaste. It runs on the VPS as a background service and handles every management operation: setting up your domain, creating tunnels, managing users, renewing certificates, and monitoring services. When you interact with the Lamaste admin panel in your browser, every action goes through this server.
8
+
9
+ It deliberately does not face the internet directly. nginx sits in front of it, handling TLS, client certificate verification, and domain routing. The panel server trusts that if a request reaches it, nginx has already verified the caller's identity.
10
+
11
+ ## Overview
12
+
13
+ ```
14
+ nginx (public)
15
+ │
16
+ ├── X-SSL-Client-Verify: SUCCESS
17
+ ├── X-SSL-Client-DN: CN=admin,O=Lamaste
18
+ │
19
+ ▼
20
+ Fastify Server (127.0.0.1:3100)
21
+ │
22
+ ├── Plugins
23
+ │ ├── @fastify/cors
24
+ │ ├── @fastify/cookie
25
+ │ ├── @fastify/multipart (50 MB limit)
26
+ │ ├── @fastify/websocket
27
+ │ └── @fastify/static (SPA serving)
28
+ │
29
+ ├── Middleware (onRequest hooks)
30
+ │ ├── mtls.js → Verify mTLS, check revocation, parse role
31
+ │ ├── twofa-session.js → Verify 2FA session cookie (after mTLS, before roleGuard)
32
+ │ ├── role-guard.js → Role-based access control (admin vs agent)
33
+ │ └── onboarding-guard → Route access by onboarding state
34
+ │
35
+ ├── Routes
36
+ │ ├── /api/health → Always accessible
37
+ │ ├── /api/invite/* → Public (no mTLS required)
38
+ │ ├── /api/onboarding/* → Guarded: 410 after COMPLETED
39
+ │ └── /api/* (management) → Guarded: 503 before COMPLETED
40
+ │
41
+ ├── Error Handler
42
+ │ └── errors.js → Zod errors → 400, AppError → status, else → 500
43
+ │
44
+ └── SPA Fallback
45
+ └── Non-/api 404s → serve index.html
46
+ ```
47
+
48
+ ## Server Entry (`src/index.js`)
49
+
50
+ The server entry point follows a straightforward initialization sequence:
51
+
52
+ ```
53
+ 1. loadConfig() → Read and validate /etc/lamalibre/lamaste/panel.json
54
+ 2. Load plugin capabilities → getPluginCapabilities() → setPluginCapabilities()
55
+ 3. Load ticket scope capabilities → loadTicketScopeCapabilities()
56
+ 4. Register Fastify plugins → CORS, cookie, multipart, websocket, static files
57
+ 5. Register publicContext → Error handler + invite + enrollment routes (no mTLS)
58
+ 6. Register protectedContext → mTLS + 2FA session + role-guard + error handler + health/onboarding/management/plugin routes
59
+ 7. Set 404 handler → SPA fallback for non-API routes
60
+ 8. Listen on 127.0.0.1:3100
61
+ 9. Start instance liveness interval → checkInstanceLiveness() every 60s
62
+ 10. Register shutdown handlers → SIGTERM, SIGINT
63
+ ```
64
+
65
+ **Route registration contexts:** The server separates routes into two Fastify encapsulated contexts:
66
+
67
+ - **`publicContext`** — registers invite routes (`/api/invite/*`) and enrollment routes (`/api/enroll`) without mTLS middleware, allowing unauthenticated users to accept invitations and agents to enroll with one-time tokens
68
+ - **`protectedContext`** — registers mTLS middleware, 2FA session guard, role-guard, and all other routes (health, onboarding, management, plugins), requiring a valid client certificate
69
+
70
+ **Static file serving** resolves the lamaste-server-ui `dist/` directory through a fallback chain:
71
+
72
+ - `config.staticDir` if set in `panel.json` (production: `/opt/lamalibre/lamaste/lamaste-server-ui/dist`)
73
+ - `../../lamaste-server-ui/dist` relative to server source (development)
74
+ - `config.dataDir/lamaste-server-ui/dist` as final fallback
75
+
76
+ **CORS origin** includes the IP-based URL (`https://<ip>:9292`) always, plus the domain-based panel URL (`https://panel.<domain>`) when a domain is configured. This prevents cross-origin requests from unrelated domains.
77
+
78
+ ## Middleware Pipeline
79
+
80
+ ### mTLS Verification (`middleware/mtls.js`)
81
+
82
+ Registered as a global `onRequest` hook that runs on every request before route handlers.
83
+
84
+ **Production behavior:**
85
+
86
+ - Reads the `X-SSL-Client-Verify` header set by nginx
87
+ - If the value is not `SUCCESS`, returns `403 { error: "mTLS certificate required" }`
88
+ - Checks the certificate serial (`X-SSL-Client-Serial` header) against `revoked.json` via `isRevoked()` — if revoked, returns `403 { error: "Certificate has been revoked" }`
89
+ - Parses `X-SSL-Client-DN` header to extract the CN field
90
+ - If CN starts with `agent:`, sets `request.certRole = 'agent'`, `request.certLabel` to the agent label, and loads `request.certCapabilities` from the agent registry
91
+ - Otherwise, sets `request.certRole = 'admin'`
92
+
93
+ **Development behavior:**
94
+
95
+ - When `NODE_ENV` is `development`, the check is bypassed
96
+ - Logs a warning on the first bypassed request
97
+ - Sets `request.certRole = 'admin'` by default
98
+
99
+ **Health check bypass:**
100
+
101
+ - `GET /api/health` always bypasses mTLS verification (used by systemd, load balancers, and internal provisioning checks)
102
+
103
+ In production, nginx uses `ssl_verify_client optional` at the server level, with per-location enforcement via `if ($ssl_client_verify != SUCCESS) { return 496; }` on protected locations. Public endpoints (`/api/enroll`, `/api/invite`) skip this check. The server middleware is a defense-in-depth measure that also performs revocation checking and role extraction.
104
+
105
+ ### 2FA Session Guard (`middleware/twofa-session.js`)
106
+
107
+ Registered as a Fastify plugin in the protected context, runs after mTLS verification and before the role guard. When built-in 2FA is enabled (`panel2fa.enabled` in `panel.json`), this middleware enforces that admin requests carry a valid session cookie.
108
+
109
+ **Behavior:**
110
+
111
+ - If 2FA is not enabled, the middleware is a no-op — all requests pass through
112
+ - Agent requests (`request.certRole === 'agent'`) always bypass the 2FA check — agents authenticate via mTLS only
113
+ - Health, 2FA status, and 2FA verification endpoints are exempt (`/api/health`, `/api/settings/2fa`, `/api/settings/2fa/verify`)
114
+ - If an admin request lacks a valid session cookie, returns `401 { error: "2fa_required" }`
115
+ - Session cookies are issued by the `POST /api/settings/2fa/verify` endpoint after a valid TOTP code is presented
116
+ - Sessions are managed by `lib/session.js` using signed cookies (secret from `sessionSecret` in `panel.json`)
117
+
118
+ The Fastify constructor sets `trustProxy: 1` so that exactly one proxy hop (nginx) is trusted for `X-Forwarded-*` headers. This is more secure than `trustProxy: true` (which trusts all hops) and matches the documented deployment model where nginx is the sole reverse proxy.
119
+
120
+ ### Onboarding Guard (`middleware/onboarding-guard.js`)
121
+
122
+ Two hook factories that enforce route access based on `config.onboarding.status`:
123
+
124
+ **`onboardingOnly()`** — applied to `/api/onboarding/*` (except `/status`):
125
+
126
+ - If status is `COMPLETED`, returns `410 Gone`
127
+ - Otherwise, allows the request
128
+
129
+ **`managementOnly()`** — applied to all `/api/*` management routes:
130
+
131
+ - If status is not `COMPLETED`, returns `503 { error: "Onboarding not complete", onboardingStatus }`
132
+ - Otherwise, allows the request
133
+
134
+ The `/api/onboarding/status` endpoint is deliberately unguarded — the panel client calls it on every page load to determine which UI mode to display.
135
+
136
+ ### Role Guard (`middleware/role-guard.js`)
137
+
138
+ Registered as a Fastify plugin that decorates the server with a `requireRole(allowedRoles, opts)` function. This returns a `preHandler` hook used on individual routes to enforce role-based access control.
139
+
140
+ **Usage patterns:**
141
+
142
+ - `fastify.requireRole(['admin'])` — admin-only routes
143
+ - `fastify.requireRole(['admin', 'agent'])` — admin or any agent
144
+ - `fastify.requireRole(['admin', 'agent'], { capability: 'tunnels:write' })` — admin or agents with a specific capability
145
+
146
+ **Behavior:**
147
+
148
+ - Admin role always passes without capability checks
149
+ - If the request's `certRole` is not in the allowed list, returns `403 { error: "Insufficient certificate scope" }`
150
+ - If a capability is required and the agent lacks it, returns `403 { error: "Insufficient certificate capability" }`
151
+
152
+ ### Error Handler (`middleware/errors.js`)
153
+
154
+ Registered as the global Fastify error handler. Normalizes all errors into a consistent response format:
155
+
156
+ ```json
157
+ {
158
+ "error": "Human-readable error summary",
159
+ "details": {}
160
+ }
161
+ ```
162
+
163
+ Error type resolution order:
164
+
165
+ | Error Type | Detection | Response |
166
+ | ---------------------- | ------------------------------------------------------------ | ---------------------------------------- |
167
+ | Zod validation | `error.name === 'ZodError'` or `Array.isArray(error.issues)` | `400` with issue paths and messages |
168
+ | Operational (AppError) | `error.isOperational === true` | Custom `statusCode` from error |
169
+ | Fastify built-in | `error.statusCode` in 400-499 | Pass through status and message |
170
+ | Unexpected | Everything else | `500 { error: "Internal server error" }` |
171
+
172
+ In development mode, unexpected errors include `details.message` and `details.stack`. In production, no internal details are leaked.
173
+
174
+ ### AppError (`lib/app-error.js`)
175
+
176
+ A lightweight error class for operational errors (expected failures):
177
+
178
+ ```javascript
179
+ export class AppError extends Error {
180
+ constructor(message, statusCode, details = null) {
181
+ super(message);
182
+ this.name = 'AppError';
183
+ this.statusCode = statusCode;
184
+ this.details = details;
185
+ this.isOperational = true;
186
+ }
187
+ }
188
+ ```
189
+
190
+ Used in library code to signal expected error conditions (e.g., "DNS is not pointing to this server", "Cannot delete the last user").
191
+
192
+ ## Route Structure
193
+
194
+ ### Registration Hierarchy
195
+
196
+ ```
197
+ publicContext (no mTLS):
198
+ server.register(inviteRoutes, { prefix: '/api/invite' })
199
+ ├── GET /api/invite/:token ← Get invitation details
200
+ └── POST /api/invite/:token/accept ← Accept invitation, set password
201
+ server.register(enrollmentRoutes, { prefix: '/api/enroll' })
202
+ └── POST /api/enroll ← Enroll agent with token + CSR
203
+
204
+ protectedContext (mTLS + role-guard):
205
+ server.register(healthRoutes, { prefix: '/api' })
206
+ └── GET /api/health
207
+
208
+ server.register(onboardingRoutes, { prefix: '/api/onboarding' })
209
+ ├── GET /api/onboarding/status ← No guard
210
+ └── [guarded: onboardingOnly()]
211
+ ├── POST /api/onboarding/domain
212
+ ├── POST /api/onboarding/verify-dns
213
+ ├── POST /api/onboarding/provision
214
+ └── WS /api/onboarding/provision/stream
215
+
216
+ server.register(managementRoutes, { prefix: '/api' })
217
+ └── [guarded: managementOnly()]
218
+ ├── GET /api/tunnels/agent-config
219
+ ├── GET /api/tunnels
220
+ ├── POST /api/tunnels
221
+ ├── PATCH /api/tunnels/:id
222
+ ├── DELETE /api/tunnels/:id
223
+ ├── GET /api/tunnels/agent-panel-status
224
+ ├── POST /api/tunnels/expose-panel
225
+ ├── DELETE /api/tunnels/retract-panel
226
+ ├── GET /api/sites
227
+ ├── POST /api/sites
228
+ ├── PATCH /api/sites/:id
229
+ ├── DELETE /api/sites/:id
230
+ ├── POST /api/sites/:id/verify-dns
231
+ ├── GET /api/sites/:id/files
232
+ ├── POST /api/sites/:id/files
233
+ ├── DELETE /api/sites/:id/files
234
+ ├── GET /api/system/stats
235
+ ├── GET /api/services
236
+ ├── POST /api/services/:name/:action
237
+ ├── WS /api/services/:name/logs
238
+ ├── GET /api/users
239
+ ├── POST /api/users
240
+ ├── PUT /api/users/:username
241
+ ├── DELETE /api/users/:username
242
+ ├── POST /api/users/:username/reset-totp
243
+ ├── GET /api/certs
244
+ ├── GET /api/certs/auto-renew-status
245
+ ├── POST /api/certs/:domain/renew
246
+ ├── POST /api/certs/mtls/rotate
247
+ ├── GET /api/certs/mtls/download
248
+ ├── POST /api/certs/admin/upgrade-to-hardware-bound
249
+ ├── GET /api/certs/admin/auth-mode
250
+ ├── POST /api/certs/agent
251
+ ├── POST /api/certs/agent/enroll
252
+ ├── GET /api/certs/agent
253
+ ├── GET /api/certs/agent/:label/download
254
+ ├── DELETE /api/certs/agent/:label
255
+ ├── PATCH /api/certs/agent/:label/capabilities
256
+ ├── PATCH /api/certs/agent/:label/allowed-sites
257
+ ├── GET /api/invitations
258
+ ├── POST /api/invitations
259
+ ├── DELETE /api/invitations/:id
260
+ ├── GET /api/plugins
261
+ ├── GET /api/plugins/:name
262
+ ├── POST /api/plugins/install
263
+ ├── POST /api/plugins/:name/enable
264
+ ├── POST /api/plugins/:name/disable
265
+ ├── DELETE /api/plugins/:name
266
+ ├── GET /api/plugins/push-install/config
267
+ ├── PATCH /api/plugins/push-install/config
268
+ ├── GET /api/plugins/push-install/policies
269
+ ├── POST /api/plugins/push-install/policies
270
+ ├── PATCH /api/plugins/push-install/policies/:id
271
+ ├── DELETE /api/plugins/push-install/policies/:id
272
+ ├── POST /api/plugins/push-install/enable/:label
273
+ ├── DELETE /api/plugins/push-install/enable/:label
274
+ ├── POST /api/plugins/push-install/:label
275
+ ├── GET /api/plugins/push-install/sessions
276
+ ├── GET /api/plugins/push-install/agent-status
277
+ ├── GET /api/settings/2fa
278
+ ├── POST /api/settings/2fa/setup
279
+ ├── POST /api/settings/2fa/confirm
280
+ ├── POST /api/settings/2fa/verify
281
+ ├── POST /api/settings/2fa/disable
282
+ ├── POST /api/tickets/scopes
283
+ ├── GET /api/tickets/scopes
284
+ ├── DELETE /api/tickets/scopes/:name
285
+ ├── POST /api/tickets/instances
286
+ ├── DELETE /api/tickets/instances/:instanceId
287
+ ├── POST /api/tickets/instances/:instanceId/heartbeat
288
+ ├── POST /api/tickets/assignments
289
+ ├── DELETE /api/tickets/assignments/:agentLabel/:instanceScope
290
+ ├── GET /api/tickets/assignments
291
+ ├── POST /api/tickets
292
+ ├── GET /api/tickets/inbox
293
+ ├── POST /api/tickets/validate
294
+ ├── GET /api/tickets
295
+ ├── DELETE /api/tickets/:ticketId
296
+ ├── POST /api/tickets/sessions
297
+ ├── POST /api/tickets/sessions/:sessionId/heartbeat
298
+ ├── PATCH /api/tickets/sessions/:sessionId
299
+ ├── DELETE /api/tickets/sessions/:sessionId
300
+ ├── GET /api/tickets/sessions
301
+ ├── GET /api/identity/self
302
+ ├── GET /api/identity/users
303
+ ├── GET /api/identity/users/:username
304
+ ├── GET /api/identity/groups
305
+ ├── POST /api/storage/servers
306
+ ├── GET /api/storage/servers
307
+ ├── DELETE /api/storage/servers/:id
308
+ ├── POST /api/storage/bindings
309
+ ├── GET /api/storage/bindings
310
+ ├── GET /api/storage/bindings/:pluginName
311
+ ├── DELETE /api/storage/bindings/:pluginName
312
+ └── POST /api/system/update
313
+
314
+ server.register(pluginRouter, { prefix: '/api' })
315
+ └── [guarded: managementOnly() + per-plugin admin-only auth scope]
316
+ ├── /api/<pluginName>/* ← Dynamic plugin server routes
317
+ └── GET /api/<pluginName>/panel.js ← Plugin panel bundle
318
+ ```
319
+
320
+ ### Onboarding Routes
321
+
322
+ The onboarding module (`routes/onboarding/index.js`) uses a nested registration pattern to apply the guard:
323
+
324
+ ```javascript
325
+ export default async function onboardingRoutes(fastify, _opts) {
326
+ // Status is always accessible — no guard
327
+ await fastify.register(statusRoute);
328
+
329
+ // All other routes are guarded: 410 after onboarding completes
330
+ await fastify.register(async function guarded(app) {
331
+ app.addHook('onRequest', onboardingOnly());
332
+ await app.register(domainRoute);
333
+ await app.register(dnsRoute);
334
+ await app.register(provisionRoute);
335
+ });
336
+ }
337
+ ```
338
+
339
+ **Provisioning** is the most complex onboarding route. It uses a background task pattern:
340
+
341
+ 1. `POST /provision` validates the onboarding state, starts the provisioning function asynchronously, and returns `202 Accepted`
342
+ 2. `WS /provision/stream` connects clients to a real-time progress feed via an `EventEmitter`
343
+ 3. The provisioning function emits progress events as it installs Chisel, Authelia, issues certificates, configures nginx, and verifies services
344
+ 4. On completion, it updates `panel.json` to `COMPLETED` and emits final credentials
345
+
346
+ ### Management Routes
347
+
348
+ The management module (`routes/management.js`) applies `managementOnly()` at the top level, guarding all child routes:
349
+
350
+ ```javascript
351
+ export default async function managementRoutes(fastify, _opts) {
352
+ fastify.addHook('onRequest', managementOnly());
353
+
354
+ await fastify.register(tunnelRoutes);
355
+ await fastify.register(sitesRoutes);
356
+ await fastify.register(systemRoutes);
357
+ await fastify.register(servicesRoutes);
358
+ await fastify.register(logsRoutes);
359
+ await fastify.register(usersRoutes);
360
+ await fastify.register(certsRoutes);
361
+ await fastify.register(invitationRoutes);
362
+ await fastify.register(pluginRoutes);
363
+ await fastify.register(settingsRoutes);
364
+ await fastify.register(ticketRoutes);
365
+ await fastify.register(identityRoutes);
366
+ await fastify.register(storageRoutes);
367
+ }
368
+ ```
369
+
370
+ ## Library Layer
371
+
372
+ Routes handle HTTP concerns only — request parsing, response formatting, status codes. All business logic lives in `src/lib/` modules.
373
+
374
+ ### config.js — Configuration Management
375
+
376
+ - Loads and validates `panel.json` at startup via Zod schema
377
+ - Caches parsed config in a module-level variable
378
+ - `getConfig()` returns a `structuredClone` (callers cannot mutate the cache)
379
+ - `updateConfig(patch)` performs deep merge, re-validates, and writes atomically (temp → rename)
380
+
381
+ Config schema:
382
+
383
+ ```
384
+ {
385
+ ip: string, // Required
386
+ domain: string | null, // Set during onboarding
387
+ email: string (email) | null, // Set during onboarding
388
+ dataDir: string, // /etc/lamalibre/lamaste
389
+ serverId?: string (uuid), // Auto-generated, bucket prefix for multi-server storage isolation
390
+ staticDir?: string, // /opt/lamalibre/lamaste/lamaste-server-ui/dist
391
+ maxSiteSize?: number, // Default: 500 MB
392
+ adminAuthMode?: "p12" | "hardware-bound", // Default: "p12"
393
+ onboarding: {
394
+ status: "FRESH" | "DOMAIN_SET" | "DNS_READY" | "PROVISIONING" | "COMPLETED"
395
+ },
396
+ panel2fa?: {
397
+ enabled: boolean, // Default: false
398
+ secret: string | null, // TOTP secret (base32)
399
+ setupComplete: boolean // Default: false
400
+ },
401
+ sessionSecret?: string | null // HMAC key for signed session cookies (default: null)
402
+ }
403
+ ```
404
+
405
+ Config path resolution:
406
+
407
+ 1. `LAMALIBRE_LAMASTE_CONFIG` environment variable (if set)
408
+ 2. `dev/panel.json` relative to package root (if `NODE_ENV` is `development`)
409
+ 3. `/etc/lamalibre/lamaste/panel.json` (production default)
410
+
411
+ ### state.js — Tunnel and Site State
412
+
413
+ Provides atomic read/write for `tunnels.json` and `sites.json`:
414
+
415
+ ```
416
+ readTunnels() → Array<Tunnel> (returns [] if file missing)
417
+ writeTunnels() → void (atomic: tmp → fsync → rename)
418
+ readSites() → Array<Site> (returns [] if file missing)
419
+ writeSites() → void (atomic: tmp → fsync → rename)
420
+ ```
421
+
422
+ The atomic write pattern:
423
+
424
+ 1. `writeFile` to `<path>.tmp`
425
+ 2. Open the temp file and call `fd.sync()` to flush to disk
426
+ 3. `rename` temp to final path (atomic on POSIX filesystems)
427
+
428
+ This ensures that a crash during write never corrupts the state file. The worst case is losing the latest write, leaving the previous state intact.
429
+
430
+ ### nginx.js — Vhost Management
431
+
432
+ Provides functions for writing, enabling, disabling, testing, and reloading nginx configurations. The core pattern is **write-with-rollback**:
433
+
434
+ 1. Backup existing vhost (if any) to `.bak`
435
+ 2. Install the new vhost: `sudo lamaste-priv nginx-site write <name>` with the text on stdin — the root helper refuses any directive outside its allow-list
436
+ 3. Create (or remove) the symlink in `sites-enabled` (`nginx-site enable|disable`)
437
+ 4. Run `nginx -t` to validate
438
+ 5. On success: `systemctl reload nginx`, delete backup
439
+ 6. On failure: restore backup (or remove the new file) and the previous link state
440
+
441
+ Tunnel vhosts (`writePublicVhost`, `writeAuthenticatedVhost`, `writeRestrictedVhost`) declare their proxy headers inside each `location` (`TUNNEL_PROXY_HEADERS`: `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, WebSocket upgrade), always clear client-supplied `X-SSL-Client-*` headers, and — on public vhosts — clear the `Remote-*` identity headers.
442
+
443
+ Additional functions `disableIpVhost()` and `enableIpVhost()` manage the IP-based panel vhost. When built-in 2FA is enabled, the IP vhost is disabled to prevent bypassing the domain-based 2FA session; disabling 2FA re-enables it.
444
+
445
+ See [nginx-configuration.md](./nginx-configuration.md) for detailed coverage.
446
+
447
+ ### chisel.js — Tunnel Server Management
448
+
449
+ - Never installs the binary or writes the unit: both are `create-lamaste`'s (the pinned 1.12.0 release at `/usr/local/bin/chisel`; the unit binds `127.0.0.1:9090`, `--reverse`, `--keyfile`, `--authfile`, `User=nobody`, `Group=lamaste-chisel`). `isChiselInstalled()` checks the binary exists; `getInstalledChiselVersion()` reads its version
450
+ - Generates the server key `chisel-server.key` as `lamaste` (`lamaste:lamaste-chisel`, `0640`), written to a temp file and renamed into place
451
+ - Manages service lifecycle (start, stop, `try-restart`)
452
+ - `syncChisel()` — called after every tunnel state write: re-renders the authfile (via `chisel-users.js`). Chisel reloads it for new sessions; when the render withdrew a grant or password, Chisel is restarted so live sessions drop what they no longer hold (`chisel-runtime.js`). Throws if that restart fails, since a withdrawn grant would otherwise stay live
453
+
454
+ ### chisel-users.js — Chisel Credentials and Grants
455
+
456
+ - Thin shim over `@lamalibre/lamaste@4.0.0/server`: per-agent credentials in `chisel-credentials.json` (0600)
457
+ - Renders the authfile `chisel-users` from the credentials and `tunnels.json`: each `agent-<label>` may bind exactly `^R:127\.0\.0\.1:<port>$` for its enabled tunnels
458
+ - Adds the `lamaste-no-grants` sentinel user (password in `chisel-sentinel`, 0600) so the authfile is never empty — Chisel disables authentication on an empty authfile
459
+ - Never grants a reserved port (3100, 9090, 9091, 9292, 9294) or a port claimed by two tunnels
460
+ - Writes the authfile itself, no sudo: `0600` temp file → `chgrp lamaste-chisel` → `chmod 0640` → fsync → rename. The serverd unit has `SupplementaryGroups=lamaste-chisel`
461
+ - Reports whether each render `revoked` something; the caller decides on the restart
462
+
463
+ ### chisel-runtime.js — When an Authfile Change Needs a Restart
464
+
465
+ - Additions take effect by Chisel's own reload; revocations call `systemctl try-restart chisel`, which leaves a deliberately stopped Chisel stopped
466
+ - Until this process has seen Chisel running the pinned release (older releases do not reload reliably across atomic renames), every change restarts it
467
+
468
+ ### chisel-reconcile.js — Startup Reconciliation (Fail Closed)
469
+
470
+ Runs in the background on every start (`startChiselReconciler`), so the panel stays reachable while it works:
471
+
472
+ 1. Mints a Chisel credential for every active agent that lacks one
473
+ 2. Under the tunnel lock, binds ownerless tunnels where the owner is certain (`bindUnownedTunnels`): an `agent-<label>` panel tunnel only to the agent it names, when active; other tunnels only when exactly one machine agent is active (plugin-agents are not counted). The rest stay unassigned and are logged, as are tunnels withheld from everyone (`withheldTunnels`: a reserved port or a port two tunnels share)
474
+ 3. Once onboarding is `COMPLETED`: ensures the server key exists and rewrites the authfile (ownership and mode re-applied). It also checks the installed Chisel version; if it is not the pinned release it logs an error ("run create-lamaste on the server") and Chisel is restarted on every authfile change until it is
475
+ 4. Restarts Chisel when a revocation requires it; starts it if enabled but not running
476
+
477
+ If the authfile cannot be established, Chisel is **stopped and disabled** and the marker `/etc/lamalibre/lamaste/chisel-failed-closed` is written — an authfile from an older version may grant every agent every port, and a disabled unit keeps a reboot from starting it on that file. Reconciliation retries with exponential backoff from 30 seconds to 10 minutes; a successful pass re-enables (`sudo systemctl enable chisel`) and starts Chisel and removes the marker.
478
+
479
+ ### tunnel-deps.js — Tunnel Workflow Dependencies
480
+
481
+ - The nginx, certbot, chisel and state objects the core tunnel workflows (`createTunnel`, `updateTunnel`, `releaseAgentTunnels`, ...) receive; shared by the tunnel routes and agent revocation
482
+
483
+ ### Tunnel routes — HTTP rules on top of the core workflows
484
+
485
+ - All tunnel workflows run under the core's single tunnel lock; `PATCH /api/tunnels/:id` is one `updateTunnel` call applying `agentLabel` → `accessMode`/`maxBodySizeMb` → `enabled`
486
+ - Plugin-agent certificates cannot create tunnels (403) — they hold no Chisel credential
487
+ - `maxBodySizeMb` above 100 MiB is admin-only (admins up to 10240)
488
+ - Reserved ports are rejected by the request schema
489
+ - `POST /api/tunnels` and `POST /api/tunnels/expose-panel` refuse a hostname a static site or site alias already serves
490
+ - `GET /api/tunnels/agent-config` returns only the tunnels Chisel actually grants the agent (`grantedTunnelsFor`) — never a withheld one, since asking for a single ungranted port gets the agent's whole session refused — and includes `chiselCredentialIssuedAt`
491
+
492
+ ### authelia.js — Authentication Management
493
+
494
+ - Never installs Authelia: the pinned binary, its unit and the `lamaste-authelia` account are `create-lamaste`'s; `isAutheliaInstalled()` checks the binary exists
495
+ - Writes YAML configuration (bcrypt cost 12, file-based users, TOTP, session cookies) directly, without sudo: `/etc/authelia` is `lamaste:lamaste-authelia 2770` (setgid), files are written as a temp file + fsync + rename (`configuration.yml` 0640, `users.yml` 0660, `.secrets.json` 0600)
496
+ - User CRUD: creates users with bcrypt-hashed passwords, reads/writes `users.yml`
497
+ - Stores a user's TOTP secret with `sudo lamaste-priv authelia-totp <username>` (secret on stdin), which runs Authelia's CLI as `lamaste-authelia` — the database is that account's file
498
+ - TOTP generation: `crypto.randomBytes(20)` → base32-encoded secret → `otpauth://` URI (parameterized `generateTotpSecret(username, { issuer })` — reused by panel 2FA via `lib/totp.js`)
499
+ - `base32Decode()` utility for decoding base32 TOTP secrets into raw bytes
500
+ - Manages service lifecycle
501
+
502
+ ### certbot.js — Certificate Management
503
+
504
+ - Every certbot call goes through `sudo /usr/local/sbin/lamaste-certbot issue|renew|renew-all|list`, and every read of a Let's Encrypt certificate through `sudo /usr/local/sbin/lamaste-cert-info <lineage> enddate|checkend|san` — root-owned wrappers that validate hostnames and email and run certbot/openssl with a fixed argument vector (the wildcard sudoers rules they replace accepted extra flags such as `--deploy-hook`)
505
+ - Issues Let's Encrypt certificates using the nginx plugin (`certbot certonly --nginx`)
506
+ - Issues one lineage per custom site covering its aliases (`lamaste-certbot issue <email> <fqdn> <fqdn> <alias> ...`), reading the current SANs (`lamaste-cert-info <fqdn> san`) to skip re-issuance when the certificate already covers every name — or, with `match: 'exact'`, exactly the requested names
507
+ - Panel-triggered renewals pass `--no-random-sleep-on-renew`
508
+ - Handles rate limit, DNS, and server block errors with specific error messages
509
+ - Supports wildcard certificate detection (skips individual issuance if wildcard covers the FQDN)
510
+ - Lists all managed certificates by parsing `certbot certificates` output
511
+ - Sets up auto-renewal via the `certbot.timer` systemd unit
512
+
513
+ ### mtls.js — mTLS Certificate Operations
514
+
515
+ - Reads certificate expiry dates via `openssl x509 -enddate` (no sudo: the panel owns the PKI directory)
516
+ - Does not rotate admin client certificates — that is `sudo lamaste-server reset-admin` on the server only; the admin rotation route returns 503 with that instruction
517
+ - Generates agent-scoped client certificates with capability-based access: key, CSR and PKCS12 as `lamaste`, signed with the panel CA by `lib/pki-sign.js` (`openssl x509 -req`, run as `lamaste`), which refuses any CN that is not an `agent:` / `plugin-agent:` label — never `CN=admin`. `lib/csr-signing.js` signs enrollment and rotation CSRs the same way after checking the CN is exactly the expected label
518
+ - Manages the agent registry: create, list, revoke, update capabilities and allowed sites
519
+ - Revoking a regular agent removes its Chisel credential and releases its tunnels (`releaseAgentTunnels`): app/plugin tunnels become unassigned, its `agent-<label>` panel tunnel is removed
520
+ - `rotateAgentChiselCredential` backs `POST /api/agents/:label/chisel-credential/rotate` (admin) and `POST /api/agents/me/chisel-credential/rotate` (agent, refused with 429 while the current credential is under 10 minutes old)
521
+ - Provides the PKCS12 download path for the certs API
522
+ - Manages dynamic capability sets: base capabilities + plugin capabilities + ticket scope capabilities via `getValidCapabilities()`
523
+ - Base capabilities: `tunnels:read`, `tunnels:write`, `services:read`, `services:write`, `system:read`, `sites:read`, `sites:write`, `panel:expose`, `identity:read`, `identity:query`
524
+
525
+ ### tickets.js — Ticket System
526
+
527
+ Agent-to-agent authorization with scopes, instances, tickets, and sessions. Provides:
528
+
529
+ - **Scope registry** — register/unregister capability sets with transport configuration
530
+ - **Instance management** — register, heartbeat, deregister with liveness tracking (active → stale → dead)
531
+ - **Assignment management** — link agents to instances (admin-only)
532
+ - **Ticket operations** — request, validate (HMAC-SHA256 + `timingSafeEqual` with per-process random key), revoke with rate limiting (10/agent/min) and hard caps (200 instances, 1000 tickets, 500 sessions)
533
+ - **Session management** — create from validated ticket (server-generated session IDs via `crypto.randomBytes(16)`, server-enforced timestamps), heartbeat with multi-layer re-validation, status updates
534
+ - **Host validation** — `transport.direct.host` rejects private/reserved IPs and cloud metadata endpoints (SSRF prevention)
535
+ - **Periodic cleanup** — stale/dead instances, expired tickets, dead sessions
536
+ - **Concurrency** — promise-chain mutex with atomic file writes (temp → fsync → rename)
537
+
538
+ Client SDK: `@lamalibre/lamaste-tickets@4.0.0` (TypeScript, undici) provides `TicketClient`, `TicketInstanceManager` (source side), and `TicketSessionManager` (target side) for plugin integration.
539
+
540
+ State files: `/etc/lamalibre/lamaste/ticket-scopes.json` (scopes, instances, assignments) and `/etc/lamalibre/lamaste/tickets.json` (tickets, sessions).
541
+
542
+ ### storage.js — Storage Server Registry
543
+
544
+ - Manages S3-compatible storage servers and their bindings to plugins
545
+ - Credentials (access key, secret key) encrypted at rest with AES-256-GCM
546
+ - Key derivation via scrypt (N=16384, r=8, p=1) from a 32-byte master key at `/etc/lamalibre/lamaste/storage-master.key`
547
+ - Encryption format: `[salt (16)] [iv (12)] [authTag (16)] [ciphertext]` base64-encoded
548
+ - Master key auto-generated on first use, persisted with mode 0600
549
+ - Server CRUD: register, remove (cascades to bindings), list (credentials redacted)
550
+ - Plugin bindings: bind, unbind, get (with redacted server info), list
551
+ - `getPluginStorageConfig(pluginName)` returns decrypted credentials for bound plugins
552
+ - Promise-chain mutex serializes all read-modify-write operations
553
+ - Atomic writes (temp file, fsync, rename) for crash safety
554
+
555
+ ### services.js — Service Management
556
+
557
+ - Allowlisted services: `nginx`, `chisel`, `authelia`, `lamalibre-lamaste-serverd`
558
+ - Allowlisted actions: `start`, `stop`, `restart`, `reload`
559
+ - Safety check: cannot stop `lamalibre-lamaste-serverd` from the UI (would terminate the session)
560
+ - Queries status and uptime via `systemctl is-active` and `systemctl show --property=ActiveEnterTimestamp`
561
+
562
+ ### system-stats.js — System Monitoring
563
+
564
+ - Uses the `systeminformation` npm package for cross-platform stats
565
+ - Returns CPU usage, core count, memory (total/used/free), disk (total/used/free), and system uptime
566
+ - 2-second cache to avoid hammering the system when multiple clients poll simultaneously
567
+
568
+ ### files.js — Static Site File Operations
569
+
570
+ - Path validation with directory traversal prevention (rejects `..`, absolute paths, null bytes, hidden files)
571
+ - Creates site directories with default `index.html`
572
+ - Streaming file upload (memory-safe on 512 MB droplets): stream → temp file next to the destination → rename (mode `0640`)
573
+ - File listing and site size via `readdir` / `lstat` (symbolic links are skipped, never followed)
574
+ - No sudo: the web root is `lamaste:www-data` with setgid `2750` directories, so the panel writes it directly and nginx reads through the `www-data` group
575
+
576
+ ## WebSocket Support
577
+
578
+ The server uses `@fastify/websocket` for two real-time features:
579
+
580
+ ### Provisioning Progress Stream (`/api/onboarding/provision/stream`)
581
+
582
+ - Module-level `EventEmitter` shared between the POST handler (which starts provisioning) and WebSocket connections (which stream progress)
583
+ - On connect, sends full current state (for late-joining clients)
584
+ - Emits `{ task, title, status, message, log, progress: { current, total } }` for each step
585
+ - Completion event includes admin credentials and URLs
586
+
587
+ ### Live Log Streaming (`/api/services/:name/logs`)
588
+
589
+ - Spawns `journalctl -f -u <service> -n 50` as a child process
590
+ - Streams stdout lines to WebSocket clients
591
+ - Cleans up the child process on WebSocket close
592
+
593
+ ## SPA Fallback
594
+
595
+ The server's `setNotFoundHandler` implements client-side routing support:
596
+
597
+ ```javascript
598
+ server.setNotFoundHandler((request, reply) => {
599
+ if (request.url.startsWith('/api')) {
600
+ return reply.code(404).send({ error: 'Not found' });
601
+ }
602
+ return reply.sendFile('index.html');
603
+ });
604
+ ```
605
+
606
+ API routes that don't match return a proper 404 JSON response. All other routes serve `index.html`, allowing React Router to handle client-side navigation.
607
+
608
+ ## Graceful Shutdown
609
+
610
+ The server registers handlers for `SIGTERM` and `SIGINT`:
611
+
612
+ ```javascript
613
+ const shutdown = async (signal) => {
614
+ server.log.info({ signal }, 'Received signal, shutting down gracefully');
615
+ clearInterval(livenessInterval);
616
+ clearRateLimitInterval();
617
+ await server.close();
618
+ process.exit(0);
619
+ };
620
+ ```
621
+
622
+ The shutdown sequence stops the periodic instance liveness check (`livenessInterval`), clears the ticket rate-limit cleanup interval (`clearRateLimitInterval()`), and then calls `server.close()` which waits for active connections to finish before shutting down, ensuring in-flight requests complete cleanly.
623
+
624
+ ## Key Files
625
+
626
+ | File | Role |
627
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------- |
628
+ | `packages/lamaste-serverd/src/index.js` | Server entry, plugin + route registration |
629
+ | `packages/lamaste-serverd/src/middleware/mtls.js` | mTLS verification, revocation check, role parsing |
630
+ | `packages/lamaste-serverd/src/middleware/role-guard.js` | Role-based access control (admin vs agent capabilities) |
631
+ | `packages/lamaste-serverd/src/middleware/onboarding-guard.js` | Route access control by onboarding state |
632
+ | `packages/lamaste-serverd/src/middleware/twofa-session.js` | 2FA session cookie verification (after mTLS, before roleGuard) |
633
+ | `packages/lamaste-serverd/src/middleware/errors.js` | Global error handler (Zod, AppError, 500) |
634
+ | `packages/lamaste-serverd/src/routes/onboarding/index.js` | Onboarding route registration + guard |
635
+ | `packages/lamaste-serverd/src/routes/onboarding/provision.js` | Provisioning POST + WebSocket stream |
636
+ | `packages/lamaste-serverd/src/routes/invite.js` | Public invite acceptance routes (no mTLS) |
637
+ | `packages/lamaste-serverd/src/routes/enrollment.js` | Public enrollment route (token + CSR, no mTLS) |
638
+ | `packages/lamaste-serverd/src/routes/management.js` | Management route registration + guard |
639
+ | `packages/lamaste-serverd/src/routes/management/certs.js` | Certificate management (mTLS, agent certs, admin auth mode) |
640
+ | `packages/lamaste-serverd/src/routes/management/plugins.js` | Plugin management + push install (admin-only) |
641
+ | `packages/lamaste-serverd/src/routes/management/invitations.js` | Invitation CRUD (admin-only) |
642
+ | `packages/lamaste-serverd/src/routes/management/settings.js` | 2FA settings: setup, confirm, verify, disable (admin-only) |
643
+ | `packages/lamaste-serverd/src/routes/plugin-router.js` | Dynamic plugin route mounting + disabled-plugin 503 handler |
644
+ | `packages/lamaste-serverd/src/lib/config.js` | Config loading, validation (Zod), atomic update |
645
+ | `packages/lamaste-serverd/src/lib/state.js` | tunnels.json + sites.json + invitations.json atomic read/write |
646
+ | `packages/lamaste-serverd/src/lib/revocation.js` | Certificate revocation list management (revoked.json) |
647
+ | `packages/lamaste-serverd/src/lib/invite-page.js` | Invitation acceptance HTML page generator |
648
+ | `packages/lamaste-serverd/src/lib/nginx.js` | Vhost generation, write-with-rollback, reload |
649
+ | `packages/lamaste-serverd/src/lib/chisel.js` | Chisel install, service management, config update |
650
+ | `packages/lamaste-serverd/src/lib/authelia.js` | Authelia install, config, user CRUD, TOTP |
651
+ | `packages/lamaste-serverd/src/lib/certbot.js` | Let's Encrypt issuance, renewal, listing |
652
+ | `packages/lamaste-serverd/src/lib/mtls.js` | mTLS cert info, client cert rotation, agent registry, capability management |
653
+ | `packages/lamaste-serverd/src/lib/plugins.js` | Plugin manifest validation, install/uninstall, registry CRUD |
654
+ | `packages/lamaste-serverd/src/lib/push-install.js` | Push install policies, sessions, agent enablement |
655
+ | `packages/lamaste-serverd/src/lib/enrollment.js` | Enrollment token creation, validation, consumption |
656
+ | `packages/lamaste-serverd/src/lib/csr-signing.js` | CSR signing for hardware-bound agent enrollment |
657
+ | `packages/lamaste-serverd/src/lib/constants.js` | Reserved API prefixes shared across plugins and tickets |
658
+ | `packages/lamaste-serverd/src/lib/services.js` | systemctl wrapper with allowlists |
659
+ | `packages/lamaste-serverd/src/lib/system-stats.js` | CPU, memory, disk stats (cached) |
660
+ | `packages/lamaste-serverd/src/lib/files.js` | Static site file operations with path validation |
661
+ | `packages/lamaste-serverd/src/lib/chisel-users.js` | Per-agent Chisel credentials and authfile port grants (core shim) |
662
+ | `packages/lamaste-serverd/src/lib/chisel-reconcile.js` | Startup reconciliation of Chisel (fail closed) |
663
+ | `packages/lamaste-serverd/src/lib/chisel-runtime.js` | Restart decision for authfile changes |
664
+ | `packages/lamaste-serverd/src/lib/tunnel-deps.js` | Dependency objects for the core tunnel workflows |
665
+ | `packages/lamaste-serverd/src/lib/totp.js` | TOTP code generation and verification |
666
+ | `packages/lamaste-serverd/src/lib/session.js` | Signed session cookie creation and validation |
667
+ | `packages/lamaste-serverd/src/lib/tickets.js` | Ticket system: scopes, instances, assignments, sessions |
668
+ | `packages/lamaste-serverd/src/routes/management/tickets.js` | Ticket management HTTP route handlers |
669
+ | `packages/lamaste-serverd/src/lib/storage.js` | Storage server registry, plugin bindings, AES-256-GCM encryption |
670
+ | `packages/lamaste-serverd/src/routes/management/identity.js` | Identity system HTTP route handlers |
671
+ | `packages/lamaste-serverd/src/routes/management/storage.js` | Storage management HTTP route handlers |
672
+ | `packages/lamaste-serverd/src/routes/management/system.js` | System stats + panel update HTTP route handlers |
673
+ | `packages/lamaste-serverd/src/lib/app-error.js` | Operational error class |
674
+
675
+ ## Design Decisions
676
+
677
+ ### Why Fastify instead of Express?
678
+
679
+ Fastify provides schema-first validation, built-in WebSocket support via plugins, structured logging (pino), and measurably lower overhead per request. On a 512 MB droplet, the ~30 MB baseline matters. Express would work but offers no advantages for this use case.
680
+
681
+ ### Why routes and lib are separate?
682
+
683
+ Routes handle HTTP concerns: parsing request bodies, setting status codes, formatting responses. Library modules handle business logic: reading files, calling system commands, managing state. This separation means the same business logic can be called from different contexts (routes, provisioning, future CLI tools) without HTTP coupling.
684
+
685
+ ### Why not use a database?
686
+
687
+ At this scale (single admin, ~10 tunnels, ~5 users), JSON files provide faster access, zero operational overhead, and simpler debugging (you can `cat` the state file). The atomic write pattern ensures crash safety. If scale requirements change, the `state.js` module can be swapped for a database adapter without touching routes or other lib modules.
688
+
689
+ ### Why check mTLS in both nginx and the server?
690
+
691
+ Defense-in-depth. nginx's `ssl_verify_client optional` with per-location `if ($ssl_client_verify != SUCCESS) { return 496; }` is the primary enforcement — it rejects connections at the TLS level before any HTTP processing on protected locations. The server middleware is a secondary check in case nginx is misconfigured or bypassed. In development mode, the server middleware is the only check (and it is bypassed).
692
+
693
+ ### Why does provisioning run in the background?
694
+
695
+ Provisioning takes 2-5 minutes (downloading binaries, issuing certificates, configuring services). A synchronous HTTP request would time out. Instead, the POST endpoint starts the work and returns immediately, while the WebSocket stream provides real-time feedback. This pattern also allows late-joining clients to receive the current state on connect.