@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,478 @@
1
+ /**
2
+ * lamaste-server reset-admin — Emergency recovery tool for admin authentication.
3
+ *
4
+ * Requires root access on the server (run via DigitalOcean console).
5
+ * Reverts admin auth from hardware-bound back to P12:
6
+ *
7
+ * 1. Generate new admin keypair + CSR + sign + P12
8
+ * 2. Revoke old hardware-bound admin cert
9
+ * 3. Set adminAuthMode: 'p12' in panel.json
10
+ * 4. Clear 2FA if enabled
11
+ * 5. Re-enable IP:9292 vhost
12
+ * 6. Restart serverd + reload nginx
13
+ * 7. Print new P12 password
14
+ *
15
+ * The PKI directory and panel.json belong to the lamaste user — and the
16
+ * panel, if compromised, could have left symlinks there for root to write
17
+ * through. So every file operation inside them runs as lamaste (openssl, cp,
18
+ * chmod) or creates a fresh file (O_EXCL | O_NOFOLLOW) that is renamed into
19
+ * place; root never writes through an existing path there.
20
+ */
21
+
22
+ import crypto from 'node:crypto';
23
+ import { readFile, rename, access, constants, unlink, open, rm } from 'node:fs/promises';
24
+ import { basename, dirname, join } from 'node:path';
25
+ import chalk from 'chalk';
26
+ import { execa } from 'execa';
27
+ import { CONFIG_PATH, PKI_DIR, PANEL_SERVICE, STATE_DIR } from '../config.js';
28
+ import { emitStep, emitError, emitComplete } from '../ndjson.js';
29
+
30
+ /**
31
+ * Atomically write the P12 password to a file (temp -> fsync -> chmod -> rename).
32
+ * @param {string} destPath
33
+ * @param {string} password
34
+ */
35
+ async function atomicWritePassword(destPath, password) {
36
+ const tmpPath = `${destPath}.tmp`;
37
+ // Best-effort cleanup of a stale temp from a prior crashed run so 'wx' below
38
+ // does not spuriously fail.
39
+ await unlink(tmpPath).catch(() => {});
40
+ // Use 'wx' to fail if a stale temp file exists (avoids races).
41
+ const fh = await open(tmpPath, 'wx', 0o600);
42
+ try {
43
+ await fh.writeFile(password, 'utf-8');
44
+ await fh.sync();
45
+ } finally {
46
+ await fh.close();
47
+ }
48
+ await rename(tmpPath, destPath);
49
+ }
50
+
51
+ /**
52
+ * uid/gid of the lamaste user, as execa options: commands run with them act
53
+ * inside the lamaste-owned PKI directory with lamaste's rights only.
54
+ *
55
+ * @returns {Promise<{ uid: number, gid: number }>}
56
+ */
57
+ async function lamasteIdentity() {
58
+ const uid = Number.parseInt((await execa('id', ['-u', 'lamaste'])).stdout.trim(), 10);
59
+ const gid = Number.parseInt((await execa('id', ['-g', 'lamaste'])).stdout.trim(), 10);
60
+ if (!Number.isInteger(uid) || !Number.isInteger(gid)) {
61
+ throw new Error('Cannot resolve the lamaste user');
62
+ }
63
+ return { uid, gid };
64
+ }
65
+
66
+ /**
67
+ * Write `content` to `destPath` (in a lamaste-owned directory) as a fresh
68
+ * file owned by `owner`, renamed into place.
69
+ *
70
+ * @param {string} destPath
71
+ * @param {string} content
72
+ * @param {{ uid: number, gid: number }} owner
73
+ * @param {number} mode
74
+ */
75
+ async function writeOwnedFile(destPath, content, owner, mode) {
76
+ const tmp = join(
77
+ dirname(destPath),
78
+ `.${basename(destPath)}.${crypto.randomBytes(8).toString('hex')}`,
79
+ );
80
+ const fh = await open(
81
+ tmp,
82
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
83
+ 0o600,
84
+ );
85
+ try {
86
+ await fh.writeFile(content, 'utf-8');
87
+ await fh.chown(owner.uid, owner.gid);
88
+ await fh.chmod(mode);
89
+ await fh.sync();
90
+ } catch (err) {
91
+ await fh.close();
92
+ await rm(tmp, { force: true });
93
+ throw err;
94
+ }
95
+ await fh.close();
96
+ await rename(tmp, destPath);
97
+ }
98
+
99
+ /**
100
+ * Add a certificate serial to the panel's revocation list (`revoked_certs`
101
+ * in `state.db`). Runs as lamaste: the database is in WAL mode, and a write
102
+ * as root could leave root-owned `-wal`/`-shm` files the panel cannot open.
103
+ * The serial and label reach the child through its environment, never
104
+ * through the code it runs.
105
+ *
106
+ * @param {string} serial
107
+ * @param {string} label
108
+ * @param {{ uid: number, gid: number }} lamaste
109
+ */
110
+ async function revokeInStateDb(serial, label, lamaste) {
111
+ const script = `
112
+ import { DatabaseSync } from 'node:sqlite';
113
+ const db = new DatabaseSync(process.env.STATE_DB);
114
+ db.exec('PRAGMA busy_timeout = 5000');
115
+ db.exec('BEGIN IMMEDIATE');
116
+ try {
117
+ const seen = db.prepare('SELECT serial FROM revoked_certs WHERE serial = ?').get(process.env.SERIAL);
118
+ if (!seen) {
119
+ db.prepare('INSERT INTO revoked_certs (serial, label, revoked_at) VALUES (?, ?, ?)')
120
+ .run(process.env.SERIAL, process.env.LABEL, new Date().toISOString());
121
+ }
122
+ db.exec('COMMIT');
123
+ } catch (err) {
124
+ db.exec('ROLLBACK');
125
+ throw err;
126
+ }
127
+ db.close();
128
+ `;
129
+ await execa(process.execPath, ['--no-warnings', '--input-type=module', '-e', script], {
130
+ ...lamaste,
131
+ cwd: STATE_DIR,
132
+ env: { STATE_DB: `${STATE_DIR}/state.db`, SERIAL: serial, LABEL: label },
133
+ extendEnv: false,
134
+ });
135
+ }
136
+
137
+ /**
138
+ * @param {{ json: boolean, passwordFile?: string }} options
139
+ */
140
+ export async function runResetAdmin({ json, passwordFile }) {
141
+ // Verify running as root
142
+ if (process.getuid && process.getuid() !== 0) {
143
+ const msg = 'lamaste-server reset-admin must be run as root.';
144
+ if (json) {
145
+ emitError(msg);
146
+ } else {
147
+ console.error(`\n Error: ${msg}`);
148
+ console.error(` Usage: sudo lamaste-server reset-admin\n`);
149
+ }
150
+ process.exit(1);
151
+ }
152
+
153
+ if (!json) {
154
+ console.log('');
155
+ console.log(' Lamaste Admin Reset');
156
+ console.log(' Reverting admin authentication to P12...');
157
+ console.log('');
158
+ }
159
+
160
+ // 1. Read current config
161
+ if (json) emitStep('read-config', 'running');
162
+ let config;
163
+ try {
164
+ const raw = await readFile(CONFIG_PATH, 'utf-8');
165
+ config = JSON.parse(raw);
166
+ if (json) emitStep('read-config', 'complete');
167
+ } catch (err) {
168
+ const msg = `Cannot read config at ${CONFIG_PATH}: ${err.message}`;
169
+ if (json) {
170
+ emitStep('read-config', 'failed', msg);
171
+ emitError(msg);
172
+ } else {
173
+ console.error(` Error: ${msg}`);
174
+ }
175
+ process.exit(1);
176
+ }
177
+
178
+ const lamaste = await lamasteIdentity();
179
+
180
+ // 2. Verify CA exists
181
+ try {
182
+ await access(`${PKI_DIR}/ca.key`, constants.R_OK);
183
+ await access(`${PKI_DIR}/ca.crt`, constants.R_OK);
184
+ } catch {
185
+ const msg = `CA key/cert not found in ${PKI_DIR}`;
186
+ if (json) emitError(msg);
187
+ else console.error(` Error: ${msg}`);
188
+ process.exit(1);
189
+ }
190
+
191
+ // 3. Read old admin cert serial for revocation
192
+ if (json) emitStep('generate-cert', 'running');
193
+ let oldSerial = '';
194
+ try {
195
+ const { stdout } = await execa(
196
+ 'openssl',
197
+ ['x509', '-in', `${PKI_DIR}/client.crt`, '-serial', '-noout'],
198
+ lamaste,
199
+ );
200
+ const match = stdout.match(/serial=([A-Fa-f0-9]+)/);
201
+ oldSerial = match ? match[1] : '';
202
+ } catch {
203
+ // Old cert may not exist
204
+ }
205
+
206
+ // 4. Generate new admin key
207
+ if (!json) console.log(' Generating new admin private key...');
208
+ await execa('openssl', ['genrsa', '-out', `${PKI_DIR}/client.key.new`, '4096'], lamaste);
209
+
210
+ // 5. Create CSR
211
+ if (!json) console.log(' Creating certificate signing request...');
212
+ await execa(
213
+ 'openssl',
214
+ [
215
+ 'req',
216
+ '-new',
217
+ '-key',
218
+ `${PKI_DIR}/client.key.new`,
219
+ '-out',
220
+ `${PKI_DIR}/client.csr`,
221
+ '-subj',
222
+ '/CN=admin/O=Lamaste',
223
+ ],
224
+ lamaste,
225
+ );
226
+
227
+ // 6. Sign with CA
228
+ if (!json) console.log(' Signing certificate with CA...');
229
+ await execa(
230
+ 'openssl',
231
+ [
232
+ 'x509',
233
+ '-req',
234
+ '-in',
235
+ `${PKI_DIR}/client.csr`,
236
+ '-CA',
237
+ `${PKI_DIR}/ca.crt`,
238
+ '-CAkey',
239
+ `${PKI_DIR}/ca.key`,
240
+ '-CAcreateserial',
241
+ '-out',
242
+ `${PKI_DIR}/client.crt.new`,
243
+ '-days',
244
+ '730',
245
+ '-sha256',
246
+ ],
247
+ lamaste,
248
+ );
249
+
250
+ // 7. Create P12 bundle
251
+ const p12Password = crypto.randomBytes(16).toString('hex');
252
+ if (!json) console.log(' Creating PKCS12 bundle...');
253
+ await execa(
254
+ 'openssl',
255
+ [
256
+ 'pkcs12',
257
+ '-export',
258
+ '-keypbe',
259
+ 'PBE-SHA1-3DES',
260
+ '-certpbe',
261
+ 'PBE-SHA1-3DES',
262
+ '-macalg',
263
+ 'sha1',
264
+ '-out',
265
+ `${PKI_DIR}/client.p12.new`,
266
+ '-inkey',
267
+ `${PKI_DIR}/client.key.new`,
268
+ '-in',
269
+ `${PKI_DIR}/client.crt.new`,
270
+ '-certfile',
271
+ `${PKI_DIR}/ca.crt`,
272
+ '-passout',
273
+ 'stdin',
274
+ ],
275
+ { ...lamaste, input: p12Password },
276
+ );
277
+
278
+ if (json) emitStep('generate-cert', 'complete');
279
+
280
+ // 8. Back up old files
281
+ if (json) emitStep('backup', 'running');
282
+ else console.log(' Backing up old certificates...');
283
+ for (const ext of ['key', 'crt', 'p12']) {
284
+ try {
285
+ await access(`${PKI_DIR}/client.${ext}`, constants.F_OK);
286
+ await execa(
287
+ 'cp',
288
+ ['--', `${PKI_DIR}/client.${ext}`, `${PKI_DIR}/client.${ext}.bak`],
289
+ lamaste,
290
+ );
291
+ } catch {
292
+ // Old file may not exist
293
+ }
294
+ }
295
+ if (json) emitStep('backup', 'complete');
296
+
297
+ // 9. Move new files into place (atomic rename)
298
+ if (json) emitStep('install-cert', 'running');
299
+ else console.log(' Installing new certificates...');
300
+ await rename(`${PKI_DIR}/client.key.new`, `${PKI_DIR}/client.key`);
301
+ await rename(`${PKI_DIR}/client.crt.new`, `${PKI_DIR}/client.crt`);
302
+ await rename(`${PKI_DIR}/client.p12.new`, `${PKI_DIR}/client.p12`);
303
+
304
+ // 10. Persist P12 password for future redeployments and for NDJSON consumers.
305
+ // Atomic write (temp -> fsync -> chmod 0600 -> rename) so partial writes never
306
+ // expose the secret. Hard-fail if the write fails — never fall back to emitting
307
+ // the password value over stdout.
308
+ const defaultPasswordPath = `${PKI_DIR}/.p12-password`;
309
+ const targetPasswordPath = passwordFile || defaultPasswordPath;
310
+ try {
311
+ await atomicWritePassword(defaultPasswordPath, p12Password);
312
+ if (targetPasswordPath !== defaultPasswordPath) {
313
+ await atomicWritePassword(targetPasswordPath, p12Password);
314
+ }
315
+ } catch (err) {
316
+ const msg = `Failed to write P12 password file: ${err.message}`;
317
+ if (json) {
318
+ emitError(msg);
319
+ } else {
320
+ console.error(` Error: ${msg}`);
321
+ }
322
+ process.exit(1);
323
+ }
324
+
325
+ // 11. Clean up
326
+ await unlink(`${PKI_DIR}/client.csr`).catch(() => {});
327
+ await unlink(`${PKI_DIR}/ca.srl`).catch(() => {});
328
+
329
+ // 12. Set file permissions (the files are lamaste's: openssl ran as lamaste)
330
+ await execa('chmod', ['600', `${PKI_DIR}/client.key`, `${PKI_DIR}/client.p12`], lamaste);
331
+ await execa('chmod', ['644', `${PKI_DIR}/client.crt`], lamaste);
332
+
333
+ if (json) emitStep('install-cert', 'complete');
334
+
335
+ // 13. Revoke old cert — in the panel's revocation list (state.db,
336
+ // revoked_certs), which the mTLS middleware checks on every request.
337
+ if (oldSerial) {
338
+ if (json) emitStep('revoke-old', 'running');
339
+ else console.log(' Revoking old admin certificate...');
340
+ try {
341
+ await revokeInStateDb(oldSerial, 'admin (reset-admin)', lamaste);
342
+ } catch (err) {
343
+ // The new certificate is already installed; a revocation that did not
344
+ // happen must not pass silently — the old certificate would still work.
345
+ const msg = `Could not revoke the old admin certificate (serial ${oldSerial}): ${err.stderr || err.message}`;
346
+ if (json) {
347
+ emitStep('revoke-old', 'failed', msg);
348
+ emitError(msg);
349
+ } else {
350
+ console.error(` Error: ${msg}`);
351
+ }
352
+ process.exit(1);
353
+ }
354
+ if (json) emitStep('revoke-old', 'complete');
355
+ }
356
+
357
+ // 14. Clear 2FA if enabled
358
+ if (config.panel2fa && config.panel2fa.enabled) {
359
+ config.panel2fa = { enabled: false, secret: null, setupComplete: false };
360
+ config.sessionSecret = null;
361
+ if (!json) console.log(' Two-factor authentication has been disabled.');
362
+ }
363
+
364
+ // 15. Re-enable IP vhost if it was disabled by 2FA
365
+ if (json) emitStep('config', 'running');
366
+ try {
367
+ const ipAvailable = '/etc/nginx/sites-available/lamalibre-lamaste-panel-ip';
368
+ const ipEnabled = '/etc/nginx/sites-enabled/lamalibre-lamaste-panel-ip';
369
+ await access(ipAvailable, constants.F_OK);
370
+ await execa('ln', ['-sf', ipAvailable, ipEnabled]);
371
+ if (!json) console.log(' IP vhost re-enabled.');
372
+ } catch {
373
+ // IP vhost may not exist
374
+ }
375
+
376
+ // 16. Update config
377
+ if (!json) console.log(' Updating panel configuration...');
378
+ config.adminAuthMode = 'p12';
379
+ await writeOwnedFile(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', lamaste, 0o640);
380
+ if (json) emitStep('config', 'complete');
381
+
382
+ // 17. Restart serverd
383
+ if (json) emitStep('restart', 'running');
384
+ else console.log(' Restarting panel server...');
385
+ try {
386
+ await execa('systemctl', ['restart', PANEL_SERVICE]);
387
+ if (!json) console.log(' Panel server restarted successfully.');
388
+ } catch (err) {
389
+ const msg = `panel server restart failed: ${err.stderr || err.message}`;
390
+ if (!json) {
391
+ console.error(` Warning: ${msg}`);
392
+ console.error(` You may need to restart it manually: systemctl restart ${PANEL_SERVICE}`);
393
+ }
394
+ }
395
+
396
+ // 18. Reload nginx
397
+ if (!json) console.log(' Reloading nginx...');
398
+ try {
399
+ await execa('nginx', ['-t']);
400
+ await execa('systemctl', ['reload', 'nginx']);
401
+ if (!json) console.log(' nginx reloaded successfully.');
402
+ } catch (err) {
403
+ const msg = `nginx reload failed: ${err.stderr || err.message}`;
404
+ if (!json) {
405
+ console.error(` Warning: ${msg}`);
406
+ console.error(' You may need to restart nginx manually: systemctl restart nginx');
407
+ }
408
+ }
409
+
410
+ if (json) {
411
+ emitStep('restart', 'complete');
412
+ // SECURITY: never emit the password value in NDJSON. The password lives at
413
+ // p12PasswordPath (mode 0600). Consumers (the desktop app) read the file
414
+ // directly via privileged escalation (e.g. `pkexec cat`) and store it in
415
+ // the OS keychain. The path-only contract avoids leaks into Tauri event
416
+ // logs, journald, or anything else that captures NDJSON.
417
+ emitComplete({ p12PasswordPath: targetPasswordPath });
418
+ return;
419
+ }
420
+
421
+ // 19. Determine IP for scp command
422
+ let serverIp = '';
423
+ try {
424
+ const { stdout } = await execa('hostname', ['-I']);
425
+ const first = stdout.trim().split(/\s+/)[0];
426
+ if (first) serverIp = first;
427
+ } catch {
428
+ serverIp = '<server-ip>';
429
+ }
430
+
431
+ // 20. Print result
432
+ const b = chalk.bold;
433
+ const c = chalk.cyan;
434
+ const d = chalk.dim;
435
+
436
+ console.log('');
437
+ console.log(' ' + '='.repeat(44));
438
+ console.log(b(' Admin certificate has been reset to P12.'));
439
+ console.log(b(' Panel 2FA has been disabled (if it was on).'));
440
+ console.log(b(' IP:9292 access has been restored.'));
441
+ console.log('');
442
+ console.log(' 1. Download your client certificate:');
443
+ console.log('');
444
+ console.log(` ${c(`scp root@${serverIp}:${PKI_DIR}/client.p12 .`)}`);
445
+ console.log('');
446
+ console.log(' 2. Import client.p12 into your browser:');
447
+ console.log('');
448
+ console.log(` ${d('macOS: Double-click the file \u2192 Keychain Access')}`);
449
+ console.log(
450
+ ` ${d(' \u2192 select "System" keychain \u2192 enter the password below')}`,
451
+ );
452
+ console.log(
453
+ ` ${d(' \u2192 find cert \u2192 double-click \u2192 Trust \u2192 Always Trust')}`,
454
+ );
455
+ console.log(` ${d('Linux: Chrome \u2192 Settings \u2192 Privacy & Security')}`);
456
+ console.log(` ${d(' \u2192 Security \u2192 Manage certificates \u2192 Import')}`);
457
+ console.log(` ${d('Windows: Double-click the file \u2192 Certificate Import Wizard')}`);
458
+ console.log('');
459
+ // SECURITY: the new certificate password is NEVER written to a log sink
460
+ // (stdout/stderr/console). It is persisted at ${defaultPasswordPath}
461
+ // (mode 0600, root-readable only) — the single source of truth for
462
+ // redeployment scripts and for the desktop app, which reads it via
463
+ // privileged escalation. Operators retrieve the password with:
464
+ // sudo cat ${defaultPasswordPath}
465
+ console.log(' 3. Certificate password:');
466
+ console.log('');
467
+ console.log(` ${d(`stored at ${defaultPasswordPath} (mode 0600)`)}`);
468
+ console.log(` ${d(`retrieve with: sudo cat ${defaultPasswordPath}`)}`);
469
+ console.log('');
470
+ console.error(` ${b('*** (redacted — read the password file above)')}`);
471
+ console.log('');
472
+ console.log(' 4. Open the Lamaste panel:');
473
+ console.log('');
474
+ console.log(` ${c(`https://${serverIp}:9292`)}`);
475
+ console.log('');
476
+ console.log(' ' + '='.repeat(44));
477
+ console.log('');
478
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * lamaste-server restart — Restart the panel server and optionally the gatekeeper.
3
+ *
4
+ * Waits for the service to reach active state after restart.
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import { execa } from 'execa';
9
+ import { PANEL_SERVICE, GATEKEEPER_SERVICE } from '../config.js';
10
+ import { emitStep, emitError, emitComplete } from '../ndjson.js';
11
+
12
+ const HEALTH_CHECK_ATTEMPTS = 10;
13
+ const HEALTH_CHECK_INTERVAL_MS = 1000;
14
+
15
+ /**
16
+ * @param {string[]} args Remaining CLI arguments after 'restart'
17
+ * @param {{ json: boolean }} options
18
+ */
19
+ export async function runRestart(args, { json }) {
20
+ const all = args.includes('--all');
21
+ const gatekeeperOnly = args.includes('--gatekeeper');
22
+ const services = gatekeeperOnly
23
+ ? [GATEKEEPER_SERVICE]
24
+ : all
25
+ ? [PANEL_SERVICE, GATEKEEPER_SERVICE]
26
+ : [PANEL_SERVICE];
27
+
28
+ for (const service of services) {
29
+ if (json) {
30
+ emitStep(service, 'running', `Restarting ${service}`);
31
+ } else {
32
+ process.stderr.write(` Restarting ${chalk.cyan(service)}...`);
33
+ }
34
+
35
+ try {
36
+ await execa('systemctl', ['restart', service]);
37
+ } catch (err) {
38
+ const msg = `Failed to restart ${service}: ${err.stderr || err.message}`;
39
+ if (json) {
40
+ emitStep(service, 'failed', msg);
41
+ emitError(msg);
42
+ } else {
43
+ console.error(` ${chalk.red('failed')}`);
44
+ console.error(` ${chalk.red(msg)}`);
45
+ }
46
+ process.exit(1);
47
+ }
48
+
49
+ // Wait for service to become active
50
+ let healthy = false;
51
+ for (let i = 0; i < HEALTH_CHECK_ATTEMPTS; i++) {
52
+ await sleep(HEALTH_CHECK_INTERVAL_MS);
53
+ try {
54
+ const { stdout } = await execa('systemctl', ['is-active', service]);
55
+ if (stdout.trim() === 'active') {
56
+ healthy = true;
57
+ break;
58
+ }
59
+ } catch {
60
+ // Not active yet, retry
61
+ }
62
+ }
63
+
64
+ if (!healthy) {
65
+ const msg = `Service ${service} did not become active within ${HEALTH_CHECK_ATTEMPTS} seconds`;
66
+ if (json) {
67
+ emitStep(service, 'failed', msg);
68
+ emitError(msg);
69
+ } else {
70
+ console.error(` ${chalk.yellow('timeout')}`);
71
+ console.error(` ${chalk.yellow(msg)}`);
72
+ }
73
+ process.exit(1);
74
+ }
75
+
76
+ if (json) {
77
+ emitStep(service, 'complete');
78
+ } else {
79
+ console.log(` ${chalk.green('ok')}`);
80
+ }
81
+ }
82
+
83
+ if (json) {
84
+ emitComplete({ restarted: services });
85
+ }
86
+ }
87
+
88
+ /**
89
+ * @param {number} ms
90
+ * @returns {Promise<void>}
91
+ */
92
+ function sleep(ms) {
93
+ return new Promise((resolve) => setTimeout(resolve, ms));
94
+ }