@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.
- package/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
# Panel Server Architecture
|
|
2
|
+
|
|
3
|
+
> The `lamalibre-lamaste-serverd` daemon is a Fastify 5 REST API that manages all Lamaste operations at runtime, from onboarding through tunnel management, running as a non-root systemd service on the VPS.
|
|
4
|
+
|
|
5
|
+
## In Plain English
|
|
6
|
+
|
|
7
|
+
The panel server is the brain of Lamaste. It runs on the VPS as a background service and handles every management operation: setting up your domain, creating tunnels, managing users, renewing certificates, and monitoring services. When you interact with the Lamaste admin panel in your browser, every action goes through this server.
|
|
8
|
+
|
|
9
|
+
It deliberately does not face the internet directly. nginx sits in front of it, handling TLS, client certificate verification, and domain routing. The panel server trusts that if a request reaches it, nginx has already verified the caller's identity.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
nginx (public)
|
|
15
|
+
│
|
|
16
|
+
├── X-SSL-Client-Verify: SUCCESS
|
|
17
|
+
├── X-SSL-Client-DN: CN=admin,O=Lamaste
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
Fastify Server (127.0.0.1:3100)
|
|
21
|
+
│
|
|
22
|
+
├── Plugins
|
|
23
|
+
│ ├── @fastify/cors
|
|
24
|
+
│ ├── @fastify/cookie
|
|
25
|
+
│ ├── @fastify/multipart (50 MB limit)
|
|
26
|
+
│ ├── @fastify/websocket
|
|
27
|
+
│ └── @fastify/static (SPA serving)
|
|
28
|
+
│
|
|
29
|
+
├── Middleware (onRequest hooks)
|
|
30
|
+
│ ├── mtls.js → Verify mTLS, check revocation, parse role
|
|
31
|
+
│ ├── twofa-session.js → Verify 2FA session cookie (after mTLS, before roleGuard)
|
|
32
|
+
│ ├── role-guard.js → Role-based access control (admin vs agent)
|
|
33
|
+
│ └── onboarding-guard → Route access by onboarding state
|
|
34
|
+
│
|
|
35
|
+
├── Routes
|
|
36
|
+
│ ├── /api/health → Always accessible
|
|
37
|
+
│ ├── /api/invite/* → Public (no mTLS required)
|
|
38
|
+
│ ├── /api/onboarding/* → Guarded: 410 after COMPLETED
|
|
39
|
+
│ └── /api/* (management) → Guarded: 503 before COMPLETED
|
|
40
|
+
│
|
|
41
|
+
├── Error Handler
|
|
42
|
+
│ └── errors.js → Zod errors → 400, AppError → status, else → 500
|
|
43
|
+
│
|
|
44
|
+
└── SPA Fallback
|
|
45
|
+
└── Non-/api 404s → serve index.html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Server Entry (`src/index.js`)
|
|
49
|
+
|
|
50
|
+
The server entry point follows a straightforward initialization sequence:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
1. loadConfig() → Read and validate /etc/lamalibre/lamaste/panel.json
|
|
54
|
+
2. Load plugin capabilities → getPluginCapabilities() → setPluginCapabilities()
|
|
55
|
+
3. Load ticket scope capabilities → loadTicketScopeCapabilities()
|
|
56
|
+
4. Register Fastify plugins → CORS, cookie, multipart, websocket, static files
|
|
57
|
+
5. Register publicContext → Error handler + invite + enrollment routes (no mTLS)
|
|
58
|
+
6. Register protectedContext → mTLS + 2FA session + role-guard + error handler + health/onboarding/management/plugin routes
|
|
59
|
+
7. Set 404 handler → SPA fallback for non-API routes
|
|
60
|
+
8. Listen on 127.0.0.1:3100
|
|
61
|
+
9. Start instance liveness interval → checkInstanceLiveness() every 60s
|
|
62
|
+
10. Register shutdown handlers → SIGTERM, SIGINT
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Route registration contexts:** The server separates routes into two Fastify encapsulated contexts:
|
|
66
|
+
|
|
67
|
+
- **`publicContext`** — registers invite routes (`/api/invite/*`) and enrollment routes (`/api/enroll`) without mTLS middleware, allowing unauthenticated users to accept invitations and agents to enroll with one-time tokens
|
|
68
|
+
- **`protectedContext`** — registers mTLS middleware, 2FA session guard, role-guard, and all other routes (health, onboarding, management, plugins), requiring a valid client certificate
|
|
69
|
+
|
|
70
|
+
**Static file serving** resolves the lamaste-server-ui `dist/` directory through a fallback chain:
|
|
71
|
+
|
|
72
|
+
- `config.staticDir` if set in `panel.json` (production: `/opt/lamalibre/lamaste/lamaste-server-ui/dist`)
|
|
73
|
+
- `../../lamaste-server-ui/dist` relative to server source (development)
|
|
74
|
+
- `config.dataDir/lamaste-server-ui/dist` as final fallback
|
|
75
|
+
|
|
76
|
+
**CORS origin** includes the IP-based URL (`https://<ip>:9292`) always, plus the domain-based panel URL (`https://panel.<domain>`) when a domain is configured. This prevents cross-origin requests from unrelated domains.
|
|
77
|
+
|
|
78
|
+
## Middleware Pipeline
|
|
79
|
+
|
|
80
|
+
### mTLS Verification (`middleware/mtls.js`)
|
|
81
|
+
|
|
82
|
+
Registered as a global `onRequest` hook that runs on every request before route handlers.
|
|
83
|
+
|
|
84
|
+
**Production behavior:**
|
|
85
|
+
|
|
86
|
+
- Reads the `X-SSL-Client-Verify` header set by nginx
|
|
87
|
+
- If the value is not `SUCCESS`, returns `403 { error: "mTLS certificate required" }`
|
|
88
|
+
- Checks the certificate serial (`X-SSL-Client-Serial` header) against `revoked.json` via `isRevoked()` — if revoked, returns `403 { error: "Certificate has been revoked" }`
|
|
89
|
+
- Parses `X-SSL-Client-DN` header to extract the CN field
|
|
90
|
+
- If CN starts with `agent:`, sets `request.certRole = 'agent'`, `request.certLabel` to the agent label, and loads `request.certCapabilities` from the agent registry
|
|
91
|
+
- Otherwise, sets `request.certRole = 'admin'`
|
|
92
|
+
|
|
93
|
+
**Development behavior:**
|
|
94
|
+
|
|
95
|
+
- When `NODE_ENV` is `development`, the check is bypassed
|
|
96
|
+
- Logs a warning on the first bypassed request
|
|
97
|
+
- Sets `request.certRole = 'admin'` by default
|
|
98
|
+
|
|
99
|
+
**Health check bypass:**
|
|
100
|
+
|
|
101
|
+
- `GET /api/health` always bypasses mTLS verification (used by systemd, load balancers, and internal provisioning checks)
|
|
102
|
+
|
|
103
|
+
In production, nginx uses `ssl_verify_client optional` at the server level, with per-location enforcement via `if ($ssl_client_verify != SUCCESS) { return 496; }` on protected locations. Public endpoints (`/api/enroll`, `/api/invite`) skip this check. The server middleware is a defense-in-depth measure that also performs revocation checking and role extraction.
|
|
104
|
+
|
|
105
|
+
### 2FA Session Guard (`middleware/twofa-session.js`)
|
|
106
|
+
|
|
107
|
+
Registered as a Fastify plugin in the protected context, runs after mTLS verification and before the role guard. When built-in 2FA is enabled (`panel2fa.enabled` in `panel.json`), this middleware enforces that admin requests carry a valid session cookie.
|
|
108
|
+
|
|
109
|
+
**Behavior:**
|
|
110
|
+
|
|
111
|
+
- If 2FA is not enabled, the middleware is a no-op — all requests pass through
|
|
112
|
+
- Agent requests (`request.certRole === 'agent'`) always bypass the 2FA check — agents authenticate via mTLS only
|
|
113
|
+
- Health, 2FA status, and 2FA verification endpoints are exempt (`/api/health`, `/api/settings/2fa`, `/api/settings/2fa/verify`)
|
|
114
|
+
- If an admin request lacks a valid session cookie, returns `401 { error: "2fa_required" }`
|
|
115
|
+
- Session cookies are issued by the `POST /api/settings/2fa/verify` endpoint after a valid TOTP code is presented
|
|
116
|
+
- Sessions are managed by `lib/session.js` using signed cookies (secret from `sessionSecret` in `panel.json`)
|
|
117
|
+
|
|
118
|
+
The Fastify constructor sets `trustProxy: 1` so that exactly one proxy hop (nginx) is trusted for `X-Forwarded-*` headers. This is more secure than `trustProxy: true` (which trusts all hops) and matches the documented deployment model where nginx is the sole reverse proxy.
|
|
119
|
+
|
|
120
|
+
### Onboarding Guard (`middleware/onboarding-guard.js`)
|
|
121
|
+
|
|
122
|
+
Two hook factories that enforce route access based on `config.onboarding.status`:
|
|
123
|
+
|
|
124
|
+
**`onboardingOnly()`** — applied to `/api/onboarding/*` (except `/status`):
|
|
125
|
+
|
|
126
|
+
- If status is `COMPLETED`, returns `410 Gone`
|
|
127
|
+
- Otherwise, allows the request
|
|
128
|
+
|
|
129
|
+
**`managementOnly()`** — applied to all `/api/*` management routes:
|
|
130
|
+
|
|
131
|
+
- If status is not `COMPLETED`, returns `503 { error: "Onboarding not complete", onboardingStatus }`
|
|
132
|
+
- Otherwise, allows the request
|
|
133
|
+
|
|
134
|
+
The `/api/onboarding/status` endpoint is deliberately unguarded — the panel client calls it on every page load to determine which UI mode to display.
|
|
135
|
+
|
|
136
|
+
### Role Guard (`middleware/role-guard.js`)
|
|
137
|
+
|
|
138
|
+
Registered as a Fastify plugin that decorates the server with a `requireRole(allowedRoles, opts)` function. This returns a `preHandler` hook used on individual routes to enforce role-based access control.
|
|
139
|
+
|
|
140
|
+
**Usage patterns:**
|
|
141
|
+
|
|
142
|
+
- `fastify.requireRole(['admin'])` — admin-only routes
|
|
143
|
+
- `fastify.requireRole(['admin', 'agent'])` — admin or any agent
|
|
144
|
+
- `fastify.requireRole(['admin', 'agent'], { capability: 'tunnels:write' })` — admin or agents with a specific capability
|
|
145
|
+
|
|
146
|
+
**Behavior:**
|
|
147
|
+
|
|
148
|
+
- Admin role always passes without capability checks
|
|
149
|
+
- If the request's `certRole` is not in the allowed list, returns `403 { error: "Insufficient certificate scope" }`
|
|
150
|
+
- If a capability is required and the agent lacks it, returns `403 { error: "Insufficient certificate capability" }`
|
|
151
|
+
|
|
152
|
+
### Error Handler (`middleware/errors.js`)
|
|
153
|
+
|
|
154
|
+
Registered as the global Fastify error handler. Normalizes all errors into a consistent response format:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"error": "Human-readable error summary",
|
|
159
|
+
"details": {}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Error type resolution order:
|
|
164
|
+
|
|
165
|
+
| Error Type | Detection | Response |
|
|
166
|
+
| ---------------------- | ------------------------------------------------------------ | ---------------------------------------- |
|
|
167
|
+
| Zod validation | `error.name === 'ZodError'` or `Array.isArray(error.issues)` | `400` with issue paths and messages |
|
|
168
|
+
| Operational (AppError) | `error.isOperational === true` | Custom `statusCode` from error |
|
|
169
|
+
| Fastify built-in | `error.statusCode` in 400-499 | Pass through status and message |
|
|
170
|
+
| Unexpected | Everything else | `500 { error: "Internal server error" }` |
|
|
171
|
+
|
|
172
|
+
In development mode, unexpected errors include `details.message` and `details.stack`. In production, no internal details are leaked.
|
|
173
|
+
|
|
174
|
+
### AppError (`lib/app-error.js`)
|
|
175
|
+
|
|
176
|
+
A lightweight error class for operational errors (expected failures):
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
export class AppError extends Error {
|
|
180
|
+
constructor(message, statusCode, details = null) {
|
|
181
|
+
super(message);
|
|
182
|
+
this.name = 'AppError';
|
|
183
|
+
this.statusCode = statusCode;
|
|
184
|
+
this.details = details;
|
|
185
|
+
this.isOperational = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Used in library code to signal expected error conditions (e.g., "DNS is not pointing to this server", "Cannot delete the last user").
|
|
191
|
+
|
|
192
|
+
## Route Structure
|
|
193
|
+
|
|
194
|
+
### Registration Hierarchy
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
publicContext (no mTLS):
|
|
198
|
+
server.register(inviteRoutes, { prefix: '/api/invite' })
|
|
199
|
+
├── GET /api/invite/:token ← Get invitation details
|
|
200
|
+
└── POST /api/invite/:token/accept ← Accept invitation, set password
|
|
201
|
+
server.register(enrollmentRoutes, { prefix: '/api/enroll' })
|
|
202
|
+
└── POST /api/enroll ← Enroll agent with token + CSR
|
|
203
|
+
|
|
204
|
+
protectedContext (mTLS + role-guard):
|
|
205
|
+
server.register(healthRoutes, { prefix: '/api' })
|
|
206
|
+
└── GET /api/health
|
|
207
|
+
|
|
208
|
+
server.register(onboardingRoutes, { prefix: '/api/onboarding' })
|
|
209
|
+
├── GET /api/onboarding/status ← No guard
|
|
210
|
+
└── [guarded: onboardingOnly()]
|
|
211
|
+
├── POST /api/onboarding/domain
|
|
212
|
+
├── POST /api/onboarding/verify-dns
|
|
213
|
+
├── POST /api/onboarding/provision
|
|
214
|
+
└── WS /api/onboarding/provision/stream
|
|
215
|
+
|
|
216
|
+
server.register(managementRoutes, { prefix: '/api' })
|
|
217
|
+
└── [guarded: managementOnly()]
|
|
218
|
+
├── GET /api/tunnels/agent-config
|
|
219
|
+
├── GET /api/tunnels
|
|
220
|
+
├── POST /api/tunnels
|
|
221
|
+
├── PATCH /api/tunnels/:id
|
|
222
|
+
├── DELETE /api/tunnels/:id
|
|
223
|
+
├── GET /api/tunnels/agent-panel-status
|
|
224
|
+
├── POST /api/tunnels/expose-panel
|
|
225
|
+
├── DELETE /api/tunnels/retract-panel
|
|
226
|
+
├── GET /api/sites
|
|
227
|
+
├── POST /api/sites
|
|
228
|
+
├── PATCH /api/sites/:id
|
|
229
|
+
├── DELETE /api/sites/:id
|
|
230
|
+
├── POST /api/sites/:id/verify-dns
|
|
231
|
+
├── GET /api/sites/:id/files
|
|
232
|
+
├── POST /api/sites/:id/files
|
|
233
|
+
├── DELETE /api/sites/:id/files
|
|
234
|
+
├── GET /api/system/stats
|
|
235
|
+
├── GET /api/services
|
|
236
|
+
├── POST /api/services/:name/:action
|
|
237
|
+
├── WS /api/services/:name/logs
|
|
238
|
+
├── GET /api/users
|
|
239
|
+
├── POST /api/users
|
|
240
|
+
├── PUT /api/users/:username
|
|
241
|
+
├── DELETE /api/users/:username
|
|
242
|
+
├── POST /api/users/:username/reset-totp
|
|
243
|
+
├── GET /api/certs
|
|
244
|
+
├── GET /api/certs/auto-renew-status
|
|
245
|
+
├── POST /api/certs/:domain/renew
|
|
246
|
+
├── POST /api/certs/mtls/rotate
|
|
247
|
+
├── GET /api/certs/mtls/download
|
|
248
|
+
├── POST /api/certs/admin/upgrade-to-hardware-bound
|
|
249
|
+
├── GET /api/certs/admin/auth-mode
|
|
250
|
+
├── POST /api/certs/agent
|
|
251
|
+
├── POST /api/certs/agent/enroll
|
|
252
|
+
├── GET /api/certs/agent
|
|
253
|
+
├── GET /api/certs/agent/:label/download
|
|
254
|
+
├── DELETE /api/certs/agent/:label
|
|
255
|
+
├── PATCH /api/certs/agent/:label/capabilities
|
|
256
|
+
├── PATCH /api/certs/agent/:label/allowed-sites
|
|
257
|
+
├── GET /api/invitations
|
|
258
|
+
├── POST /api/invitations
|
|
259
|
+
├── DELETE /api/invitations/:id
|
|
260
|
+
├── GET /api/plugins
|
|
261
|
+
├── GET /api/plugins/:name
|
|
262
|
+
├── POST /api/plugins/install
|
|
263
|
+
├── POST /api/plugins/:name/enable
|
|
264
|
+
├── POST /api/plugins/:name/disable
|
|
265
|
+
├── DELETE /api/plugins/:name
|
|
266
|
+
├── GET /api/plugins/push-install/config
|
|
267
|
+
├── PATCH /api/plugins/push-install/config
|
|
268
|
+
├── GET /api/plugins/push-install/policies
|
|
269
|
+
├── POST /api/plugins/push-install/policies
|
|
270
|
+
├── PATCH /api/plugins/push-install/policies/:id
|
|
271
|
+
├── DELETE /api/plugins/push-install/policies/:id
|
|
272
|
+
├── POST /api/plugins/push-install/enable/:label
|
|
273
|
+
├── DELETE /api/plugins/push-install/enable/:label
|
|
274
|
+
├── POST /api/plugins/push-install/:label
|
|
275
|
+
├── GET /api/plugins/push-install/sessions
|
|
276
|
+
├── GET /api/plugins/push-install/agent-status
|
|
277
|
+
├── GET /api/settings/2fa
|
|
278
|
+
├── POST /api/settings/2fa/setup
|
|
279
|
+
├── POST /api/settings/2fa/confirm
|
|
280
|
+
├── POST /api/settings/2fa/verify
|
|
281
|
+
├── POST /api/settings/2fa/disable
|
|
282
|
+
├── POST /api/tickets/scopes
|
|
283
|
+
├── GET /api/tickets/scopes
|
|
284
|
+
├── DELETE /api/tickets/scopes/:name
|
|
285
|
+
├── POST /api/tickets/instances
|
|
286
|
+
├── DELETE /api/tickets/instances/:instanceId
|
|
287
|
+
├── POST /api/tickets/instances/:instanceId/heartbeat
|
|
288
|
+
├── POST /api/tickets/assignments
|
|
289
|
+
├── DELETE /api/tickets/assignments/:agentLabel/:instanceScope
|
|
290
|
+
├── GET /api/tickets/assignments
|
|
291
|
+
├── POST /api/tickets
|
|
292
|
+
├── GET /api/tickets/inbox
|
|
293
|
+
├── POST /api/tickets/validate
|
|
294
|
+
├── GET /api/tickets
|
|
295
|
+
├── DELETE /api/tickets/:ticketId
|
|
296
|
+
├── POST /api/tickets/sessions
|
|
297
|
+
├── POST /api/tickets/sessions/:sessionId/heartbeat
|
|
298
|
+
├── PATCH /api/tickets/sessions/:sessionId
|
|
299
|
+
├── DELETE /api/tickets/sessions/:sessionId
|
|
300
|
+
├── GET /api/tickets/sessions
|
|
301
|
+
├── GET /api/identity/self
|
|
302
|
+
├── GET /api/identity/users
|
|
303
|
+
├── GET /api/identity/users/:username
|
|
304
|
+
├── GET /api/identity/groups
|
|
305
|
+
├── POST /api/storage/servers
|
|
306
|
+
├── GET /api/storage/servers
|
|
307
|
+
├── DELETE /api/storage/servers/:id
|
|
308
|
+
├── POST /api/storage/bindings
|
|
309
|
+
├── GET /api/storage/bindings
|
|
310
|
+
├── GET /api/storage/bindings/:pluginName
|
|
311
|
+
├── DELETE /api/storage/bindings/:pluginName
|
|
312
|
+
└── POST /api/system/update
|
|
313
|
+
|
|
314
|
+
server.register(pluginRouter, { prefix: '/api' })
|
|
315
|
+
└── [guarded: managementOnly() + per-plugin admin-only auth scope]
|
|
316
|
+
├── /api/<pluginName>/* ← Dynamic plugin server routes
|
|
317
|
+
└── GET /api/<pluginName>/panel.js ← Plugin panel bundle
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Onboarding Routes
|
|
321
|
+
|
|
322
|
+
The onboarding module (`routes/onboarding/index.js`) uses a nested registration pattern to apply the guard:
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
export default async function onboardingRoutes(fastify, _opts) {
|
|
326
|
+
// Status is always accessible — no guard
|
|
327
|
+
await fastify.register(statusRoute);
|
|
328
|
+
|
|
329
|
+
// All other routes are guarded: 410 after onboarding completes
|
|
330
|
+
await fastify.register(async function guarded(app) {
|
|
331
|
+
app.addHook('onRequest', onboardingOnly());
|
|
332
|
+
await app.register(domainRoute);
|
|
333
|
+
await app.register(dnsRoute);
|
|
334
|
+
await app.register(provisionRoute);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Provisioning** is the most complex onboarding route. It uses a background task pattern:
|
|
340
|
+
|
|
341
|
+
1. `POST /provision` validates the onboarding state, starts the provisioning function asynchronously, and returns `202 Accepted`
|
|
342
|
+
2. `WS /provision/stream` connects clients to a real-time progress feed via an `EventEmitter`
|
|
343
|
+
3. The provisioning function emits progress events as it installs Chisel, Authelia, issues certificates, configures nginx, and verifies services
|
|
344
|
+
4. On completion, it updates `panel.json` to `COMPLETED` and emits final credentials
|
|
345
|
+
|
|
346
|
+
### Management Routes
|
|
347
|
+
|
|
348
|
+
The management module (`routes/management.js`) applies `managementOnly()` at the top level, guarding all child routes:
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
export default async function managementRoutes(fastify, _opts) {
|
|
352
|
+
fastify.addHook('onRequest', managementOnly());
|
|
353
|
+
|
|
354
|
+
await fastify.register(tunnelRoutes);
|
|
355
|
+
await fastify.register(sitesRoutes);
|
|
356
|
+
await fastify.register(systemRoutes);
|
|
357
|
+
await fastify.register(servicesRoutes);
|
|
358
|
+
await fastify.register(logsRoutes);
|
|
359
|
+
await fastify.register(usersRoutes);
|
|
360
|
+
await fastify.register(certsRoutes);
|
|
361
|
+
await fastify.register(invitationRoutes);
|
|
362
|
+
await fastify.register(pluginRoutes);
|
|
363
|
+
await fastify.register(settingsRoutes);
|
|
364
|
+
await fastify.register(ticketRoutes);
|
|
365
|
+
await fastify.register(identityRoutes);
|
|
366
|
+
await fastify.register(storageRoutes);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Library Layer
|
|
371
|
+
|
|
372
|
+
Routes handle HTTP concerns only — request parsing, response formatting, status codes. All business logic lives in `src/lib/` modules.
|
|
373
|
+
|
|
374
|
+
### config.js — Configuration Management
|
|
375
|
+
|
|
376
|
+
- Loads and validates `panel.json` at startup via Zod schema
|
|
377
|
+
- Caches parsed config in a module-level variable
|
|
378
|
+
- `getConfig()` returns a `structuredClone` (callers cannot mutate the cache)
|
|
379
|
+
- `updateConfig(patch)` performs deep merge, re-validates, and writes atomically (temp → rename)
|
|
380
|
+
|
|
381
|
+
Config schema:
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
{
|
|
385
|
+
ip: string, // Required
|
|
386
|
+
domain: string | null, // Set during onboarding
|
|
387
|
+
email: string (email) | null, // Set during onboarding
|
|
388
|
+
dataDir: string, // /etc/lamalibre/lamaste
|
|
389
|
+
serverId?: string (uuid), // Auto-generated, bucket prefix for multi-server storage isolation
|
|
390
|
+
staticDir?: string, // /opt/lamalibre/lamaste/lamaste-server-ui/dist
|
|
391
|
+
maxSiteSize?: number, // Default: 500 MB
|
|
392
|
+
adminAuthMode?: "p12" | "hardware-bound", // Default: "p12"
|
|
393
|
+
onboarding: {
|
|
394
|
+
status: "FRESH" | "DOMAIN_SET" | "DNS_READY" | "PROVISIONING" | "COMPLETED"
|
|
395
|
+
},
|
|
396
|
+
panel2fa?: {
|
|
397
|
+
enabled: boolean, // Default: false
|
|
398
|
+
secret: string | null, // TOTP secret (base32)
|
|
399
|
+
setupComplete: boolean // Default: false
|
|
400
|
+
},
|
|
401
|
+
sessionSecret?: string | null // HMAC key for signed session cookies (default: null)
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Config path resolution:
|
|
406
|
+
|
|
407
|
+
1. `LAMALIBRE_LAMASTE_CONFIG` environment variable (if set)
|
|
408
|
+
2. `dev/panel.json` relative to package root (if `NODE_ENV` is `development`)
|
|
409
|
+
3. `/etc/lamalibre/lamaste/panel.json` (production default)
|
|
410
|
+
|
|
411
|
+
### state.js — Tunnel and Site State
|
|
412
|
+
|
|
413
|
+
Provides atomic read/write for `tunnels.json` and `sites.json`:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
readTunnels() → Array<Tunnel> (returns [] if file missing)
|
|
417
|
+
writeTunnels() → void (atomic: tmp → fsync → rename)
|
|
418
|
+
readSites() → Array<Site> (returns [] if file missing)
|
|
419
|
+
writeSites() → void (atomic: tmp → fsync → rename)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The atomic write pattern:
|
|
423
|
+
|
|
424
|
+
1. `writeFile` to `<path>.tmp`
|
|
425
|
+
2. Open the temp file and call `fd.sync()` to flush to disk
|
|
426
|
+
3. `rename` temp to final path (atomic on POSIX filesystems)
|
|
427
|
+
|
|
428
|
+
This ensures that a crash during write never corrupts the state file. The worst case is losing the latest write, leaving the previous state intact.
|
|
429
|
+
|
|
430
|
+
### nginx.js — Vhost Management
|
|
431
|
+
|
|
432
|
+
Provides functions for writing, enabling, disabling, testing, and reloading nginx configurations. The core pattern is **write-with-rollback**:
|
|
433
|
+
|
|
434
|
+
1. Backup existing vhost (if any) to `.bak`
|
|
435
|
+
2. Install the new vhost: `sudo lamaste-priv nginx-site write <name>` with the text on stdin — the root helper refuses any directive outside its allow-list
|
|
436
|
+
3. Create (or remove) the symlink in `sites-enabled` (`nginx-site enable|disable`)
|
|
437
|
+
4. Run `nginx -t` to validate
|
|
438
|
+
5. On success: `systemctl reload nginx`, delete backup
|
|
439
|
+
6. On failure: restore backup (or remove the new file) and the previous link state
|
|
440
|
+
|
|
441
|
+
Tunnel vhosts (`writePublicVhost`, `writeAuthenticatedVhost`, `writeRestrictedVhost`) declare their proxy headers inside each `location` (`TUNNEL_PROXY_HEADERS`: `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, WebSocket upgrade), always clear client-supplied `X-SSL-Client-*` headers, and — on public vhosts — clear the `Remote-*` identity headers.
|
|
442
|
+
|
|
443
|
+
Additional functions `disableIpVhost()` and `enableIpVhost()` manage the IP-based panel vhost. When built-in 2FA is enabled, the IP vhost is disabled to prevent bypassing the domain-based 2FA session; disabling 2FA re-enables it.
|
|
444
|
+
|
|
445
|
+
See [nginx-configuration.md](./nginx-configuration.md) for detailed coverage.
|
|
446
|
+
|
|
447
|
+
### chisel.js — Tunnel Server Management
|
|
448
|
+
|
|
449
|
+
- Never installs the binary or writes the unit: both are `create-lamaste`'s (the pinned 1.12.0 release at `/usr/local/bin/chisel`; the unit binds `127.0.0.1:9090`, `--reverse`, `--keyfile`, `--authfile`, `User=nobody`, `Group=lamaste-chisel`). `isChiselInstalled()` checks the binary exists; `getInstalledChiselVersion()` reads its version
|
|
450
|
+
- Generates the server key `chisel-server.key` as `lamaste` (`lamaste:lamaste-chisel`, `0640`), written to a temp file and renamed into place
|
|
451
|
+
- Manages service lifecycle (start, stop, `try-restart`)
|
|
452
|
+
- `syncChisel()` — called after every tunnel state write: re-renders the authfile (via `chisel-users.js`). Chisel reloads it for new sessions; when the render withdrew a grant or password, Chisel is restarted so live sessions drop what they no longer hold (`chisel-runtime.js`). Throws if that restart fails, since a withdrawn grant would otherwise stay live
|
|
453
|
+
|
|
454
|
+
### chisel-users.js — Chisel Credentials and Grants
|
|
455
|
+
|
|
456
|
+
- Thin shim over `@lamalibre/lamaste@4.0.0/server`: per-agent credentials in `chisel-credentials.json` (0600)
|
|
457
|
+
- Renders the authfile `chisel-users` from the credentials and `tunnels.json`: each `agent-<label>` may bind exactly `^R:127\.0\.0\.1:<port>$` for its enabled tunnels
|
|
458
|
+
- Adds the `lamaste-no-grants` sentinel user (password in `chisel-sentinel`, 0600) so the authfile is never empty — Chisel disables authentication on an empty authfile
|
|
459
|
+
- Never grants a reserved port (3100, 9090, 9091, 9292, 9294) or a port claimed by two tunnels
|
|
460
|
+
- Writes the authfile itself, no sudo: `0600` temp file → `chgrp lamaste-chisel` → `chmod 0640` → fsync → rename. The serverd unit has `SupplementaryGroups=lamaste-chisel`
|
|
461
|
+
- Reports whether each render `revoked` something; the caller decides on the restart
|
|
462
|
+
|
|
463
|
+
### chisel-runtime.js — When an Authfile Change Needs a Restart
|
|
464
|
+
|
|
465
|
+
- Additions take effect by Chisel's own reload; revocations call `systemctl try-restart chisel`, which leaves a deliberately stopped Chisel stopped
|
|
466
|
+
- Until this process has seen Chisel running the pinned release (older releases do not reload reliably across atomic renames), every change restarts it
|
|
467
|
+
|
|
468
|
+
### chisel-reconcile.js — Startup Reconciliation (Fail Closed)
|
|
469
|
+
|
|
470
|
+
Runs in the background on every start (`startChiselReconciler`), so the panel stays reachable while it works:
|
|
471
|
+
|
|
472
|
+
1. Mints a Chisel credential for every active agent that lacks one
|
|
473
|
+
2. Under the tunnel lock, binds ownerless tunnels where the owner is certain (`bindUnownedTunnels`): an `agent-<label>` panel tunnel only to the agent it names, when active; other tunnels only when exactly one machine agent is active (plugin-agents are not counted). The rest stay unassigned and are logged, as are tunnels withheld from everyone (`withheldTunnels`: a reserved port or a port two tunnels share)
|
|
474
|
+
3. Once onboarding is `COMPLETED`: ensures the server key exists and rewrites the authfile (ownership and mode re-applied). It also checks the installed Chisel version; if it is not the pinned release it logs an error ("run create-lamaste on the server") and Chisel is restarted on every authfile change until it is
|
|
475
|
+
4. Restarts Chisel when a revocation requires it; starts it if enabled but not running
|
|
476
|
+
|
|
477
|
+
If the authfile cannot be established, Chisel is **stopped and disabled** and the marker `/etc/lamalibre/lamaste/chisel-failed-closed` is written — an authfile from an older version may grant every agent every port, and a disabled unit keeps a reboot from starting it on that file. Reconciliation retries with exponential backoff from 30 seconds to 10 minutes; a successful pass re-enables (`sudo systemctl enable chisel`) and starts Chisel and removes the marker.
|
|
478
|
+
|
|
479
|
+
### tunnel-deps.js — Tunnel Workflow Dependencies
|
|
480
|
+
|
|
481
|
+
- The nginx, certbot, chisel and state objects the core tunnel workflows (`createTunnel`, `updateTunnel`, `releaseAgentTunnels`, ...) receive; shared by the tunnel routes and agent revocation
|
|
482
|
+
|
|
483
|
+
### Tunnel routes — HTTP rules on top of the core workflows
|
|
484
|
+
|
|
485
|
+
- All tunnel workflows run under the core's single tunnel lock; `PATCH /api/tunnels/:id` is one `updateTunnel` call applying `agentLabel` → `accessMode`/`maxBodySizeMb` → `enabled`
|
|
486
|
+
- Plugin-agent certificates cannot create tunnels (403) — they hold no Chisel credential
|
|
487
|
+
- `maxBodySizeMb` above 100 MiB is admin-only (admins up to 10240)
|
|
488
|
+
- Reserved ports are rejected by the request schema
|
|
489
|
+
- `POST /api/tunnels` and `POST /api/tunnels/expose-panel` refuse a hostname a static site or site alias already serves
|
|
490
|
+
- `GET /api/tunnels/agent-config` returns only the tunnels Chisel actually grants the agent (`grantedTunnelsFor`) — never a withheld one, since asking for a single ungranted port gets the agent's whole session refused — and includes `chiselCredentialIssuedAt`
|
|
491
|
+
|
|
492
|
+
### authelia.js — Authentication Management
|
|
493
|
+
|
|
494
|
+
- Never installs Authelia: the pinned binary, its unit and the `lamaste-authelia` account are `create-lamaste`'s; `isAutheliaInstalled()` checks the binary exists
|
|
495
|
+
- Writes YAML configuration (bcrypt cost 12, file-based users, TOTP, session cookies) directly, without sudo: `/etc/authelia` is `lamaste:lamaste-authelia 2770` (setgid), files are written as a temp file + fsync + rename (`configuration.yml` 0640, `users.yml` 0660, `.secrets.json` 0600)
|
|
496
|
+
- User CRUD: creates users with bcrypt-hashed passwords, reads/writes `users.yml`
|
|
497
|
+
- Stores a user's TOTP secret with `sudo lamaste-priv authelia-totp <username>` (secret on stdin), which runs Authelia's CLI as `lamaste-authelia` — the database is that account's file
|
|
498
|
+
- TOTP generation: `crypto.randomBytes(20)` → base32-encoded secret → `otpauth://` URI (parameterized `generateTotpSecret(username, { issuer })` — reused by panel 2FA via `lib/totp.js`)
|
|
499
|
+
- `base32Decode()` utility for decoding base32 TOTP secrets into raw bytes
|
|
500
|
+
- Manages service lifecycle
|
|
501
|
+
|
|
502
|
+
### certbot.js — Certificate Management
|
|
503
|
+
|
|
504
|
+
- Every certbot call goes through `sudo /usr/local/sbin/lamaste-certbot issue|renew|renew-all|list`, and every read of a Let's Encrypt certificate through `sudo /usr/local/sbin/lamaste-cert-info <lineage> enddate|checkend|san` — root-owned wrappers that validate hostnames and email and run certbot/openssl with a fixed argument vector (the wildcard sudoers rules they replace accepted extra flags such as `--deploy-hook`)
|
|
505
|
+
- Issues Let's Encrypt certificates using the nginx plugin (`certbot certonly --nginx`)
|
|
506
|
+
- Issues one lineage per custom site covering its aliases (`lamaste-certbot issue <email> <fqdn> <fqdn> <alias> ...`), reading the current SANs (`lamaste-cert-info <fqdn> san`) to skip re-issuance when the certificate already covers every name — or, with `match: 'exact'`, exactly the requested names
|
|
507
|
+
- Panel-triggered renewals pass `--no-random-sleep-on-renew`
|
|
508
|
+
- Handles rate limit, DNS, and server block errors with specific error messages
|
|
509
|
+
- Supports wildcard certificate detection (skips individual issuance if wildcard covers the FQDN)
|
|
510
|
+
- Lists all managed certificates by parsing `certbot certificates` output
|
|
511
|
+
- Sets up auto-renewal via the `certbot.timer` systemd unit
|
|
512
|
+
|
|
513
|
+
### mtls.js — mTLS Certificate Operations
|
|
514
|
+
|
|
515
|
+
- Reads certificate expiry dates via `openssl x509 -enddate` (no sudo: the panel owns the PKI directory)
|
|
516
|
+
- Does not rotate admin client certificates — that is `sudo lamaste-server reset-admin` on the server only; the admin rotation route returns 503 with that instruction
|
|
517
|
+
- Generates agent-scoped client certificates with capability-based access: key, CSR and PKCS12 as `lamaste`, signed with the panel CA by `lib/pki-sign.js` (`openssl x509 -req`, run as `lamaste`), which refuses any CN that is not an `agent:` / `plugin-agent:` label — never `CN=admin`. `lib/csr-signing.js` signs enrollment and rotation CSRs the same way after checking the CN is exactly the expected label
|
|
518
|
+
- Manages the agent registry: create, list, revoke, update capabilities and allowed sites
|
|
519
|
+
- Revoking a regular agent removes its Chisel credential and releases its tunnels (`releaseAgentTunnels`): app/plugin tunnels become unassigned, its `agent-<label>` panel tunnel is removed
|
|
520
|
+
- `rotateAgentChiselCredential` backs `POST /api/agents/:label/chisel-credential/rotate` (admin) and `POST /api/agents/me/chisel-credential/rotate` (agent, refused with 429 while the current credential is under 10 minutes old)
|
|
521
|
+
- Provides the PKCS12 download path for the certs API
|
|
522
|
+
- Manages dynamic capability sets: base capabilities + plugin capabilities + ticket scope capabilities via `getValidCapabilities()`
|
|
523
|
+
- Base capabilities: `tunnels:read`, `tunnels:write`, `services:read`, `services:write`, `system:read`, `sites:read`, `sites:write`, `panel:expose`, `identity:read`, `identity:query`
|
|
524
|
+
|
|
525
|
+
### tickets.js — Ticket System
|
|
526
|
+
|
|
527
|
+
Agent-to-agent authorization with scopes, instances, tickets, and sessions. Provides:
|
|
528
|
+
|
|
529
|
+
- **Scope registry** — register/unregister capability sets with transport configuration
|
|
530
|
+
- **Instance management** — register, heartbeat, deregister with liveness tracking (active → stale → dead)
|
|
531
|
+
- **Assignment management** — link agents to instances (admin-only)
|
|
532
|
+
- **Ticket operations** — request, validate (HMAC-SHA256 + `timingSafeEqual` with per-process random key), revoke with rate limiting (10/agent/min) and hard caps (200 instances, 1000 tickets, 500 sessions)
|
|
533
|
+
- **Session management** — create from validated ticket (server-generated session IDs via `crypto.randomBytes(16)`, server-enforced timestamps), heartbeat with multi-layer re-validation, status updates
|
|
534
|
+
- **Host validation** — `transport.direct.host` rejects private/reserved IPs and cloud metadata endpoints (SSRF prevention)
|
|
535
|
+
- **Periodic cleanup** — stale/dead instances, expired tickets, dead sessions
|
|
536
|
+
- **Concurrency** — promise-chain mutex with atomic file writes (temp → fsync → rename)
|
|
537
|
+
|
|
538
|
+
Client SDK: `@lamalibre/lamaste-tickets@4.0.0` (TypeScript, undici) provides `TicketClient`, `TicketInstanceManager` (source side), and `TicketSessionManager` (target side) for plugin integration.
|
|
539
|
+
|
|
540
|
+
State files: `/etc/lamalibre/lamaste/ticket-scopes.json` (scopes, instances, assignments) and `/etc/lamalibre/lamaste/tickets.json` (tickets, sessions).
|
|
541
|
+
|
|
542
|
+
### storage.js — Storage Server Registry
|
|
543
|
+
|
|
544
|
+
- Manages S3-compatible storage servers and their bindings to plugins
|
|
545
|
+
- Credentials (access key, secret key) encrypted at rest with AES-256-GCM
|
|
546
|
+
- Key derivation via scrypt (N=16384, r=8, p=1) from a 32-byte master key at `/etc/lamalibre/lamaste/storage-master.key`
|
|
547
|
+
- Encryption format: `[salt (16)] [iv (12)] [authTag (16)] [ciphertext]` base64-encoded
|
|
548
|
+
- Master key auto-generated on first use, persisted with mode 0600
|
|
549
|
+
- Server CRUD: register, remove (cascades to bindings), list (credentials redacted)
|
|
550
|
+
- Plugin bindings: bind, unbind, get (with redacted server info), list
|
|
551
|
+
- `getPluginStorageConfig(pluginName)` returns decrypted credentials for bound plugins
|
|
552
|
+
- Promise-chain mutex serializes all read-modify-write operations
|
|
553
|
+
- Atomic writes (temp file, fsync, rename) for crash safety
|
|
554
|
+
|
|
555
|
+
### services.js — Service Management
|
|
556
|
+
|
|
557
|
+
- Allowlisted services: `nginx`, `chisel`, `authelia`, `lamalibre-lamaste-serverd`
|
|
558
|
+
- Allowlisted actions: `start`, `stop`, `restart`, `reload`
|
|
559
|
+
- Safety check: cannot stop `lamalibre-lamaste-serverd` from the UI (would terminate the session)
|
|
560
|
+
- Queries status and uptime via `systemctl is-active` and `systemctl show --property=ActiveEnterTimestamp`
|
|
561
|
+
|
|
562
|
+
### system-stats.js — System Monitoring
|
|
563
|
+
|
|
564
|
+
- Uses the `systeminformation` npm package for cross-platform stats
|
|
565
|
+
- Returns CPU usage, core count, memory (total/used/free), disk (total/used/free), and system uptime
|
|
566
|
+
- 2-second cache to avoid hammering the system when multiple clients poll simultaneously
|
|
567
|
+
|
|
568
|
+
### files.js — Static Site File Operations
|
|
569
|
+
|
|
570
|
+
- Path validation with directory traversal prevention (rejects `..`, absolute paths, null bytes, hidden files)
|
|
571
|
+
- Creates site directories with default `index.html`
|
|
572
|
+
- Streaming file upload (memory-safe on 512 MB droplets): stream → temp file next to the destination → rename (mode `0640`)
|
|
573
|
+
- File listing and site size via `readdir` / `lstat` (symbolic links are skipped, never followed)
|
|
574
|
+
- No sudo: the web root is `lamaste:www-data` with setgid `2750` directories, so the panel writes it directly and nginx reads through the `www-data` group
|
|
575
|
+
|
|
576
|
+
## WebSocket Support
|
|
577
|
+
|
|
578
|
+
The server uses `@fastify/websocket` for two real-time features:
|
|
579
|
+
|
|
580
|
+
### Provisioning Progress Stream (`/api/onboarding/provision/stream`)
|
|
581
|
+
|
|
582
|
+
- Module-level `EventEmitter` shared between the POST handler (which starts provisioning) and WebSocket connections (which stream progress)
|
|
583
|
+
- On connect, sends full current state (for late-joining clients)
|
|
584
|
+
- Emits `{ task, title, status, message, log, progress: { current, total } }` for each step
|
|
585
|
+
- Completion event includes admin credentials and URLs
|
|
586
|
+
|
|
587
|
+
### Live Log Streaming (`/api/services/:name/logs`)
|
|
588
|
+
|
|
589
|
+
- Spawns `journalctl -f -u <service> -n 50` as a child process
|
|
590
|
+
- Streams stdout lines to WebSocket clients
|
|
591
|
+
- Cleans up the child process on WebSocket close
|
|
592
|
+
|
|
593
|
+
## SPA Fallback
|
|
594
|
+
|
|
595
|
+
The server's `setNotFoundHandler` implements client-side routing support:
|
|
596
|
+
|
|
597
|
+
```javascript
|
|
598
|
+
server.setNotFoundHandler((request, reply) => {
|
|
599
|
+
if (request.url.startsWith('/api')) {
|
|
600
|
+
return reply.code(404).send({ error: 'Not found' });
|
|
601
|
+
}
|
|
602
|
+
return reply.sendFile('index.html');
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
API routes that don't match return a proper 404 JSON response. All other routes serve `index.html`, allowing React Router to handle client-side navigation.
|
|
607
|
+
|
|
608
|
+
## Graceful Shutdown
|
|
609
|
+
|
|
610
|
+
The server registers handlers for `SIGTERM` and `SIGINT`:
|
|
611
|
+
|
|
612
|
+
```javascript
|
|
613
|
+
const shutdown = async (signal) => {
|
|
614
|
+
server.log.info({ signal }, 'Received signal, shutting down gracefully');
|
|
615
|
+
clearInterval(livenessInterval);
|
|
616
|
+
clearRateLimitInterval();
|
|
617
|
+
await server.close();
|
|
618
|
+
process.exit(0);
|
|
619
|
+
};
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
The shutdown sequence stops the periodic instance liveness check (`livenessInterval`), clears the ticket rate-limit cleanup interval (`clearRateLimitInterval()`), and then calls `server.close()` which waits for active connections to finish before shutting down, ensuring in-flight requests complete cleanly.
|
|
623
|
+
|
|
624
|
+
## Key Files
|
|
625
|
+
|
|
626
|
+
| File | Role |
|
|
627
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
628
|
+
| `packages/lamaste-serverd/src/index.js` | Server entry, plugin + route registration |
|
|
629
|
+
| `packages/lamaste-serverd/src/middleware/mtls.js` | mTLS verification, revocation check, role parsing |
|
|
630
|
+
| `packages/lamaste-serverd/src/middleware/role-guard.js` | Role-based access control (admin vs agent capabilities) |
|
|
631
|
+
| `packages/lamaste-serverd/src/middleware/onboarding-guard.js` | Route access control by onboarding state |
|
|
632
|
+
| `packages/lamaste-serverd/src/middleware/twofa-session.js` | 2FA session cookie verification (after mTLS, before roleGuard) |
|
|
633
|
+
| `packages/lamaste-serverd/src/middleware/errors.js` | Global error handler (Zod, AppError, 500) |
|
|
634
|
+
| `packages/lamaste-serverd/src/routes/onboarding/index.js` | Onboarding route registration + guard |
|
|
635
|
+
| `packages/lamaste-serverd/src/routes/onboarding/provision.js` | Provisioning POST + WebSocket stream |
|
|
636
|
+
| `packages/lamaste-serverd/src/routes/invite.js` | Public invite acceptance routes (no mTLS) |
|
|
637
|
+
| `packages/lamaste-serverd/src/routes/enrollment.js` | Public enrollment route (token + CSR, no mTLS) |
|
|
638
|
+
| `packages/lamaste-serverd/src/routes/management.js` | Management route registration + guard |
|
|
639
|
+
| `packages/lamaste-serverd/src/routes/management/certs.js` | Certificate management (mTLS, agent certs, admin auth mode) |
|
|
640
|
+
| `packages/lamaste-serverd/src/routes/management/plugins.js` | Plugin management + push install (admin-only) |
|
|
641
|
+
| `packages/lamaste-serverd/src/routes/management/invitations.js` | Invitation CRUD (admin-only) |
|
|
642
|
+
| `packages/lamaste-serverd/src/routes/management/settings.js` | 2FA settings: setup, confirm, verify, disable (admin-only) |
|
|
643
|
+
| `packages/lamaste-serverd/src/routes/plugin-router.js` | Dynamic plugin route mounting + disabled-plugin 503 handler |
|
|
644
|
+
| `packages/lamaste-serverd/src/lib/config.js` | Config loading, validation (Zod), atomic update |
|
|
645
|
+
| `packages/lamaste-serverd/src/lib/state.js` | tunnels.json + sites.json + invitations.json atomic read/write |
|
|
646
|
+
| `packages/lamaste-serverd/src/lib/revocation.js` | Certificate revocation list management (revoked.json) |
|
|
647
|
+
| `packages/lamaste-serverd/src/lib/invite-page.js` | Invitation acceptance HTML page generator |
|
|
648
|
+
| `packages/lamaste-serverd/src/lib/nginx.js` | Vhost generation, write-with-rollback, reload |
|
|
649
|
+
| `packages/lamaste-serverd/src/lib/chisel.js` | Chisel install, service management, config update |
|
|
650
|
+
| `packages/lamaste-serverd/src/lib/authelia.js` | Authelia install, config, user CRUD, TOTP |
|
|
651
|
+
| `packages/lamaste-serverd/src/lib/certbot.js` | Let's Encrypt issuance, renewal, listing |
|
|
652
|
+
| `packages/lamaste-serverd/src/lib/mtls.js` | mTLS cert info, client cert rotation, agent registry, capability management |
|
|
653
|
+
| `packages/lamaste-serverd/src/lib/plugins.js` | Plugin manifest validation, install/uninstall, registry CRUD |
|
|
654
|
+
| `packages/lamaste-serverd/src/lib/push-install.js` | Push install policies, sessions, agent enablement |
|
|
655
|
+
| `packages/lamaste-serverd/src/lib/enrollment.js` | Enrollment token creation, validation, consumption |
|
|
656
|
+
| `packages/lamaste-serverd/src/lib/csr-signing.js` | CSR signing for hardware-bound agent enrollment |
|
|
657
|
+
| `packages/lamaste-serverd/src/lib/constants.js` | Reserved API prefixes shared across plugins and tickets |
|
|
658
|
+
| `packages/lamaste-serverd/src/lib/services.js` | systemctl wrapper with allowlists |
|
|
659
|
+
| `packages/lamaste-serverd/src/lib/system-stats.js` | CPU, memory, disk stats (cached) |
|
|
660
|
+
| `packages/lamaste-serverd/src/lib/files.js` | Static site file operations with path validation |
|
|
661
|
+
| `packages/lamaste-serverd/src/lib/chisel-users.js` | Per-agent Chisel credentials and authfile port grants (core shim) |
|
|
662
|
+
| `packages/lamaste-serverd/src/lib/chisel-reconcile.js` | Startup reconciliation of Chisel (fail closed) |
|
|
663
|
+
| `packages/lamaste-serverd/src/lib/chisel-runtime.js` | Restart decision for authfile changes |
|
|
664
|
+
| `packages/lamaste-serverd/src/lib/tunnel-deps.js` | Dependency objects for the core tunnel workflows |
|
|
665
|
+
| `packages/lamaste-serverd/src/lib/totp.js` | TOTP code generation and verification |
|
|
666
|
+
| `packages/lamaste-serverd/src/lib/session.js` | Signed session cookie creation and validation |
|
|
667
|
+
| `packages/lamaste-serverd/src/lib/tickets.js` | Ticket system: scopes, instances, assignments, sessions |
|
|
668
|
+
| `packages/lamaste-serverd/src/routes/management/tickets.js` | Ticket management HTTP route handlers |
|
|
669
|
+
| `packages/lamaste-serverd/src/lib/storage.js` | Storage server registry, plugin bindings, AES-256-GCM encryption |
|
|
670
|
+
| `packages/lamaste-serverd/src/routes/management/identity.js` | Identity system HTTP route handlers |
|
|
671
|
+
| `packages/lamaste-serverd/src/routes/management/storage.js` | Storage management HTTP route handlers |
|
|
672
|
+
| `packages/lamaste-serverd/src/routes/management/system.js` | System stats + panel update HTTP route handlers |
|
|
673
|
+
| `packages/lamaste-serverd/src/lib/app-error.js` | Operational error class |
|
|
674
|
+
|
|
675
|
+
## Design Decisions
|
|
676
|
+
|
|
677
|
+
### Why Fastify instead of Express?
|
|
678
|
+
|
|
679
|
+
Fastify provides schema-first validation, built-in WebSocket support via plugins, structured logging (pino), and measurably lower overhead per request. On a 512 MB droplet, the ~30 MB baseline matters. Express would work but offers no advantages for this use case.
|
|
680
|
+
|
|
681
|
+
### Why routes and lib are separate?
|
|
682
|
+
|
|
683
|
+
Routes handle HTTP concerns: parsing request bodies, setting status codes, formatting responses. Library modules handle business logic: reading files, calling system commands, managing state. This separation means the same business logic can be called from different contexts (routes, provisioning, future CLI tools) without HTTP coupling.
|
|
684
|
+
|
|
685
|
+
### Why not use a database?
|
|
686
|
+
|
|
687
|
+
At this scale (single admin, ~10 tunnels, ~5 users), JSON files provide faster access, zero operational overhead, and simpler debugging (you can `cat` the state file). The atomic write pattern ensures crash safety. If scale requirements change, the `state.js` module can be swapped for a database adapter without touching routes or other lib modules.
|
|
688
|
+
|
|
689
|
+
### Why check mTLS in both nginx and the server?
|
|
690
|
+
|
|
691
|
+
Defense-in-depth. nginx's `ssl_verify_client optional` with per-location `if ($ssl_client_verify != SUCCESS) { return 496; }` is the primary enforcement — it rejects connections at the TLS level before any HTTP processing on protected locations. The server middleware is a secondary check in case nginx is misconfigured or bypassed. In development mode, the server middleware is the only check (and it is bypassed).
|
|
692
|
+
|
|
693
|
+
### Why does provisioning run in the background?
|
|
694
|
+
|
|
695
|
+
Provisioning takes 2-5 minutes (downloading binaries, issuing certificates, configuring services). A synchronous HTTP request would time out. Instead, the POST endpoint starts the work and returns immediately, while the WebSocket stream provides real-time feedback. This pattern also allows late-joining clients to receive the current state on connect.
|