@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,86 @@
1
+ import { execa } from 'execa';
2
+ import { AUTHELIA_USER, CHISEL_GROUP } from './service-config.js';
3
+
4
+ /**
5
+ * Service accounts create-lamaste manages. All functions are idempotent and
6
+ * run as root.
7
+ */
8
+
9
+ async function groupExists(name) {
10
+ try {
11
+ await execa('getent', ['group', name]);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ async function userExists(name) {
19
+ try {
20
+ await execa('getent', ['passwd', name]);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Create the group the chisel server runs as and add the lamaste user to it.
29
+ * Run after the lamaste user exists and before the panel service (re)starts —
30
+ * the service unit names the group in SupplementaryGroups=, and the panel
31
+ * writes the chisel authfile and key with it.
32
+ *
33
+ * @returns {Promise<{ created: boolean }>}
34
+ */
35
+ export async function ensureChiselGroup() {
36
+ let created = false;
37
+ if (!(await groupExists(CHISEL_GROUP))) {
38
+ await execa('groupadd', ['--system', CHISEL_GROUP]);
39
+ created = true;
40
+ }
41
+ await execa('usermod', ['--append', '--groups', CHISEL_GROUP, 'lamaste']);
42
+ return { created };
43
+ }
44
+
45
+ /**
46
+ * Create the system account Authelia runs as. Authelia is reachable from
47
+ * the internet (auth.<domain>), so it runs as neither root nor lamaste: it
48
+ * can read its configuration (group lamaste-authelia) and write only its
49
+ * database, notification file and log.
50
+ *
51
+ * @returns {Promise<{ created: boolean }>}
52
+ */
53
+ export async function ensureAutheliaUser() {
54
+ if (await userExists(AUTHELIA_USER)) return { created: false };
55
+ if (!(await groupExists(AUTHELIA_USER))) {
56
+ await execa('groupadd', ['--system', AUTHELIA_USER]);
57
+ }
58
+ await execa('useradd', [
59
+ '--system',
60
+ '--gid',
61
+ AUTHELIA_USER,
62
+ '--no-create-home',
63
+ '--home-dir',
64
+ '/nonexistent',
65
+ '--shell',
66
+ '/usr/sbin/nologin',
67
+ '--comment',
68
+ 'Lamaste Authelia',
69
+ AUTHELIA_USER,
70
+ ]);
71
+ return { created: true };
72
+ }
73
+
74
+ /**
75
+ * Numeric uid/gid of a user (primary group) or of a group.
76
+ *
77
+ * @param {'user'|'group'} kind
78
+ * @param {string} name
79
+ * @returns {Promise<number>}
80
+ */
81
+ export async function lookupId(kind, name) {
82
+ const { stdout } = await execa('getent', [kind === 'user' ? 'passwd' : 'group', name]);
83
+ const id = Number.parseInt(stdout.split(':')[2] ?? '', 10);
84
+ if (!Number.isInteger(id)) throw new Error(`Cannot resolve ${kind} ${name}`);
85
+ return id;
86
+ }
@@ -0,0 +1,137 @@
1
+ import { execa } from 'execa';
2
+ import { mkdtemp, rename, rm } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import {
7
+ AUTHELIA_RELEASE,
8
+ CHISEL_RELEASE,
9
+ curlDownloadArgs,
10
+ downloadVerifiedAuthelia,
11
+ downloadVerifiedChisel,
12
+ isPinnedChiselVersion,
13
+ normaliseAutheliaVersion,
14
+ } from '@lamalibre/lamaste';
15
+ import { AUTHELIA_BIN, CHISEL_BIN } from './service-config.js';
16
+
17
+ /**
18
+ * Install the pinned chisel and Authelia releases under /usr/local/bin.
19
+ *
20
+ * Downloads are verified against the SHA-256 digests pinned in
21
+ * @lamalibre/lamaste before anything is unpacked, staged in a root-private
22
+ * temporary directory, and moved into place with a rename, so a running
23
+ * service never sees a half-written binary. Runs as root; the panel never
24
+ * installs a binary.
25
+ */
26
+
27
+ const CHISEL_ARCH = { x64: 'linux_amd64', arm64: 'linux_arm64' };
28
+ const AUTHELIA_ARCH = { x64: 'linux-amd64', arm64: 'linux-arm64' };
29
+
30
+ function archFor(table, what) {
31
+ const arch = table[process.arch];
32
+ if (!arch) throw new Error(`Unsupported architecture for ${what}: ${process.arch}`);
33
+ return arch;
34
+ }
35
+
36
+ async function fetchToFile(url, outPath) {
37
+ await execa('curl', curlDownloadArgs(url, outPath));
38
+ }
39
+
40
+ /** Put `src` at `dest` as a root-owned 0755 executable, atomically. */
41
+ async function installExecutable(src, dest) {
42
+ const staged = `${dest}.lamaste-new`;
43
+ await execa('install', ['-o', 'root', '-g', 'root', '-m', '0755', src, staged]);
44
+ await rename(staged, dest);
45
+ }
46
+
47
+ async function versionOutput(bin, args) {
48
+ if (!existsSync(bin)) return null;
49
+ try {
50
+ const { stdout, stderr } = await execa(bin, args, { timeout: 15_000 });
51
+ return (stdout || stderr || '').trim() || null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Install the pinned chisel release unless it is already installed.
59
+ *
60
+ * @returns {Promise<{ changed: boolean, version: string, previous: string | null }>}
61
+ */
62
+ export async function installPinnedChisel() {
63
+ const previous = await versionOutput(CHISEL_BIN, ['--version']);
64
+ if (isPinnedChiselVersion(previous)) {
65
+ return { changed: false, version: CHISEL_RELEASE.version, previous };
66
+ }
67
+ const work = await mkdtemp(join(tmpdir(), 'lamaste-chisel-'));
68
+ try {
69
+ const bin = join(work, 'chisel');
70
+ await downloadVerifiedChisel(
71
+ archFor(CHISEL_ARCH, 'chisel'),
72
+ join(work, 'chisel.gz'),
73
+ bin,
74
+ fetchToFile,
75
+ );
76
+ await installExecutable(bin, CHISEL_BIN);
77
+ } finally {
78
+ await rm(work, { recursive: true, force: true });
79
+ }
80
+ const installed = await versionOutput(CHISEL_BIN, ['--version']);
81
+ if (!isPinnedChiselVersion(installed)) {
82
+ throw new Error(
83
+ `chisel was installed but reports ${installed ?? 'nothing'}, expected ${CHISEL_RELEASE.version}`,
84
+ );
85
+ }
86
+ return { changed: true, version: CHISEL_RELEASE.version, previous };
87
+ }
88
+
89
+ function compareVersions(a, b) {
90
+ const pa = a.split('.').map(Number);
91
+ const pb = b.split('.').map(Number);
92
+ for (let i = 0; i < 3; i++) {
93
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
94
+ if (d !== 0) return d;
95
+ }
96
+ return 0;
97
+ }
98
+
99
+ /**
100
+ * Install the pinned Authelia release unless it (or a newer release) is
101
+ * already installed. A newer Authelia is kept: its database may already use
102
+ * a schema the pinned release cannot read, and Authelia refuses to start on
103
+ * a database newer than itself.
104
+ *
105
+ * @returns {Promise<{ changed: boolean, version: string, previous: string | null, keptNewer: boolean }>}
106
+ */
107
+ export async function installPinnedAuthelia() {
108
+ const previousOutput = await versionOutput(AUTHELIA_BIN, ['--version']);
109
+ const previous = previousOutput ? normaliseAutheliaVersion(previousOutput) : null;
110
+ if (previous !== null && compareVersions(previous, AUTHELIA_RELEASE.version) >= 0) {
111
+ return {
112
+ changed: false,
113
+ version: previous,
114
+ previous,
115
+ keptNewer: previous !== AUTHELIA_RELEASE.version,
116
+ };
117
+ }
118
+ const work = await mkdtemp(join(tmpdir(), 'lamaste-authelia-'));
119
+ try {
120
+ const tgz = join(work, 'authelia.tar.gz');
121
+ await downloadVerifiedAuthelia(archFor(AUTHELIA_ARCH, 'Authelia'), tgz, fetchToFile);
122
+ // The tarball holds the binary at its top level, named `authelia`, next
123
+ // to upstream packaging files (units, sysusers, a config template).
124
+ await execa('tar', ['-xzf', tgz, '-C', work, '--no-same-owner', 'authelia']);
125
+ await installExecutable(join(work, 'authelia'), AUTHELIA_BIN);
126
+ } finally {
127
+ await rm(work, { recursive: true, force: true });
128
+ }
129
+ const installedOutput = await versionOutput(AUTHELIA_BIN, ['--version']);
130
+ const installed = installedOutput ? normaliseAutheliaVersion(installedOutput) : null;
131
+ if (installed !== AUTHELIA_RELEASE.version) {
132
+ throw new Error(
133
+ `Authelia was installed but reports ${installed ?? 'nothing'}, expected ${AUTHELIA_RELEASE.version}`,
134
+ );
135
+ }
136
+ return { changed: true, version: AUTHELIA_RELEASE.version, previous, keptNewer: false };
137
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Generates the static HTML page shown when a browser visits without a client certificate.
3
+ * nginx serves this via error_page 495/496 so the user sees helpful instructions
4
+ * instead of a raw "400 Bad Request".
5
+ *
6
+ * @param {{ ip: string, pkiDir: string }} ctx - Installer context with IP and PKI path.
7
+ * @returns {string} Complete HTML document.
8
+ */
9
+ export function generateCertHelpPage(ctx) {
10
+ const ip = ctx.ip;
11
+ const scpCmd = `scp root@${ip}:${ctx.pkiDir}/client.p12 .`;
12
+
13
+ return `<!DOCTYPE html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="utf-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1">
18
+ <title>Lamaste — Certificate Required</title>
19
+ <style>
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+ body {
22
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
23
+ background: #09090b; color: #a1a1aa;
24
+ min-height: 100vh; display: flex; align-items: center; justify-content: center;
25
+ padding: 1.5rem;
26
+ }
27
+ .card {
28
+ background: #18181b; border: 1px solid #27272a; border-radius: 0.75rem;
29
+ max-width: 640px; width: 100%; padding: 2.5rem;
30
+ }
31
+ .icon { text-align: center; font-size: 3rem; margin-bottom: 1rem; }
32
+ h1 { color: #f4f4f5; font-size: 1.25rem; text-align: center; margin-bottom: 0.5rem; }
33
+ .subtitle { text-align: center; margin-bottom: 2rem; font-size: 0.875rem; }
34
+ .step { margin-bottom: 1.75rem; }
35
+ .step-header {
36
+ display: flex; align-items: center; gap: 0.625rem;
37
+ margin-bottom: 0.625rem;
38
+ }
39
+ .step-num {
40
+ background: #22d3ee; color: #09090b; font-weight: 700;
41
+ width: 1.5rem; height: 1.5rem; border-radius: 50%;
42
+ display: flex; align-items: center; justify-content: center;
43
+ font-size: 0.75rem; flex-shrink: 0;
44
+ }
45
+ .step-title { color: #e4e4e7; font-size: 0.9375rem; font-weight: 600; }
46
+ .step-body { margin-left: 2.125rem; font-size: 0.8125rem; line-height: 1.6; }
47
+ code {
48
+ background: #09090b; border: 1px solid #27272a; border-radius: 0.375rem;
49
+ padding: 0.625rem 0.875rem; display: block; margin: 0.5rem 0;
50
+ color: #22d3ee; font-size: 0.8125rem; word-break: break-all;
51
+ cursor: pointer; position: relative;
52
+ }
53
+ code:hover { border-color: #22d3ee; }
54
+ code::after {
55
+ content: 'click to copy'; position: absolute; right: 0.625rem; top: 50%;
56
+ transform: translateY(-50%); color: #52525b; font-size: 0.6875rem;
57
+ }
58
+ code.copied::after { content: 'copied!'; color: #22d3ee; }
59
+ .platform { margin-bottom: 0.375rem; }
60
+ .platform strong { color: #e4e4e7; }
61
+ .note {
62
+ background: #1c1917; border: 1px solid #422006; border-radius: 0.5rem;
63
+ padding: 0.875rem 1rem; margin-top: 1.5rem; font-size: 0.8125rem;
64
+ line-height: 1.5;
65
+ }
66
+ .note strong { color: #fbbf24; }
67
+ .refresh {
68
+ display: block; width: 100%; margin-top: 1.5rem;
69
+ background: #22d3ee; color: #09090b; font-weight: 700;
70
+ border: none; border-radius: 0.5rem; padding: 0.75rem;
71
+ font-size: 0.875rem; cursor: pointer; font-family: inherit;
72
+ }
73
+ .refresh:hover { background: #06b6d4; }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div class="card">
78
+ <div class="icon">\u{1f510}</div>
79
+ <h1>Client Certificate Required</h1>
80
+ <p class="subtitle">
81
+ This Lamaste panel is protected by mTLS.<br>
82
+ Import your client certificate to continue.
83
+ </p>
84
+
85
+ <div class="step">
86
+ <div class="step-header">
87
+ <span class="step-num">1</span>
88
+ <span class="step-title">Download your certificate</span>
89
+ </div>
90
+ <div class="step-body">
91
+ Run this on your local machine:
92
+ <code id="scp-cmd">${escapeHtml(scpCmd)}</code>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="step">
97
+ <div class="step-header">
98
+ <span class="step-num">2</span>
99
+ <span class="step-title">Import into your browser</span>
100
+ </div>
101
+ <div class="step-body">
102
+ <div class="platform">
103
+ <strong>macOS:</strong> Double-click <code style="display:inline;padding:0.125rem 0.375rem;cursor:default" class="no-copy">client.p12</code>
104
+ &rarr; Keychain Access opens &rarr; <strong style="color:#e4e4e7">select &ldquo;System&rdquo; keychain</strong> (not &ldquo;login&rdquo;)
105
+ &rarr; enter the password from the installer output &rarr; authenticate with your Mac password.
106
+ If you see an &ldquo;Unable to import&rdquo; error for Local Items, ignore it &mdash; the System import succeeds.
107
+ Then find the certificate in the <strong style="color:#e4e4e7">System</strong> keychain &rarr;
108
+ double-click it &rarr; expand <strong style="color:#e4e4e7">Trust</strong> &rarr; set to <strong style="color:#e4e4e7">Always Trust</strong> &rarr; close and authenticate.
109
+ </div>
110
+ <div class="platform">
111
+ <strong>Linux (Chrome):</strong> Settings &rarr; Privacy &amp; Security &rarr; Security &rarr; Manage certificates &rarr; Import
112
+ </div>
113
+ <div class="platform">
114
+ <strong>Windows:</strong> Double-click the file &rarr; Certificate Import Wizard &rarr; enter the password
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <div class="step">
120
+ <div class="step-header">
121
+ <span class="step-num">3</span>
122
+ <span class="step-title">Reload this page</span>
123
+ </div>
124
+ <div class="step-body">
125
+ After importing, your browser will present the certificate automatically.
126
+ You may need to restart your browser first.
127
+ </div>
128
+ </div>
129
+
130
+ <div class="note">
131
+ <strong>Where is the password?</strong> The certificate password was printed
132
+ in your terminal when the installer finished. It is also stored on the server
133
+ at <code style="display:inline;padding:0.125rem 0.375rem;color:#fbbf24;cursor:default" class="no-copy">${escapeHtml(ctx.pkiDir)}/.p12-password</code>
134
+ </div>
135
+
136
+ <button class="refresh" onclick="location.reload()">
137
+ Reload Page
138
+ </button>
139
+ </div>
140
+
141
+ <script>
142
+ document.querySelectorAll('code:not(.no-copy)').forEach(function(el) {
143
+ el.addEventListener('click', function() {
144
+ navigator.clipboard.writeText(el.textContent.trim());
145
+ el.classList.add('copied');
146
+ setTimeout(function() { el.classList.remove('copied'); }, 2000);
147
+ });
148
+ });
149
+ </script>
150
+ </body>
151
+ </html>`;
152
+ }
153
+
154
+ function escapeHtml(str) {
155
+ return str
156
+ .replace(/&/g, '&amp;')
157
+ .replace(/</g, '&lt;')
158
+ .replace(/>/g, '&gt;')
159
+ .replace(/"/g, '&quot;');
160
+ }
package/src/lib/env.js ADDED
@@ -0,0 +1,132 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { execa } from 'execa';
3
+
4
+ /**
5
+ * Parse /etc/os-release and validate that we are on Ubuntu 24.04.
6
+ * @returns {{ id: string, versionId: string, prettyName: string }}
7
+ */
8
+ export async function detectOS() {
9
+ let content;
10
+ try {
11
+ content = await readFile('/etc/os-release', 'utf8');
12
+ } catch {
13
+ throw new Error('Could not read /etc/os-release. Lamaste requires Ubuntu 24.04.');
14
+ }
15
+
16
+ const fields = {};
17
+ for (const line of content.split('\n')) {
18
+ const match = line.match(/^(\w+)=(.*)$/);
19
+ if (match) {
20
+ // Strip surrounding quotes if present
21
+ fields[match[1]] = match[2].replace(/^"|"$/g, '');
22
+ }
23
+ }
24
+
25
+ const id = fields.ID || '';
26
+ const versionId = fields.VERSION_ID || '';
27
+ const prettyName = fields.PRETTY_NAME || `${id} ${versionId}`;
28
+
29
+ if (id !== 'ubuntu' || !versionId.startsWith('24.04')) {
30
+ throw new Error(`Lamaste requires Ubuntu 24.04. Detected: ${prettyName}`);
31
+ }
32
+
33
+ return { id, versionId, prettyName };
34
+ }
35
+
36
+ /**
37
+ * Detect the public IP address of this machine.
38
+ * Tries the DigitalOcean metadata API first, falls back to hostname -I.
39
+ * When allowPrivate is true (--dev mode), accepts RFC 1918 addresses.
40
+ * @param {{ allowPrivate?: boolean }} options
41
+ * @returns {string} The IPv4 address.
42
+ */
43
+ export async function detectIP({ allowPrivate = false } = {}) {
44
+ const isAcceptable = (ip) => (allowPrivate ? isValidIPv4(ip) : isPublicIP(ip));
45
+
46
+ // Try DigitalOcean metadata API first
47
+ try {
48
+ const { stdout } = await execa('curl', [
49
+ '-s',
50
+ '--max-time',
51
+ '2',
52
+ 'http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address',
53
+ ]);
54
+ const ip = stdout.trim();
55
+ if (ip && isAcceptable(ip)) {
56
+ return ip;
57
+ }
58
+ } catch {
59
+ // Metadata API not available, fall back
60
+ }
61
+
62
+ // Fallback: hostname -I
63
+ try {
64
+ const { stdout } = await execa('hostname', ['-I']);
65
+ const addresses = stdout.trim().split(/\s+/);
66
+ for (const addr of addresses) {
67
+ if (isAcceptable(addr)) {
68
+ return addr;
69
+ }
70
+ }
71
+ } catch {
72
+ // hostname -I failed
73
+ }
74
+
75
+ throw new Error(
76
+ 'Could not detect a public IP address. Lamaste must be installed on a server with a public IP.',
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Check whether the given string is a valid IPv4 address (any routable address, not loopback/link-local).
82
+ * @param {string} ip
83
+ * @returns {boolean}
84
+ */
85
+ function isValidIPv4(ip) {
86
+ const parts = ip.split('.');
87
+ if (parts.length !== 4) return false;
88
+
89
+ const octets = parts.map(Number);
90
+ if (octets.some((o) => Number.isNaN(o) || o < 0 || o > 255)) return false;
91
+
92
+ const [a, b] = octets;
93
+
94
+ // Reject loopback, link-local, and 0.0.0.0
95
+ if (a === 127) return false;
96
+ if (a === 169 && b === 254) return false;
97
+ if (octets.every((o) => o === 0)) return false;
98
+
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Check whether the given IPv4 address is public (not RFC 1918 / loopback / link-local).
104
+ * @param {string} ip
105
+ * @returns {boolean}
106
+ */
107
+ function isPublicIP(ip) {
108
+ if (!isValidIPv4(ip)) return false;
109
+
110
+ const [a, b] = ip.split('.').map(Number);
111
+
112
+ // 10.0.0.0/8
113
+ if (a === 10) return false;
114
+ // 172.16.0.0/12
115
+ if (a === 172 && b >= 16 && b <= 31) return false;
116
+ // 192.168.0.0/16
117
+ if (a === 192 && b === 168) return false;
118
+
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Verify the process is running as root.
124
+ * @throws {Error} if not root
125
+ */
126
+ export function checkRoot() {
127
+ if (process.getuid() !== 0) {
128
+ throw new Error(
129
+ 'Lamaste installer must be run as root. Try: sudo npx @lamalibre/create-lamaste',
130
+ );
131
+ }
132
+ }
@@ -0,0 +1,38 @@
1
+ import { execa } from 'execa';
2
+ import { readFile, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { HTTP_REDIRECT_SITE, generateHttpRedirectVhost } from './service-config.js';
5
+
6
+ const AVAILABLE = `/etc/nginx/sites-available/${HTTP_REDIRECT_SITE}`;
7
+ const ENABLED = `/etc/nginx/sites-enabled/${HTTP_REDIRECT_SITE}`;
8
+
9
+ /**
10
+ * Install or refresh the port-80 → HTTPS redirect site, test nginx, and
11
+ * reload it. Idempotent. Run as root. On a failed `nginx -t` the previous
12
+ * state is restored and the error is thrown.
13
+ *
14
+ * @param {{ reload?: boolean }} [options] - reload nginx afterwards (default true)
15
+ * @returns {Promise<{ changed: boolean }>}
16
+ */
17
+ export async function ensureHttpRedirect({ reload = true } = {}) {
18
+ const content = generateHttpRedirectVhost();
19
+ const previous = existsSync(AVAILABLE) ? await readFile(AVAILABLE, 'utf8') : null;
20
+ const wasEnabled = existsSync(ENABLED);
21
+ if (previous === content && wasEnabled) return { changed: false };
22
+
23
+ await writeFile(AVAILABLE, content, { mode: 0o644 });
24
+ if (!wasEnabled) await symlink(AVAILABLE, ENABLED);
25
+
26
+ try {
27
+ await execa('nginx', ['-t']);
28
+ } catch (err) {
29
+ if (previous === null) await rm(AVAILABLE, { force: true });
30
+ else await writeFile(AVAILABLE, previous, { mode: 0o644 });
31
+ if (!wasEnabled) await rm(ENABLED, { force: true });
32
+ throw new Error(
33
+ `nginx rejected the HTTP → HTTPS redirect site; left as it was:\n${err.stderr || err.message}`,
34
+ );
35
+ }
36
+ if (reload) await execa('systemctl', ['reload', 'nginx']);
37
+ return { changed: true };
38
+ }