@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,231 @@
1
+ import { execa } from 'execa';
2
+ import { constants } from 'node:fs';
3
+ import { chmod, chown, lstat, mkdir, open, readdir, rename, rm } from 'node:fs/promises';
4
+ import crypto from 'node:crypto';
5
+ import { basename, dirname, join } from 'node:path';
6
+
7
+ /**
8
+ * Filesystem operations create-lamaste performs as root on paths the lamaste
9
+ * user can write to (its config directory, the web root, /etc/authelia).
10
+ *
11
+ * The panel may be compromised, and it can trigger create-lamaste itself
12
+ * (self-update), so anything it controls may be a trap: a symlink where a
13
+ * file is expected, planted between a check and a use. These helpers never
14
+ * follow a symlink the lamaste user could have placed:
15
+ * - new files are created with O_EXCL | O_NOFOLLOW under a random name and
16
+ * renamed into place (rename replaces a symlink, it does not follow it);
17
+ * - ownership and mode are changed through the open file descriptor
18
+ * (fchown/fchmod), never by path;
19
+ * - recursive changes use coreutils (chown -R, rm -rf), which do not follow
20
+ * symlinks and are safe against a tree changing underneath them.
21
+ */
22
+
23
+ const NOFOLLOW_READ = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
24
+
25
+ /**
26
+ * Atomically write `content` to `path`, owned by uid:gid with `mode`.
27
+ *
28
+ * @param {string} path
29
+ * @param {string | Uint8Array} content
30
+ * @param {{ mode: number, uid: number, gid: number }} options
31
+ */
32
+ export async function writeFileNoFollow(path, content, { mode, uid, gid }) {
33
+ const tmp = join(dirname(path), `.${basename(path)}.${crypto.randomBytes(8).toString('hex')}`);
34
+ const fh = await open(
35
+ tmp,
36
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
37
+ 0o600,
38
+ );
39
+ try {
40
+ await fh.writeFile(content);
41
+ await fh.chown(uid, gid);
42
+ await fh.chmod(mode);
43
+ await fh.sync();
44
+ } catch (err) {
45
+ await fh.close();
46
+ await rm(tmp, { force: true });
47
+ throw err;
48
+ }
49
+ await fh.close();
50
+ try {
51
+ await rename(tmp, path);
52
+ } catch (err) {
53
+ await rm(tmp, { force: true });
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Set owner and mode of the regular file at `path` through a descriptor.
60
+ * A symlink, a missing file or anything but a regular file is left alone.
61
+ *
62
+ * @returns {Promise<boolean>} whether the file exists and was checked
63
+ */
64
+ export async function setFileOwnership(path, { uid, gid, mode }) {
65
+ let fh;
66
+ try {
67
+ fh = await open(path, NOFOLLOW_READ);
68
+ } catch (err) {
69
+ if (err.code === 'ENOENT' || err.code === 'ELOOP') return false;
70
+ throw err;
71
+ }
72
+ try {
73
+ const st = await fh.stat();
74
+ if (!st.isFile()) return false;
75
+ if (st.uid !== uid || st.gid !== gid) await fh.chown(uid, gid);
76
+ if ((st.mode & 0o7777) !== mode) await fh.chmod(mode);
77
+ return true;
78
+ } finally {
79
+ await fh.close();
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Set owner and mode of the directory at `path` through a descriptor,
85
+ * creating it (root-owned parent assumed) if it does not exist.
86
+ */
87
+ export async function ensureDirectory(path, { uid, gid, mode }) {
88
+ await mkdir(path, { recursive: true, mode });
89
+ const fh = await open(path, NOFOLLOW_READ | constants.O_DIRECTORY);
90
+ try {
91
+ const st = await fh.stat();
92
+ if (st.uid !== uid || st.gid !== gid) await fh.chown(uid, gid);
93
+ if ((st.mode & 0o7777) !== mode) await fh.chmod(mode);
94
+ } finally {
95
+ await fh.close();
96
+ }
97
+ }
98
+
99
+ /** `rm -rf` without following symlinks or crossing filesystems. */
100
+ export async function removeTree(path) {
101
+ await execa('rm', ['-rf', '--one-file-system', '--', path]);
102
+ }
103
+
104
+ /** `chown -R` without following symlinks (coreutils' default for -R). */
105
+ export async function chownTree(path, owner) {
106
+ await execa('chown', ['-R', '-P', '--', owner, path]);
107
+ }
108
+
109
+ /**
110
+ * Make the install directory (code the panel runs, and the lamaste-server
111
+ * CLI that administrators run as root) writable by root only. Earlier
112
+ * versions handed it to the lamaste user: a compromised panel could then
113
+ * rewrite code that root executes. Its contents are not reused — the tree
114
+ * may hold symlinks that would redirect the redeploy's writes — so a tree
115
+ * not already owned by root is moved aside, a fresh root-owned directory
116
+ * takes its place, and the caller redeploys every component into it and
117
+ * then removes the old tree.
118
+ *
119
+ * @param {string} installDir
120
+ * @returns {Promise<{ previous: string | null }>} the moved-aside tree, if any
121
+ */
122
+ export async function ensureRootOwnedInstallDir(installDir) {
123
+ let needsReset = false;
124
+ try {
125
+ const st = await lstat(installDir);
126
+ if (!st.isDirectory() || st.uid !== 0) {
127
+ needsReset = true;
128
+ } else {
129
+ for (const entry of await readdir(installDir)) {
130
+ if ((await lstat(join(installDir, entry))).uid !== 0) {
131
+ needsReset = true;
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ } catch (err) {
137
+ if (err.code !== 'ENOENT') throw err;
138
+ }
139
+
140
+ let previous = null;
141
+ if (needsReset) {
142
+ previous = `${installDir}.lamaste-owned-${Date.now()}`;
143
+ await rename(installDir, previous);
144
+ }
145
+ await ensureDirectory(installDir, { uid: 0, gid: 0, mode: 0o755 });
146
+ return { previous };
147
+ }
148
+
149
+ /**
150
+ * Hand the web root to the lamaste user, readable by nginx through the
151
+ * www-data group: directories 2750 (new files inherit the group), files
152
+ * 0640. Earlier versions kept it www-data-owned and let the panel write it
153
+ * through wildcard sudo rules. Modes are fixed while the tree is still
154
+ * owned by www-data, before the lamaste user can touch it.
155
+ *
156
+ * @param {string} webRoot
157
+ * @param {{ lamasteUid: number, wwwDataGid: number }} ids
158
+ * @returns {Promise<{ migrated: boolean }>}
159
+ */
160
+ export async function migrateWebRoot(webRoot, { lamasteUid, wwwDataGid }) {
161
+ let st = null;
162
+ try {
163
+ st = await lstat(webRoot);
164
+ } catch (err) {
165
+ if (err.code !== 'ENOENT') throw err;
166
+ }
167
+ if (st && st.isDirectory() && st.uid === lamasteUid) {
168
+ await ensureDirectory(webRoot, { uid: lamasteUid, gid: wwwDataGid, mode: 0o2750 });
169
+ return { migrated: false };
170
+ }
171
+ if (st && !st.isDirectory()) throw new Error(`${webRoot} is not a directory`);
172
+ if (st) {
173
+ await execa('find', [webRoot, '-xdev', '-type', 'd', '-exec', 'chmod', '2750', '{}', '+']);
174
+ await execa('find', [webRoot, '-xdev', '-type', 'f', '-exec', 'chmod', '0640', '{}', '+']);
175
+ await chownTree(webRoot, `${lamasteUid}:${wwwDataGid}`);
176
+ }
177
+ await ensureDirectory(webRoot, { uid: lamasteUid, gid: wwwDataGid, mode: 0o2750 });
178
+ return { migrated: st !== null };
179
+ }
180
+
181
+ /**
182
+ * Give /etc/authelia to the lamaste user (the panel writes Authelia's
183
+ * configuration) and its group to the Authelia account (which reads the
184
+ * configuration and keeps its database and notification file there).
185
+ * Earlier versions ran Authelia as root with every file root-owned. Files
186
+ * are re-owned while the directory still belongs to root, so the lamaste
187
+ * user cannot interfere; the directory itself is handed over last.
188
+ *
189
+ * @param {string} dir
190
+ * @param {{ lamasteUid: number, lamasteGid: number, autheliaUid: number, autheliaGid: number }} ids
191
+ * @returns {Promise<{ migrated: boolean }>}
192
+ */
193
+ export async function migrateAutheliaDir(dir, ids) {
194
+ const { lamasteUid, lamasteGid, autheliaUid, autheliaGid } = ids;
195
+ let st = null;
196
+ try {
197
+ st = await lstat(dir);
198
+ } catch (err) {
199
+ if (err.code !== 'ENOENT') throw err;
200
+ }
201
+ if (st && st.isDirectory() && st.uid === lamasteUid) {
202
+ await ensureDirectory(dir, { uid: lamasteUid, gid: autheliaGid, mode: 0o2770 });
203
+ return { migrated: false };
204
+ }
205
+ if (st && !st.isDirectory()) throw new Error(`${dir} is not a directory`);
206
+
207
+ if (st) {
208
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
209
+ if (!entry.isFile()) continue;
210
+ const path = join(dir, entry.name);
211
+ const name = entry.name;
212
+ if (name === '.secrets.json') {
213
+ // The panel's own record of the secrets; Authelia reads them from its config.
214
+ await chown(path, lamasteUid, lamasteGid);
215
+ await chmod(path, 0o600);
216
+ } else if (name.startsWith('db.sqlite3') || name === 'notifications.txt') {
217
+ await chown(path, autheliaUid, autheliaGid);
218
+ await chmod(path, 0o600);
219
+ } else if (name === 'users.yml') {
220
+ // Authelia rewrites it in place when a user changes their password.
221
+ await chown(path, lamasteUid, autheliaGid);
222
+ await chmod(path, 0o660);
223
+ } else {
224
+ await chown(path, lamasteUid, autheliaGid);
225
+ await chmod(path, 0o640);
226
+ }
227
+ }
228
+ }
229
+ await ensureDirectory(dir, { uid: lamasteUid, gid: autheliaGid, mode: 0o2770 });
230
+ return { migrated: st !== null };
231
+ }
@@ -0,0 +1,103 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { ensureAutheliaUser, ensureChiselGroup, lookupId } from './accounts.js';
4
+ import { installPinnedAuthelia, installPinnedChisel } from './binaries.js';
5
+ import {
6
+ chownTree,
7
+ ensureDirectory,
8
+ migrateAutheliaDir,
9
+ migrateWebRoot,
10
+ setFileOwnership,
11
+ } from './ownership.js';
12
+ import {
13
+ AUTHELIA_CONFIG_DIR,
14
+ AUTHELIA_LOG_DIR,
15
+ AUTHELIA_UNIT_PATH,
16
+ AUTHELIA_USER,
17
+ CHISEL_GROUP,
18
+ CHISEL_UNIT_PATH,
19
+ WEB_ROOT,
20
+ generateAutheliaUnit,
21
+ generateChiselUnit,
22
+ } from './service-config.js';
23
+
24
+ async function writeIfChanged(path, content) {
25
+ let current = null;
26
+ try {
27
+ current = await readFile(path, 'utf8');
28
+ } catch (err) {
29
+ if (err.code !== 'ENOENT') throw err;
30
+ }
31
+ if (current === content) return false;
32
+ await writeFile(path, content, { mode: 0o644 });
33
+ return true;
34
+ }
35
+
36
+ /**
37
+ * Everything the relay's services need from root, for a fresh install and
38
+ * for every redeploy: service accounts, the pinned chisel and Authelia
39
+ * binaries, their systemd units, and the ownership of the directories the
40
+ * panel writes without privileges. Idempotent.
41
+ *
42
+ * Does not start, stop or restart anything: the caller runs
43
+ * `systemctl daemon-reload` and restarts what changed. Onboarding enables
44
+ * and starts chisel and Authelia on a fresh install.
45
+ *
46
+ * @param {{ configDir: string, pkiDir: string }} ctx
47
+ * @param {(line: string) => void} report
48
+ * @returns {Promise<{ chiselChanged: boolean, autheliaChanged: boolean }>}
49
+ * whether the binary or unit of each changed (a running service needs a
50
+ * restart to pick it up)
51
+ */
52
+ export async function provisionRelayServices(ctx, report) {
53
+ report(`Ensuring the ${CHISEL_GROUP} group and the ${AUTHELIA_USER} account...`);
54
+ await ensureChiselGroup();
55
+ await ensureAutheliaUser();
56
+
57
+ const lamasteUid = await lookupId('user', 'lamaste');
58
+ const lamasteGid = await lookupId('group', 'lamaste');
59
+ const wwwDataGid = await lookupId('group', 'www-data');
60
+ const chiselGid = await lookupId('group', CHISEL_GROUP);
61
+ const autheliaUid = await lookupId('user', AUTHELIA_USER);
62
+ const autheliaGid = await lookupId('group', AUTHELIA_USER);
63
+
64
+ report('Installing the pinned chisel release...');
65
+ const chisel = await installPinnedChisel();
66
+ report('Installing the pinned Authelia release...');
67
+ const authelia = await installPinnedAuthelia();
68
+ if (authelia.keptNewer) {
69
+ report(`Keeping the installed Authelia ${authelia.version} (newer than the pinned release)`);
70
+ }
71
+
72
+ report('Setting ownership of the web root, Authelia and chisel files...');
73
+ await migrateWebRoot(WEB_ROOT, { lamasteUid, wwwDataGid });
74
+ const autheliaDir = await migrateAutheliaDir(AUTHELIA_CONFIG_DIR, {
75
+ lamasteUid,
76
+ lamasteGid,
77
+ autheliaUid,
78
+ autheliaGid,
79
+ });
80
+ await ensureDirectory(AUTHELIA_LOG_DIR, { uid: autheliaUid, gid: autheliaGid, mode: 0o750 });
81
+ await chownTree(AUTHELIA_LOG_DIR, `${autheliaUid}:${autheliaGid}`);
82
+ // chisel runs as nobody:lamaste-chisel; the panel creates the key (as
83
+ // lamaste, group lamaste-chisel). Earlier versions made it nobody:nogroup 0400.
84
+ await setFileOwnership(join(ctx.configDir, 'chisel-server.key'), {
85
+ uid: lamasteUid,
86
+ gid: chiselGid,
87
+ mode: 0o640,
88
+ });
89
+ // The panel is the CA: the PKI directory is its own. Files earlier
90
+ // versions created through sudo may still be root-owned.
91
+ await chownTree(ctx.pkiDir, `${lamasteUid}:${lamasteGid}`).catch((err) => {
92
+ if (!/No such file/.test(err.stderr ?? '')) throw err;
93
+ });
94
+
95
+ report('Writing the chisel and Authelia units...');
96
+ const chiselUnitChanged = await writeIfChanged(CHISEL_UNIT_PATH, generateChiselUnit(ctx));
97
+ const autheliaUnitChanged = await writeIfChanged(AUTHELIA_UNIT_PATH, generateAutheliaUnit());
98
+
99
+ return {
100
+ chiselChanged: chisel.changed || chiselUnitChanged,
101
+ autheliaChanged: authelia.changed || autheliaUnitChanged || autheliaDir.migrated,
102
+ };
103
+ }
@@ -0,0 +1,19 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ /**
4
+ * Generate a URL-safe random password string.
5
+ * @param {number} [length=24] Number of random bytes (output is base64url-encoded, so longer).
6
+ * @returns {string}
7
+ */
8
+ export function generatePassword(length = 24) {
9
+ return randomBytes(length).toString('base64url');
10
+ }
11
+
12
+ /**
13
+ * Generate a hex-encoded random secret.
14
+ * @param {number} [length=32] Number of random bytes.
15
+ * @returns {string}
16
+ */
17
+ export function generateHex(length = 32) {
18
+ return randomBytes(length).toString('hex');
19
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Shared systemd unit and sudoers content generators.
3
+ * Used by both the full installer (panel.js) and the redeploy flow (redeploy.js).
4
+ */
5
+
6
+ /**
7
+ * Generate the lamalibre-lamaste-serverd systemd service unit content.
8
+ *
9
+ * @param {{ installDir: string, configDir: string }} ctx
10
+ * @returns {string}
11
+ */
12
+ export function generateServiceUnit(ctx) {
13
+ return `[Unit]
14
+ Description=Lamaste Panel Server
15
+ After=network.target
16
+
17
+ [Service]
18
+ Type=simple
19
+ User=lamaste
20
+ Group=lamaste
21
+ # Lets the panel hand the chisel authfile to the group chisel runs as.
22
+ SupplementaryGroups=lamaste-chisel
23
+ WorkingDirectory=${ctx.installDir}/serverd
24
+ ExecStart=/usr/bin/node src/index.js
25
+ Environment=NODE_ENV=production
26
+ Environment=CONFIG_FILE=${ctx.configDir}/panel.json
27
+ Restart=always
28
+ RestartSec=5
29
+ StandardOutput=journal
30
+ StandardError=journal
31
+ SyslogIdentifier=lamalibre-lamaste-serverd
32
+
33
+ # Security hardening
34
+ # Note: NoNewPrivileges is intentionally omitted — the panel uses sudo for
35
+ # the fixed commands and root-owned helpers listed in /etc/sudoers.d/lamaste
36
+ # (systemctl, nginx -t, lamaste-priv, lamaste-certbot, lamaste-cert-info).
37
+ ProtectHome=true
38
+ ReadWritePaths=${ctx.configDir} /var/www/lamaste
39
+ PrivateTmp=true
40
+
41
+ [Install]
42
+ WantedBy=multi-user.target
43
+ `;
44
+ }
45
+
46
+ /**
47
+ * Generate the lamaste sudoers file content.
48
+ *
49
+ * The panel runs as `lamaste`; this file is everything it may do as root.
50
+ * Every rule is a fixed command line or a root-owned program that validates
51
+ * its own arguments — no rule ends in, or contains, a wildcard. (A sudoers
52
+ * `*` matches spaces, so a wildcard rule such as `mv /tmp/x-* /var/www/*`
53
+ * also accepts `mv /tmp/x-a -t /etc/sudoers.d /var/www/y`.)
54
+ *
55
+ * What the panel does NOT need root for, and therefore has no rule for:
56
+ * - its PKI (/etc/lamalibre/lamaste/pki, owned by lamaste — the panel is the CA),
57
+ * - static sites (/var/www/lamaste, owned lamaste:www-data),
58
+ * - Authelia's configuration (/etc/authelia, owned lamaste:lamaste-authelia),
59
+ * - the chisel authfile and key (group lamaste-chisel, of which lamaste is a member).
60
+ * Binaries, systemd units and service accounts are installed by create-lamaste,
61
+ * which runs as root; the panel never writes a unit or a binary.
62
+ *
63
+ * @returns {string}
64
+ */
65
+ export function generateSudoersContent() {
66
+ return `# Lamaste serverd sudo rules — generated by create-lamaste; do not edit.
67
+ # Every rule is a fixed command line or a root-owned program that validates its
68
+ # own arguments. There are no wildcards: a sudoers \`*\` matches spaces too.
69
+
70
+ # --- systemctl: managed services ---
71
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start nginx
72
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop nginx
73
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
74
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx
75
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start chisel
76
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop chisel
77
+ # Chisel reloads its authfile by itself. The panel restarts it only when a
78
+ # change revokes access (a removed or rotated credential, a withdrawn port
79
+ # grant) — \`try-restart\`, so a chisel stopped on purpose stays stopped.
80
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart chisel
81
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl try-restart chisel
82
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl enable chisel
83
+ # Disabled (with stop) when startup reconciliation cannot secure chisel, so a
84
+ # reboot does not start it on a stale authfile; re-enabled once it succeeds.
85
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl disable chisel
86
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start authelia
87
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop authelia
88
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart authelia
89
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl reload authelia
90
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl enable authelia
91
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl enable certbot.timer
92
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start certbot.timer
93
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start lamalibre-lamaste-serverd
94
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop lamalibre-lamaste-serverd
95
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart lamalibre-lamaste-serverd
96
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start lamalibre-lamaste-gatekeeper
97
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop lamalibre-lamaste-gatekeeper
98
+ lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart lamalibre-lamaste-gatekeeper
99
+
100
+ # --- nginx config test ---
101
+ lamaste ALL=(root) NOPASSWD: /usr/sbin/nginx -t
102
+
103
+ # --- root-owned programs that validate their own arguments ---
104
+ # lamaste-priv: install/enable/remove the panel's nginx vhosts (every directive
105
+ # checked against an allow-list — nginx opens the files a vhost names as
106
+ # root), store a TOTP secret with Authelia's CLI as the Authelia user, and
107
+ # start a self-update (create-lamaste@<version>) in a transient unit.
108
+ lamaste ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-priv
109
+ # Let's Encrypt: certbot and openssl with fixed flags, hostnames and email
110
+ # validated. See scripts/lamaste-certbot, scripts/lamaste-cert-info.
111
+ lamaste ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-certbot
112
+ lamaste ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-cert-info
113
+ `;
114
+ }
115
+
116
+ /**
117
+ * Generate the lamalibre-lamaste-gatekeeper systemd service unit content.
118
+ *
119
+ * @param {{ installDir: string, configDir: string }} ctx
120
+ * @returns {string}
121
+ */
122
+ export function generateGatekeeperServiceUnit(ctx) {
123
+ return `[Unit]
124
+ Description=Lamaste Gatekeeper — tunnel authorization service
125
+ After=network.target authelia.service
126
+
127
+ [Service]
128
+ Type=simple
129
+ User=lamaste
130
+ Group=lamaste
131
+ WorkingDirectory=${ctx.installDir}/gatekeeper
132
+ ExecStart=/usr/bin/node dist/server/index.js
133
+ Environment=NODE_ENV=production
134
+ Environment=LAMALIBRE_LAMASTE_DATA_DIR=${ctx.configDir}
135
+ Restart=always
136
+ RestartSec=5
137
+ StandardOutput=journal
138
+ StandardError=journal
139
+ SyslogIdentifier=lamalibre-lamaste-gatekeeper
140
+
141
+ # Security hardening
142
+ ProtectHome=true
143
+ ReadWritePaths=${ctx.configDir}
144
+ PrivateTmp=true
145
+ NoNewPrivileges=true
146
+
147
+ [Install]
148
+ WantedBy=multi-user.target
149
+ `;
150
+ }
151
+
152
+ /**
153
+ * Root-owned sudoers wrapper scripts shipped in this package's scripts/
154
+ * directory and installed to /usr/local/sbin. The sudoers rules reference
155
+ * these absolute paths; each script validates its arguments and runs its
156
+ * command with a fixed argument vector.
157
+ */
158
+ export const SUDOERS_WRAPPERS = [
159
+ { name: 'lamaste-priv', dest: '/usr/local/sbin/lamaste-priv', interpreter: 'node' },
160
+ { name: 'lamaste-certbot', dest: '/usr/local/sbin/lamaste-certbot', interpreter: 'bash' },
161
+ { name: 'lamaste-cert-info', dest: '/usr/local/sbin/lamaste-cert-info', interpreter: 'bash' },
162
+ ];
163
+
164
+ /**
165
+ * Wrappers earlier versions installed that no sudoers rule references any
166
+ * more; create-lamaste removes them on upgrade. The panel signs agent CSRs
167
+ * and renames PKI files itself: it owns the PKI directory and the CA key.
168
+ */
169
+ export const RETIRED_SUDOERS_WRAPPERS = [
170
+ '/usr/local/sbin/lamaste-sign-csr',
171
+ '/usr/local/sbin/lamaste-pki-rename',
172
+ ];
173
+
174
+ /**
175
+ * The group the chisel server runs as. The 0640 chisel authfile (every
176
+ * agent's tunnel password) belongs to it; the lamaste user is a member so the
177
+ * panel can hand the file to the group without privileges. Must match
178
+ * CHISEL_AUTHFILE_GROUP in @lamalibre/lamaste/server.
179
+ */
180
+ export const CHISEL_GROUP = 'lamaste-chisel';
181
+
182
+ /** The system account Authelia runs as (see accounts.js). */
183
+ export const AUTHELIA_USER = 'lamaste-authelia';
184
+
185
+ export const CHISEL_BIN = '/usr/local/bin/chisel';
186
+ export const AUTHELIA_BIN = '/usr/local/bin/authelia';
187
+ export const CHISEL_UNIT_PATH = '/etc/systemd/system/chisel.service';
188
+ export const AUTHELIA_UNIT_PATH = '/etc/systemd/system/authelia.service';
189
+ export const AUTHELIA_CONFIG_DIR = '/etc/authelia';
190
+ export const AUTHELIA_LOG_DIR = '/var/log/authelia';
191
+ export const WEB_ROOT = '/var/www/lamaste';
192
+
193
+ /**
194
+ * The chisel server unit. The authfile carries per-agent credentials and
195
+ * port grants; chisel reloads it on change. The process runs as `nobody`
196
+ * with group {@link CHISEL_GROUP}, the only group that can read the 0640
197
+ * authfile and key the panel writes. `--keyfile` keeps the SSH host identity
198
+ * stable across restarts.
199
+ *
200
+ * Installed by create-lamaste only; onboarding enables and starts it.
201
+ *
202
+ * @param {{ configDir: string }} ctx
203
+ * @returns {string}
204
+ */
205
+ export function generateChiselUnit(ctx) {
206
+ return `[Unit]
207
+ Description=Chisel Tunnel Server
208
+ After=network.target
209
+
210
+ [Service]
211
+ Type=simple
212
+ User=nobody
213
+ Group=${CHISEL_GROUP}
214
+ ExecStart=${CHISEL_BIN} server --reverse --port 9090 --host 127.0.0.1 --keyfile ${ctx.configDir}/chisel-server.key --authfile ${ctx.configDir}/chisel-users
215
+ Restart=always
216
+ RestartSec=5
217
+ StandardOutput=journal
218
+ StandardError=journal
219
+ SyslogIdentifier=chisel
220
+
221
+ NoNewPrivileges=yes
222
+ CapabilityBoundingSet=
223
+ ProtectSystem=strict
224
+ ProtectHome=yes
225
+ PrivateTmp=yes
226
+ PrivateDevices=yes
227
+ ProtectKernelTunables=yes
228
+ ProtectKernelModules=yes
229
+ ProtectControlGroups=yes
230
+ RestrictNamespaces=yes
231
+ LockPersonality=yes
232
+
233
+ [Install]
234
+ WantedBy=multi-user.target
235
+ `;
236
+ }
237
+
238
+ /**
239
+ * The Authelia unit, after upstream's packaged unit: a dedicated
240
+ * unprivileged account, files it creates readable by its group only, and a
241
+ * read-only view of the system except its configuration directory (the
242
+ * SQLite database and notification file live there) and its log directory.
243
+ *
244
+ * Installed by create-lamaste only; onboarding writes the configuration,
245
+ * then enables and starts it.
246
+ *
247
+ * @returns {string}
248
+ */
249
+ export function generateAutheliaUnit() {
250
+ return `[Unit]
251
+ Description=Authelia Authentication Server
252
+ After=network.target
253
+
254
+ [Service]
255
+ Type=simple
256
+ User=${AUTHELIA_USER}
257
+ Group=${AUTHELIA_USER}
258
+ UMask=027
259
+ Environment=AUTHELIA_SERVER_DISABLE_HEALTHCHECK=true
260
+ ExecStart=${AUTHELIA_BIN} --config ${AUTHELIA_CONFIG_DIR}/configuration.yml
261
+ Restart=always
262
+ RestartSec=5
263
+ StandardOutput=journal
264
+ StandardError=journal
265
+ SyslogIdentifier=authelia
266
+
267
+ NoNewPrivileges=yes
268
+ CapabilityBoundingSet=
269
+ ProtectSystem=strict
270
+ ReadWritePaths=${AUTHELIA_CONFIG_DIR} ${AUTHELIA_LOG_DIR}
271
+ ProtectHome=yes
272
+ PrivateTmp=yes
273
+ PrivateDevices=yes
274
+ ProtectKernelTunables=yes
275
+ ProtectKernelModules=yes
276
+ ProtectControlGroups=yes
277
+ RestrictNamespaces=yes
278
+ LockPersonality=yes
279
+ SystemCallArchitectures=native
280
+ SystemCallFilter=@system-service
281
+ SystemCallErrorNumber=EPERM
282
+
283
+ [Install]
284
+ WantedBy=multi-user.target
285
+ `;
286
+ }
287
+
288
+ /** sites-available name of the port-80 catch-all. */
289
+ export const HTTP_REDIRECT_SITE = 'lamalibre-lamaste-http-redirect';
290
+
291
+ /**
292
+ * The port-80 catch-all: every plain-HTTP request is redirected to HTTPS on
293
+ * the same host and path. Without it nothing listens on :80 and `http://`
294
+ * links to the relay's hostnames are refused.
295
+ *
296
+ * It does not get in the way of Let's Encrypt: certbot's nginx authenticator
297
+ * (`certonly --nginx`, and renewals) clones this default_server block per
298
+ * hostname for the length of an HTTP-01 challenge, answers the challenge
299
+ * path ahead of the redirect, and restores the file afterwards — verified
300
+ * on Ubuntu 24.04 (nginx 1.24, certbot 2.9) for new names, names with an
301
+ * existing 443 block, multi-name lineages, and forced renewals.
302
+ */
303
+ export function generateHttpRedirectVhost() {
304
+ return `# Managed by Lamaste — plain HTTP is redirected to HTTPS.
305
+ server {
306
+ listen 80 default_server;
307
+ server_name _;
308
+ return 301 https://$host$request_uri;
309
+ }
310
+ `;
311
+ }