@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,293 @@
1
+ import { execa } from 'execa';
2
+ import { readFile, writeFile, unlink } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ /**
6
+ * OS hardening subtasks: swap, firewall, fail2ban, SSH hardening, system dependencies.
7
+ *
8
+ * @param {object} ctx Shared installer context.
9
+ * @param {object} task Parent Listr2 task reference.
10
+ * @returns {import('listr2').ListrTask[]}
11
+ */
12
+ export function hardenTasks(ctx, task) {
13
+ return task.newListr([
14
+ {
15
+ title: 'Creating swap file',
16
+ // Swap is always created — critical for 512MB droplets regardless of hardening
17
+ task: async (_ctx, subtask) => {
18
+ // Check if swap already exists
19
+ const { stdout: swapInfo } = await execa('swapon', ['--show']);
20
+ if (swapInfo.trim().length > 0) {
21
+ subtask.output = 'Swap already active, skipping creation';
22
+ return;
23
+ }
24
+
25
+ subtask.output = 'Allocating 1GB swap file...';
26
+ await execa('fallocate', ['-l', '1G', '/swapfile']);
27
+ await execa('chmod', ['600', '/swapfile']);
28
+ await execa('mkswap', ['/swapfile']);
29
+ await execa('swapon', ['/swapfile']);
30
+
31
+ // Add to /etc/fstab if not already present
32
+ const fstab = await readFile('/etc/fstab', 'utf8');
33
+ if (!fstab.includes('/swapfile')) {
34
+ await writeFile('/etc/fstab', fstab.trimEnd() + '\n/swapfile none swap sw 0 0\n');
35
+ }
36
+
37
+ // Set swappiness
38
+ await execa('sysctl', ['vm.swappiness=10']);
39
+ await writeFile('/etc/sysctl.d/99-lamaste.conf', 'vm.swappiness=10\n');
40
+
41
+ subtask.output = 'Swap file created and activated';
42
+ },
43
+ rendererOptions: { persistentOutput: true },
44
+ },
45
+ {
46
+ title: 'Configuring UFW firewall',
47
+ skip: async () => {
48
+ if (ctx.skipHarden) return 'Skipped via --skip-harden';
49
+
50
+ // Check if UFW is active and all required ports are already allowed
51
+ try {
52
+ const { stdout } = await execa('ufw', ['status']);
53
+ const isActive = stdout.includes('Status: active');
54
+ if (!isActive) return false;
55
+
56
+ const requiredPorts = ['22/tcp', '80/tcp', '443/tcp', '9292/tcp'];
57
+ const allPresent = requiredPorts.every((port) => {
58
+ // Match lines like "22/tcp ALLOW Anywhere"
59
+ const regex = new RegExp(`^${port.replace('/', '\\/')}\\s+ALLOW`, 'm');
60
+ return regex.test(stdout);
61
+ });
62
+
63
+ if (allPresent) return 'UFW active with all required ports already allowed';
64
+ } catch {
65
+ // ufw not installed or not available — proceed with task
66
+ }
67
+
68
+ return false;
69
+ },
70
+ task: async (_ctx, subtask) => {
71
+ const requiredPorts = ['22/tcp', '80/tcp', '443/tcp', '9292/tcp'];
72
+
73
+ // Determine if UFW is currently active
74
+ let isActive = false;
75
+ let statusOutput = '';
76
+ try {
77
+ const { stdout } = await execa('ufw', ['status']);
78
+ statusOutput = stdout;
79
+ isActive = stdout.includes('Status: active');
80
+ } catch {
81
+ // ufw not available or inactive
82
+ }
83
+
84
+ if (isActive) {
85
+ // UFW is already active — only add missing port rules
86
+ const actions = [];
87
+ for (const port of requiredPorts) {
88
+ const regex = new RegExp(`^${port.replace('/', '\\/')}\\s+ALLOW`, 'm');
89
+ if (regex.test(statusOutput)) {
90
+ actions.push(`Port ${port} already allowed`);
91
+ } else {
92
+ await execa('ufw', ['allow', port]);
93
+ actions.push(`Added rule for ${port}`);
94
+ }
95
+ }
96
+ subtask.output = actions.join(', ');
97
+ } else {
98
+ // UFW is not active — set up from scratch (without reset)
99
+ subtask.output = 'Setting UFW defaults...';
100
+ await execa('ufw', ['default', 'deny', 'incoming']);
101
+ await execa('ufw', ['default', 'allow', 'outgoing']);
102
+
103
+ subtask.output = 'Allowing ports 22, 80, 443, 9292...';
104
+ for (const port of requiredPorts) {
105
+ await execa('ufw', ['allow', port]);
106
+ }
107
+
108
+ await execa('ufw', ['--force', 'enable']);
109
+ subtask.output = 'UFW enabled with ports 22, 80, 443, 9292 allowed';
110
+ }
111
+ },
112
+ rendererOptions: { persistentOutput: true },
113
+ },
114
+ {
115
+ title: 'Installing and configuring fail2ban',
116
+ skip: async () => {
117
+ if (ctx.skipHarden) return 'Skipped via --skip-harden';
118
+
119
+ // Skip if our drop-in config already exists with expected content
120
+ const configPath = '/etc/fail2ban/jail.d/lamaste.conf';
121
+ if (existsSync(configPath)) {
122
+ try {
123
+ const existing = await readFile(configPath, 'utf8');
124
+ if (existing.includes('[sshd]') && existing.includes('[nginx-http-auth]')) {
125
+ const { stdout } = await execa('systemctl', ['is-active', 'fail2ban']);
126
+ if (stdout.trim() === 'active') {
127
+ return 'fail2ban already configured and running';
128
+ }
129
+ }
130
+ } catch {
131
+ // Config exists but can't be read or fail2ban not running — proceed
132
+ }
133
+ }
134
+
135
+ return false;
136
+ },
137
+ task: async (_ctx, subtask) => {
138
+ subtask.output = 'Installing fail2ban...';
139
+ try {
140
+ await execa('apt-get', ['install', '-y', 'fail2ban']);
141
+ } catch (err) {
142
+ throw new Error(
143
+ `Failed to install fail2ban. Check your network connection and package sources.\n${err.stderr || err.message}`,
144
+ );
145
+ }
146
+
147
+ const jailConfig = `[sshd]
148
+ enabled = true
149
+ port = ssh
150
+ filter = sshd
151
+ logpath = /var/log/auth.log
152
+ maxretry = 5
153
+ bantime = 3600
154
+
155
+ [nginx-http-auth]
156
+ enabled = true
157
+ port = http,https
158
+ filter = nginx-http-auth
159
+ logpath = /var/log/nginx/error.log
160
+ maxretry = 5
161
+ bantime = 3600
162
+ `;
163
+
164
+ // Write to jail.d/ drop-in instead of overwriting jail.local
165
+ const jailDir = '/etc/fail2ban/jail.d';
166
+ if (!existsSync(jailDir)) {
167
+ await execa('mkdir', ['-p', jailDir]);
168
+ }
169
+ await writeFile(`${jailDir}/lamaste.conf`, jailConfig);
170
+
171
+ subtask.output = 'Restarting fail2ban...';
172
+ await execa('systemctl', ['restart', 'fail2ban']);
173
+
174
+ const { stdout: status } = await execa('systemctl', ['is-active', 'fail2ban']);
175
+ subtask.output = `fail2ban status: ${status.trim()}`;
176
+ },
177
+ rendererOptions: { persistentOutput: true },
178
+ },
179
+ {
180
+ title: 'Hardening SSH configuration',
181
+ skip: () => ctx.skipHarden && 'Skipped via --skip-harden',
182
+ task: async (_ctx, subtask) => {
183
+ const sshdConfigPath = '/etc/ssh/sshd_config';
184
+ const tempPath = '/etc/ssh/sshd_config.lamaste-new';
185
+ const backupPath = '/etc/ssh/sshd_config.pre-lamaste';
186
+
187
+ const originalContent = await readFile(sshdConfigPath, 'utf8');
188
+
189
+ const settings = {
190
+ PasswordAuthentication: 'no',
191
+ PermitRootLogin: 'prohibit-password',
192
+ ChallengeResponseAuthentication: 'no',
193
+ };
194
+
195
+ // Check if all settings are already correctly applied
196
+ const allCorrect = Object.entries(settings).every(([key, value]) => {
197
+ const regex = new RegExp(`^${key}\\s+${value}$`, 'm');
198
+ return regex.test(originalContent);
199
+ });
200
+
201
+ if (allCorrect) {
202
+ subtask.output = 'SSH already hardened, skipping';
203
+ return;
204
+ }
205
+
206
+ // Apply regex modifications to produce new content
207
+ let content = originalContent;
208
+ for (const [key, value] of Object.entries(settings)) {
209
+ // Match both commented and uncommented lines with any spacing
210
+ const regex = new RegExp(`^#?\\s*${key}\\s+.*$`, 'gm');
211
+ if (regex.test(content)) {
212
+ content = content.replace(regex, `${key} ${value}`);
213
+ } else {
214
+ content = content.trimEnd() + `\n${key} ${value}\n`;
215
+ }
216
+ }
217
+
218
+ // Write modified content to a temp file for validation
219
+ await writeFile(tempPath, content);
220
+
221
+ subtask.output = 'Validating sshd config...';
222
+ try {
223
+ await execa('sshd', ['-t', '-f', tempPath]);
224
+ } catch (err) {
225
+ // Validation failed — remove temp file, leave original untouched
226
+ await unlink(tempPath).catch(() => {});
227
+ throw new Error(
228
+ `sshd config validation failed, original config is untouched: ${err.message}`,
229
+ );
230
+ }
231
+
232
+ // Validation passed — back up original (only if no prior backup exists)
233
+ if (!existsSync(backupPath)) {
234
+ await writeFile(backupPath, originalContent);
235
+ }
236
+
237
+ // Atomically move temp file into place
238
+ await execa('mv', [tempPath, sshdConfigPath]);
239
+
240
+ subtask.output = 'Restarting ssh...';
241
+ await execa('systemctl', ['restart', 'ssh']);
242
+
243
+ subtask.output = 'SSH hardened: key-auth only, root with keys only';
244
+ },
245
+ rendererOptions: { persistentOutput: true },
246
+ },
247
+ {
248
+ title: 'Installing system dependencies',
249
+ task: async (_ctx, subtask) => {
250
+ subtask.output = 'Updating package lists...';
251
+ try {
252
+ await execa('apt-get', ['update']);
253
+ } catch (err) {
254
+ throw new Error(
255
+ `Failed to update package lists. Check your network connection and /etc/apt/sources.list.\n${err.stderr || err.message}`,
256
+ );
257
+ }
258
+
259
+ subtask.output = 'Installing curl, openssl, nginx, certbot...';
260
+ try {
261
+ await execa('apt-get', [
262
+ 'install',
263
+ '-y',
264
+ 'curl',
265
+ 'openssl',
266
+ 'nginx',
267
+ 'certbot',
268
+ 'python3-certbot-nginx',
269
+ ]);
270
+ } catch (err) {
271
+ throw new Error(
272
+ `Failed to install system packages. Check your network connection and package sources.\n${err.stderr || err.message}`,
273
+ );
274
+ }
275
+
276
+ const { stdout: nginxVersion } = await execa('nginx', ['-v']);
277
+ subtask.output = `Installed: ${nginxVersion || 'nginx'}`;
278
+
279
+ // Stop nginx for now (will be configured and started later)
280
+ await execa('systemctl', ['stop', 'nginx']);
281
+
282
+ // Disable default nginx site
283
+ const defaultSite = '/etc/nginx/sites-enabled/default';
284
+ if (existsSync(defaultSite)) {
285
+ await unlink(defaultSite);
286
+ }
287
+
288
+ subtask.output = 'System dependencies installed, nginx stopped';
289
+ },
290
+ rendererOptions: { persistentOutput: true },
291
+ },
292
+ ]);
293
+ }
@@ -0,0 +1,193 @@
1
+ import { execa } from 'execa';
2
+ import { writeFile, readFile, access } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { generatePassword } from '../lib/secrets.js';
5
+
6
+ /**
7
+ * mTLS certificate generation subtasks.
8
+ * Creates a self-signed CA, client certificate, and PKCS12 bundle.
9
+ *
10
+ * Idempotent: if the CA key and client PKCS12 bundle already exist,
11
+ * all generation steps are skipped to avoid invalidating previously
12
+ * imported client certificates.
13
+ *
14
+ * @param {object} ctx Shared installer context.
15
+ * @param {object} task Parent Listr2 task reference.
16
+ * @returns {import('listr2').ListrTask[]}
17
+ */
18
+ export function mtlsTasks(ctx, task) {
19
+ const pkiDir = ctx.pkiDir;
20
+ const alreadyProvisioned = existsSync(`${pkiDir}/ca.key`) && existsSync(`${pkiDir}/client.p12`);
21
+
22
+ if (alreadyProvisioned) {
23
+ return task.newListr([
24
+ {
25
+ title: 'mTLS certificates already exist — skipping generation',
26
+ task: async () => {
27
+ // Read the existing p12 password so the summary can display it.
28
+ ctx.p12Password = await readFile(`${pkiDir}/.p12-password`, 'utf8');
29
+ },
30
+ },
31
+ ]);
32
+ }
33
+
34
+ return task.newListr([
35
+ {
36
+ title: 'Creating PKI directory',
37
+ task: async (_ctx, subtask) => {
38
+ await execa('mkdir', ['-p', pkiDir]);
39
+ await execa('chmod', ['700', pkiDir]);
40
+ subtask.output = `PKI directory: ${pkiDir}`;
41
+ },
42
+ rendererOptions: { persistentOutput: true },
43
+ },
44
+ {
45
+ title: 'Generating CA private key and certificate',
46
+ task: async (_ctx, subtask) => {
47
+ subtask.output = 'Generating 4096-bit RSA CA key...';
48
+ await execa('openssl', ['genrsa', '-out', `${pkiDir}/ca.key`, '4096']);
49
+
50
+ subtask.output = 'Creating self-signed CA certificate (10 year validity)...';
51
+ await execa('openssl', [
52
+ 'req',
53
+ '-x509',
54
+ '-new',
55
+ '-nodes',
56
+ '-key',
57
+ `${pkiDir}/ca.key`,
58
+ '-sha256',
59
+ '-days',
60
+ '3650',
61
+ '-out',
62
+ `${pkiDir}/ca.crt`,
63
+ '-subj',
64
+ '/CN=Lamaste CA/O=Lamaste',
65
+ ]);
66
+
67
+ await execa('chmod', ['600', `${pkiDir}/ca.key`]);
68
+ await execa('chmod', ['644', `${pkiDir}/ca.crt`]);
69
+
70
+ // Verify files exist
71
+ await assertFileExists(`${pkiDir}/ca.key`);
72
+ await assertFileExists(`${pkiDir}/ca.crt`);
73
+
74
+ subtask.output = 'CA key and certificate generated';
75
+ },
76
+ rendererOptions: { persistentOutput: true },
77
+ },
78
+ {
79
+ title: 'Generating client key and CSR',
80
+ task: async (_ctx, subtask) => {
81
+ subtask.output = 'Generating 4096-bit RSA client key...';
82
+ await execa('openssl', ['genrsa', '-out', `${pkiDir}/client.key`, '4096']);
83
+
84
+ subtask.output = 'Creating certificate signing request...';
85
+ await execa('openssl', [
86
+ 'req',
87
+ '-new',
88
+ '-key',
89
+ `${pkiDir}/client.key`,
90
+ '-out',
91
+ `${pkiDir}/client.csr`,
92
+ '-subj',
93
+ '/CN=admin/O=Lamaste',
94
+ ]);
95
+
96
+ await execa('chmod', ['600', `${pkiDir}/client.key`]);
97
+
98
+ await assertFileExists(`${pkiDir}/client.key`);
99
+ await assertFileExists(`${pkiDir}/client.csr`);
100
+
101
+ subtask.output = 'Client key and CSR generated';
102
+ },
103
+ rendererOptions: { persistentOutput: true },
104
+ },
105
+ {
106
+ title: 'Signing client certificate with CA',
107
+ task: async (_ctx, subtask) => {
108
+ subtask.output = 'Signing client certificate (2 year validity)...';
109
+ await execa('openssl', [
110
+ 'x509',
111
+ '-req',
112
+ '-in',
113
+ `${pkiDir}/client.csr`,
114
+ '-CA',
115
+ `${pkiDir}/ca.crt`,
116
+ '-CAkey',
117
+ `${pkiDir}/ca.key`,
118
+ '-CAcreateserial',
119
+ '-out',
120
+ `${pkiDir}/client.crt`,
121
+ '-days',
122
+ '730',
123
+ '-sha256',
124
+ ]);
125
+
126
+ await execa('chmod', ['644', `${pkiDir}/client.crt`]);
127
+
128
+ await assertFileExists(`${pkiDir}/client.crt`);
129
+
130
+ // Clean up CSR (no longer needed)
131
+ await execa('rm', ['-f', `${pkiDir}/client.csr`]);
132
+
133
+ subtask.output = 'Client certificate signed and CSR removed';
134
+ },
135
+ rendererOptions: { persistentOutput: true },
136
+ },
137
+ {
138
+ title: 'Creating PKCS12 bundle',
139
+ task: async (_ctx, subtask) => {
140
+ const password = generatePassword();
141
+
142
+ subtask.output = 'Creating PKCS12 bundle for browser import...';
143
+ await execa(
144
+ 'openssl',
145
+ [
146
+ 'pkcs12',
147
+ '-export',
148
+ '-keypbe',
149
+ 'PBE-SHA1-3DES',
150
+ '-certpbe',
151
+ 'PBE-SHA1-3DES',
152
+ '-macalg',
153
+ 'sha1',
154
+ '-out',
155
+ `${pkiDir}/client.p12`,
156
+ '-inkey',
157
+ `${pkiDir}/client.key`,
158
+ '-in',
159
+ `${pkiDir}/client.crt`,
160
+ '-certfile',
161
+ `${pkiDir}/ca.crt`,
162
+ '-passout',
163
+ 'stdin',
164
+ ],
165
+ { input: password },
166
+ );
167
+
168
+ // Save password to file (no trailing newline)
169
+ await writeFile(`${pkiDir}/.p12-password`, password, { mode: 0o600 });
170
+ await execa('chmod', ['600', `${pkiDir}/client.p12`]);
171
+
172
+ await assertFileExists(`${pkiDir}/client.p12`);
173
+ await assertFileExists(`${pkiDir}/.p12-password`);
174
+
175
+ ctx.p12Password = password;
176
+ subtask.output = 'PKCS12 bundle created';
177
+ },
178
+ rendererOptions: { persistentOutput: true },
179
+ },
180
+ ]);
181
+ }
182
+
183
+ /**
184
+ * Assert that a file exists, throwing a descriptive error if it does not.
185
+ * @param {string} filePath
186
+ */
187
+ async function assertFileExists(filePath) {
188
+ try {
189
+ await access(filePath);
190
+ } catch {
191
+ throw new Error(`Expected file was not created: ${filePath}`);
192
+ }
193
+ }