@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,76 @@
1
+ import crypto from 'node:crypto';
2
+ import { base32Decode, generateTotpSecret } from './authelia.js';
3
+
4
+ /**
5
+ * Generate a 6-digit TOTP code for a given secret and time step (RFC 6238 / RFC 4226).
6
+ *
7
+ * @param {string} secretBase32 - Base32-encoded secret
8
+ * @param {number} timeStep - The time step counter value
9
+ * @returns {string} 6-digit zero-padded code
10
+ */
11
+ export function generateCode(secretBase32, timeStep) {
12
+ const key = base32Decode(secretBase32);
13
+ const timeBuffer = Buffer.alloc(8);
14
+ // Write 64-bit big-endian counter
15
+ timeBuffer.writeUInt32BE(Math.floor(timeStep / 0x100000000), 0);
16
+ timeBuffer.writeUInt32BE(timeStep >>> 0, 4);
17
+
18
+ const hmac = crypto.createHmac('sha1', key).update(timeBuffer).digest();
19
+ const offset = hmac[hmac.length - 1] & 0x0f;
20
+ const code =
21
+ ((hmac[offset] & 0x7f) << 24) |
22
+ ((hmac[offset + 1] & 0xff) << 16) |
23
+ ((hmac[offset + 2] & 0xff) << 8) |
24
+ (hmac[offset + 3] & 0xff);
25
+
26
+ return String(code % 1000000).padStart(6, '0');
27
+ }
28
+
29
+ // Replay protection: track recently used codes per RFC 6238 Section 5.2.
30
+ // Key = "code:timeStep", purged after 90s (covers the full +/- 1 step window).
31
+ const usedCodes = new Map();
32
+ const USED_CODE_TTL_MS = 90_000;
33
+
34
+ function pruneUsedCodes() {
35
+ const now = Date.now();
36
+ for (const [key, ts] of usedCodes) {
37
+ if (now - ts > USED_CODE_TTL_MS) usedCodes.delete(key);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Verify a TOTP code against the secret, allowing +/- 1 time step (30s each = 60s window).
43
+ * Each code can only be used once (replay protection per RFC 6238 Section 5.2).
44
+ *
45
+ * @param {string} secretBase32 - Base32-encoded secret
46
+ * @param {string} code - The 6-digit code to verify
47
+ * @returns {boolean}
48
+ */
49
+ export function verifyTotp(secretBase32, code) {
50
+ const now = Math.floor(Date.now() / 1000);
51
+ const period = 30;
52
+ const currentStep = Math.floor(now / period);
53
+
54
+ for (let i = -1; i <= 1; i++) {
55
+ const step = currentStep + i;
56
+ if (generateCode(secretBase32, step) === code) {
57
+ const replayKey = `${code}:${step}`;
58
+ if (usedCodes.has(replayKey)) {
59
+ return false;
60
+ }
61
+ usedCodes.set(replayKey, Date.now());
62
+ pruneUsedCodes();
63
+ return true;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * Generate a TOTP secret and otpauth URI for the admin panel.
71
+ *
72
+ * @returns {{ secret: string, uri: string }}
73
+ */
74
+ export function generateAdminTotpSecret() {
75
+ return generateTotpSecret('admin', { issuer: 'Lamaste Panel' });
76
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The dependency objects the core tunnel workflows take
3
+ * (`@lamalibre/lamaste/server` createTunnel, updateTunnel, ...), wired to the
4
+ * daemon's nginx, certbot, chisel and state modules. Shared by the tunnel
5
+ * routes and by agent revocation.
6
+ */
7
+
8
+ import {
9
+ writePublicVhost,
10
+ writeAuthenticatedVhost,
11
+ writeRestrictedVhost,
12
+ removeAppVhost,
13
+ enableAppVhost,
14
+ disableAppVhost,
15
+ writeAgentPanelVhost,
16
+ removeAgentPanelVhost,
17
+ enableAgentPanelVhost,
18
+ disableAgentPanelVhost,
19
+ } from './nginx.js';
20
+ import { issueTunnelCert } from './certbot.js';
21
+ import { syncChisel } from './chisel.js';
22
+ import { readTunnels, writeTunnels } from './state.js';
23
+
24
+ export const tunnelNginxDeps = Object.freeze({
25
+ writePublicVhost,
26
+ writeAuthenticatedVhost,
27
+ writeRestrictedVhost,
28
+ writeAgentPanelVhost,
29
+ removeAppVhost,
30
+ removeAgentPanelVhost,
31
+ enableAppVhost,
32
+ disableAppVhost,
33
+ enableAgentPanelVhost,
34
+ disableAgentPanelVhost,
35
+ });
36
+
37
+ export const tunnelCertbotDeps = Object.freeze({ issueTunnelCert });
38
+
39
+ export const tunnelChiselDeps = Object.freeze({ syncChisel });
40
+
41
+ export const tunnelStateDeps = Object.freeze({ readTunnels, writeTunnels });
@@ -0,0 +1,142 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ const SESSION_MAX_AGE = 12 * 60 * 60; // 12 hours in seconds
4
+ const INACTIVITY_TIMEOUT = 2 * 60 * 60; // 2 hours in seconds
5
+ const REFRESH_THRESHOLD = 60; // Only re-sign if lastActivity is older than 60s
6
+
7
+ export { SESSION_MAX_AGE, INACTIVITY_TIMEOUT, REFRESH_THRESHOLD };
8
+
9
+ /**
10
+ * HMAC-sign a base64url payload string.
11
+ *
12
+ * @param {string} payloadB64 - Base64url-encoded payload
13
+ * @param {string} secretHex - Hex-encoded secret
14
+ * @returns {string} Base64url-encoded HMAC signature
15
+ */
16
+ function signPayload(payloadB64, secretHex) {
17
+ return crypto
18
+ .createHmac('sha256', Buffer.from(secretHex, 'hex'))
19
+ .update(payloadB64)
20
+ .digest('base64url');
21
+ }
22
+
23
+ /**
24
+ * Create a signed user-access session token.
25
+ *
26
+ * @param {string} sessionSecret - Hex-encoded session secret
27
+ * @param {string} username - Authelia username to bind the session to
28
+ * @returns {{ value: string, maxAge: number, expiresAt: string }}
29
+ */
30
+ export function createUserSession(sessionSecret, username) {
31
+ const now = Math.floor(Date.now() / 1000);
32
+ const payload = {
33
+ iat: now,
34
+ exp: now + SESSION_MAX_AGE,
35
+ lastActivity: now,
36
+ username,
37
+ type: 'user-access',
38
+ };
39
+ const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
40
+ const sig = signPayload(payloadB64, sessionSecret);
41
+
42
+ return {
43
+ value: `${payloadB64}.${sig}`,
44
+ maxAge: SESSION_MAX_AGE,
45
+ expiresAt: new Date((now + SESSION_MAX_AGE) * 1000).toISOString(),
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Validate a user-access session token.
51
+ *
52
+ * @param {string} tokenValue - The token (payload.sig)
53
+ * @param {string} sessionSecret - Hex-encoded session secret
54
+ * @param {{ userEpochs?: Record<string, number> }} [options]
55
+ * userEpochs maps username → Unix-second epoch. If the session's `iat`
56
+ * is older than the user's epoch, the session is rejected as revoked.
57
+ * This is how Authelia user deletions invalidate live sessions whose
58
+ * HMAC signature is otherwise still valid.
59
+ * @returns {{ valid: boolean, payload?: object, reason?: string }}
60
+ */
61
+ export function validateUserSession(tokenValue, sessionSecret, options = {}) {
62
+ if (!tokenValue || typeof tokenValue !== 'string') {
63
+ return { valid: false, reason: 'missing' };
64
+ }
65
+
66
+ const dotIndex = tokenValue.indexOf('.');
67
+ if (dotIndex === -1) {
68
+ return { valid: false, reason: 'malformed' };
69
+ }
70
+
71
+ const payloadB64 = tokenValue.slice(0, dotIndex);
72
+ const sig = tokenValue.slice(dotIndex + 1);
73
+
74
+ const expectedSig = signPayload(payloadB64, sessionSecret);
75
+
76
+ // Timing-safe comparison
77
+ const sigBuf = Buffer.from(sig);
78
+ const expectedBuf = Buffer.from(expectedSig);
79
+ if (sigBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(sigBuf, expectedBuf)) {
80
+ return { valid: false, reason: 'invalid_signature' };
81
+ }
82
+
83
+ let payload;
84
+ try {
85
+ payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
86
+ } catch {
87
+ return { valid: false, reason: 'parse_error' };
88
+ }
89
+
90
+ // Must be a user-access session (not a 2FA session)
91
+ if (payload.type !== 'user-access') {
92
+ return { valid: false, reason: 'wrong_type' };
93
+ }
94
+
95
+ if (!payload.username) {
96
+ return { valid: false, reason: 'missing_username' };
97
+ }
98
+
99
+ const now = Math.floor(Date.now() / 1000);
100
+
101
+ if (payload.exp && now > payload.exp) {
102
+ return { valid: false, reason: 'expired' };
103
+ }
104
+
105
+ if (payload.lastActivity && now - payload.lastActivity > INACTIVITY_TIMEOUT) {
106
+ return { valid: false, reason: 'inactive' };
107
+ }
108
+
109
+ // Per-user invalidation epoch (set when a user is deleted from Authelia).
110
+ // Reject sessions issued before that epoch — they belong to a user who no
111
+ // longer exists, even if a same-named user is later re-created.
112
+ const userEpochs = options.userEpochs;
113
+ if (userEpochs && typeof userEpochs === 'object') {
114
+ const epoch = userEpochs[payload.username];
115
+ if (typeof epoch === 'number' && typeof payload.iat === 'number' && payload.iat < epoch) {
116
+ return { valid: false, reason: 'revoked' };
117
+ }
118
+ }
119
+
120
+ return { valid: true, payload };
121
+ }
122
+
123
+ /**
124
+ * Refresh a user-access session by updating lastActivity and re-signing.
125
+ *
126
+ * @param {object} payload - The existing session payload
127
+ * @param {string} sessionSecret - Hex-encoded session secret
128
+ * @returns {{ value: string, maxAge: number }}
129
+ */
130
+ export function refreshUserSession(payload, sessionSecret) {
131
+ const now = Math.floor(Date.now() / 1000);
132
+ const updated = { ...payload, lastActivity: now };
133
+ const payloadB64 = Buffer.from(JSON.stringify(updated)).toString('base64url');
134
+ const sig = signPayload(payloadB64, sessionSecret);
135
+
136
+ const remaining = Math.max(0, updated.exp - now);
137
+
138
+ return {
139
+ value: `${payloadB64}.${sig}`,
140
+ maxAge: remaining,
141
+ };
142
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shim — user-access grant + OTP management now lives in
3
+ * `@lamalibre/lamaste/server`. This file resolves the dataDir from env
4
+ * and forwards to the core library, preserving the daemon-local API.
5
+ */
6
+
7
+ import {
8
+ createGrant as createGrantCore,
9
+ listGrants as listGrantsCore,
10
+ listGrantsForUser as listGrantsForUserCore,
11
+ revokeGrant as revokeGrantCore,
12
+ removeGrantsForUser as removeGrantsForUserCore,
13
+ consumeGrant as consumeGrantCore,
14
+ createOTP as createOTPCore,
15
+ validateAndConsumeOTP as validateAndConsumeOTPCore,
16
+ } from '@lamalibre/lamaste/server';
17
+
18
+ function dataDir() {
19
+ return process.env.LAMALIBRE_LAMASTE_DATA_DIR || '/etc/lamalibre/lamaste';
20
+ }
21
+
22
+ export function createGrant(username, pluginName, logger, options = {}) {
23
+ return createGrantCore(dataDir(), username, pluginName, logger, options);
24
+ }
25
+
26
+ export function listGrants() {
27
+ return listGrantsCore(dataDir());
28
+ }
29
+
30
+ export function listGrantsForUser(username) {
31
+ return listGrantsForUserCore(dataDir(), username);
32
+ }
33
+
34
+ export function revokeGrant(grantId, logger) {
35
+ return revokeGrantCore(dataDir(), grantId, logger);
36
+ }
37
+
38
+ export function removeGrantsForUser(username, logger) {
39
+ return removeGrantsForUserCore(dataDir(), username, logger);
40
+ }
41
+
42
+ export function consumeGrant(grantId, username, logger) {
43
+ return consumeGrantCore(dataDir(), grantId, username, logger);
44
+ }
45
+
46
+ export function createOTP(username, input, logger) {
47
+ return createOTPCore(dataDir(), username, input, logger);
48
+ }
49
+
50
+ export function validateAndConsumeOTP(token, verifier) {
51
+ return validateAndConsumeOTPCore(dataDir(), token, verifier);
52
+ }
@@ -0,0 +1,63 @@
1
+ import fp from 'fastify-plugin';
2
+
3
+ /**
4
+ * Global error handler plugin for Fastify.
5
+ *
6
+ * Every error response follows this contract:
7
+ *
8
+ * {
9
+ * error: string; // Always present. Human-readable error summary.
10
+ * details?: object; // Optional. Additional structured information.
11
+ * }
12
+ *
13
+ * Wrapped with fastify-plugin to break encapsulation — the handler applies
14
+ * to all routes, not just those registered in the same plugin context.
15
+ */
16
+ async function errorHandlerPlugin(fastify, _opts) {
17
+ fastify.setErrorHandler((error, request, reply) => {
18
+ request.log.error(error);
19
+
20
+ const isDev = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
21
+
22
+ // Zod validation errors
23
+ if (error.name === 'ZodError' || Array.isArray(error.issues)) {
24
+ const issues = error.issues.map((issue) => ({
25
+ path: issue.path,
26
+ message: issue.message,
27
+ }));
28
+
29
+ return reply.code(400).send({
30
+ error: 'Validation failed',
31
+ details: { issues },
32
+ });
33
+ }
34
+
35
+ // Operational errors (AppError)
36
+ if (error.isOperational === true) {
37
+ const body = { error: error.message };
38
+ if (error.details !== null && error.details !== undefined) {
39
+ body.details = error.details;
40
+ }
41
+ return reply.code(error.statusCode).send(body);
42
+ }
43
+
44
+ // Fastify built-in errors (malformed JSON, missing content-type, etc.)
45
+ if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
46
+ return reply.code(error.statusCode).send({
47
+ error: error.message,
48
+ });
49
+ }
50
+
51
+ // Unexpected errors — never leak internals in production
52
+ const body = { error: 'Internal server error' };
53
+ if (isDev) {
54
+ body.details = {
55
+ message: error.message,
56
+ stack: error.stack,
57
+ };
58
+ }
59
+ return reply.code(500).send(body);
60
+ });
61
+ }
62
+
63
+ export default fp(errorHandlerPlugin);
@@ -0,0 +1,120 @@
1
+ import fp from 'fastify-plugin';
2
+ import { isRevoked } from '../lib/revocation.js';
3
+ import { getAgentCapabilities, getAgentAllowedSites, PLUGIN_AGENT_CN_PREFIX } from '../lib/mtls.js';
4
+
5
+ let devWarningLogged = false;
6
+
7
+ async function mtlsPlugin(fastify, _opts) {
8
+ fastify.addHook('onRequest', async (request, reply) => {
9
+ // Health check endpoint is always accessible without mTLS
10
+ // (used by systemd, load balancers, and internal provisioning checks)
11
+ if (request.url === '/api/health') {
12
+ request.certRole = 'admin';
13
+ request.certLabel = null;
14
+ request.certCapabilities = null;
15
+ request.certAllowedSites = null;
16
+ return;
17
+ }
18
+
19
+ const isDev = process.env.NODE_ENV === 'development';
20
+
21
+ if (isDev) {
22
+ if (!devWarningLogged) {
23
+ request.log.warn('mTLS verification bypassed — running in development mode');
24
+ devWarningLogged = true;
25
+ }
26
+ request.certRole = 'admin';
27
+ request.certLabel = null;
28
+ request.certCapabilities = null;
29
+ request.certAllowedSites = null;
30
+ return;
31
+ }
32
+
33
+ const clientVerify = request.headers['x-ssl-client-verify'];
34
+
35
+ if (clientVerify !== 'SUCCESS') {
36
+ return reply.code(403).send({
37
+ error: 'mTLS certificate required',
38
+ details: {
39
+ hint: 'Access to the Lamaste panel requires a valid client certificate.',
40
+ },
41
+ });
42
+ }
43
+
44
+ // Check certificate serial against revocation list
45
+ const serial = request.headers['x-ssl-client-serial'];
46
+ if (serial && (await isRevoked(serial))) {
47
+ return reply.code(403).send({
48
+ error: 'Certificate has been revoked',
49
+ });
50
+ }
51
+
52
+ // Parse the certificate DN to extract role
53
+ const dn = request.headers['x-ssl-client-dn'] || '';
54
+ const cnMatch = dn.match(/CN=([^,]+)/);
55
+ const cn = cnMatch ? cnMatch[1] : '';
56
+
57
+ if (cn.startsWith(PLUGIN_AGENT_CN_PREFIX)) {
58
+ // Plugin-agent: CN = plugin-agent:<delegatingLabel>:<pluginAgentLabel>
59
+ // certLabel includes the full CN so it matches the registry label
60
+ request.certRole = 'plugin-agent';
61
+ request.certLabel = cn;
62
+ // Extract the delegating agent label
63
+ const afterPrefix = cn.slice(PLUGIN_AGENT_CN_PREFIX.length);
64
+ const colonIndex = afterPrefix.indexOf(':');
65
+ request.certDelegatedBy = colonIndex !== -1 ? afterPrefix.slice(0, colonIndex) : afterPrefix;
66
+ try {
67
+ request.certCapabilities = await getAgentCapabilities(cn);
68
+ } catch (err) {
69
+ request.log.warn(
70
+ { err, label: cn },
71
+ 'Failed to load plugin-agent capabilities, using defaults',
72
+ );
73
+ request.certCapabilities = [];
74
+ }
75
+ try {
76
+ request.certAllowedSites = await getAgentAllowedSites(cn);
77
+ } catch (err) {
78
+ request.log.warn(
79
+ { err, label: cn },
80
+ 'Failed to load plugin-agent allowed sites, using defaults',
81
+ );
82
+ request.certAllowedSites = [];
83
+ }
84
+ } else if (cn.startsWith('agent:')) {
85
+ request.certRole = 'agent';
86
+ request.certLabel = cn.slice('agent:'.length);
87
+ try {
88
+ request.certCapabilities = await getAgentCapabilities(request.certLabel);
89
+ } catch (err) {
90
+ request.log.warn(
91
+ { err, label: request.certLabel },
92
+ 'Failed to load agent capabilities, using defaults',
93
+ );
94
+ request.certCapabilities = ['tunnels:read'];
95
+ }
96
+ try {
97
+ request.certAllowedSites = await getAgentAllowedSites(request.certLabel);
98
+ } catch (err) {
99
+ request.log.warn(
100
+ { err, label: request.certLabel },
101
+ 'Failed to load agent allowed sites, using defaults',
102
+ );
103
+ request.certAllowedSites = [];
104
+ }
105
+ } else if (cn === 'admin') {
106
+ request.certRole = 'admin';
107
+ request.certLabel = null;
108
+ request.certCapabilities = null;
109
+ request.certAllowedSites = null;
110
+ } else {
111
+ // Defense-in-depth: nginx is the primary gate, but never grant any role
112
+ // for an unrecognized CN (formerly defaulted to admin — privilege-escalation
113
+ // hazard if nginx config drifts).
114
+ request.log.warn({ cn, dn }, 'Rejecting unrecognized CN');
115
+ return reply.code(403).send({ error: 'Unrecognized client certificate' });
116
+ }
117
+ });
118
+ }
119
+
120
+ export default fp(mtlsPlugin);
@@ -0,0 +1,30 @@
1
+ import { getConfig } from '../lib/config.js';
2
+
3
+ /**
4
+ * Fastify onRequest hook that blocks access when onboarding is already completed.
5
+ * Used to protect onboarding routes.
6
+ */
7
+ export function onboardingOnly() {
8
+ return async function onboardingOnlyHook(_request, reply) {
9
+ const config = getConfig();
10
+ if (config.onboarding.status === 'COMPLETED') {
11
+ return reply.code(410).send({ error: 'Onboarding already completed' });
12
+ }
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Fastify onRequest hook that blocks access when onboarding is not yet completed.
18
+ * Used to protect management routes.
19
+ */
20
+ export function managementOnly() {
21
+ return async function managementOnlyHook(_request, reply) {
22
+ const config = getConfig();
23
+ if (config.onboarding.status !== 'COMPLETED') {
24
+ return reply.code(503).send({
25
+ error: 'Onboarding not complete',
26
+ onboardingStatus: config.onboarding.status,
27
+ });
28
+ }
29
+ };
30
+ }
@@ -0,0 +1,58 @@
1
+ import fp from 'fastify-plugin';
2
+
3
+ /**
4
+ * Role guard plugin — decorates fastify with `requireRole(allowedRoles, opts)`.
5
+ *
6
+ * Returns a preHandler function that checks `request.certRole` against
7
+ * the provided allowedRoles array. For agent roles, optionally checks
8
+ * `request.certCapabilities` for a required capability.
9
+ *
10
+ * Usage:
11
+ * fastify.requireRole(['admin']) — admin only
12
+ * fastify.requireRole(['admin', 'agent']) — admin + any agent
13
+ * fastify.requireRole(['admin', 'agent'], { capability: 'tunnels:write' }) — admin + agents with capability
14
+ */
15
+ function roleGuard(fastify, _opts, done) {
16
+ fastify.decorate('requireRole', function (allowedRoles, opts) {
17
+ const { capability } = opts || {};
18
+
19
+ return async function (request, reply) {
20
+ const role = request.certRole || 'unknown';
21
+
22
+ // Admin always passes — no capability check needed
23
+ if (role === 'admin') return;
24
+
25
+ // plugin-agent is a subset of agent — accept it wherever agent is accepted
26
+ const effectiveRole =
27
+ role === 'plugin-agent' && allowedRoles.includes('agent') ? 'agent' : role;
28
+
29
+ // Check if the role is in the allowed list
30
+ if (!allowedRoles.includes(effectiveRole)) {
31
+ return reply.code(403).send({
32
+ error: 'Insufficient certificate scope',
33
+ details: {
34
+ required: allowedRoles,
35
+ current: role,
36
+ },
37
+ });
38
+ }
39
+
40
+ // If a capability is required, check the agent (or plugin-agent) has it
41
+ if (capability && (effectiveRole === 'agent' || role === 'plugin-agent')) {
42
+ const caps = request.certCapabilities || [];
43
+ if (!caps.includes(capability)) {
44
+ return reply.code(403).send({
45
+ error: 'Insufficient certificate capability',
46
+ details: {
47
+ required: capability,
48
+ granted: caps,
49
+ },
50
+ });
51
+ }
52
+ }
53
+ };
54
+ });
55
+ done();
56
+ }
57
+
58
+ export default fp(roleGuard, { name: 'role-guard' });
@@ -0,0 +1,65 @@
1
+ import fp from 'fastify-plugin';
2
+ import { getConfig } from '../lib/config.js';
3
+ import {
4
+ COOKIE_NAME,
5
+ COOKIE_OPTIONS,
6
+ REFRESH_THRESHOLD,
7
+ validateSession,
8
+ refreshSession,
9
+ } from '../lib/session.js';
10
+
11
+ /**
12
+ * Exempt paths that must remain accessible without a 2FA session.
13
+ * These include the health check, the 2FA status endpoint, and the verify endpoint.
14
+ */
15
+ const EXEMPT_PATHS = new Set(['/api/health', '/api/settings/2fa', '/api/settings/2fa/verify']);
16
+
17
+ async function twofaSessionPlugin(fastify, _opts) {
18
+ fastify.addHook('onRequest', async (request, reply) => {
19
+ const config = getConfig();
20
+
21
+ // 2FA not enabled — skip entirely
22
+ if (!config.panel2fa || !config.panel2fa.enabled) {
23
+ return;
24
+ }
25
+
26
+ // Agents bypass 2FA — both regular agent certs and plugin-agent certs
27
+ // (plugin-agents are automated callers; only humans need the second factor)
28
+ if (request.certRole === 'agent' || request.certRole === 'plugin-agent') {
29
+ return;
30
+ }
31
+
32
+ // Exempt paths
33
+ if (EXEMPT_PATHS.has(request.url) || EXEMPT_PATHS.has(request.url.split('?')[0])) {
34
+ return;
35
+ }
36
+
37
+ // Admin must have a valid 2FA session
38
+ if (request.certRole === 'admin') {
39
+ const sessionSecret = config.sessionSecret;
40
+ if (!sessionSecret) {
41
+ return reply.code(401).send({ error: '2fa_required' });
42
+ }
43
+
44
+ const cookieValue = request.cookies?.[COOKIE_NAME];
45
+ const certSerial = request.headers['x-ssl-client-serial'] || undefined;
46
+ const result = validateSession(cookieValue, sessionSecret, certSerial);
47
+
48
+ if (!result.valid) {
49
+ return reply.code(401).send({ error: '2fa_required' });
50
+ }
51
+
52
+ // Throttle refresh: only re-sign if lastActivity is stale (>60s)
53
+ const now = Math.floor(Date.now() / 1000);
54
+ if (now - result.payload.lastActivity >= REFRESH_THRESHOLD) {
55
+ const refreshed = refreshSession(result.payload, sessionSecret);
56
+ reply.setCookie(COOKIE_NAME, refreshed.value, {
57
+ ...COOKIE_OPTIONS,
58
+ maxAge: refreshed.maxAge,
59
+ });
60
+ }
61
+ }
62
+ });
63
+ }
64
+
65
+ export default fp(twofaSessionPlugin, { name: 'twofa-session' });