@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,367 @@
1
+ # Tunneling
2
+
3
+ > Lamaste uses WebSocket tunnels to securely relay traffic from the internet to web apps running behind your firewall.
4
+
5
+ ## In Plain English
6
+
7
+ Imagine your home computer runs a web app, but your router blocks anyone from the internet from reaching it. It is like having a shop inside a locked building with no front door.
8
+
9
+ Lamaste solves this with a tunnel. Your home computer reaches _out_ to a small server on the internet (the VPS) and holds open a connection. When someone visits your domain, the VPS sends the request back through that open connection to your home computer, which responds as if the visitor connected directly.
10
+
11
+ Think of it like a phone call. Your home computer calls the VPS and stays on the line. When a visitor arrives at the VPS, the VPS says "someone is here for you" over the open line, and your home computer handles the conversation through that same call.
12
+
13
+ The connection uses WebSockets — a technology that keeps a two-way channel open over HTTPS. This means the tunnel looks like normal web traffic, so firewalls and corporate networks that block unusual protocols let it through without interference.
14
+
15
+ If the connection drops (internet hiccup, VPS restart, laptop sleep), the client automatically reconnects and picks up where it left off. You do not need to intervene.
16
+
17
+ ## For Users
18
+
19
+ ### When you encounter tunneling
20
+
21
+ Tunneling is the core of Lamaste. Every time you create a tunnel in the management UI, you are telling Lamaste to relay traffic from a subdomain on the internet to a specific port on your local machine.
22
+
23
+ For example, if you run a web app on port 3000 of your machine, you create a tunnel in the panel that maps `myapp.example.com` to port 3000. Visitors go to `https://myapp.example.com`, and Lamaste relays their requests through the tunnel to your machine's port 3000.
24
+
25
+ ### How a tunnel gets created
26
+
27
+ 1. Your machine is enrolled as an **agent** — once, with `lamaste-agent setup` or the Desktop App (see [Agent Setup](../02-guides/agent-setup.md))
28
+ 2. You click "Add Tunnel" in the management panel
29
+ 3. You enter a subdomain name (e.g., `myapp`), a port number (e.g., `3000`), and the agent that carries it
30
+ 4. Lamaste issues a TLS certificate for `myapp.example.com` via Let's Encrypt
31
+ 5. Lamaste writes an nginx vhost configuration to route `myapp.example.com` traffic
32
+ 6. Lamaste grants that agent — and only that agent — the port on the tunnel server (the tunnel server picks up the grant without a restart)
33
+ 7. Within about 30 seconds the agent's sync timer notices the new tunnel and starts relaying traffic — there is nothing to run on the agent (`lamaste-agent update` applies it at once)
34
+
35
+ ### What runs where
36
+
37
+ ```
38
+ Your machine (behind firewall) Internet Your VPS ($4 droplet)
39
+ ┌─────────────────────┐ ┌──────────────────────┐
40
+ │ │ │ │
41
+ │ Web app (:3000) │ │ nginx (TLS) │
42
+ │ ↑ │ │ ↓ │
43
+ │ Chisel client ────┼──── WebSocket over HTTPS ──────▶│ Chisel server │
44
+ │ (auto-reconnect) │ │ (127.0.0.1:9090) │
45
+ │ │ │ │
46
+ └─────────────────────┘ └──────────────────────┘
47
+ ↑
48
+ Visitor browses
49
+ myapp.example.com
50
+ ```
51
+
52
+ ### Auto-reconnect
53
+
54
+ The Chisel client runs as a user-level service (launchd on macOS, systemd on Linux). If the connection drops for any reason — network outage, VPS restart, laptop waking from sleep — it reconnects on its own, backing off between attempts to at most 30 seconds (`--max-retry-interval 30s`). You do not need to manually restart anything.
55
+
56
+ ### Tunnel changes apply by themselves
57
+
58
+ Next to the Chisel client, every agent runs a small **sync timer** that asks the panel every 30 seconds which tunnels it carries and adjusts the client when that changed. Creating, deleting, disabling or moving a tunnel in the panel therefore reaches the agent within about 30 seconds.
59
+
60
+ This is more than a convenience. The tunnel server checks every port an agent asks for, and if even one of them is no longer granted — say, a tunnel was just disabled — it rejects the agent's **whole** connection, taking all of its other tunnels down with it. The timer makes the agent stop asking for a revoked port promptly.
61
+
62
+ An agent that carries no tunnel at all keeps its Chisel client stopped (Chisel cannot run without at least one port to forward) and starts it as soon as a tunnel is assigned. Stopping the tunnels on purpose from the agent panel or desktop app is remembered, and the timer leaves them stopped.
63
+
64
+ ### Multiple tunnels
65
+
66
+ You can run multiple tunnels simultaneously. Each tunnel maps a different subdomain to a different local port:
67
+
68
+ | Subdomain | Local port | What it exposes |
69
+ | ------------------- | ---------- | ---------------- |
70
+ | `myapp.example.com` | 3000 | React dev server |
71
+ | `api.example.com` | 8080 | Backend API |
72
+ | `blog.example.com` | 4000 | Blog engine |
73
+
74
+ All tunnels carried by one agent share that agent's Chisel client connection. The client multiplexes all its port mappings over a single WebSocket — which is also why one refused port refuses them all.
75
+
76
+ ### One agent per tunnel
77
+
78
+ Every tunnel belongs to exactly one agent. You can enroll many machines — a laptop, a build server, a VM — and each one carries its own tunnels. An agent is only told about its own tunnels, and the tunnel server only lets an agent open the ports of the tunnels it carries, so enrolling a second machine never lets it take over the first machine's hostnames. An administrator can move a tunnel to another agent from the panel (**Edit** on the tunnel row).
79
+
80
+ ## For Developers
81
+
82
+ ### Chisel overview
83
+
84
+ Lamaste uses [Chisel](https://github.com/jpillora/chisel), an open-source tunnel tool written in Go. Chisel encapsulates TCP connections inside WebSocket frames, which travel over standard HTTPS. This makes the tunnel traffic indistinguishable from normal web traffic to firewalls and DPI (Deep Packet Inspection) systems.
85
+
86
+ Lamaste uses Chisel in **reverse mode**. In reverse mode, the client connects to the server and registers ports it wants to expose. The server then listens on those local ports and forwards incoming connections back through the WebSocket to the client.
87
+
88
+ ### Server configuration
89
+
90
+ The Chisel server runs as a systemd service on the VPS, binding to `127.0.0.1:9090`. Server and agents run the same pinned release, Chisel 1.12.0 — the first whose `--authfile` reload survives atomic renames and re-checks a user's grants on every new tunnel.
91
+
92
+ ```ini
93
+ [Unit]
94
+ Description=Chisel Tunnel Server
95
+ After=network.target
96
+
97
+ [Service]
98
+ Type=simple
99
+ User=nobody
100
+ Group=lamaste-chisel
101
+ 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
102
+ Restart=always
103
+ RestartSec=5
104
+ StandardOutput=journal
105
+ StandardError=journal
106
+ SyslogIdentifier=chisel
107
+
108
+ [Install]
109
+ WantedBy=multi-user.target
110
+ ```
111
+
112
+ Key points:
113
+
114
+ - **`--reverse`** — enables reverse tunneling (clients declare which ports to expose)
115
+ - **`--port 9090`** — the port Chisel listens on for WebSocket connections
116
+ - **`--host 127.0.0.1`** — binds only to localhost; nginx handles public-facing TLS
117
+ - **`--keyfile`** — pins the server's SSH identity across restarts
118
+ - **`--authfile`** — per-agent credentials and per-agent port grants (see below)
119
+ - **`User=nobody`, `Group=lamaste-chisel`** — runs with minimal privileges; the group is the only one that may read the 0640 authfile
120
+ - **`Restart=always`** — systemd restarts the process on any failure
121
+
122
+ The server is not exposed directly to the internet. nginx terminates TLS on `tunnel.example.com` and proxies the WebSocket connection to `127.0.0.1:9090`.
123
+
124
+ ### Ownership and grants
125
+
126
+ Each tunnel in `tunnels.json` carries an `agentLabel` — the agent that carries it. The panel renders Chisel's authfile, `/etc/lamalibre/lamaste/chisel-users`, from two persisted inputs: the per-agent credential store (`chisel-credentials.json`) and the tunnel state. Each `agent-<label>` user is granted exactly the reverse remotes of the enabled tunnels it owns:
127
+
128
+ ```json
129
+ {
130
+ "lamaste-no-grants:<random>": [],
131
+ "agent-laptop:<password>": ["^R:127\\.0\\.0\\.1:3000$", "^R:127\\.0\\.0\\.1:8080$"],
132
+ "agent-build-server:<password>": []
133
+ }
134
+ ```
135
+
136
+ - Chisel matches these patterns unanchored, so the `^…$` anchors are what keep a grant for port `2001` from also matching `20010`.
137
+ - An agent with no tunnels can authenticate but bind nothing. Disabled and unassigned tunnels grant nothing.
138
+ - A reserved port (3100, 9090, 9091, 9292, 9294 — Lamaste's own services) and a port claimed by two tunnels are never granted, whatever the state file says. Such a tunnel (only possible in state written before these checks) is also left out of its owner's `agent-config`, so the agent never asks for a port Chisel would refuse; it is carried by no one until it is deleted and recreated on a free port.
139
+ - `lamaste-no-grants` is a sentinel user with no grants. Chisel disables authentication entirely when its authfile has no users, so without it a fresh server (or one whose agents are all revoked) would be an open relay. Its random password lives in `/etc/lamalibre/lamaste/chisel-sentinel` (0600) and is never handed out.
140
+ - The authfile is `0640`, owner `lamaste`, group `lamaste-chisel`. The panel (a member of that group through `SupplementaryGroups=lamaste-chisel`) writes it directly, without sudo: a `0600` temp file in the same directory, `chgrp lamaste-chisel`, `chmod 0640`, fsync, rename. Chisel's file watcher therefore never sees it with the wrong permissions.
141
+ - Every tunnel change writes the state first, then re-renders the authfile. Chisel reloads the file on every replace, but a reload only applies to **new** sessions: a live session keeps the listeners it already bound. So an addition (a new agent, a new grant) takes effect without a restart, while a revocation (a removed or rotated password, a withdrawn grant) restarts Chisel with `systemctl try-restart` so live sessions drop what they no longer hold. `try-restart` leaves a Chisel that was stopped on purpose stopped. Until the panel knows Chisel is running the pinned release, every change restarts it.
142
+
143
+ On the API side the same rule holds: `GET /api/tunnels/agent-config` returns only the calling agent's enabled tunnels, an agent's `GET /api/tunnels` omits other agents' tunnels, and acting on one of those answers `404`.
144
+
145
+ ### Client configuration
146
+
147
+ The Chisel client runs as a user-level service managed by `lamaste-agent` (and `lamaste-agentd`): a LaunchAgent plist on macOS, a systemd **user** unit on Linux. Both are generated by one implementation in `@lamalibre/lamaste@4.0.0/agent`. The client command looks like:
148
+
149
+ ```bash
150
+ chisel client \
151
+ --max-retry-interval 30s \
152
+ https://tunnel.example.com:443 \
153
+ R:127.0.0.1:3000:127.0.0.1:3000 \
154
+ R:127.0.0.1:8080:127.0.0.1:8080
155
+ ```
156
+
157
+ - **The relay is fixed.** The server URL is always `https://tunnel.<enrolled domain>:443`. The agent records the domain at enrollment and refuses a panel response that names another one; moving an agent to another relay means re-enrolling it.
158
+ - **TLS is verified.** `tunnel.example.com` carries a publicly trusted Let's Encrypt certificate and the client verifies it like any HTTPS client — there is no `--tls-skip-verify`. An agent drops that flag if an older panel still sends it.
159
+ - **The credential is not an argument.** Chisel reads `AUTH=<user>:<password>` from its environment. On Linux it comes from `EnvironmentFile=~/.lamalibre/lamaste/agents/<label>/chisel.env` (0600); on macOS from the plist's `EnvironmentVariables` (the plist is written 0600). `ps` shows nothing secret.
160
+ - **Loopback only.** Every remote must be `R:127.0.0.1:<port>:127.0.0.1:<port>`. The agent rejects any other form, even from the panel.
161
+
162
+ The `R:` prefix means "reverse" — the client asks the server to listen on that port on its `127.0.0.1` and forward connections back to the same port on the agent's `127.0.0.1`. Chisel's default 25-second keepalive keeps the WebSocket alive through NATs and firewalls.
163
+
164
+ On Linux, a user unit only runs while that user has a session unless _lingering_ is enabled. `lamaste-agent setup` and `status` report it; on headless machines run `sudo loginctl enable-linger <user>` once.
165
+
166
+ ### Agent sync
167
+
168
+ `lamaste-agent sync` (run every 30 seconds by a systemd user timer `lamalibre-lamaste-sync-<label>` on Linux, or the LaunchAgent `com.lamalibre.lamaste.sync-<label>` on macOS) fetches `GET /api/tunnels/agent-config` and converges the client with it: verifies the relay URL, fetches a credential an administrator rotated (`chiselCredentialIssuedAt`), replaces a Chisel binary that is not the pinned release, rewrites the service definition when the granted remotes changed, and starts or stops the client. It rewrites and restarts nothing when everything already matches. `setup`, `update`, the agent panel and the agent daemon all use the same convergence code. Details: [Agent Setup](../02-guides/agent-setup.md#how-tunnel-changes-reach-the-agent).
169
+
170
+ ### Data flow for a single request
171
+
172
+ Here is the complete path of an HTTP request through the tunnel:
173
+
174
+ ```
175
+ 1. Visitor requests https://myapp.example.com/api/data
176
+ 2. DNS resolves to 203.0.113.42 (your VPS IP)
177
+ 3. nginx on VPS:
178
+ a. Terminates TLS (Let's Encrypt cert for myapp.example.com)
179
+ b. Checks Gatekeeper/Authelia (unless the tunnel is public) → user is allowed
180
+ c. Proxies to 127.0.0.1:3000 (where Chisel server is listening for this port,
181
+ because the carrying agent holds the grant for it)
182
+ 4. Chisel server:
183
+ a. Receives the proxied request on local port 3000
184
+ b. Forwards through WebSocket to the connected Chisel client
185
+ 5. Chisel client (on your machine):
186
+ a. Receives the request from the WebSocket
187
+ b. Connects to localhost:3000 on your machine
188
+ c. Forwards the request to your web app
189
+ 6. Response travels back the same path in reverse
190
+ ```
191
+
192
+ ### WebSocket upgrade in nginx
193
+
194
+ The tunnel vhost uses WebSocket upgrade headers with a 24-hour read/send timeout to keep long-lived connections alive:
195
+
196
+ ```nginx
197
+ location / {
198
+ proxy_pass http://127.0.0.1:9090;
199
+ proxy_http_version 1.1;
200
+ proxy_set_header Upgrade $http_upgrade;
201
+ proxy_set_header Connection "upgrade";
202
+
203
+ # Long timeout for WebSocket tunnel connections
204
+ proxy_read_timeout 86400s;
205
+ proxy_send_timeout 86400s;
206
+ }
207
+ ```
208
+
209
+ ### Concurrency and locking
210
+
211
+ Every tunnel workflow — create, delete, toggle, assign, reconfigure, the combined `PATCH`, releasing a revoked agent's tunnels, and startup reconciliation — runs under one tunnel lock (`withTunnelLock` in `@lamalibre/lamaste@4.0.0/server`'s `tunnels.ts`), with its validation inside the lock. Two concurrent requests therefore cannot both pass a uniqueness check for the same subdomain or port. Authfile renders are additionally serialized with a keyed promise-chain mutex in `chisel-users.ts`, so enrollments and credential rotations never interleave authfile writes with tunnel changes.
212
+
213
+ ### Startup reconciliation
214
+
215
+ Every time the panel starts it brings Chisel in line with the persisted state, in the background so the panel stays reachable meanwhile (`chisel-reconcile.js`):
216
+
217
+ 1. Every active agent gets a Chisel credential if it lacks one.
218
+ 2. Ownerless tunnels from versions before tunnel ownership are bound where the owner is certain: an `agent-<label>` panel tunnel only to that agent (when active), other tunnels only when exactly one machine agent is active (plugin-agent certificates carry no tunnels and are not counted). The rest stay unassigned and are logged. Tunnels that are withheld from every agent — a legacy tunnel on a reserved port, or two tunnels sharing a port — are logged too; delete each and recreate it on a free port.
219
+ 3. Once onboarding is `COMPLETED`: the server key is ensured and the authfile is rewritten with its ownership and mode. The installed version is checked; if it is not the pinned release, the panel logs an error telling you to run `create-lamaste`, and restarts Chisel on every authfile change until you do (older releases do not reload the authfile reliably).
220
+ 4. Chisel is restarted when a revocation requires it, and started if it is enabled but not running.
221
+
222
+ It **fails closed**: if the authfile cannot be established, Chisel is stopped **and disabled**, and the marker file `/etc/lamalibre/lamaste/chisel-failed-closed` records when — an authfile left by an older version may grant every agent every port, and keeping it live would re-open what this version closes. Disabling the unit keeps a reboot from starting Chisel on that authfile before the panel has reconciled. Reconciliation retries with backoff (30 seconds, doubling up to 10 minutes); once it succeeds it re-enables and starts Chisel and removes the marker.
223
+
224
+ ### Installation
225
+
226
+ On the server, Chisel is installed by `npx @lamalibre/create-lamaste@4.0.0` — as root, together with its hardened systemd unit (`User=nobody`, `Group=lamaste-chisel`, `NoNewPrivileges`, `ProtectSystem=strict`, empty `CapabilityBoundingSet`) — and kept at the pinned release by every redeploy. The panel never installs a binary or writes the unit; onboarding only checks the binary exists, creates the server key and authfile, and starts the service. Both the server installer and the agents download the pinned release (`CHISEL_RELEASE` in `@lamalibre/lamaste@4.0.0`'s `constants.ts`) from its fixed URL, with no GitHub API lookup and no "latest":
227
+
228
+ ```
229
+ https://github.com/jpillora/chisel/releases/download/v1.12.0/chisel_1.12.0_<os>_<arch>.gz
230
+ → curl (HTTPS only, size and time bounded) → SHA-256 compared with the pinned digest
231
+ → gunzip → /usr/local/bin/chisel (server) or ~/.lamalibre/lamaste/bin/chisel (agent)
232
+ ```
233
+
234
+ A digest mismatch aborts the install and leaves any existing binary untouched. An installed binary reporting another version is replaced — on the server by the next `create-lamaste` run, on an agent by its next sync. The binary is a single static Go executable with no runtime dependencies.
235
+
236
+ ### Source files
237
+
238
+ | File | Purpose |
239
+ | --------------------------------------------------------- | ------------------------------------------------------------------- |
240
+ | `packages/core/lib/src/server/chisel.ts` | Server key, start, stop, restart, status, installed version |
241
+ | `packages/provisioners/server/src/lib/binaries.js` | Server install of the pinned Chisel (and Authelia) release |
242
+ | `packages/core/lib/src/server/chisel-users.ts` | Per-agent credentials, grants, authfile rendering, sentinel user |
243
+ | `packages/core/lib/src/server/chisel-args.ts` | Client `chiselArgs` returned by `agent-config` |
244
+ | `packages/core/lib/src/server/tunnels.ts` | Tunnel create/delete/toggle/assign/reconfigure |
245
+ | `packages/core/lib/src/chisel-download.ts` | Pinned, checksum-verified Chisel download (server and agent) |
246
+ | `packages/core/lib/src/agent/chisel-service.ts` | Agent-side plist / systemd unit generation, `chiselArgs` validation |
247
+ | `packages/core/lib/src/agent/converge.ts` | Agent convergence with the panel (setup, update, sync) |
248
+ | `packages/core/lib/src/agent/sync-service.ts` | Agent sync timer |
249
+ | `packages/server/daemon/src/routes/management/tunnels.js` | Tunnel API endpoints |
250
+ | `packages/server/daemon/src/lib/chisel-reconcile.js` | Startup reconciliation, fail closed |
251
+ | `packages/server/daemon/src/lib/chisel-runtime.js` | Whether an authfile change needs a Chisel restart |
252
+
253
+ ### Panel tunnels
254
+
255
+ In addition to regular application tunnels (type `app`), Lamaste supports panel tunnels (type `panel`) that expose an agent's management panel at a public subdomain.
256
+
257
+ **Key differences from app tunnels:**
258
+
259
+ - **Type:** Panel tunnels have type `panel` in `tunnels.json`, while regular tunnels have type `app`.
260
+ - **Subdomain prefix:** Panel tunnels use the reserved `agent-` subdomain prefix (e.g., `agent-macbook.example.com`). Regular tunnels cannot use subdomains starting with `agent-`.
261
+ - **nginx vhost:** Panel tunnel vhosts use mTLS client certificate verification (same CA as the main panel) instead of Authelia forward auth. The agent panel server validates that the certificate CN is either `agent:<label>` (the owning agent) or `admin`.
262
+ - **Lifecycle:** Panel tunnels are created via `POST /api/tunnels/expose-panel` and removed via `DELETE /api/tunnels/retract-panel`, not through the standard tunnel CRUD endpoints.
263
+ - **Capability:** The agent must have the `panel:expose` capability assigned to its certificate.
264
+
265
+ ### Port mapping model
266
+
267
+ Every tunnel creates a chain of port mappings:
268
+
269
+ ```
270
+ Public FQDN (443) → nginx vhost → Chisel server (local port) → WebSocket → Chisel client → localhost (local port)
271
+ ```
272
+
273
+ The Chisel server in `--reverse` mode does not need per-tunnel port entries in its unit. The client declares which ports to expose when it connects, and the server opens a local listener for each one — but only if the authfile grants **every** remote the client asks for; one ungranted remote rejects the session. The restart after a revocation is what makes live sessions drop a binding the authfile no longer grants, and the agent's sync timer is what makes the agent stop asking for it.
274
+
275
+ ## Quick Reference
276
+
277
+ ### Architecture
278
+
279
+ | Component | Location | Port | Role |
280
+ | ------------------ | -------- | ---------------- | ---------------------------------------------- |
281
+ | Chisel server | VPS | `127.0.0.1:9090` | Accepts WebSocket connections from clients |
282
+ | Chisel client | Agent | outbound only | Connects to VPS, exposes the agent's ports |
283
+ | nginx tunnel vhost | VPS | `443` | TLS termination for `tunnel.example.com` |
284
+ | nginx app vhost | VPS | `443` | TLS + Authelia auth for each `app.example.com` |
285
+
286
+ ### Chisel server flags
287
+
288
+ | Flag | Value | Purpose |
289
+ | ------------ | ----------- | ------------------------ |
290
+ | `--reverse` | (no value) | Enable reverse tunneling |
291
+ | `--port` | `9090` | WebSocket listen port |
292
+ | `--host` | `127.0.0.1` | Bind to localhost only |
293
+ | `--keyfile` | path | Stable server identity |
294
+ | `--authfile` | path | Credentials and grants |
295
+
296
+ The client adds `--max-retry-interval 30s`.
297
+
298
+ ### Chisel client flags
299
+
300
+ | Argument | Value | Purpose |
301
+ | ------------ | --------------------------------- | --------------------------------- |
302
+ | Server URL | `https://tunnel.example.com:443` | Where to connect (TLS verified) |
303
+ | Port mapping | `R:127.0.0.1:3000:127.0.0.1:3000` | Reverse-map a loopback port |
304
+ | `AUTH` (env) | `agent-<label>:<password>` | Credential, never on command line |
305
+
306
+ ### Systemd commands
307
+
308
+ ```bash
309
+ # Check Chisel server status
310
+ systemctl status chisel
311
+
312
+ # View recent logs
313
+ journalctl -u chisel -n 50 --no-pager
314
+
315
+ # Restart the server (the panel does this itself when a grant is revoked)
316
+ sudo systemctl restart chisel
317
+ ```
318
+
319
+ ### Agent commands
320
+
321
+ ```bash
322
+ # Install globally, then set up the agent (interactive)
323
+ npm install -g @lamalibre/lamaste-agent@4.0.0
324
+ lamaste-agent setup --label <label>
325
+
326
+ # Apply tunnel changes now (the sync timer does it within 30 s anyway)
327
+ lamaste-agent update
328
+
329
+ # Check agent status (boot persistence on Linux, sync timer, tunnels)
330
+ lamaste-agent status
331
+
332
+ # View logs
333
+ lamaste-agent logs
334
+ ```
335
+
336
+ ### macOS launchd commands
337
+
338
+ ```bash
339
+ # Check if running (the service is managed by lamaste-agent; prefer
340
+ # lamaste-agent status / update over editing the plist by hand)
341
+ launchctl list | grep com.lamalibre.lamaste.chisel-<label>
342
+
343
+ # The sync timer
344
+ launchctl list | grep com.lamalibre.lamaste.sync-<label>
345
+ ```
346
+
347
+ ### Linux systemd commands
348
+
349
+ ```bash
350
+ # Check agent service status (a systemd user unit)
351
+ systemctl --user status lamalibre-lamaste-chisel-<label>
352
+
353
+ # Check the sync timer
354
+ systemctl --user list-timers lamalibre-lamaste-sync-<label>.timer
355
+
356
+ # Keep the tunnel running after reboot without a login (once per user)
357
+ sudo loginctl enable-linger "$USER"
358
+ ```
359
+
360
+ ### Related documentation
361
+
362
+ - [mTLS](mtls.md) — how the admin panel connection is secured
363
+ - [Agent Setup](../02-guides/agent-setup.md) — enrolling macOS and Linux agents
364
+ - [Authentication](authentication.md) — how tunneled apps are protected with TOTP 2FA
365
+ - [nginx Reverse Proxy](nginx-reverse-proxy.md) — how nginx routes tunnel traffic
366
+ - [Certificates](certificates.md) — TLS certificates for tunnel subdomains
367
+ - [DNS and Domains](dns-and-domains.md) — how subdomains map to tunnels
@@ -0,0 +1,194 @@
1
+ # Agent Setup (macOS and Linux)
2
+
3
+ > Enroll a machine as a Lamaste agent so the tunnels it carries reach your domain.
4
+
5
+ > **Prefer the Desktop App?** The [Lamaste Desktop App](desktop-app-setup.md) wraps the same agent in a GUI with service discovery and one-click tunnels. This guide covers the command-line agent, which is also the way to enroll servers, VMs and other headless machines.
6
+
7
+ ## In Plain English
8
+
9
+ Your Lamaste server is a relay. An **agent** is a machine that holds the actual services and connects out to the relay over one encrypted WebSocket. Because the agent dials out, it works from behind NAT and carrier-grade NAT with no inbound firewall rule.
10
+
11
+ Every tunnel belongs to exactly one agent. That agent is the only one told about the tunnel, and the only one whose credential lets the relay open the tunnel's port. Enrolling a second machine never lets it take over the first machine's hostnames.
12
+
13
+ ## What the agent runs
14
+
15
+ | Piece | Where |
16
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | Agent program | `lamaste-agent`, installed globally with npm |
18
+ | Chisel client binary | `~/.lamalibre/lamaste/bin/chisel` — Chisel 1.12.0, checksum-verified (downloaded at setup) |
19
+ | Agent data directory | `~/.lamalibre/lamaste/agents/<label>/` (0700) |
20
+ | Agent certificate | `~/.lamalibre/lamaste/agents/<label>/client.p12` (0600) |
21
+ | Chisel credential | `~/.lamalibre/lamaste/agents/<label>/chisel.json` (0600) |
22
+ | Tunnel service | Linux: systemd **user** unit `lamalibre-lamaste-chisel-<label>` + `chisel.env` (0600). macOS: LaunchAgent `com.lamalibre.lamaste.chisel-<label>` (0600) |
23
+ | Sync timer | Linux: user service + timer `lamalibre-lamaste-sync-<label>`. macOS: LaunchAgent `com.lamalibre.lamaste.sync-<label>` |
24
+ | Logs | `~/.lamalibre/lamaste/agents/<label>/logs/` (`sync.log` for the timer) |
25
+
26
+ Four properties of the service are deliberate:
27
+
28
+ - **TLS is verified, and the relay is fixed.** The client connects to `https://tunnel.<domain>:443`, which carries a publicly trusted Let's Encrypt certificate, and verifies it like any HTTPS client. A machine on the path cannot impersonate your relay. `<domain>` is the domain the agent enrolled with: the agent refuses a panel that reports a different domain (re-enroll to move an agent to another relay).
29
+ - **The credential is never a process argument.** On Linux it lives in the 0600 `chisel.env` referenced by `EnvironmentFile=`; on macOS in the 0600 plist's environment. `ps` shows nothing secret.
30
+ - **The Chisel release is pinned.** Agent and relay both run Chisel 1.12.0, downloaded from its fixed GitHub release URL and checked against a pinned SHA-256 before it is unpacked. A binary reporting another version is replaced by the next sync (on the relay, by re-running `create-lamaste`). The client reconnects with `--max-retry-interval 30s`, so it is back within half a minute of a relay restart.
31
+ - **Only loopback ports.** The agent forwards `127.0.0.1:<port>` on the relay to `127.0.0.1:<port>` on the agent, and refuses any other address even if a panel response asks for one. To expose a service that is not on the agent's own loopback, run the agent on the machine (or VM) that hosts the service.
32
+
33
+ ## Prerequisites
34
+
35
+ - A Lamaste server with onboarding complete — see [Installation](installation.md)
36
+ - Node.js 20 or later and npm on the agent machine
37
+ - macOS (arm64 or x64) or Linux (arm64 or x64) with systemd
38
+ - An **enrollment token** or an **agent certificate** (`.p12` + password) from the panel
39
+
40
+ ## 1. Issue a credential on the panel
41
+
42
+ **Enrollment token (recommended).** The agent generates its private key locally; the key never leaves the machine. In the panel: **Certificates → Enrollment Token**, or via the API:
43
+
44
+ ```bash
45
+ curl -s --cert client.p12:password \
46
+ -X POST -H 'Content-Type: application/json' \
47
+ -d '{"label":"build-server","capabilities":["tunnels:read"]}' \
48
+ https://203.0.113.42:9292/api/certs/agent/enroll | jq
49
+ ```
50
+
51
+ The token is single-use and expires after 10 minutes. `tunnels:read` is enough for an agent whose tunnels an administrator creates; add `tunnels:write` only if the agent should create its own.
52
+
53
+ **P12 certificate.** Alternatively generate an agent certificate under **Certificates → Agent Certificates** and copy the `.p12` and its password to the machine over a secure channel.
54
+
55
+ > Never use the admin certificate on an agent. It has unrestricted access to the panel.
56
+
57
+ ## 2. Install the agent and run setup
58
+
59
+ Install the agent **globally** first:
60
+
61
+ ```bash
62
+ npm install -g @lamalibre/lamaste-agent@4.0.0
63
+ ```
64
+
65
+ A global install is required, not a convenience. Setup installs a sync timer (see [How tunnel changes reach the agent](#how-tunnel-changes-reach-the-agent)) that runs the installed `lamaste-agent` every 30 seconds for as long as the agent exists. An `npx` copy lives in npm's cache, which npm may clear at any time — the timer would silently stop. `setup` and `update` therefore refuse to run from `npx`, and they check this **before** an enrollment token is spent. For an installation npm does not manage, set `LAMALIBRE_LAMASTE_AGENT_CLI_PATH` to the absolute path of the `lamaste-agent` script the timer should run.
66
+
67
+ With a token — non-interactive, and the token stays out of the process list:
68
+
69
+ ```bash
70
+ LAMALIBRE_LAMASTE_ENROLLMENT_TOKEN=<token> \
71
+ lamaste-agent setup --label build-server --panel-url https://203.0.113.42:9292
72
+ ```
73
+
74
+ With a P12 certificate — interactive; it asks for the panel URL, the `.p12` path and its password:
75
+
76
+ ```bash
77
+ lamaste-agent setup --label build-server
78
+ ```
79
+
80
+ Setup pins the panel's TLS public key on first contact (trust on first use) and prints it — compare it with the value shown in the panel before continuing. It then downloads and verifies Chisel, fetches the agent's chisel credential straight into a 0600 file, records the enrolled domain, starts the tunnel client if the agent already carries tunnels, and installs the sync timer.
81
+
82
+ ## 3. Linux servers and VMs: survive reboot
83
+
84
+ The Chisel client and the sync timer are systemd **user** units. Without _lingering_, a user's units only run while that user has a login session — a headless machine loses its tunnels at every reboot until someone logs in. Setup and `lamaste-agent status` report this; enable it once:
85
+
86
+ ```bash
87
+ sudo loginctl enable-linger "$USER"
88
+ ```
89
+
90
+ `lamaste-agent status` then shows `At boot: starts without login (linger enabled)`. macOS LaunchAgents start at login by design and need nothing here.
91
+
92
+ ## 4. Carry tunnels
93
+
94
+ An administrator creates a tunnel and names the agent that carries it (**Tunnels → Add Tunnel → Agent**, or `agentLabel` in `POST /api/tunnels` — see the [Tunnels API](../04-api-reference/tunnels.md)). The relay starts accepting that port from this agent immediately, and the agent's sync timer starts forwarding it within about 30 seconds. Nothing has to be run on the agent.
95
+
96
+ The same holds for every other change: a tunnel deleted, disabled, re-enabled, or moved to or from this agent on the panel is applied by the next sync. To apply a change at once, run:
97
+
98
+ ```bash
99
+ lamaste-agent update
100
+ ```
101
+
102
+ An agent with no tunnels keeps its Chisel client **stopped** — Chisel cannot run without at least one remote — and starts it automatically once a tunnel is assigned.
103
+
104
+ ## How tunnel changes reach the agent
105
+
106
+ ### In Plain English
107
+
108
+ The relay only lets an agent open the ports of the enabled tunnels it carries, and Chisel is strict about it: if an agent asks for even one port it is no longer allowed, the relay rejects the agent's **whole** connection. So when a tunnel is disabled or moved away, the agent has to stop asking for it quickly — otherwise all of its other tunnels go dark too. A small timer on the agent checks with the panel every 30 seconds and adjusts the tunnel client when something changed.
109
+
110
+ ### The sync timer
111
+
112
+ `lamaste-agent sync --label <label>` fetches the agent's configuration (`GET /api/tunnels/agent-config`) and converges the local tunnel client with it:
113
+
114
+ 1. The relay URL must be `https://tunnel.<enrolled domain>:443`; a panel reporting another domain is refused.
115
+ 2. The chisel credential is current — a credential rotated by an administrator is fetched again (see [Credentials](#credentials)).
116
+ 3. The Chisel binary is the pinned release.
117
+ 4. The service definition lists exactly the tunnels the relay grants this agent.
118
+ 5. The client runs when there is something to carry, and is stopped when the agent carries no tunnel or the operator stopped it.
119
+
120
+ When everything already matches, a run costs one panel request and changes nothing. The timer runs it:
121
+
122
+ | Platform | Units | Schedule |
123
+ | -------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
124
+ | Linux | `lamalibre-lamaste-sync-<label>.service` (oneshot) and `.timer` in `~/.config/systemd/user/` | 5 s after the timer starts, then 30 s after each run finishes |
125
+ | macOS | LaunchAgent `com.lamalibre.lamaste.sync-<label>` in `~/Library/LaunchAgents/` | At load, then every 30 s (`StartInterval`) |
126
+
127
+ The timer logs quietly to `~/.lamalibre/lamaste/agents/<label>/logs/sync.log`: only runs that changed something, a warning when it first appears, and an error when it first appears and again once it clears — a failure or warning repeated every 30 seconds is logged once. The outcome of the last run is kept in `sync-state.json` in the agent directory, and `lamaste-agent status` shows it on its **Sync** line (for example `every 30s, last ok <time> (2 tunnel(s))`, or `failing since <time>: <error>`). A last run more than 2 minutes old is flagged — the timer is not running its program — with the advice to run `lamaste-agent update`.
128
+
129
+ The timer runs `<node> <installed lamaste-agent script> sync --label <label> --quiet` with absolute paths. For Node.js it records a stable path — when a `node` on `PATH` resolves to the running binary (e.g. `/opt/homebrew/bin/node` rather than Homebrew's versioned `Cellar/node/<version>/bin/node`), that path is used — so upgrading Node.js does not silently stop the timer. After moving the agent to another Node.js installation, run `lamaste-agent update` to rewrite the timer.
130
+
131
+ Setup, `update`, `sync`, the agent panel and the agent daemon all change the same service and `config.json`, possibly at the same moment. They serialize on a per-agent lock file, `~/.lamalibre/lamaste/agents/<label>/agent.lock`, which records the owner's PID, boot and start time. A lock left behind by a crash — its process gone, from an earlier boot, or older than 15 minutes — is taken over automatically.
132
+
133
+ `lamaste-agent update` applies the panel's view now, restarts the tunnel client, and (re)installs the sync timer — which is also how an agent set up before the timer existed gets one.
134
+
135
+ ### Stopping tunnels on purpose
136
+
137
+ Stopping the tunnels from the agent panel or the desktop app is remembered (`tunnelsStopped` in the agent's `config.json`), so the timer keeps them stopped. **Start** and **Restart** clear it and converge with the panel again.
138
+
139
+ ## Credentials
140
+
141
+ Each agent authenticates to the relay with its own chisel credential, stored in `chisel.json` (0600) and handed to the service through a 0600 file — never a process argument.
142
+
143
+ - **Rotated by an administrator.** The panel's agent configuration carries `chiselCredentialIssuedAt`, the time the agent's current credential was issued. When it differs from the stored one, the next sync fetches the new credential and restarts the client; `lamaste-agent chisel refresh-credential` does the same immediately.
144
+ - **Upgraded agents rotate once.** Agents set up before this version kept the credential in process arguments and 0644 unit files, where any local user could read it. On its first sync after the upgrade, such an agent replaces its credential through `POST /api/agents/me/chisel-credential/rotate`. If the relay refuses (it allows one self-rotation per 10 minutes, or it has not been upgraded yet), the agent keeps its current credential and tries again on the next sync. Agents enrolled with this version never need the rotation: their credential went straight from enrollment into a 0600 file.
145
+
146
+ ## Upgrading
147
+
148
+ Upgrade agents **before** the server. Agents from before this version reject the new shape of the relay's Chisel arguments; a current agent works against an older server, keeping its credential until the server offers rotation.
149
+
150
+ ```bash
151
+ npm install -g @lamalibre/lamaste-agent@4.0.0
152
+ lamaste-agent update
153
+ ```
154
+
155
+ `update` installs the sync timer on an agent that was set up without one. An agent originally set up with `npx` must be installed globally first.
156
+
157
+ ## Everyday commands
158
+
159
+ ```bash
160
+ lamaste-agent status # health, boot persistence, sync timer, carried tunnels
161
+ lamaste-agent logs # follow the Chisel log
162
+ lamaste-agent sync # converge with the panel once (what the timer runs)
163
+ lamaste-agent update # apply the panel's view now, restart, (re)install the timer
164
+ lamaste-agent chisel refresh-credential # fetch a rotated credential now instead of at the next sync
165
+ lamaste-agent list # agents configured on this machine
166
+ lamaste-agent switch <label> # change the default agent
167
+ lamaste-agent uninstall --label <l> # stop and remove one agent, including its sync timer
168
+ ```
169
+
170
+ ## Troubleshooting
171
+
172
+ | Symptom | Cause and fix |
173
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174
+ | `setup` says the agent must be installed globally | It was run through `npx`. Run `npm install -g @lamalibre/lamaste-agent@4.0.0`, then `lamaste-agent setup ...` again. The enrollment token was not spent. |
175
+ | Log shows a TLS / x509 error | `tunnel.<domain>` does not present a valid certificate for its name. Check the relay's certificate (`Certificates` page) and that the agent's clock is correct. |
176
+ | Log shows authentication failed | The credential was rotated on the panel. The next sync picks it up; `lamaste-agent chisel refresh-credential` applies it now. |
177
+ | Log shows `access denied` for a remote | The relay no longer grants this agent that port: the tunnel was disabled, deleted, or moved to another agent. The next sync drops it. If it persists, check `lamaste-agent status` — the sync timer may not be running. |
178
+ | `status` shows `Sync: not installed` | The agent was set up before the timer existed. Install globally, then run `lamaste-agent update`. |
179
+ | `status` shows `Sync: last ran ... not running its program` | The timer's last run is over 2 minutes old: the timer was unloaded, or the program it runs is gone (Node.js or the agent was moved or uninstalled). Run `lamaste-agent update` from the global install to rewrite it. |
180
+ | `status` shows `Sync: failing since ...` | The error is the last sync's. `The panel reports domain X, but this agent is enrolled with Y` means the panel moved to another domain: re-enroll the agent. Otherwise check the panel is reachable. Details in `logs/sync.log`. |
181
+ | Tunnels do not start although the agent carries some | The tunnels were stopped from the agent panel or desktop app, and the timer respects that. Start them there. |
182
+ | Tunnel works until reboot | Lingering is off. See step 3. |
183
+ | New tunnel returns 502 | The agent has not synced yet (up to ~30 s), or nothing listens on `127.0.0.1:<port>` on the agent. Run `lamaste-agent update` to apply at once. |
184
+ | Upload fails with 413 | The request is larger than the tunnel's body limit. Raise **Largest request body** on the tunnel (`maxBodySizeMb`). |
185
+
186
+ ## For Developers
187
+
188
+ The agent-side logic lives in `packages/core/lib/src/agent/` and is shared by the CLI (`lamaste-agent`) and the agent daemon (`lamaste-agentd`):
189
+
190
+ - `converge.ts` — `convergeChiselService(label, agentConfigResponse, panelCalls, { forceRestart })`, the one refresh path of `setup`, `update`, `sync`, `chisel refresh-credential`, the agent panel and the daemon. Returns the resulting state (`running`, `idle` for no tunnels, `stopped` by the operator).
191
+ - `chisel-service.ts` — renders the service definition. `parseChiselArgs` validates the panel's `chiselArgs` (`['client', <serverUrl>, ...R:127.0.0.1:<p>:127.0.0.1:<p>]`) and discards a legacy `--tls-skip-verify` from older panels; `assertRelayServerUrl` pins the server URL to the enrolled domain. The stored credential is always combined in, so no refresh path can drop authentication.
192
+ - `sync-service.ts` — the sync timer (`installSyncService`, `removeSyncService`), `resolveInstalledAgentCli` (the global-install check, stable Node.js path) and `sync-state.json` (`isSyncStale`).
193
+ - `agent-lock.ts` — `withAgentLock(label, fn)`, the per-agent cross-process lock (`agents/<label>/agent.lock`) that every operation touching the service or `config.json` takes; `updateAgentConfig` in `converge.ts` is the locked read-modify-write for `config.json`.
194
+ - `chisel-binary.ts` + `../chisel-download.ts` — the pinned, checksum-verified Chisel download (`CHISEL_RELEASE` in `constants.ts`).