@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,600 @@
1
+ # Security Model
2
+
3
+ > Lamaste uses a defense-in-depth strategy with multiple independent security layers — OS hardening, firewall, TLS encryption, mTLS authentication, TOTP 2FA, and service isolation — so that no single vulnerability compromises the system.
4
+
5
+ ## In Plain English
6
+
7
+ Security in Lamaste works like layers of protection around a castle. Each layer is independent, so even if an attacker gets past one, they face another.
8
+
9
+ The outermost layer is the **firewall** — only four doors exist (ports 22, 80, 443, and 9292), and all others are sealed shut. The next layer is **encryption** — every conversation is in a secret language (TLS). Then comes **authentication** — the admin panel checks your digital ID card (client certificate), and the tunneled apps check your password and phone code (TOTP). The innermost layer is **isolation** — each service runs in its own room, and even if one is compromised, it cannot reach the others.
10
+
11
+ No single layer is perfect. Firewalls can be misconfigured. TLS has had vulnerabilities. Passwords get stolen. But the chance of all layers failing simultaneously is vanishingly small. This approach — multiple imperfect layers that together provide strong security — is called defense in depth.
12
+
13
+ ## For Users
14
+
15
+ ### What protects you
16
+
17
+ Here is every security measure Lamaste puts in place, from the outside in:
18
+
19
+ #### 1. Firewall (UFW)
20
+
21
+ Only four ports are open on your VPS:
22
+
23
+ | Port | Service | Who needs it |
24
+ | -------- | ------- | ------------------------------------------------------------------ |
25
+ | 22/tcp | SSH | You, during installation only |
26
+ | 80/tcp | HTTP | Let's Encrypt HTTP-01 challenge (certificate issuance and renewal) |
27
+ | 443/tcp | HTTPS | Everyone (domains) |
28
+ | 9292/tcp | HTTPS | You (admin panel via IP; disabled when panel 2FA is enabled) |
29
+
30
+ Every other port is blocked. A port scan of your VPS shows only these four services.
31
+
32
+ #### 2. fail2ban
33
+
34
+ An automated intrusion prevention system that watches log files for suspicious activity:
35
+
36
+ - **SSH jail** — bans an IP for 1 hour after 5 failed login attempts
37
+ - **nginx jail** — bans an IP for 1 hour after 5 failed HTTP authentication attempts
38
+
39
+ Banning means adding a firewall rule that drops all packets from the offending IP.
40
+
41
+ #### 3. SSH hardening
42
+
43
+ After installation, SSH is locked down:
44
+
45
+ | Setting | Value | Effect |
46
+ | --------------------------------- | ------------------- | ------------------------------- |
47
+ | `PasswordAuthentication` | `no` | Only key-based auth accepted |
48
+ | `PermitRootLogin` | `prohibit-password` | Root can log in with keys only |
49
+ | `ChallengeResponseAuthentication` | `no` | No keyboard-interactive prompts |
50
+
51
+ This means SSH brute-force attacks (trying passwords) are impossible. An attacker would need your private SSH key.
52
+
53
+ #### 4. TLS encryption
54
+
55
+ Every connection to your VPS is encrypted with TLS 1.2 or 1.3. Even if someone intercepts the traffic (e.g., on a public Wi-Fi network), they cannot read or modify it.
56
+
57
+ - Domain-based vhosts use Let's Encrypt certificates (trusted by all browsers)
58
+ - The IP-based admin panel uses a self-signed certificate (browser warning, but still encrypted)
59
+
60
+ #### 5. mTLS for admin and agent access
61
+
62
+ The admin panel requires a client certificate at the TLS layer. Without the certificate, nginx rejects the connection before any HTTP traffic is exchanged. See [mTLS](mtls.md) for full details.
63
+
64
+ **Agent-side TLS verification:** The agent verifies both servers it talks to. The panel uses a self-signed TLS server certificate that is separate from the mTLS CA used to sign client certificates, so `lamaste-agent setup` pins the panel's public key on first contact (trust on first use) and every later panel call rejects any other key; `lamaste-agent panel reset-pin` re-captures it after a deliberate rotation. The tunnel relay, `tunnel.<domain>`, has a publicly trusted Let's Encrypt certificate that the agent's Chisel client verifies normally (see 5a below).
65
+
66
+ **P12 password security:** The agent never passes the P12 password as a command-line argument. Curl calls use a temporary config file (created with mode `0600`, deleted after use), and openssl calls use the `LAMALIBRE_LAMASTE_P12_PASS` environment variable. This prevents the password from being visible in process listings (`ps aux`).
67
+
68
+ **Hardware-bound certificates:** Agents can enroll using a one-time token and a locally generated CSR, binding the private key to the macOS Keychain so it is non-extractable. The public `POST /api/enroll` endpoint uses the token as its sole authentication gate — no mTLS is required for enrollment itself, since the agent does not yet have a certificate. After enrollment, the agent authenticates with the Keychain-backed identity just like any other mTLS client. Admins can optionally upgrade their own authentication to hardware-bound mode (set `adminAuthMode` to `"hardware-bound"` in `panel.json`), which disables P12 download and certificate rotation through the panel UI. If the admin Keychain identity is lost, recovery is possible by running `lamaste-reset-admin` on the server, which reverts to standard P12 authentication.
69
+
70
+ Lamaste supports three types of certificates with different access levels:
71
+
72
+ - **Admin certificate** (`CN=admin`) — full access to all panel endpoints (for browser-based management)
73
+ - **Agent certificate** (`CN=agent:<label>`) — capability-based access (for tunnel agents on macOS and Linux)
74
+ - **Plugin-agent certificate** (`CN=plugin-agent:<delegator>:<name>`) — minimal identity for plugin agents participating in the ticket system, created via delegated enrollment
75
+
76
+ Agent certificates are generated from the panel UI and should be used instead of the admin certificate when connecting agents. Plugin-agent certificates are created when an existing agent delegates enrollment for a plugin's agents (e.g., a Sync server vouching for its Sync agents). Each agent is assigned granular capabilities that control what it can access:
77
+
78
+ | Capability | Grants |
79
+ | ---------------- | -------------------------------------------------------------------------------------- |
80
+ | `tunnels:read` | List the agent's own tunnels and fetch its Chisel config (always-on) |
81
+ | `tunnels:write` | Create, update and delete tunnels the agent carries (restricted access mode only) |
82
+ | `services:read` | View service status |
83
+ | `services:write` | Start/stop/restart services |
84
+ | `system:read` | View system stats (CPU, RAM, disk) |
85
+ | `sites:read` | List assigned sites and browse their files |
86
+ | `sites:write` | Upload and delete files on assigned sites |
87
+ | `panel:expose` | Expose agent management panel at `agent-<label>.<domain>` via mTLS-protected subdomain |
88
+ | `identity:read` | Parse Authelia identity headers on plugin routes |
89
+ | `identity:query` | Query panel for Authelia user metadata |
90
+
91
+ Capabilities are stored server-side and can be updated without reissuing the certificate. Regular agents default to `["tunnels:read"]`, while plugin-agents start with no capabilities at all — the admin must explicitly grant capabilities. Plugins can declare additional capabilities in their manifest (flat array or nested `{ agent: [...] }` format); these are merged with base capabilities dynamically and available for assignment to agent certificates. Ticket scopes also contribute capabilities dynamically: when a scope like `shell` declares `scopes: [{ name: 'shell:connect' }]`, the capability `shell:connect` becomes available for assignment alongside base and plugin capabilities. Users, certificates, agent management, and logs always remain admin-only. Site creation and deletion are also admin-only operations.
92
+
93
+ In addition to capabilities, regular agent certificates support **per-site scoping** via `allowedSites`. Each agent has a list of site names it is permitted to access. When an agent calls `GET /api/sites`, it only sees sites in its `allowedSites` list. File operations (upload, list, delete) require both the relevant capability and the site name in the agent's `allowedSites`. The admin manages site assignments from **Panel** > **Certificates** > edit agent > **Site Access**, or via the `PATCH /api/certs/agent/:label/allowed-sites` endpoint. Plugin-agents never have site access.
94
+
95
+ **Cascade revocation:** When a regular agent is revoked, all plugin-agents it delegated are automatically cascade-revoked in the same atomic operation. This ensures that revoking a compromised agent immediately terminates all downstream identities.
96
+
97
+ This three-level model (certificate type + capabilities + site scoping) means that even if a machine is compromised, the attacker is limited to whichever capabilities and sites were assigned to that agent — and the admin can revoke or reduce them immediately.
98
+
99
+ #### 5a. Tunnel ownership and the tunnel relay
100
+
101
+ Every tunnel is carried by exactly one agent (its `agentLabel`), and three things enforce that:
102
+
103
+ - **The panel API.** An agent certificate sees and acts on only its own tunnels — others are absent from its list and answer `404`. `GET /api/tunnels/agent-config` returns only the caller's tunnels. An agent cannot create a tunnel for another agent or move one; only an admin can. A plugin-agent certificate cannot create tunnels at all (it holds no Chisel credential). Every tunnel change runs under one lock with its validation inside it, so two concurrent requests cannot both claim a subdomain or port.
104
+ - **The Chisel authfile.** Each `agent-<label>` Chisel user may reverse-bind exactly `^R:127\.0\.0\.1:<port>$` for the enabled tunnels it owns, and nothing else. A second enrolled machine cannot bind another agent's port and take over its hostname. A `lamaste-no-grants` sentinel user keeps the authfile non-empty, because Chisel disables authentication outright on an empty authfile. The authfile holds every agent's password, so it is `0640`, group `lamaste-chisel` — readable by the Chisel process (`User=nobody`, `Group=lamaste-chisel`) and by nobody else.
105
+ - **Reserved ports.** A tunnel vhost proxies to `127.0.0.1:<port>` on the relay, so the ports of Lamaste's own services — 3100 (panel), 9090 (Chisel), 9091 (Authelia), 9292 (IP panel listener), 9294 (Gatekeeper) — can never be tunnel ports. A tunnel on 3100 or 9292 would otherwise publish the panel API, which trusts the client-certificate headers nginx sets: an administrator takeover. The rule is enforced by the request schema, inside the tunnel workflow, and again when the authfile grants are rendered; a port claimed by two tunnels is never granted either.
106
+ - **TLS verification on the agent.** The agent's Chisel client connects to `https://tunnel.<domain>:443` and verifies its Let's Encrypt certificate — there is no `--tls-skip-verify`. A machine on the path cannot impersonate the relay to collect the agent's credential or read tunnelled traffic. The credential itself is delivered through a 0600 environment file (Linux) or 0600 plist (macOS), never on the command line. The agent only ever connects to `tunnel.<enrolled domain>` and refuses a panel that reports a different domain. Agents upgraded from a version that exposed the credential in process arguments and 0644 unit files rotate it once, automatically.
107
+ - **Revocation releases tunnels.** Revoking an agent certificate removes its Chisel credential and detaches it from its tunnels (app tunnels become unassigned, its panel tunnel is removed), so a later enrollment reusing the label inherits nothing.
108
+ - **Fail closed at startup.** On every start the panel rewrites the Chisel unit and authfile from its state in the background. If that fails, Chisel is **stopped and disabled** (so a reboot cannot start it either) rather than left running with an authfile from an older version that may grant more than it should; reconciliation retries with backoff until it succeeds, then re-enables and starts it.
109
+
110
+ See [Tunneling](tunneling.md#ownership-and-grants) for the mechanics.
111
+
112
+ #### 5b. Tickets for agent-to-agent authorization
113
+
114
+ When agents need to communicate with each other (e.g., remote shell, file transfer), the ticket system adds two more layers of isolation on top of certificate capabilities:
115
+
116
+ 1. **Certificate capability check** — both source and target agents must have the relevant scope capability on their certificates
117
+ 2. **Ticket binding** — the source must own the instance, and the target must be explicitly assigned to it by the admin
118
+
119
+ Tickets are 256-bit random tokens, valid for 30 seconds, single-use, and validated with timing-safe comparison. After a ticket is consumed, a session tracks the connection with heartbeat re-validation every 60 seconds — checking that both certificates are still valid, capabilities still present, and the assignment still exists. If any condition fails, the session is immediately terminated.
120
+
121
+ A third isolation layer (transport CA) is available plugin-side for end-to-end verification, but is not enforced by the panel.
122
+
123
+ Rate limiting (10 tickets/agent/minute) and hard caps (200 instances, 1000 tickets, 500 sessions) protect against resource exhaustion on the 512 MB server.
124
+
125
+ See [Tickets](tickets.md) for the full model.
126
+
127
+ mTLS is stronger than a login page because:
128
+
129
+ - There is no password to brute-force
130
+ - There is no session to hijack
131
+ - There is no login endpoint to discover or attack
132
+ - The rejection happens before any application code runs
133
+
134
+ #### 6. TOTP 2FA for app access
135
+
136
+ Visitors to your tunneled apps authenticate through Authelia with a password and a TOTP code from their phone. See [Authentication](authentication.md) for full details.
137
+
138
+ #### 6b. Optional panel 2FA (admin only)
139
+
140
+ The admin can optionally enable a built-in TOTP 2FA layer for the panel itself. When enabled, the admin must present their mTLS client certificate **and** enter a TOTP code. Agents are exempt and continue to authenticate with mTLS only.
141
+
142
+ Key details:
143
+
144
+ - **Session cookie:** `lamaste_2fa_session`, HMAC-SHA256 signed, with 12-hour absolute expiry and 2-hour inactivity timeout. Cookie flags: `HttpOnly`, `Secure`, `SameSite=Strict`
145
+ - **Rate limiting:** 5 failed attempts within 2 minutes trigger a 5-minute ban
146
+ - **TOTP standard:** RFC 6238, SHA-1, 30-second period, +/-1 step clock drift, replay protection
147
+ - **IP vhost disabled:** When panel 2FA is enabled, the IP-based vhost (`https://<IP>:9292`) is disabled and access is domain-only. This is necessary because session cookies require a domain scope.
148
+ - **Recovery:** `sudo lamaste-reset-admin` clears 2FA settings and re-enables the IP vhost
149
+
150
+ See [Authentication](authentication.md) for setup details.
151
+
152
+ #### 7. Service isolation
153
+
154
+ Every internal service binds to `127.0.0.1` (localhost) only. Even if an attacker somehow reaches your VPS's internal network, they cannot connect to these services from outside:
155
+
156
+ | Service | Bind address | Port |
157
+ | ------------- | ------------ | ---- |
158
+ | Panel server | `127.0.0.1` | 3100 |
159
+ | Authelia | `127.0.0.1` | 9091 |
160
+ | Chisel server | `127.0.0.1` | 9090 |
161
+
162
+ nginx is the only service listening on public interfaces. It acts as a gateway, proxying authenticated requests to the internal services.
163
+
164
+ ### The RAM constraint and bcrypt
165
+
166
+ Your VPS has only 512MB of RAM. This matters for security because some password hashing algorithms are memory-hungry. Argon2id (the "gold standard" for password hashing) allocates ~93MB per hash. On a 512MB system running multiple services, a single authentication attempt with argon2id can consume all available memory and crash everything.
167
+
168
+ Lamaste uses bcrypt instead. Bcrypt uses ~4KB per hash — over 23,000 times less memory — while still providing strong protection against brute-force attacks. The cost factor is set to 12, meaning each hash computation takes roughly 250ms, making large-scale password cracking impractical.
169
+
170
+ ### Cloud provisioning security
171
+
172
+ When using the desktop app to create servers on DigitalOcean, the app enforces several safeguards:
173
+
174
+ - **Custom-scoped tokens** — the app requires 5 DigitalOcean resource groups (account, droplet, regions, ssh_key, tag) and rejects tokens with dangerous permissions (`database:delete`, `kubernetes:create`, `account:write`, etc.). An optional 6th group (`domain`) enables automatic DNS record creation during provisioning
175
+ - **Credential storage** — API tokens and P12 passwords are stored in the OS credential store (macOS Keychain / Linux libsecret), never in plaintext files or CLI arguments
176
+ - **`lamalibre:managed` + `product:lamaste` tag** — the app tags droplets it creates and refuses to destroy untagged droplets
177
+ - **Ephemeral SSH keys** — temporary ed25519 keys are generated for installation, then securely deleted afterward
178
+
179
+ **If you have other infrastructure on DigitalOcean, create a dedicated DigitalOcean team for Lamaste.** API tokens are account-wide — a token with `droplet:delete` can delete any droplet in the account, not just ones created by Lamaste. A separate team provides true resource-level isolation at no extra cost. See the [Cloud Provisioning guide](../02-guides/cloud-provisioning.md) for setup instructions.
180
+
181
+ ### What is NOT included
182
+
183
+ Lamaste does not include:
184
+
185
+ - **Rate limiting at the application level** — fail2ban handles this at the network level
186
+ - **WAF (Web Application Firewall)** — your tunneled apps should implement their own input validation
187
+ - **DDoS protection** — consider Cloudflare or DigitalOcean's cloud firewall for DDoS mitigation
188
+ - **Automatic security updates** — enable Ubuntu's unattended-upgrades for OS patches
189
+
190
+ ## For Developers
191
+
192
+ ### Layer diagram
193
+
194
+ ```
195
+ ┌─────────────────────────────────────────────────────────────────┐
196
+ │ Internet │
197
+ └────────────────────────────┬────────────────────────────────────┘
198
+ │
199
+ ┌────────────────────────────▼────────────────────────────────────┐
200
+ │ Layer 1: UFW Firewall │
201
+ │ Only ports 22, 80, 443, 9292 open │
202
+ │ Everything else: DROP │
203
+ └────────────────────────────┬────────────────────────────────────┘
204
+ │
205
+ ┌────────────────────────────▼────────────────────────────────────┐
206
+ │ Layer 2: fail2ban │
207
+ │ Monitors /var/log/auth.log and /var/log/nginx/error.log │
208
+ │ Bans IPs after 5 failed attempts (1 hour) │
209
+ └────────────────────────────┬────────────────────────────────────┘
210
+ │
211
+ ┌────────────────────────────▼────────────────────────────────────┐
212
+ │ Layer 3: nginx TLS Termination │
213
+ │ TLS 1.2/1.3 only, strong ciphers │
214
+ │ Panel vhosts: mTLS (client cert required) │
215
+ │ App vhosts: Gatekeeper/Authelia (per access mode) │
216
+ └────────────────────────────┬────────────────────────────────────┘
217
+ │
218
+ ┌──────────────┼──────────────┐
219
+ │ │ │
220
+ ┌─────────────▼──┐ ┌───────▼───────┐ ┌──▼──────────────┐
221
+ │ Panel Server │ │ Authelia │ │ Chisel Server │
222
+ │ 127.0.0.1:3100 │ │ 127.0.0.1:9091│ │ 127.0.0.1:9090 │
223
+ │ mTLS verified │ │ TOTP verified │ │ per-agent grants│
224
+ └────────────────┘ └───────────────┘ └─────────────────┘
225
+ ```
226
+
227
+ ### UFW firewall implementation
228
+
229
+ The installer configures UFW in `packages/create-lamaste/src/tasks/harden.js`:
230
+
231
+ ```javascript
232
+ // Set defaults
233
+ await execa('ufw', ['default', 'deny', 'incoming']);
234
+ await execa('ufw', ['default', 'allow', 'outgoing']);
235
+
236
+ // Allow only required ports
237
+ const requiredPorts = ['22/tcp', '80/tcp', '443/tcp', '9292/tcp'];
238
+ for (const port of requiredPorts) {
239
+ await execa('ufw', ['allow', port]);
240
+ }
241
+
242
+ // Enable firewall
243
+ await execa('ufw', ['--force', 'enable']);
244
+ ```
245
+
246
+ The task is idempotent — if UFW is already active with all required ports allowed, it skips the setup entirely. If UFW is active but missing some ports, it adds only the missing rules without resetting existing configuration.
247
+
248
+ ### fail2ban configuration
249
+
250
+ fail2ban is configured via a drop-in file at `/etc/fail2ban/jail.d/lamaste.conf`:
251
+
252
+ ```ini
253
+ [sshd]
254
+ enabled = true
255
+ port = ssh
256
+ filter = sshd
257
+ logpath = /var/log/auth.log
258
+ maxretry = 5
259
+ bantime = 3600
260
+
261
+ [nginx-http-auth]
262
+ enabled = true
263
+ port = http,https
264
+ filter = nginx-http-auth
265
+ logpath = /var/log/nginx/error.log
266
+ maxretry = 5
267
+ bantime = 3600
268
+ ```
269
+
270
+ Two jails are configured:
271
+
272
+ | Jail | Monitors | Trigger | Ban duration |
273
+ | ----------------- | -------------------------- | ------------------- | ------------ |
274
+ | `sshd` | `/var/log/auth.log` | 5 failed SSH logins | 1 hour |
275
+ | `nginx-http-auth` | `/var/log/nginx/error.log` | 5 failed HTTP auths | 1 hour |
276
+
277
+ Using a drop-in file in `jail.d/` rather than modifying `jail.local` preserves any existing fail2ban configuration and makes the Lamaste rules easy to identify and remove.
278
+
279
+ ### SSH hardening implementation
280
+
281
+ The installer modifies `/etc/ssh/sshd_config` with a safe sequence:
282
+
283
+ ```
284
+ 1. Read original sshd_config
285
+ 2. Check if all settings are already correct → skip if so
286
+ 3. Apply regex modifications to produce new content
287
+ 4. Write modified content to a temp file
288
+ 5. Validate with sshd -t -f /path/to/temp
289
+ 6. If validation fails → delete temp, leave original untouched
290
+ 7. If validation passes → back up original, move temp into place
291
+ 8. Restart sshd
292
+ ```
293
+
294
+ The settings applied:
295
+
296
+ ```
297
+ PasswordAuthentication no
298
+ PermitRootLogin prohibit-password
299
+ ChallengeResponseAuthentication no
300
+ ```
301
+
302
+ The pre-installation backup is saved to `/etc/ssh/sshd_config.pre-lamaste` and is only created once (subsequent re-runs skip the backup step to preserve the original).
303
+
304
+ ### Swap file
305
+
306
+ The installer creates a 1GB swap file as a safety net against memory pressure:
307
+
308
+ ```javascript
309
+ await execa('fallocate', ['-l', '1G', '/swapfile']);
310
+ await execa('chmod', ['600', '/swapfile']);
311
+ await execa('mkswap', ['/swapfile']);
312
+ await execa('swapon', ['/swapfile']);
313
+ ```
314
+
315
+ Swappiness is set to 10 (conservative — the kernel prefers using RAM and only swaps under heavy pressure):
316
+
317
+ ```ini
318
+ # /etc/sysctl.d/99-lamaste.conf
319
+ vm.swappiness=10
320
+ ```
321
+
322
+ The swap file is added to `/etc/fstab` to persist across reboots.
323
+
324
+ ### RAM budget
325
+
326
+ The 512MB VPS RAM is carefully allocated:
327
+
328
+ | Service | Typical RAM | Notes |
329
+ | ------------ | ----------- | --------------------------------- |
330
+ | OS baseline | ~120MB | Kernel, systemd, base processes |
331
+ | nginx | ~15MB | Low-memory reverse proxy |
332
+ | Authelia | ~25MB | Go binary, minimal footprint |
333
+ | Chisel | ~20MB | Go binary, WebSocket multiplexing |
334
+ | Panel server | ~30MB | Node.js, Fastify |
335
+ | fail2ban | ~35MB | Python-based, log monitoring |
336
+ | **Total** | **~245MB** | |
337
+ | **Headroom** | **~265MB** | Available for spikes |
338
+ | **Swap** | **1GB** | Safety net |
339
+
340
+ This budget is why bcrypt (not argon2id) is mandatory for password hashing. Argon2id's ~93MB per hash would consume over a third of total RAM on a single authentication attempt.
341
+
342
+ ### bcrypt configuration
343
+
344
+ Authelia is configured with bcrypt cost factor 12:
345
+
346
+ ```yaml
347
+ authentication_backend:
348
+ file:
349
+ path: /etc/authelia/users.yml
350
+ password:
351
+ algorithm: bcrypt
352
+ bcrypt:
353
+ cost: 12
354
+ ```
355
+
356
+ Cost factor 12 means 2^12 = 4096 iterations. Benchmarks on typical hardware:
357
+
358
+ | Cost factor | Time per hash | Suitable for |
359
+ | ----------- | ------------- | ------------------------------- |
360
+ | 10 | ~65ms | High-traffic sites |
361
+ | 12 | ~250ms | Lamaste (good balance) |
362
+ | 14 | ~1000ms | Very high security requirements |
363
+
364
+ At cost 12, an attacker with a stolen hash trying 4 passwords per second would need ~8 years to try 100 million passwords. This is sufficient for a self-hosted system that also has fail2ban and network-level protections.
365
+
366
+ ### Service isolation details
367
+
368
+ All internal services bind to `127.0.0.1`, which means they only accept connections from the same machine. Even if an attacker gains network access to the VPS (e.g., through a compromised container on the same network segment), they cannot reach these services.
369
+
370
+ The Chisel server explicitly sets `--host 127.0.0.1` in its systemd unit, and runs unprivileged:
371
+
372
+ ```ini
373
+ User=nobody
374
+ Group=lamaste-chisel
375
+ ExecStart=/usr/local/bin/chisel server --reverse --port 9090 --host 127.0.0.1 --keyfile /etc/lamalibre/lamaste/chisel-server.key --authfile /etc/lamalibre/lamaste/chisel-users
376
+ ```
377
+
378
+ The unit also sets `NoNewPrivileges`, an empty `CapabilityBoundingSet`, `ProtectSystem=strict`, `ProtectHome`, `PrivateTmp` and `PrivateDevices`. The binary is Chisel 1.12.0, which `create-lamaste` downloads from its fixed release URL and verifies against a pinned SHA-256 before unpacking it. The panel never installs or replaces it: at startup it only checks the installed version and, if it is not the pinned release, logs an error and restarts Chisel on every authfile change until `create-lamaste` is re-run.
379
+
380
+ `--authfile` restricts every connecting client to its own credential and port grants. The reverse listeners Chisel opens for agents also bind `127.0.0.1` only: agents may only request `R:127.0.0.1:<port>` remotes.
381
+
382
+ Authelia is configured in its YAML:
383
+
384
+ ```yaml
385
+ server:
386
+ address: 'tcp://127.0.0.1:9091/'
387
+ ```
388
+
389
+ Authelia is reachable from the internet (`auth.<domain>`), so it runs as neither root nor `lamaste` but as its own system account, `lamaste-authelia`. Its unit (written by `create-lamaste`) sets `UMask=027`, `NoNewPrivileges`, an empty `CapabilityBoundingSet`, `ProtectSystem=strict` with `ReadWritePaths=/etc/authelia /var/log/authelia`, `PrivateTmp`, `PrivateDevices` and `SystemCallFilter=@system-service`. It can read its configuration through its group and write only its database, notification file, `users.yml` (rewritten on a password change) and its log. The binary is Authelia 4.39.28, downloaded by `create-lamaste` from its fixed release URL and verified against a pinned SHA-256 per architecture; a newer installed Authelia is kept, never downgraded.
390
+
391
+ The panel server binds to localhost in its Fastify configuration.
392
+
393
+ nginx is the only service with a public-facing socket. It listens on:
394
+
395
+ - `0.0.0.0:80`, which only redirects to HTTPS (and answers Let's Encrypt HTTP-01 challenges while certbot runs)
396
+ - `0.0.0.0:443` for domain-based vhosts
397
+ - `0.0.0.0:9292` for the IP-based admin panel
398
+
399
+ Tunnel vhosts never pass client-supplied `X-SSL-Client-*` headers to the app (the panel treats those headers from nginx as proof of a client certificate), and public tunnel vhosts also clear the `Remote-*` identity headers, which only an authenticated vhost may set.
400
+
401
+ ### File permissions
402
+
403
+ Sensitive files use restrictive permissions:
404
+
405
+ | File | Mode | Rationale |
406
+ | ------------------------------------------------------------------- | ----- | ------------------------------------------------- |
407
+ | `/etc/lamalibre/lamaste/pki/ca.key` | `600` | CA private key — can sign new client certs |
408
+ | `/etc/lamalibre/lamaste/pki/client.key` | `600` | Client private key |
409
+ | `/etc/lamalibre/lamaste/pki/client.p12` | `600` | PKCS12 bundle with private key |
410
+ | `/etc/lamalibre/lamaste/pki/.p12-password` | `600` | Password for the .p12 file |
411
+ | `/etc/authelia/configuration.yml` | `640` | JWT and session secrets; group `lamaste-authelia` |
412
+ | `/etc/authelia/.secrets.json` | `600` | Secret backup; the panel (`lamaste`) only |
413
+ | `/etc/authelia/users.yml` | `660` | Password hashes; Authelia rewrites it |
414
+ | `/etc/authelia/db.sqlite3`, `notifications.txt` | `600` | Owned by `lamaste-authelia` |
415
+ | `/etc/lamalibre/lamaste/chisel-server.key` | `640` | Chisel SSH host key; group `lamaste-chisel` |
416
+ | `/etc/lamalibre/lamaste/chisel-credentials.json` | `600` | Per-agent Chisel passwords |
417
+ | `/etc/lamalibre/lamaste/chisel-sentinel` | `600` | Sentinel Chisel user password |
418
+ | `/etc/lamalibre/lamaste/chisel-users` | `640` | Chisel authfile; group `lamaste-chisel` only |
419
+ | `~/.lamalibre/lamaste/agents/<label>/` | `700` | Agent data directory (agent machine) |
420
+ | `~/.lamalibre/lamaste/agents/<label>/chisel.json`, `chisel.env` | `600` | Agent's Chisel credential (agent machine) |
421
+ | `~/Library/LaunchAgents/com.lamalibre.lamaste.chisel-<label>.plist` | `600` | Carries `AUTH` in its environment (macOS agent) |
422
+ | `/etc/lamalibre/lamaste/pki/` | `700` | PKI directory itself |
423
+
424
+ Mode `600` means only the file owner can read or write. Mode `640` adds read access for the file's group, `660` also write access. Mode `700` means only the directory owner can list, read, or modify contents. See [Configuration Files](../06-reference/config-files.md#file-permissions-table) for the owner of every directory.
425
+
426
+ ### Privilege boundary
427
+
428
+ The panel runs as the unprivileged `lamaste` user and cannot become root. `/etc/sudoers.d/lamaste` contains no wildcard: a sudoers `*` matches any characters, spaces included, so a rule such as `mv /tmp/site-index-* /var/www/lamaste/*` also accepts `-t /etc/sudoers.d`, `find /var/www/lamaste/*` accepts `-exec`, `cat /etc/authelia/*` reads `/etc/shadow`, `certbot renew --cert-name * ...` accepts `--deploy-hook <command>`, and an `openssl` rule accepts `-engine`. Every rule is either a fixed command line or a root-owned program that validates its own arguments:
429
+
430
+ | Rule | What it allows |
431
+ | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
432
+ | `systemctl <verb> <service>` (fixed lines) | `start`/`stop`/`restart` of `nginx`, `chisel`, `authelia`, `lamalibre-lamaste-serverd`, `lamalibre-lamaste-gatekeeper`; `reload` of `nginx` and `authelia`; `try-restart`/`enable`/`disable` of `chisel`; `enable` of `authelia`; `enable`/`start` of `certbot.timer` |
433
+ | `nginx -t` | Test the nginx configuration |
434
+ | `/usr/local/sbin/lamaste-priv` | Install/enable/remove the panel's own vhosts, store a TOTP secret, start a self-update (see below) |
435
+ | `/usr/local/sbin/lamaste-certbot` | `certbot certonly --nginx` (validated hostnames and email), `renew`, `renew-all`, `list` |
436
+ | `/usr/local/sbin/lamaste-cert-info` | Read-only `openssl` queries (expiry, 24 h validity, SAN) on a Let's Encrypt lineage |
437
+
438
+ There is no rule for `mv`, `cp`, `rm`, `chmod`, `chown`, `mkdir`, `cat`, `find`, `du`, `test`, `ln`, `openssl`, `systemd-run` or `systemctl daemon-reload`. What the panel does without them:
439
+
440
+ - **Its own files.** The PKI directory (the panel is the CA, and signs agent CSRs itself — see [Certificates](certificates.md)), the web root (`/var/www/lamaste`, `lamaste:www-data`), Authelia's configuration (`/etc/authelia`, `lamaste:lamaste-authelia`) and the Chisel authfile and key (group `lamaste-chisel`, of which `lamaste` is a member) belong to it or to a group it shares.
441
+ - **Binaries, units, accounts.** Chisel and Authelia (pinned, digest-verified releases), their systemd units and the service accounts are installed by `create-lamaste`, which runs as root. The panel never writes a unit or a binary; onboarding only checks they exist.
442
+ - **Code.** `/opt/lamalibre/lamaste` (panel, gatekeeper, `lamaste-server` CLI, UI, docs) is root-owned and read-only for `lamaste`, so a compromised panel cannot rewrite code that root later runs through `sudo lamaste-server`.
443
+
444
+ #### `lamaste-priv`
445
+
446
+ `/usr/local/sbin/lamaste-priv` is a root-owned (`0755`), zero-dependency Node.js program installed by `create-lamaste`. Content (vhost text, TOTP secrets) arrives on stdin, never on the command line, so it is not written to the sudo log. It reads nothing the `lamaste` user can write.
447
+
448
+ | Operation | Does |
449
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
450
+ | `nginx-site write\|backup\|restore\|discard-backup\|enable\|disable\|remove <name>` | Manage `sites-available/<name>` (and its `.bak`) and its `sites-enabled` link. `write` validates the vhost on stdin first. |
451
+ | `authelia-totp <username>` | Store the TOTP secret from stdin with Authelia's CLI, run as `lamaste-authelia` (the database is that account's file) |
452
+ | `self-update <version>` | Start `npx @lamalibre/create-lamaste@<version> --yes` in a transient systemd unit two seconds later (`MAJOR.MINOR.PATCH` only) |
453
+
454
+ Site names are limited to the panel's own vhosts: `lamalibre-lamaste-panel-domain`, `-auth`, `-tunnel`, `-app-<subdomain>`, `-agent-panel-<label>` and `-site-<uuid>`. `lamalibre-lamaste-panel-ip` (written by `create-lamaste`) may only be enabled or disabled.
455
+
456
+ **Why vhosts are validated.** nginx's master process runs as root and opens the files a configuration names: `access_log /etc/cron.d/x` with a crafted `log_format` would be a root shell. `lamaste-priv` parses every vhost and accepts only directives on a per-context allow-list, with arguments that cannot name a file outside what Lamaste serves:
457
+
458
+ - `listen 443` (`[::]:443`), optionally `ssl` / `http2` — port 80 belongs to the installer's HTTPS redirect
459
+ - `server_name` with hostnames only
460
+ - `ssl_certificate` / `ssl_certificate_key` only under `/etc/letsencrypt/live/<name>/{fullchain,privkey}.pem`
461
+ - `include` only `/etc/nginx/snippets/lamalibre-lamaste-*.conf`
462
+ - `root` / `alias` only under `/var/www/lamaste/`
463
+ - `proxy_pass` only `http://127.0.0.1:<port>[/path]`
464
+ - the `proxy_*`, `auth_request`, `auth_request_set`, `limit_req`, `limit_req_zone`, `map`, `add_header`, `error_page`, `return`, `rewrite`, `try_files`, `if`, `index`, `ssl_protocols`/`ssl_ciphers` directives the panel's generators use
465
+
466
+ Anything else is refused — including directives nginx would accept, backslashes, `${`, and non-ASCII text outside comments. A refused write leaves the previous vhost in place. `npm test` runs the allow-list's refusal tests and checks that every vhost the panel generates passes it.
467
+
468
+ #### Root code never writes through the panel's paths
469
+
470
+ `create-lamaste` (including a self-update the panel triggers) and `sudo lamaste-server` run as root on directories `lamaste` can write to, which a compromised panel could have seeded with symlinks. So root never writes through an existing path there: new files are created `O_EXCL | O_NOFOLLOW` under a random name, `fchown`ed and renamed into place; temporary directories are `mkdtemp`; `lamaste-server reset-admin` runs `openssl`, `cp` and `chmod` as `lamaste`; `lamaste-server plugins install|enable|disable|uninstall` re-runs itself as `lamaste` (npm and state writes) and only restarts the panel as root.
471
+
472
+ ### Secret generation
473
+
474
+ All secrets are generated using `crypto.randomBytes`, which reads from the operating system's cryptographically secure random number generator (`/dev/urandom` on Linux):
475
+
476
+ ```javascript
477
+ import { randomBytes } from 'crypto';
478
+ const password = randomBytes(24).toString('base64url');
479
+ ```
480
+
481
+ No secrets are hardcoded in the codebase. Each installation generates unique secrets for:
482
+
483
+ - PKCS12 bundle password
484
+ - Authelia JWT secret
485
+ - Authelia session secret
486
+ - Authelia storage encryption key
487
+
488
+ ### Onboarding state transitions
489
+
490
+ The onboarding system enforces a strict state machine that prevents accessing management features before the system is fully provisioned:
491
+
492
+ ```
493
+ FRESH → DOMAIN_SET → DNS_READY → PROVISIONING → COMPLETED
494
+ ```
495
+
496
+ | State | Onboarding endpoints | Management endpoints |
497
+ | -------------- | -------------------- | -------------------- |
498
+ | `FRESH` | Available | Return 503 |
499
+ | `DOMAIN_SET` | Available | Return 503 |
500
+ | `DNS_READY` | Available | Return 503 |
501
+ | `PROVISIONING` | Available | Return 503 |
502
+ | `COMPLETED` | Return 410 Gone | Available |
503
+
504
+ After onboarding completes, all onboarding endpoints return 410 Gone. This prevents re-running onboarding, which could overwrite configuration or issue duplicate certificates.
505
+
506
+ ### Atomic file writes
507
+
508
+ Configuration files that are read live by services (like Authelia's `users.yml`) are written atomically: a temporary file in the same directory, `fsync`, then `rename`. The rename is atomic on the same filesystem, so Authelia never reads a partially written file. `/etc/authelia` is setgid `lamaste-authelia`, so the new file belongs to that group; its mode decides whether Authelia may read it (`0640`), also rewrite it (`0660`), or not see it (`0600`). No `sudo` is involved.
509
+
510
+ ### Source files
511
+
512
+ | File | Purpose |
513
+ | ---------------------------------------------------------- | ------------------------------------------------------ |
514
+ | `packages/create-lamaste/src/tasks/harden.js` | Swap, UFW, fail2ban, SSH hardening |
515
+ | `packages/create-lamaste/src/tasks/mtls.js` | PKI generation, file permissions |
516
+ | `packages/create-lamaste/src/tasks/nginx.js` | mTLS snippet, self-signed cert |
517
+ | `packages/lamaste-serverd/src/lib/authelia.js` | bcrypt hashing, atomic writes |
518
+ | `packages/lamaste-serverd/src/lib/mtls.js` | Certificate rotation with rollback |
519
+ | `packages/lamaste-serverd/src/lib/nginx.js` | Vhost write-with-rollback pattern |
520
+ | `packages/lamaste-serverd/src/middleware/mtls.js` | Request-level mTLS check |
521
+ | `packages/lamaste-serverd/src/lib/totp.js` | Panel 2FA TOTP verification and replay protection |
522
+ | `packages/lamaste-serverd/src/lib/session.js` | HMAC-SHA256 session cookie signing |
523
+ | `packages/lamaste-serverd/src/middleware/twofa-session.js` | 2FA session enforcement (after mTLS, before roleGuard) |
524
+
525
+ ## Quick Reference
526
+
527
+ ### Security layers
528
+
529
+ | Layer | Technology | Blocks |
530
+ | -------------------- | ------------------------------------ | ------------------------------------- |
531
+ | Firewall | UFW | Connections to non-allowed ports |
532
+ | Intrusion prevention | fail2ban | Repeated failed login attempts |
533
+ | SSH hardening | sshd_config | Password-based SSH access |
534
+ | TLS encryption | Let's Encrypt / self-signed | Traffic interception and tampering |
535
+ | Admin auth | mTLS client certificates | Unauthorized admin access |
536
+ | Admin 2FA (optional) | Built-in TOTP | Stolen certificate abuse |
537
+ | Agent-to-agent auth | Tickets (time-limited, single-use) | Unauthorized cross-agent access |
538
+ | Tunnel ownership | Chisel authfile per-agent grants | One agent hijacking another's host |
539
+ | Reserved ports | Tunnel port validation | Publishing internal services |
540
+ | Privileged commands | No-wildcard sudoers, `lamaste-priv` | Root code execution via extra flags |
541
+ | Relay authenticity | Agent verifies `tunnel.<domain>` TLS | Relay impersonation, credential theft |
542
+ | App auth | Authelia TOTP 2FA | Unauthorized app access |
543
+ | Service isolation | `127.0.0.1` binding | Direct access to internal services |
544
+ | File permissions | `chmod 600` | Unauthorized secret access |
545
+ | Atomic writes | `mv` pattern | Partial file reads by services |
546
+
547
+ ### Firewall rules
548
+
549
+ ```bash
550
+ # View current UFW rules
551
+ sudo ufw status verbose
552
+
553
+ # Expected output:
554
+ # 22/tcp ALLOW IN Anywhere
555
+ # 80/tcp ALLOW IN Anywhere
556
+ # 443/tcp ALLOW IN Anywhere
557
+ # 9292/tcp ALLOW IN Anywhere
558
+ ```
559
+
560
+ ### fail2ban commands
561
+
562
+ ```bash
563
+ # Check jail status
564
+ sudo fail2ban-client status
565
+
566
+ # Check specific jail
567
+ sudo fail2ban-client status sshd
568
+ sudo fail2ban-client status nginx-http-auth
569
+
570
+ # Unban an IP manually
571
+ sudo fail2ban-client set sshd unbanip 1.2.3.4
572
+ ```
573
+
574
+ ### RAM budget
575
+
576
+ | Service | RAM | Percentage of 512MB |
577
+ | ----------------- | ---------- | ------------------- |
578
+ | OS baseline | ~120MB | 23% |
579
+ | nginx | ~15MB | 3% |
580
+ | Authelia (bcrypt) | ~25MB | 5% |
581
+ | Chisel | ~20MB | 4% |
582
+ | Panel server | ~30MB | 6% |
583
+ | fail2ban | ~35MB | 7% |
584
+ | **Total** | **~245MB** | **48%** |
585
+ | **Available** | **~265MB** | **52%** |
586
+
587
+ ### Password hashing comparison
588
+
589
+ | Algorithm | Memory per hash | Time per hash | Suitable for 512MB VPS |
590
+ | ------------------ | --------------- | ------------- | ---------------------- |
591
+ | bcrypt (cost 12) | ~4KB | ~250ms | Yes |
592
+ | argon2id (default) | ~93MB | ~300ms | No (causes OOM) |
593
+
594
+ ### Related documentation
595
+
596
+ - [mTLS](mtls.md) — client certificate authentication in detail
597
+ - [Authentication](authentication.md) — Authelia TOTP 2FA in detail
598
+ - [nginx Reverse Proxy](nginx-reverse-proxy.md) — nginx as the security gateway
599
+ - [Certificates](certificates.md) — TLS certificate management
600
+ - [Tunneling](tunneling.md) — secure tunnel architecture