@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,52 @@
1
+ import fp from 'fastify-plugin';
2
+ import { getConfig } from '../lib/config.js';
3
+ import {
4
+ REFRESH_THRESHOLD,
5
+ validateUserSession,
6
+ refreshUserSession,
7
+ } from '../lib/user-access-session.js';
8
+
9
+ /**
10
+ * Fastify plugin that validates user-access session tokens from the
11
+ * Authorization: Bearer header. Sets request.userAccessUsername on success.
12
+ *
13
+ * Refreshed tokens are returned via X-User-Session response header so the
14
+ * desktop client can update its stored token.
15
+ */
16
+ async function userAccessSessionPlugin(fastify, _opts) {
17
+ fastify.addHook('onRequest', async (request, reply) => {
18
+ const config = getConfig();
19
+ const sessionSecret = config.sessionSecret;
20
+
21
+ if (!sessionSecret) {
22
+ return reply.code(401).send({ error: 'user_session_required' });
23
+ }
24
+
25
+ // Extract Bearer token from Authorization header
26
+ const authHeader = request.headers.authorization;
27
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
28
+ return reply.code(401).send({ error: 'user_session_required' });
29
+ }
30
+
31
+ const tokenValue = authHeader.slice(7);
32
+ const result = validateUserSession(tokenValue, sessionSecret, {
33
+ userEpochs: config.userEpochs ?? {},
34
+ });
35
+
36
+ if (!result.valid) {
37
+ return reply.code(401).send({ error: 'user_session_required' });
38
+ }
39
+
40
+ // Set username on request for downstream route handlers
41
+ request.userAccessUsername = result.payload.username;
42
+
43
+ // Throttle refresh: only re-sign if lastActivity is stale (>60s)
44
+ const now = Math.floor(Date.now() / 1000);
45
+ if (now - result.payload.lastActivity >= REFRESH_THRESHOLD) {
46
+ const refreshed = refreshUserSession(result.payload, sessionSecret);
47
+ reply.header('X-User-Session', refreshed.value);
48
+ }
49
+ });
50
+ }
51
+
52
+ export default fp(userAccessSessionPlugin, { name: 'user-access-session' });
@@ -0,0 +1,120 @@
1
+ import { z } from 'zod';
2
+ import { validateAndConsumeToken, lookupToken } from '../lib/enrollment.js';
3
+ import { signCSR } from '../lib/csr-signing.js';
4
+ import { fingerprintCertPem } from '../lib/mtls.js';
5
+
6
+ const EnrollBodySchema = z.object({
7
+ token: z.string().min(1, 'Enrollment token is required'),
8
+ csr: z
9
+ .string()
10
+ .min(1, 'CSR is required')
11
+ .refine((v) => v.includes('BEGIN CERTIFICATE REQUEST'), {
12
+ message: 'CSR must be PEM-encoded',
13
+ }),
14
+ });
15
+
16
+ const LookupBodySchema = z.object({
17
+ token: z.string().min(1, 'Enrollment token is required'),
18
+ });
19
+
20
+ /**
21
+ * Public enrollment routes (no mTLS required).
22
+ *
23
+ * The agent doesn't have a cert yet — the one-time token (single-use,
24
+ * 10-minute expiry) is the sole authentication gate.
25
+ */
26
+ export default async function enrollmentRoutes(fastify, _opts) {
27
+ // ------------------------------------------------------------------
28
+ // POST /lookup — return the expected label for a token without
29
+ // consuming it. Required because the panel's CSR signing wrapper no
30
+ // longer overrides the CSR subject — the agent must build a CSR with
31
+ // the correct CN before submitting it for signing.
32
+ // ------------------------------------------------------------------
33
+ fastify.post('/lookup', async (request, reply) => {
34
+ const body = LookupBodySchema.parse(request.body);
35
+
36
+ try {
37
+ const result = await lookupToken(body.token);
38
+ return { ok: true, label: result.label, type: result.type };
39
+ } catch (err) {
40
+ const statusCode = err.statusCode || 401;
41
+ return reply.code(statusCode).send({
42
+ error: err.message || 'Invalid enrollment token',
43
+ });
44
+ }
45
+ });
46
+
47
+ // ------------------------------------------------------------------
48
+ // POST / — enroll an agent using a one-time token + CSR
49
+ // ------------------------------------------------------------------
50
+ fastify.post('/', async (request, reply) => {
51
+ const body = EnrollBodySchema.parse(request.body);
52
+
53
+ let tokenData;
54
+ try {
55
+ tokenData = await validateAndConsumeToken(body.token);
56
+ } catch (err) {
57
+ const statusCode = err.statusCode || 401;
58
+ return reply.code(statusCode).send({
59
+ error: err.message || 'Invalid enrollment token',
60
+ });
61
+ }
62
+
63
+ try {
64
+ // Build opts for delegated enrollments
65
+ const signOpts =
66
+ tokenData.type === 'delegated'
67
+ ? { type: /** @type {const} */ ('delegated'), delegatedBy: tokenData.delegatedBy }
68
+ : undefined;
69
+
70
+ const result = await signCSR(
71
+ body.csr,
72
+ tokenData.label,
73
+ tokenData.capabilities,
74
+ tokenData.allowedSites,
75
+ request.log,
76
+ signOpts,
77
+ );
78
+
79
+ // Audit: enrollment is the moment a fresh agent cert enters circulation.
80
+ // The token holder is the de facto principal — there is no client cert at
81
+ // this point. Token type ('p12'/'delegated') and delegating label
82
+ // (when present) capture the chain of trust.
83
+ request.log.info(
84
+ {
85
+ admin:
86
+ tokenData.type === 'delegated'
87
+ ? `agent:${tokenData.delegatedBy ?? 'unknown'}`
88
+ : 'enrollment-token',
89
+ issuedFor: tokenData.type === 'delegated' ? result.label : `agent:${result.label}`,
90
+ mode: tokenData.type === 'delegated' ? 'delegated' : 'hardware-bound',
91
+ newSerial: result.serial,
92
+ fingerprintSha256: result.certPem ? fingerprintCertPem(result.certPem) : null,
93
+ },
94
+ 'mTLS certificate issued (agent enrollment)',
95
+ );
96
+
97
+ return {
98
+ ok: true,
99
+ cert: result.certPem,
100
+ caCert: result.caCertPem,
101
+ label: result.label,
102
+ serial: result.serial,
103
+ expiresAt: result.expiresAt,
104
+ };
105
+ } catch (err) {
106
+ request.log.error(
107
+ {
108
+ err,
109
+ issuedFor: tokenData.type === 'delegated' ? tokenData.label : `agent:${tokenData.label}`,
110
+ mode: tokenData.type === 'delegated' ? 'delegated' : 'hardware-bound',
111
+ },
112
+ 'Enrollment CSR signing failed',
113
+ );
114
+ const statusCode = err.statusCode || 500;
115
+ // Only pass through client error messages (4xx); hide internal details for 5xx
116
+ const message = statusCode < 500 ? err.message : 'Enrollment failed';
117
+ return reply.code(statusCode).send({ error: message });
118
+ }
119
+ });
120
+ }
@@ -0,0 +1,22 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ // Read version once at module load time — not on every request
9
+ let version = '0.0.0';
10
+ try {
11
+ const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
12
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
13
+ version = pkg.version || '0.0.0';
14
+ } catch {
15
+ // If package.json can't be read, use fallback version
16
+ }
17
+
18
+ export default async function healthRoutes(fastify, _opts) {
19
+ fastify.get('/health', async (_request, _reply) => {
20
+ return { status: 'ok', version };
21
+ });
22
+ }
@@ -0,0 +1,163 @@
1
+ import crypto from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { readInvitations, writeInvitations } from '../lib/state.js';
4
+ import {
5
+ createUserFromInvitation,
6
+ hashPassword,
7
+ readUsersRaw,
8
+ generateTotpSecret,
9
+ writeTotpToDatabase,
10
+ } from '../lib/authelia.js';
11
+ import { getConfig } from '../lib/config.js';
12
+
13
+ /**
14
+ * Find an invitation by token using a constant-time comparison.
15
+ * Both the candidate and stored token are validated as 64-hex strings, so
16
+ * we can compare the raw byte buffers via timingSafeEqual without HMAC equalization.
17
+ * Iterating the full list (no early exit on match) keeps the lookup time
18
+ * proportional to inventory size, not which entry matched.
19
+ */
20
+ function findInvitationByToken(invitations, token) {
21
+ const candidate = Buffer.from(token, 'hex');
22
+ if (candidate.length !== 32) return undefined;
23
+ let match;
24
+ for (const inv of invitations) {
25
+ if (typeof inv.token !== 'string' || !/^[a-f0-9]{64}$/.test(inv.token)) continue;
26
+ const stored = Buffer.from(inv.token, 'hex');
27
+ if (stored.length === candidate.length && crypto.timingSafeEqual(stored, candidate)) {
28
+ match = inv;
29
+ }
30
+ }
31
+ return match;
32
+ }
33
+
34
+ const AcceptInvitationSchema = z.object({
35
+ password: z.string().min(8).max(128),
36
+ });
37
+
38
+ const TokenParamSchema = z.object({ token: z.string().regex(/^[a-f0-9]{64}$/) });
39
+
40
+ export default async function inviteRoutes(fastify, _opts) {
41
+ // GET /api/invite/:token — get invitation details (public)
42
+ fastify.get('/:token', async (request, reply) => {
43
+ const { token } = TokenParamSchema.parse(request.params);
44
+
45
+ let invitations;
46
+ try {
47
+ invitations = await readInvitations();
48
+ } catch (err) {
49
+ request.log.error(err, 'Failed to read invitations');
50
+ return reply.code(500).send({ error: 'Internal server error' });
51
+ }
52
+
53
+ const invitation = findInvitationByToken(invitations, token);
54
+ if (!invitation) {
55
+ return reply.code(404).send({ error: 'Invitation not found' });
56
+ }
57
+
58
+ if (invitation.used) {
59
+ return reply.code(410).send({ error: 'This invitation has already been used' });
60
+ }
61
+
62
+ if (new Date(invitation.expiresAt) < new Date()) {
63
+ return reply.code(410).send({ error: 'This invitation has expired' });
64
+ }
65
+
66
+ return {
67
+ username: invitation.username,
68
+ email: invitation.email,
69
+ expiresAt: invitation.expiresAt,
70
+ };
71
+ });
72
+
73
+ // POST /api/invite/:token/accept — accept invitation and set password (public)
74
+ fastify.post('/:token/accept', async (request, reply) => {
75
+ const { token } = TokenParamSchema.parse(request.params);
76
+ const body = AcceptInvitationSchema.parse(request.body);
77
+
78
+ let invitations;
79
+ try {
80
+ invitations = await readInvitations();
81
+ } catch (err) {
82
+ request.log.error(err, 'Failed to read invitations');
83
+ return reply.code(500).send({ error: 'Internal server error' });
84
+ }
85
+
86
+ const invitation = findInvitationByToken(invitations, token);
87
+ if (!invitation) {
88
+ return reply.code(404).send({ error: 'Invitation not found' });
89
+ }
90
+
91
+ if (invitation.used) {
92
+ return reply.code(410).send({ error: 'This invitation has already been used' });
93
+ }
94
+
95
+ if (new Date(invitation.expiresAt) < new Date()) {
96
+ return reply.code(410).send({ error: 'This invitation has expired' });
97
+ }
98
+
99
+ // Race condition guard: check user doesn't already exist
100
+ let usersData;
101
+ try {
102
+ usersData = await readUsersRaw();
103
+ } catch {
104
+ usersData = { users: {} };
105
+ }
106
+
107
+ if (usersData.users[invitation.username]) {
108
+ return reply.code(409).send({ error: 'This username already exists' });
109
+ }
110
+
111
+ // Hash password and create user
112
+ let hashedPassword;
113
+ try {
114
+ hashedPassword = await hashPassword(body.password);
115
+ } catch (err) {
116
+ request.log.error(err, 'Failed to hash password');
117
+ return reply.code(500).send({ error: 'Failed to process password' });
118
+ }
119
+
120
+ try {
121
+ await createUserFromInvitation(
122
+ invitation.username,
123
+ invitation.email,
124
+ invitation.groups,
125
+ hashedPassword,
126
+ );
127
+ } catch (err) {
128
+ request.log.error(err, 'Failed to create user from invitation');
129
+ return reply.code(500).send({ error: 'Failed to create user account' });
130
+ }
131
+
132
+ // Generate TOTP so the user can scan the QR code immediately
133
+ let totpUri = null;
134
+ try {
135
+ const { secret, uri } = generateTotpSecret(invitation.username);
136
+ await writeTotpToDatabase(invitation.username, secret);
137
+ totpUri = uri;
138
+ } catch (err) {
139
+ request.log.error(err, 'Failed to generate TOTP for invited user');
140
+ // Non-fatal: user was created, they can have admin reset TOTP later
141
+ }
142
+
143
+ // Mark invitation as used
144
+ invitation.used = true;
145
+ invitation.acceptedAt = new Date().toISOString();
146
+
147
+ try {
148
+ await writeInvitations(invitations);
149
+ } catch (err) {
150
+ request.log.warn(err, 'Failed to update invitation status (user was created)');
151
+ }
152
+
153
+ const config = getConfig();
154
+ const loginUrl = config.domain ? `https://auth.${config.domain}/` : null;
155
+
156
+ return {
157
+ ok: true,
158
+ username: invitation.username,
159
+ loginUrl,
160
+ totpUri,
161
+ };
162
+ });
163
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Per-agent self-service routes.
3
+ *
4
+ * `/api/agents/me/*` returns information scoped to the agent identified by
5
+ * its mTLS client certificate (CN=agent:<label>). Admins must specify the
6
+ * label explicitly via separate routes — they have no implicit "me" agent.
7
+ */
8
+
9
+ import { getChiselCredential, getChiselCredentialIssuedAt } from '../../lib/chisel-users.js';
10
+ import {
11
+ rotateAgentChiselCredential,
12
+ getAgentCapabilities,
13
+ getAgentAllowedSites,
14
+ } from '../../lib/mtls.js';
15
+ import { z } from 'zod';
16
+ import { AGENT_LABEL_REGEX } from '@lamalibre/lamaste';
17
+
18
+ /**
19
+ * An agent may rotate its own chisel credential at most this often. Every
20
+ * rotation restarts chisel (the old password must stop working), which drops
21
+ * every agent's tunnels for a few seconds.
22
+ */
23
+ const SELF_ROTATE_MIN_INTERVAL_MS = 10 * 60 * 1000;
24
+
25
+ const LabelParamSchema = z.object({
26
+ label: z
27
+ .string()
28
+ .min(1)
29
+ .max(50)
30
+ .regex(/^[a-z0-9-]+$/, 'Invalid agent label'),
31
+ });
32
+
33
+ export default async function agentsRoutes(fastify, _opts) {
34
+ // ------------------------------------------------------------------
35
+ // GET /agents/me/chisel-credential
36
+ //
37
+ // Returns the calling agent's chisel tunnel-server credential.
38
+ // Identifies the agent strictly via the mTLS cert label — never accepts
39
+ // a label query parameter, so a compromised agent cannot leak others'
40
+ // credentials.
41
+ // ------------------------------------------------------------------
42
+ fastify.get(
43
+ '/agents/me/chisel-credential',
44
+ {
45
+ preHandler: fastify.requireRole(['agent'], { capability: 'tunnels:read' }),
46
+ },
47
+ async (request, reply) => {
48
+ const label = request.certLabel;
49
+ if (!label) {
50
+ return reply.code(400).send({
51
+ error: 'Agent label could not be determined from client certificate',
52
+ });
53
+ }
54
+ const credential = await getChiselCredential(label);
55
+ if (!credential) {
56
+ return reply.code(404).send({
57
+ error: 'No chisel credential found for this agent',
58
+ hint:
59
+ 'Ask an admin to run `lamaste-server chisel rotate-credential --label ' +
60
+ label +
61
+ '` to provision a credential.',
62
+ });
63
+ }
64
+ return credential;
65
+ },
66
+ );
67
+
68
+ // ------------------------------------------------------------------
69
+ // POST /agents/me/chisel-credential/rotate
70
+ //
71
+ // Replaces the calling agent's chisel credential and returns the new one.
72
+ // An upgraded agent calls this once: versions before the credential moved
73
+ // into a 0600 environment file exposed it in process arguments and a
74
+ // world-readable unit file. Identity comes strictly from the mTLS cert.
75
+ // ------------------------------------------------------------------
76
+ fastify.post(
77
+ '/agents/me/chisel-credential/rotate',
78
+ {
79
+ preHandler: fastify.requireRole(['agent'], { capability: 'tunnels:read' }),
80
+ },
81
+ async (request, reply) => {
82
+ const label = request.certLabel;
83
+ if (!label) {
84
+ return reply.code(400).send({
85
+ error: 'Agent label could not be determined from client certificate',
86
+ });
87
+ }
88
+ if (!AGENT_LABEL_REGEX.test(label)) {
89
+ return reply
90
+ .code(400)
91
+ .send({ error: 'Plugin-agents do not use chisel tunnel credentials' });
92
+ }
93
+ const issuedAt = await getChiselCredentialIssuedAt(label);
94
+ if (issuedAt && Date.now() - Date.parse(issuedAt) < SELF_ROTATE_MIN_INTERVAL_MS) {
95
+ return reply.code(429).send({
96
+ error: 'The chisel credential was issued less than 10 minutes ago',
97
+ issuedAt,
98
+ });
99
+ }
100
+ try {
101
+ const result = await rotateAgentChiselCredential(label, request.log);
102
+ return {
103
+ user: result.user,
104
+ password: result.password,
105
+ createdAt: result.createdAt,
106
+ };
107
+ } catch (err) {
108
+ const status = err.statusCode || 500;
109
+ return reply.code(status).send({
110
+ error: err.message || 'Failed to rotate chisel credential',
111
+ });
112
+ }
113
+ },
114
+ );
115
+
116
+ // ------------------------------------------------------------------
117
+ // GET /agents/me/capabilities
118
+ //
119
+ // Returns the calling agent's capabilities and allowedSites. Used by the
120
+ // agent daemon (lamaste-agentd) to enforce per-route capability checks
121
+ // when a non-admin mTLS cert calls its REST API.
122
+ //
123
+ // Cert identity is taken strictly from `request.certLabel` (set by the
124
+ // mtls middleware from the cert CN). No body/query inputs — a compromised
125
+ // agent cannot ask for another agent's capabilities.
126
+ //
127
+ // Admin callers need an explicit label since they have no implicit "me"
128
+ // agent — they get an empty payload (admins are unconstrained).
129
+ // ------------------------------------------------------------------
130
+ fastify.get(
131
+ '/agents/me/capabilities',
132
+ {
133
+ preHandler: fastify.requireRole(['admin', 'agent']),
134
+ },
135
+ async (request, reply) => {
136
+ // Admin has no "me" agent — return empty (admin is master, no caps needed)
137
+ if (request.certRole === 'admin') {
138
+ return { capabilities: [], allowedSites: [], role: 'admin' };
139
+ }
140
+ const label = request.certLabel;
141
+ if (!label) {
142
+ return reply.code(400).send({
143
+ error: 'Agent label could not be determined from client certificate',
144
+ });
145
+ }
146
+ // Re-fetch from registry rather than reusing request.certCapabilities.
147
+ // The mtls middleware filters caps against currently-valid ones, but
148
+ // an explicit registry read here is the canonical source — keeps this
149
+ // endpoint robust to middleware changes.
150
+ const capabilities = await getAgentCapabilities(label);
151
+ const allowedSites = await getAgentAllowedSites(label);
152
+ return { capabilities, allowedSites, role: request.certRole };
153
+ },
154
+ );
155
+
156
+ // ------------------------------------------------------------------
157
+ // POST /agents/:label/chisel-credential/rotate (admin only)
158
+ //
159
+ // Mints a new chisel password for the named agent and writes the
160
+ // updated authfile. Triggers a chisel restart. Returns the new
161
+ // credential so the admin can hand it off out-of-band — but the
162
+ // expected flow is for the agent to re-fetch via the /me endpoint.
163
+ // ------------------------------------------------------------------
164
+ fastify.post(
165
+ '/agents/:label/chisel-credential/rotate',
166
+ {
167
+ preHandler: fastify.requireRole(['admin']),
168
+ },
169
+ async (request, reply) => {
170
+ const params = LabelParamSchema.parse(request.params);
171
+ try {
172
+ const result = await rotateAgentChiselCredential(params.label, request.log);
173
+ return result;
174
+ } catch (err) {
175
+ const status = err.statusCode || 500;
176
+ return reply.code(status).send({
177
+ error: err.message || 'Failed to rotate chisel credential',
178
+ });
179
+ }
180
+ },
181
+ );
182
+ }