@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,830 @@
1
+ # nginx Configuration Architecture
2
+
3
+ > nginx is the only public-facing service in Lamaste. It handles TLS termination, mTLS enforcement, reverse proxying, forward authentication, and WebSocket upgrades for all traffic.
4
+
5
+ ## In Plain English
6
+
7
+ Every request to Lamaste passes through nginx first. For the admin panel, nginx checks that your browser has a valid client certificate before letting the request through. For tunneled apps, nginx asks Authelia to verify the user's identity. For tunnel connections from your machine, nginx upgrades the connection to a WebSocket and forwards it to the Chisel tunnel server.
8
+
9
+ nginx is the gatekeeper. The services behind it (Panel Server, Authelia, Chisel) all bind to `127.0.0.1` and are invisible from the internet. nginx is what makes them accessible — securely and selectively.
10
+
11
+ ## Overview
12
+
13
+ ```
14
+ Internet
15
+ │
16
+ ├── :80 (every hostname)
17
+ │ └── lamalibre-lamaste-http-redirect (default_server)
18
+ │ └── 301 → https://$host$request_uri
19
+ │
20
+ ├── :9292 (IP-based, always available)
21
+ │ └── lamalibre-lamaste-panel-ip
22
+ │ ├── Self-signed TLS cert
23
+ │ ├── mTLS client cert required
24
+ │ └── Proxy → 127.0.0.1:3100 (Panel Server)
25
+ │
26
+ └── :443 (domain-based, after onboarding)
27
+ │
28
+ ├── panel.<domain>
29
+ │ └── lamalibre-lamaste-panel-domain
30
+ │ ├── Let's Encrypt TLS cert
31
+ │ ├── mTLS client cert required
32
+ │ └── Proxy → 127.0.0.1:3100 (Panel Server)
33
+ │
34
+ ├── auth.<domain>
35
+ │ └── lamalibre-lamaste-auth
36
+ │ ├── Let's Encrypt TLS cert
37
+ │ └── Proxy → 127.0.0.1:9091 (Authelia)
38
+ │
39
+ ├── tunnel.<domain>
40
+ │ └── lamalibre-lamaste-tunnel
41
+ │ ├── Let's Encrypt TLS cert
42
+ │ ├── WebSocket upgrade
43
+ │ └── Proxy → 127.0.0.1:9090 (Chisel Server)
44
+ │
45
+ └── <app>.<domain>
46
+ └── lamalibre-lamaste-app-<name>
47
+ ├── Let's Encrypt TLS cert
48
+ ├── Gatekeeper/Authelia auth (unless the tunnel is public)
49
+ ├── client_max_body_size (per tunnel)
50
+ ├── Proxy headers per location; X-SSL-Client-* cleared
51
+ ├── WebSocket upgrade
52
+ └── Proxy → 127.0.0.1:<port> (Chisel → carrying agent)
53
+ ```
54
+
55
+ ## Vhost Types
56
+
57
+ Lamaste manages nine categories of nginx vhosts. Each is generated programmatically by the Panel Server's `nginx.js` library or the installer's `nginx.js` task.
58
+
59
+ ### 1. IP Panel Vhost (`lamalibre-lamaste-panel-ip`)
60
+
61
+ Created by the installer. The fallback admin access path that always works.
62
+
63
+ ```nginx
64
+ map $http_upgrade $connection_upgrade {
65
+ default upgrade;
66
+ '' close;
67
+ }
68
+
69
+ server {
70
+ listen 9292 ssl;
71
+ server_name _;
72
+
73
+ ssl_certificate /etc/lamalibre/lamaste/pki/self-signed.pem;
74
+ ssl_certificate_key /etc/lamalibre/lamaste/pki/self-signed-key.pem;
75
+
76
+ include /etc/nginx/snippets/lamalibre-lamaste-mtls.conf;
77
+
78
+ ssl_protocols TLSv1.2 TLSv1.3;
79
+ ssl_ciphers HIGH:!aNULL:!MD5;
80
+ ssl_prefer_server_ciphers on;
81
+
82
+ # Certificate help page for visitors without client cert
83
+ error_page 495 496 /cert-help.html;
84
+ location = /cert-help.html {
85
+ root /opt/lamalibre/lamaste/lamaste-server-ui;
86
+ internal;
87
+ }
88
+
89
+ # Proxy to lamalibre-lamaste-serverd
90
+ location / {
91
+ proxy_pass http://127.0.0.1:3100;
92
+
93
+ # Client cert headers — set from nginx TLS variables, never passed through from client
94
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
95
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
96
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
97
+
98
+ # Standard proxy headers
99
+ proxy_set_header Host $host;
100
+ proxy_set_header X-Real-IP $remote_addr;
101
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
102
+ proxy_set_header X-Forwarded-Proto $scheme;
103
+ }
104
+
105
+ # API paths with WebSocket upgrade support
106
+ location /api {
107
+ proxy_pass http://127.0.0.1:3100;
108
+ proxy_http_version 1.1;
109
+
110
+ # Client cert headers — set from nginx TLS variables, never passed through from client
111
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
112
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
113
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
114
+
115
+ # Standard proxy headers
116
+ proxy_set_header Host $host;
117
+ proxy_set_header X-Real-IP $remote_addr;
118
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
119
+ proxy_set_header X-Forwarded-Proto $scheme;
120
+
121
+ # WebSocket: only upgrade when client requests it
122
+ proxy_set_header Upgrade $http_upgrade;
123
+ proxy_set_header Connection $connection_upgrade;
124
+ }
125
+ }
126
+ ```
127
+
128
+ Key characteristics:
129
+
130
+ - Listens on port 9292, separate from standard HTTPS port 443
131
+ - Uses a self-signed TLS certificate (browser will warn, this is expected)
132
+ - Includes the mTLS snippet for client certificate enforcement
133
+ - Error pages 495 (no cert) and 496 (bad cert) serve a static help page with import instructions
134
+ - Two `location` blocks: `/` for standard HTTP proxy, `/api` for WebSocket-capable proxy
135
+
136
+ ### 2. Domain Panel Vhost (`lamalibre-lamaste-panel-domain`)
137
+
138
+ Created during onboarding provisioning. Domain-based panel access with proper TLS.
139
+
140
+ ```nginx
141
+ server {
142
+ listen 443 ssl;
143
+ server_name panel.example.com;
144
+
145
+ ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
146
+ ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
147
+
148
+ # mTLS — same as IP-based access
149
+ include /etc/nginx/snippets/lamalibre-lamaste-mtls.conf;
150
+
151
+ ssl_protocols TLSv1.2 TLSv1.3;
152
+ ssl_ciphers HIGH:!aNULL:!MD5;
153
+ ssl_prefer_server_ciphers on;
154
+
155
+ location / {
156
+ proxy_pass http://127.0.0.1:3100;
157
+
158
+ # Client cert headers — set from nginx TLS variables, never passed through from client
159
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
160
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
161
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
162
+
163
+ # Standard proxy headers
164
+ proxy_set_header Host $host;
165
+ proxy_set_header X-Real-IP $remote_addr;
166
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
167
+ proxy_set_header X-Forwarded-Proto $scheme;
168
+ }
169
+
170
+ # API paths with WebSocket upgrade support
171
+ location /api {
172
+ proxy_pass http://127.0.0.1:3100;
173
+ proxy_http_version 1.1;
174
+
175
+ # Client cert headers — set from nginx TLS variables, never passed through from client
176
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
177
+ proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
178
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
179
+
180
+ # Standard proxy headers
181
+ proxy_set_header Host $host;
182
+ proxy_set_header X-Real-IP $remote_addr;
183
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
184
+ proxy_set_header X-Forwarded-Proto $scheme;
185
+
186
+ # WebSocket: only upgrade when client requests it
187
+ proxy_set_header Upgrade $http_upgrade;
188
+ proxy_set_header Connection $connection_upgrade;
189
+ }
190
+ }
191
+ ```
192
+
193
+ Identical to the IP vhost except:
194
+
195
+ - Listens on port 443 (standard HTTPS)
196
+ - Uses Let's Encrypt certificate (no browser warning)
197
+ - `server_name` is `panel.<domain>`
198
+ - Requires the `map $http_upgrade $connection_upgrade` block from the IP vhost (or a shared config) to be present
199
+
200
+ ### 3. Auth Vhost (`lamalibre-lamaste-auth`)
201
+
202
+ Created during onboarding provisioning. Serves the Authelia authentication portal.
203
+
204
+ ```nginx
205
+ server {
206
+ listen 443 ssl;
207
+ server_name auth.example.com;
208
+
209
+ ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
210
+ ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
211
+
212
+ ssl_protocols TLSv1.2 TLSv1.3;
213
+ ssl_ciphers HIGH:!aNULL:!MD5;
214
+ ssl_prefer_server_ciphers on;
215
+
216
+ proxy_set_header Host $host;
217
+ proxy_set_header X-Real-IP $remote_addr;
218
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
219
+ proxy_set_header X-Forwarded-Proto $scheme;
220
+
221
+ # Invitation page — static HTML
222
+ location /invite/ {
223
+ alias /var/www/lamaste/invite/;
224
+ try_files $uri /invite/index.html;
225
+ }
226
+
227
+ # Invitation API — proxied to panel server (no mTLS)
228
+ location /api/invite/ {
229
+ proxy_pass http://127.0.0.1:3100/api/invite/;
230
+ proxy_set_header Host $host;
231
+ proxy_set_header X-Real-IP $remote_addr;
232
+ proxy_set_header X-Forwarded-For $remote_addr;
233
+ proxy_set_header X-Forwarded-Proto $scheme;
234
+ }
235
+
236
+ # Default — proxy to Authelia
237
+ location / {
238
+ proxy_pass http://127.0.0.1:9091;
239
+ }
240
+ }
241
+ ```
242
+
243
+ No mTLS requirement — end users access this to authenticate. Protected by Authelia's own login form and TOTP. The auth vhost also serves the invitation page (static HTML at `/invite/`) and proxies invitation API requests (`/api/invite/`) to the panel server, allowing invited users to set up their accounts without needing mTLS credentials.
244
+
245
+ ### 4. Tunnel Vhost (`lamalibre-lamaste-tunnel`)
246
+
247
+ Created during onboarding provisioning. WebSocket endpoint for Chisel client connections.
248
+
249
+ ```nginx
250
+ server {
251
+ listen 443 ssl;
252
+ server_name tunnel.example.com;
253
+
254
+ ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem;
255
+ ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem;
256
+
257
+ # ... standard SSL and proxy headers
258
+
259
+ location / {
260
+ proxy_pass http://127.0.0.1:9090;
261
+ proxy_http_version 1.1;
262
+ proxy_set_header Upgrade $http_upgrade;
263
+ proxy_set_header Connection "upgrade";
264
+
265
+ # Long timeout for persistent tunnel connections
266
+ proxy_read_timeout 86400s;
267
+ proxy_send_timeout 86400s;
268
+ }
269
+ }
270
+ ```
271
+
272
+ Key characteristics:
273
+
274
+ - WebSocket upgrade is mandatory (Chisel uses WebSocket transport)
275
+ - 24-hour read/send timeouts to keep tunnel connections alive without nginx closing them
276
+ - No mTLS and no Authelia — Chisel handles its own authentication
277
+
278
+ ### 5. App Vhost (`lamalibre-lamaste-app-<name>`)
279
+
280
+ Created when a tunnel is added via the management UI or `POST /api/tunnels`. Each tunnel gets its own vhost, rendered by one of three writers according to the tunnel's `accessMode`:
281
+
282
+ | `accessMode` | Writer | Authorization | Section |
283
+ | ---------------------- | --------------------------- | -------------------------------------------- | ------- |
284
+ | `restricted` (default) | `writeRestrictedVhost()` | Gatekeeper: Authelia session **and** a grant | 9 |
285
+ | `authenticated` | `writeAuthenticatedVhost()` | Gatekeeper: any Authelia session | 8 |
286
+ | `public` | `writePublicVhost()` | None | 7 |
287
+
288
+ All three proxy to `127.0.0.1:<port>` — the port Chisel binds on the server for the tunnel's carrying agent — with WebSocket upgrade and 24-hour timeouts, and all three emit the tunnel's request body limit:
289
+
290
+ ```nginx
291
+ client_max_body_size 10m; # from the tunnel's maxBodySizeMb (1–10240, default 10)
292
+ ```
293
+
294
+ All three also declare the same request headers inside their `location /` (`TUNNEL_PROXY_HEADERS` in `nginx.js`):
295
+
296
+ ```nginx
297
+ proxy_set_header Host $host;
298
+ proxy_set_header X-Real-IP $remote_addr;
299
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
300
+ proxy_set_header X-Forwarded-Proto $scheme;
301
+
302
+ # Never forward client-supplied certificate headers
303
+ proxy_set_header X-SSL-Client-Verify "";
304
+ proxy_set_header X-SSL-Client-DN "";
305
+ proxy_set_header X-SSL-Client-Serial "";
306
+
307
+ # WebSocket support
308
+ proxy_set_header Upgrade $http_upgrade;
309
+ proxy_set_header Connection "upgrade";
310
+ ```
311
+
312
+ - **Per location, not per server.** nginx inherits `proxy_set_header` from the enclosing level only when a level declares none of its own. Every tunnel location declares some (`Upgrade`, `Remote-*`), so headers placed at server level never reached the app — it saw `Host: 127.0.0.1:<port>` and no client address. Declared in the location, `Host`, `X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto` now reach the app.
313
+ - **Client-certificate headers are always cleared.** The panel treats `X-SSL-Client-*` from nginx as proof of an mTLS client certificate. A tunnel vhost never passes client-supplied values through, whatever listens on its port.
314
+ - **Identity headers.** Public vhosts also clear `Remote-User`, `Remote-Groups`, `Remote-Name` and `Remote-Email` — a public tunnel has no authenticated user, and an app might trust those headers from an authenticated vhost. Authenticated and restricted vhosts set them from the Gatekeeper's answer, which replaces anything the client sent.
315
+
316
+ A tunnel created before `maxBodySizeMb` existed has no value and its vhost has no directive, so nginx applies its built-in 1 MiB limit until a body limit is set on it (`PATCH /api/tunnels/:id` with `maxBodySizeMb`, or **Edit** in the panel). Changing the access mode or body limit re-renders the vhost in place through the write-with-rollback pattern below; a disabled tunnel's vhost stays disabled.
317
+
318
+ Agent panel tunnels (`type: 'panel'`) use a separate mTLS vhost (`writeAgentPanelVhost()`) with no body limit setting.
319
+
320
+ ### 6. Static Site Vhost (`lamalibre-lamaste-site-<id>`)
321
+
322
+ Created when a static site is added via the management UI. Serves files directly from disk.
323
+
324
+ ```nginx
325
+ server {
326
+ listen 443 ssl;
327
+ server_name blog.example.com;
328
+
329
+ ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
330
+ ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
331
+
332
+ root /var/www/lamaste/<site-id>/;
333
+ index index.html;
334
+
335
+ # Security headers
336
+ add_header X-Frame-Options SAMEORIGIN always;
337
+ add_header X-Content-Type-Options nosniff always;
338
+
339
+ # Optional Authelia forward auth (auth_request to 127.0.0.1:9091/api/authz/auth-request)
340
+
341
+ location / {
342
+ try_files $uri $uri/ =404; # Standard mode
343
+ # OR
344
+ try_files $uri $uri/ /index.html; # SPA mode
345
+ }
346
+ }
347
+
348
+ # Only for a custom-domain site with aliases — same certificate lineage
349
+ server {
350
+ listen 443 ssl;
351
+ server_name www.myblog.net;
352
+
353
+ ssl_certificate /etc/letsencrypt/live/myblog.net/fullchain.pem;
354
+ ssl_certificate_key /etc/letsencrypt/live/myblog.net/privkey.pem;
355
+
356
+ return 301 https://myblog.net$request_uri;
357
+ }
358
+ ```
359
+
360
+ Two serving modes based on the `spaMode` flag:
361
+
362
+ - **Standard**: `try_files $uri $uri/ =404` — serves files as-is, 404 for missing paths
363
+ - **SPA**: `try_files $uri $uri/ /index.html` — falls back to `index.html` for client-side routing
364
+
365
+ Optional Authelia protection is controlled by the `autheliaProtected` flag on the site object.
366
+
367
+ A custom-domain site may list `aliases` (e.g. `www.myblog.net`). They are served by a second server block that answers with a 301 to the primary host. One certificate lineage, named after the site's domain (`certbot certonly --nginx --cert-name <fqdn> -d <fqdn> -d <alias> ...`), covers the primary name and every alias, so both blocks point at the same `live/<fqdn>/` directory.
368
+
369
+ ### 7. Public Tunnel Vhost (`lamalibre-lamaste-app-<name>`, access mode: public)
370
+
371
+ Created when a tunnel is configured with public access. Direct proxy with no authentication.
372
+
373
+ ```nginx
374
+ server {
375
+ listen 443 ssl;
376
+ server_name public-app.example.com;
377
+
378
+ ssl_certificate /etc/letsencrypt/live/public-app.example.com/fullchain.pem;
379
+ ssl_certificate_key /etc/letsencrypt/live/public-app.example.com/privkey.pem;
380
+
381
+ # ... standard SSL settings
382
+ client_max_body_size 10m;
383
+
384
+ location / {
385
+ # A public tunnel has no authenticated user: clear identity headers
386
+ # an app might trust from an authenticated vhost.
387
+ proxy_set_header Remote-User "";
388
+ proxy_set_header Remote-Groups "";
389
+ proxy_set_header Remote-Name "";
390
+ proxy_set_header Remote-Email "";
391
+
392
+ # ... the tunnel proxy headers (section 5): Host, X-Real-IP,
393
+ # X-Forwarded-For/Proto, X-SSL-Client-* cleared, WebSocket
394
+
395
+ proxy_pass http://127.0.0.1:<port>;
396
+ proxy_http_version 1.1;
397
+ proxy_read_timeout 86400s;
398
+ proxy_send_timeout 86400s;
399
+ }
400
+ }
401
+ ```
402
+
403
+ No `auth_request`, no Authelia, no Gatekeeper. Traffic is proxied directly to the tunneled app. Use only for services that handle their own authentication or are intentionally public.
404
+
405
+ ### 8. Authenticated Tunnel Vhost (`lamalibre-lamaste-app-<name>`, access mode: authenticated)
406
+
407
+ Created when a tunnel requires Authelia login but no per-user grant checking. Any authenticated Authelia user can access the tunnel.
408
+
409
+ ```nginx
410
+ server {
411
+ listen 443 ssl;
412
+ server_name app.example.com;
413
+
414
+ ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
415
+ ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
416
+
417
+ # ... standard SSL settings
418
+ client_max_body_size 10m;
419
+
420
+ # Gatekeeper authorization subrequest (handles Authelia validation + grant check)
421
+ location /internal/lamaste/authz {
422
+ internal;
423
+
424
+ proxy_pass http://127.0.0.1:9294/authz/check;
425
+ proxy_pass_request_body off;
426
+
427
+ proxy_set_header Content-Length "";
428
+ proxy_set_header Connection "";
429
+ proxy_set_header X-Original-Method $request_method;
430
+ proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
431
+ proxy_set_header X-Forwarded-For $remote_addr;
432
+ proxy_set_header Host $http_host;
433
+ proxy_set_header Cookie $http_cookie;
434
+
435
+ proxy_http_version 1.1;
436
+ proxy_buffers 4 32k;
437
+ proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
438
+ }
439
+
440
+ location / {
441
+ auth_request /internal/lamaste/authz;
442
+ auth_request_set $user $upstream_http_remote_user;
443
+ auth_request_set $groups $upstream_http_remote_groups;
444
+ auth_request_set $name $upstream_http_remote_name;
445
+ auth_request_set $email $upstream_http_remote_email;
446
+
447
+ # Identity comes from the Gatekeeper's answer, replacing anything
448
+ # the client sent under these names
449
+ proxy_set_header Remote-User $user;
450
+ proxy_set_header Remote-Groups $groups;
451
+ proxy_set_header Remote-Name $name;
452
+ proxy_set_header Remote-Email $email;
453
+
454
+ # ... the tunnel proxy headers (section 5): Host, X-Real-IP,
455
+ # X-Forwarded-For/Proto, X-SSL-Client-* cleared, WebSocket
456
+
457
+ proxy_pass http://127.0.0.1:<port>;
458
+ proxy_http_version 1.1;
459
+ proxy_read_timeout 86400s;
460
+ proxy_send_timeout 86400s;
461
+
462
+ # Authentication failure -> redirect to Authelia login
463
+ error_page 401 =302 https://auth.<domain>/?rd=$scheme://$http_host$request_uri;
464
+ }
465
+ }
466
+ ```
467
+
468
+ Key characteristics:
469
+
470
+ - `auth_request` targets the Gatekeeper at `127.0.0.1:9294/authz/check` instead of Authelia directly
471
+ - The Gatekeeper validates the Authelia session cookie and returns 200 (authenticated) or 401 (unauthenticated)
472
+ - The upstream receives `Remote-*` headers only as set from the Gatekeeper's response; client-supplied values under those names are replaced
473
+ - 401 redirects to the Authelia login portal
474
+
475
+ > **Note:** The `lamalibre-lamaste-authz-cache.conf` nginx snippet is installed by the Gatekeeper installer task, but the generated vhosts do not currently include `proxy_cache` directives. Authorization caching relies on the Gatekeeper's in-memory session cache (30-second TTL) rather than nginx-level proxy caching.
476
+
477
+ ### 9. Restricted Tunnel Vhost (`lamalibre-lamaste-app-<name>`, access mode: restricted)
478
+
479
+ Created when a tunnel requires both Authelia login and an explicit access grant. Only users with a matching grant in `access-grants.json` can access the tunnel.
480
+
481
+ ```nginx
482
+ server {
483
+ listen 443 ssl;
484
+ server_name restricted-app.example.com;
485
+
486
+ ssl_certificate /etc/letsencrypt/live/restricted-app.example.com/fullchain.pem;
487
+ ssl_certificate_key /etc/letsencrypt/live/restricted-app.example.com/privkey.pem;
488
+
489
+ # ... standard SSL settings
490
+ client_max_body_size 10m;
491
+
492
+ # Gatekeeper authorization subrequest (handles Authelia validation + grant check)
493
+ location /internal/lamaste/authz {
494
+ internal;
495
+
496
+ proxy_pass http://127.0.0.1:9294/authz/check;
497
+ proxy_pass_request_body off;
498
+
499
+ proxy_set_header Content-Length "";
500
+ proxy_set_header Connection "";
501
+ proxy_set_header X-Original-Method $request_method;
502
+ proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
503
+ proxy_set_header X-Forwarded-For $remote_addr;
504
+ proxy_set_header Host $http_host;
505
+ proxy_set_header Cookie $http_cookie;
506
+
507
+ proxy_http_version 1.1;
508
+ proxy_buffers 4 32k;
509
+ proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
510
+ }
511
+
512
+ location / {
513
+ auth_request /internal/lamaste/authz;
514
+ auth_request_set $user $upstream_http_remote_user;
515
+ auth_request_set $groups $upstream_http_remote_groups;
516
+ auth_request_set $name $upstream_http_remote_name;
517
+ auth_request_set $email $upstream_http_remote_email;
518
+
519
+ # Identity comes from the Gatekeeper's answer, replacing anything
520
+ # the client sent under these names
521
+ proxy_set_header Remote-User $user;
522
+ proxy_set_header Remote-Groups $groups;
523
+ proxy_set_header Remote-Name $name;
524
+ proxy_set_header Remote-Email $email;
525
+
526
+ # ... the tunnel proxy headers (section 5): Host, X-Real-IP,
527
+ # X-Forwarded-For/Proto, X-SSL-Client-* cleared, WebSocket
528
+
529
+ proxy_pass http://127.0.0.1:<port>;
530
+ proxy_http_version 1.1;
531
+ proxy_read_timeout 86400s;
532
+ proxy_send_timeout 86400s;
533
+
534
+ # Authentication failure -> redirect to Authelia login
535
+ error_page 401 =302 https://auth.<domain>/?rd=$scheme://$http_host$request_uri;
536
+
537
+ # Authorization failure -> Gatekeeper serves inline access-request page
538
+ error_page 403 = /internal/lamaste/authz;
539
+ }
540
+ }
541
+ ```
542
+
543
+ Key differences from the authenticated vhost:
544
+
545
+ - The Gatekeeper checks both the Authelia session and the access grant. If the user is authenticated but has no grant for this resource, the Gatekeeper returns 403 instead of 200
546
+ - `error_page 403 = /internal/lamaste/authz` replays the request to the Gatekeeper internal location. The `=` prefix causes nginx to use the response body and status code from the Gatekeeper, which serves an inline HTML access-request page with pre-filled message templates
547
+ - 401 (unauthenticated) still redirects to Authelia login as before
548
+
549
+ ## nginx Cache Zone (`lamaste_authz`)
550
+
551
+ The Gatekeeper authorization responses are cached by nginx to reduce subrequest overhead. The cache zone is defined in a shared snippet:
552
+
553
+ **File:** `/etc/nginx/snippets/lamalibre-lamaste-authz-cache.conf`
554
+
555
+ ```nginx
556
+ proxy_cache_path /var/cache/nginx/authz levels=1:2 keys_zone=lamaste_authz:1m max_size=10m inactive=5m;
557
+ ```
558
+
559
+ | Parameter | Value | Purpose |
560
+ | ----------- | ----- | --------------------------------------------------------- |
561
+ | `keys_zone` | `1m` | 1 MB for cache keys (sufficient for thousands of entries) |
562
+ | `max_size` | `10m` | 10 MB maximum disk storage for cached responses |
563
+ | `inactive` | `5m` | Evict entries not accessed within 5 minutes |
564
+
565
+ **Cache key:** `$cookie_authelia_session$http_host` — ensures each user session gets its own cache entry per host, preventing authorization decisions from leaking across users or subdomains.
566
+
567
+ **Cache TTLs (when used):**
568
+
569
+ - `200` (authorized): 30 seconds — reduces subrequests during rapid page loads
570
+ - `401` / `403` (denied): 10 seconds — allows quick re-check after login or grant changes
571
+
572
+ > **Note:** The cache zone is installed by the Gatekeeper installer task, but the generated vhosts (`buildGatekeeperVhost()`) do not currently include `proxy_cache` directives in the internal location block. Authorization caching currently relies on the Gatekeeper's in-memory session cache (30-second TTL). The nginx cache zone is available for future use if nginx-level caching is needed.
573
+
574
+ ## mTLS Snippet Pattern
575
+
576
+ The mTLS configuration is shared across panel vhosts via an nginx snippet:
577
+
578
+ **File:** `/etc/nginx/snippets/lamalibre-lamaste-mtls.conf`
579
+
580
+ ```nginx
581
+ ssl_client_certificate /etc/lamalibre/lamaste/pki/ca.crt;
582
+ ssl_verify_client optional;
583
+ ```
584
+
585
+ Included with `include /etc/nginx/snippets/lamalibre-lamaste-mtls.conf;` in any vhost that requires client certificate verification.
586
+
587
+ **What `ssl_verify_client optional` does:**
588
+
589
+ - During the TLS handshake, nginx requests a client certificate from the browser
590
+ - The browser presents the imported `client.p12` certificate (or sends no certificate)
591
+ - nginx verifies the certificate was signed by the CA at `/etc/lamalibre/lamaste/pki/ca.crt`
592
+ - If verification succeeds: `$ssl_client_verify` is set to `SUCCESS`
593
+ - If no certificate is sent or verification fails: the TLS handshake still completes, but `$ssl_client_verify` is not `SUCCESS`
594
+
595
+ **Location-level mTLS enforcement:**
596
+
597
+ Protected locations (`/`, `/api`) enforce mTLS explicitly:
598
+
599
+ ```nginx
600
+ if ($ssl_client_verify != SUCCESS) { return 496; }
601
+ ```
602
+
603
+ Public locations (`/api/enroll`, `/api/invite`) skip the mTLS check and clear cert headers to prevent spoofing:
604
+
605
+ ```nginx
606
+ location /api/enroll {
607
+ proxy_set_header X-SSL-Client-Verify "";
608
+ proxy_set_header X-SSL-Client-DN "";
609
+ proxy_set_header X-SSL-Client-Serial "";
610
+ # ... proxy_pass to lamalibre-lamaste-serverd
611
+ limit_req zone=enroll burst=5 nodelay;
612
+ }
613
+ ```
614
+
615
+ The `/api/enroll` endpoint is rate-limited (`limit_req zone=enroll burst=5 nodelay`) to prevent brute-force token attempts. The `enroll` zone is defined in the `http` block.
616
+
617
+ This design allows the panel vhosts to serve both mTLS-protected and public endpoints on the same port, while keeping the security boundary at the location level.
618
+
619
+ ## Write-With-Rollback Pattern
620
+
621
+ All vhost modifications in the Panel Server follow a safe write-with-rollback sequence, implemented once in `safeWriteVhost()` in `packages/server/daemon/src/lib/nginx.js` and used by tunnel, static site, agent panel and core vhosts. The panel cannot write `/etc/nginx` itself: every file operation is a call to the root-owned helper `sudo /usr/local/sbin/lamaste-priv nginx-site <action> <name>`, with the vhost text on stdin.
622
+
623
+ ```
624
+ ┌──────────────────────────────────────────────────────┐
625
+ │ 1. Backup existing vhost (if any) │
626
+ │ lamaste-priv nginx-site backup <name> │
627
+ ├──────────────────────────────────────────────────────┤
628
+ │ 2. Install the new vhost (text on stdin) │
629
+ │ lamaste-priv nginx-site write <name> │
630
+ │ → validated against the allow-list, then written │
631
+ │ atomically to sites-available/<name> (0644) │
632
+ ├──────────────────────────────────────────────────────┤
633
+ │ 3. Enable (or disable) the site │
634
+ │ lamaste-priv nginx-site enable|disable <name> │
635
+ ├──────────────────────────────────────────────────────┤
636
+ │ 4. Test configuration │
637
+ │ sudo nginx -t │
638
+ │ ├── Success → continue to step 5 │
639
+ │ └── Failure → rollback │
640
+ │ If backup exists: nginx-site restore <name> │
641
+ │ If no backup: nginx-site remove <name> │
642
+ │ Restore the previous enabled/disabled state │
643
+ │ Throw error with nginx -t output │
644
+ ├──────────────────────────────────────────────────────┤
645
+ │ 5. Reload nginx │
646
+ │ sudo systemctl reload nginx │
647
+ ├──────────────────────────────────────────────────────┤
648
+ │ 6. Clean up backup │
649
+ │ lamaste-priv nginx-site discard-backup <name> │
650
+ └──────────────────────────────────────────────────────┘
651
+ ```
652
+
653
+ **Why this matters:** A bad vhost configuration can prevent nginx from reloading, breaking all sites on the server. The `nginx -t` test catches syntax errors and missing certificate files before the reload. The rollback ensures the previous working configuration — file and link state — is restored if the test fails. Any other error (including a vhost `lamaste-priv` refuses) triggers the same rollback.
654
+
655
+ Removing a vhost is `lamaste-priv nginx-site remove <name>` (the file, its `sites-enabled` link and its backup).
656
+
657
+ ### Vhost allow-list
658
+
659
+ 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 for whoever controls the panel. `lamaste-priv` therefore installs a vhost only if:
660
+
661
+ - its **name** is one the panel manages: `lamalibre-lamaste-panel-domain`, `-auth`, `-tunnel`, `-app-<subdomain>`, `-agent-panel-<label>` or `-site-<uuid>`. `lamalibre-lamaste-panel-ip` (written by the installer) may only be enabled or disabled — which is how panel 2FA turns the IP vhost off;
662
+ - every **directive** is on the allow-list for its context, with arguments that cannot name a file outside what Lamaste serves:
663
+
664
+ | Context | Allowed |
665
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
666
+ | top level | `server`, `map` (plain `key value;` entries, no `include`), `limit_req_zone` |
667
+ | `server` | `listen 443` / `[::]:443` with optional `ssl` / `http2`; `server_name` (hostnames); `ssl_certificate(_key)` only `/etc/letsencrypt/live/<name>/{fullchain,privkey}.pem`; `include` only `/etc/nginx/snippets/lamalibre-lamaste-*.conf`; `root` only under `/var/www/lamaste/`; `ssl_protocols`, `ssl_ciphers`, `ssl_prefer_server_ciphers`, `client_max_body_size`, `index`, `add_header`, `proxy_set_header`, `error_page`, `return`, `rewrite`, `location` |
668
+ | `location` | `proxy_pass` only `http://127.0.0.1:<port>[/path]`; `root` / `alias` only under `/var/www/lamaste/`; `proxy_set_header`, `proxy_http_version`, `proxy_read_timeout`, `proxy_send_timeout`, `proxy_buffers`, `proxy_next_upstream`, `proxy_pass_request_body`, `auth_request`, `auth_request_set`, `limit_req`, `try_files`, `index`, `add_header`, `error_page`, `client_max_body_size`, `internal`, `return`, `rewrite`, `if`, nested `location` |
669
+ | `if` | `return`, `rewrite` |
670
+
671
+ Port 80 is not allowed: it belongs to the installer's HTTPS redirect and to certbot's HTTP-01 challenges. Backslashes, `${`, control characters and non-ASCII text outside comments are refused rather than interpreted, as is any directive not listed — including ones nginx would accept (`access_log`, `log_format`, `ssl_*` files elsewhere, `proxy_pass` to another host, ...). A refused `write` fails with `nginx site write failed for <name>: lamaste-priv: vhost line <n>: ...` and leaves the previous vhost in place.
672
+
673
+ Adding a directive to a generator therefore means adding it to the allow-list in `packages/provisioners/server/scripts/lamaste-priv` too. `npm test` checks both sides: `packages/server/daemon/test/nginx-vhosts.test.js` runs every generator's output through the allow-list, and `packages/provisioners/server/test/lamaste-priv.test.js` covers the refusals.
674
+
675
+ ## Forward Auth Block
676
+
677
+ The Authelia forward auth integration uses nginx's `auth_request` module. Protected static sites call Authelia directly as shown below; `restricted` and `authenticated` tunnels send the same subrequest to Gatekeeper (`127.0.0.1:9294/authz/check`), which validates the Authelia session and, for restricted tunnels, the grant (sections 8 and 9). Here is the flow:
678
+
679
+ ```
680
+ 1. User requests https://myapp.example.com/page
681
+ │
682
+ 2. nginx sends internal subrequest to /internal/lamaste/authz
683
+ │ ├── proxy_pass → http://127.0.0.1:9091/api/authz/auth-request
684
+ │ ├── Sends X-Original-URL, X-Original-Method headers
685
+ │ └── Strips request body (Content-Length: "")
686
+ │
687
+ 3. Authelia checks the session cookie
688
+ │ ├── Valid session → 200 + user info headers
689
+ │ └── No session → 401 + Location header with redirect URL
690
+ │
691
+ 4. nginx processes auth_request result
692
+ │ ├── 200 → Extract user info headers (auth_request_set)
693
+ │ │ Set Remote-User, Remote-Groups, etc.
694
+ │ │ Proxy to tunneled app at 127.0.0.1:<port>
695
+ │ │
696
+ │ └── 401 → auth_request_set captures $redirection_url from Location header
697
+ │ error_page 401 =302 $redirection_url
698
+ │ Redirect to Authelia login with return URL
699
+ │
700
+ 5. After Authelia login (TOTP verified):
701
+ │ Authelia sets session cookie on the domain
702
+ │ Redirects back to original URL
703
+ │ Step 3 succeeds on retry
704
+ ```
705
+
706
+ The forward auth pattern means the tunneled app never needs to implement authentication. It receives pre-verified user identity via HTTP headers.
707
+
708
+ ## WebSocket Upgrade Support
709
+
710
+ WebSocket support requires specific nginx configuration:
711
+
712
+ ```nginx
713
+ proxy_http_version 1.1;
714
+ proxy_set_header Upgrade $http_upgrade;
715
+ proxy_set_header Connection "upgrade";
716
+ ```
717
+
718
+ These directives are applied in:
719
+
720
+ - **Panel vhosts** (`/api` location) — for provisioning progress stream and live log streaming
721
+ - **Tunnel vhost** — for Chisel WebSocket transport
722
+ - **App vhosts** (`/` location) — for tunneled apps that use WebSocket
723
+
724
+ The 24-hour timeout (`proxy_read_timeout 86400s`) is set on tunnel and app vhosts to prevent nginx from closing long-lived WebSocket connections.
725
+
726
+ ## Directory Structure
727
+
728
+ ```
729
+ /etc/nginx/
730
+ ├── nginx.conf ← Default nginx config (not modified by Lamaste)
731
+ ├── sites-available/
732
+ │ ├── lamalibre-lamaste-http-redirect ← :80 catch-all → HTTPS (created by installer)
733
+ │ ├── lamalibre-lamaste-panel-ip ← IP:9292 panel access (created by installer)
734
+ │ ├── lamalibre-lamaste-panel-domain ← panel.<domain> (created during onboarding)
735
+ │ ├── lamalibre-lamaste-auth ← auth.<domain> (created during onboarding)
736
+ │ ├── lamalibre-lamaste-tunnel ← tunnel.<domain> (created during onboarding)
737
+ │ ├── lamalibre-lamaste-app-myapp ← <app>.<domain> (created per tunnel)
738
+ │ └── lamalibre-lamaste-site-<uuid> ← <site>.<domain> (created per static site)
739
+ ├── sites-enabled/
740
+ │ ├── lamalibre-lamaste-http-redirect → ../sites-available/lamalibre-lamaste-http-redirect
741
+ │ ├── lamalibre-lamaste-panel-ip → ../sites-available/lamalibre-lamaste-panel-ip
742
+ │ ├── lamalibre-lamaste-panel-domain → ../sites-available/lamalibre-lamaste-panel-domain
743
+ │ ├── lamalibre-lamaste-auth → ../sites-available/lamalibre-lamaste-auth
744
+ │ ├── lamalibre-lamaste-tunnel → ../sites-available/lamalibre-lamaste-tunnel
745
+ │ ├── lamalibre-lamaste-app-myapp → ../sites-available/lamalibre-lamaste-app-myapp
746
+ │ └── lamalibre-lamaste-site-<uuid> → ../sites-available/lamalibre-lamaste-site-<uuid>
747
+ └── snippets/
748
+ ├── lamalibre-lamaste-mtls.conf ← Shared mTLS client cert verification
749
+ └── lamalibre-lamaste-authz-cache.conf ← Gatekeeper proxy_cache zone definition
750
+ ```
751
+
752
+ All Lamaste-managed files are prefixed with `lamalibre-lamaste-` to distinguish them from other nginx configurations on the system. The installer writes its own files as root; the panel's vhosts (everything created during onboarding and at runtime, including `lamalibre-lamaste-agent-panel-<label>`) are installed through `lamaste-priv`.
753
+
754
+ ## Vhost Lifecycle
755
+
756
+ ### Created by the Installer
757
+
758
+ | Vhost | When | Removed |
759
+ | --------------------------------- | ---------------------- | ----------------------- |
760
+ | `lamalibre-lamaste-panel-ip` | Installation | Never (fallback access) |
761
+ | `lamalibre-lamaste-http-redirect` | Installation, redeploy | Uninstall |
762
+
763
+ `lamalibre-lamaste-http-redirect` is the port-80 catch-all:
764
+
765
+ ```nginx
766
+ # Managed by Lamaste — plain HTTP is redirected to HTTPS.
767
+ server {
768
+ listen 80 default_server;
769
+ server_name _;
770
+ return 301 https://$host$request_uri;
771
+ }
772
+ ```
773
+
774
+ Without it nothing listens on port 80 and `http://` links to the relay's hostnames are refused. It does not interfere with Let's Encrypt: for an HTTP-01 challenge (`certbot certonly --nginx` and renewals) certbot's nginx plugin clones this default server block for the name being validated, answers the challenge path ahead of the redirect, and restores the file afterwards. This was verified on Ubuntu 24.04 (nginx 1.24, certbot 2.9) for new names, names that already have a 443 block, multi-name lineages and forced renewals. The installer tests the file with `nginx -t` and restores the previous state on failure; if another site already owns port 80's `default_server`, the step is skipped — not fatal — and the relay works without the redirect.
775
+
776
+ ### Created During Onboarding
777
+
778
+ | Vhost | When | Removed |
779
+ | -------------------------------- | ------------------- | ---------------------------- |
780
+ | `lamalibre-lamaste-panel-domain` | Provisioning step 4 | Never (primary panel access) |
781
+ | `lamalibre-lamaste-auth` | Provisioning step 4 | Never (auth portal) |
782
+ | `lamalibre-lamaste-tunnel` | Provisioning step 4 | Never (tunnel endpoint) |
783
+
784
+ ### Created/Removed at Runtime
785
+
786
+ | Vhost | Created | Removed |
787
+ | ------------------------------ | ----------------- | ----------------------- |
788
+ | `lamalibre-lamaste-app-<name>` | POST /api/tunnels | DELETE /api/tunnels/:id |
789
+
790
+ `PATCH /api/tunnels/:id` removes or restores the `sites-enabled` symlink when a tunnel is disabled or enabled (the file in `sites-available` is kept), and re-renders the vhost in place when `accessMode` or `maxBodySizeMb` changes. `PATCH /api/sites/:id` re-renders a site vhost when `spaMode`, `autheliaProtected` or `aliases` change.
791
+ | `lamalibre-lamaste-site-<uuid>` | POST /api/sites | DELETE /api/sites/:id |
792
+
793
+ ## Key Files
794
+
795
+ | File | Role |
796
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
797
+ | `packages/provisioners/server/src/tasks/nginx.js` | Installer: self-signed cert, mTLS snippet, IP vhost, port-80 redirect |
798
+ | `packages/provisioners/server/src/lib/http-redirect.js` | Installs/refreshes the port-80 redirect site (installer and redeploy) |
799
+ | `packages/server/daemon/src/lib/nginx.js` | Runtime: vhost generation, write-with-rollback, enable/disable/reload |
800
+ | `packages/provisioners/server/scripts/lamaste-priv` | Root helper (`/usr/local/sbin/lamaste-priv`): site names, vhost allow-list, install/enable/remove |
801
+ | `/etc/nginx/snippets/lamalibre-lamaste-mtls.conf` | Shared mTLS snippet |
802
+ | `/etc/nginx/snippets/lamalibre-lamaste-authz-cache.conf` | Gatekeeper proxy_cache zone definition |
803
+ | `/etc/nginx/sites-available/lamalibre-lamaste-*` | Vhost configuration files |
804
+ | `/etc/nginx/sites-enabled/lamalibre-lamaste-*` | Symlinks to enabled vhosts |
805
+
806
+ ## Design Decisions
807
+
808
+ ### Why port 9292 for IP-based access?
809
+
810
+ Port 9292 is above the privileged port range (no root needed to listen) and is unlikely to conflict with other services. It is deliberately different from port 443 to ensure the IP-based fallback always works, even if domain-based HTTPS on 443 is misconfigured.
811
+
812
+ ### Why self-signed certificates for IP access?
813
+
814
+ Let's Encrypt cannot issue certificates for IP addresses. A self-signed certificate provides encryption for the connection, even though the browser shows a warning. The mTLS client certificate is the real security mechanism — it proves the caller's identity regardless of the server certificate.
815
+
816
+ ### Why include the mTLS snippet instead of inline?
817
+
818
+ The mTLS configuration (two lines) is identical across panel vhosts. An nginx snippet avoids duplication and ensures that if the CA certificate path changes, it only needs to be updated in one place. This is especially important because the IP vhost is created by the installer and the domain vhost is created by the Panel Server — they must stay in sync.
819
+
820
+ ### Why error pages 495/496 for certificate help?
821
+
822
+ nginx emits error 495 when no client certificate is provided and 496 when the certificate is invalid. These are nginx-specific error codes (not standard HTTP). By mapping them to a static HTML page with import instructions, visitors who forgot to import their certificate see helpful guidance instead of a cryptic "400 Bad Request".
823
+
824
+ ### Why 24-hour WebSocket timeouts?
825
+
826
+ Chisel tunnel connections and some app WebSocket connections are long-lived. nginx's default `proxy_read_timeout` of 60 seconds would close these connections, causing tunnel drops. The 24-hour timeout is effectively "never timeout" for practical purposes, while still allowing nginx to reclaim resources from genuinely dead connections.
827
+
828
+ ### Why a root helper for nginx operations?
829
+
830
+ The Panel Server runs as the `lamaste` user, not root. Writing to `/etc/nginx/sites-available/` and reloading nginx require root privileges. Instead of running the Panel Server as root, Lamaste allows the `lamaste` user a few fixed command lines (`nginx -t`, `systemctl reload nginx`, ...) and the root-owned `lamaste-priv` helper, which accepts only the panel's own site names and only vhosts built from the allow-listed directives above. Earlier versions used sudoers rules with wildcards (`mv /tmp/nginx-* /etc/nginx/sites-available/*`); a sudoers `*` also matches spaces, so such a rule accepted extra arguments, and even an exact rule would have let the panel install any nginx configuration — which nginx's root master process would act on. Certbot is reached only through the root-owned `/usr/local/sbin/lamaste-certbot` wrapper, which validates hostnames and email and runs certbot with a fixed argument vector. See [Security Model](../01-concepts/security-model.md#privilege-boundary).