@lamalibre/create-lamaste 4.2.0 → 4.3.1

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 (128) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +13 -8
  3. package/scripts/lamaste-priv +205 -2
  4. package/src/index.js +8 -4
  5. package/src/lib/node-version.js +114 -0
  6. package/src/lib/panel-pin.js +41 -0
  7. package/src/lib/relay-services.js +15 -1
  8. package/src/lib/service-config.js +113 -0
  9. package/src/tasks/gatekeeper.js +8 -3
  10. package/src/tasks/node.js +34 -75
  11. package/src/tasks/redeploy.js +36 -0
  12. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  13. package/vendor/docs/00-introduction/quickstart.md +5 -5
  14. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  15. package/vendor/docs/01-concepts/authentication.md +6 -3
  16. package/vendor/docs/01-concepts/authorization.md +29 -17
  17. package/vendor/docs/01-concepts/certificates.md +3 -3
  18. package/vendor/docs/01-concepts/mtls.md +2 -2
  19. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
  20. package/vendor/docs/01-concepts/security-model.md +110 -35
  21. package/vendor/docs/01-concepts/tickets.md +2 -2
  22. package/vendor/docs/01-concepts/tunneling.md +4 -4
  23. package/vendor/docs/02-guides/agent-setup.md +34 -16
  24. package/vendor/docs/02-guides/certificate-management.md +2 -2
  25. package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
  26. package/vendor/docs/02-guides/desktop-app-setup.md +11 -10
  27. package/vendor/docs/02-guides/installation.md +3 -3
  28. package/vendor/docs/02-guides/onboarding.md +2 -2
  29. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  30. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  31. package/vendor/docs/03-architecture/installer.md +4 -4
  32. package/vendor/docs/03-architecture/management-flow.md +1 -1
  33. package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
  34. package/vendor/docs/03-architecture/overview.md +8 -8
  35. package/vendor/docs/03-architecture/panel-client.md +6 -6
  36. package/vendor/docs/03-architecture/panel-server.md +2 -2
  37. package/vendor/docs/03-architecture/state-management.md +2 -2
  38. package/vendor/docs/03-architecture/system-overview.md +1 -1
  39. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
  40. package/vendor/docs/04-api-reference/app-host.md +360 -0
  41. package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
  42. package/vendor/docs/04-api-reference/certificates.md +95 -38
  43. package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
  44. package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
  45. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  46. package/vendor/docs/04-api-reference/overview.md +137 -111
  47. package/vendor/docs/04-api-reference/registry-and-oidc.md +8 -4
  48. package/vendor/docs/04-api-reference/tunnels.md +15 -13
  49. package/vendor/docs/05-operations/backup-and-restore.md +3 -1
  50. package/vendor/docs/05-operations/uninstalling.md +1 -1
  51. package/vendor/docs/05-operations/upgrades.md +25 -20
  52. package/vendor/docs/06-reference/config-files.md +195 -97
  53. package/vendor/docs/06-reference/glossary.md +3 -3
  54. package/vendor/docs/06-reference/installer-flags.md +13 -13
  55. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  56. package/vendor/docs/06-reference/troubleshooting.md +33 -8
  57. package/vendor/docs/_index.json +20 -0
  58. package/vendor/docs/gatekeeper-design.md +22 -19
  59. package/vendor/docs/identity-directory-design.md +57 -56
  60. package/vendor/docs/package.json +1 -1
  61. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  62. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  63. package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
  64. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  65. package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
  66. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/server/index.js +47 -8
  68. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  69. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
  71. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  72. package/vendor/gatekeeper/package.json +4 -3
  73. package/vendor/server/bin/lamaste-server.js +25 -1
  74. package/vendor/server/package.json +2 -2
  75. package/vendor/server/src/commands/audit.js +60 -0
  76. package/vendor/server/src/commands/panel-pin.js +40 -0
  77. package/vendor/server/src/commands/plugins.js +963 -12
  78. package/vendor/server/src/commands/push-install.js +317 -0
  79. package/vendor/server/src/commands/registry-credential.js +2 -1
  80. package/vendor/server/src/commands/uninstall.js +28 -1
  81. package/vendor/server/src/ndjson.js +4 -2
  82. package/vendor/server/src/panel-api.js +277 -34
  83. package/vendor/server/src/panel-identity.js +165 -0
  84. package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
  85. package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
  86. package/vendor/server-ui/dist/index.html +2 -2
  87. package/vendor/serverd/package.json +2 -2
  88. package/vendor/serverd/src/index.js +56 -4
  89. package/vendor/serverd/src/lib/actor.js +24 -0
  90. package/vendor/serverd/src/lib/agent-jobs.js +330 -0
  91. package/vendor/serverd/src/lib/app-error.js +10 -0
  92. package/vendor/serverd/src/lib/audit.js +145 -0
  93. package/vendor/serverd/src/lib/hook-effects.js +123 -0
  94. package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
  95. package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
  96. package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
  97. package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
  98. package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
  99. package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
  100. package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
  101. package/vendor/serverd/src/lib/mtls.js +89 -8
  102. package/vendor/serverd/src/lib/nginx.js +298 -44
  103. package/vendor/serverd/src/lib/notifications.js +107 -0
  104. package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
  105. package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
  106. package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
  107. package/vendor/serverd/src/lib/plugin-operations.js +295 -0
  108. package/vendor/serverd/src/lib/plugin-packages.js +249 -0
  109. package/vendor/serverd/src/lib/plugins.js +298 -68
  110. package/vendor/serverd/src/lib/push-install.js +88 -2
  111. package/vendor/serverd/src/lib/relay-hooks.js +100 -0
  112. package/vendor/serverd/src/lib/relay-signing.js +43 -0
  113. package/vendor/serverd/src/lib/request-hooks.js +79 -0
  114. package/vendor/serverd/src/lib/retained-data.js +99 -0
  115. package/vendor/serverd/src/lib/trust-proxy.js +34 -0
  116. package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
  117. package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
  118. package/vendor/serverd/src/middleware/twofa-session.js +11 -1
  119. package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
  120. package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
  121. package/vendor/serverd/src/routes/management/agents.js +262 -1
  122. package/vendor/serverd/src/routes/management/audit.js +81 -0
  123. package/vendor/serverd/src/routes/management/certs.js +1 -15
  124. package/vendor/serverd/src/routes/management/plugins.js +569 -54
  125. package/vendor/serverd/src/routes/management/tunnels.js +26 -0
  126. package/vendor/serverd/src/routes/management.js +4 -0
  127. package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
  128. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
@@ -130,7 +130,12 @@ export function gatekeeperTasks(ctx, task) {
130
130
  // immediately. Successful auth (200) is cached for 10s — matching
131
131
  // the Cache-Control max-age Gatekeeper sets on the response. Omitting
132
132
  // `proxy_ignore_headers Cache-Control` means nginx will honor the
133
- // upstream max-age as a further shortening signal.
133
+ // upstream max-age as a further shortening signal. The key names the
134
+ // vhost by `$server_name`, never `$http_host`: an absolute-form request
135
+ // line selects one vhost while the client's Host header names another,
136
+ // and a decision cached for that other tunnel would be replayed here.
137
+ // The hostname comes first and `|` separates it: a hostname cannot
138
+ // contain `|`, so no other session and hostname make the same key.
134
139
  const cacheConfig = `# Lamaste Gatekeeper auth response cache zone
135
140
  # Scope: http{}. Included from nginx.conf.
136
141
  proxy_cache_path /var/cache/nginx/authz levels=1:2
@@ -139,9 +144,9 @@ proxy_cache_path /var/cache/nginx/authz levels=1:2
139
144
  inactive=5m;
140
145
  `;
141
146
  const cacheLocConfig = `# Lamaste Gatekeeper auth_request cache policy
142
- # Scope: location{} using auth_request /authz/check.
147
+ # Scope: the location{} a tunnel vhost's auth_request targets.
143
148
  proxy_cache lamaste_authz;
144
- proxy_cache_key "$cookie_authelia_session$http_host";
149
+ proxy_cache_key "$server_name|$cookie_authelia_session";
145
150
  proxy_cache_valid 200 10s;
146
151
  proxy_cache_valid 403 0;
147
152
  proxy_cache_use_stale off;
package/src/tasks/node.js CHANGED
@@ -1,13 +1,19 @@
1
1
  import { execa } from 'execa';
2
- import { unlink } from 'node:fs/promises';
3
-
4
- const REQUIRED_MAJOR = 22;
2
+ import {
3
+ MIN_NODE_VERSION,
4
+ NODE_BIN,
5
+ NODE_MAJOR,
6
+ installNodeFromNodeSource,
7
+ installedNodeVersion,
8
+ nodeTooNewError,
9
+ nodeVersionSupported,
10
+ nodeVersionTooNew,
11
+ } from '../lib/node-version.js';
5
12
 
6
13
  /**
7
- * Node.js 22 LTS installation subtasks.
8
- *
9
- * 22.5+ is the minimum because lamaste-serverd uses the built-in
10
- * `node:sqlite` module (which requires 22.5+).
14
+ * Node.js installation subtasks: {@link NODE_BIN} must be at least
15
+ * {@link MIN_NODE_VERSION} and below `MAX_NODE_MAJOR_EXCLUSIVE` (see
16
+ * lib/node-version.js for why). An older Node.js is upgraded from NodeSource.
11
17
  *
12
18
  * @param {object} ctx Shared installer context.
13
19
  * @param {object} task Parent Listr2 task reference.
@@ -18,67 +24,27 @@ export function nodeTasks(ctx, task) {
18
24
  {
19
25
  title: 'Checking existing Node.js installation',
20
26
  task: async (_ctx, subtask) => {
21
- try {
22
- const { stdout } = await execa('node', ['--version']);
23
- const version = stdout.trim();
24
- const major = parseInt(version.replace(/^v/, ''), 10);
25
-
26
- if (major >= REQUIRED_MAJOR) {
27
- ctx.nodeAlreadyInstalled = true;
28
- subtask.output = `Node.js ${version} found, skipping install`;
29
- } else {
30
- subtask.output = `Node.js ${version} found (below v${REQUIRED_MAJOR}), will upgrade`;
31
- }
32
- } catch {
33
- subtask.output = `Node.js not found, installing v${REQUIRED_MAJOR} LTS`;
34
- }
35
- },
36
- rendererOptions: { persistentOutput: true },
37
- },
38
- {
39
- title: 'Installing NodeSource repository',
40
- skip: () => ctx.nodeAlreadyInstalled && `Node.js ${REQUIRED_MAJOR}+ already installed`,
41
- task: async (_ctx, subtask) => {
42
- const setupScript = '/tmp/nodesource_setup.sh';
43
-
44
- subtask.output = 'Downloading NodeSource setup script...';
45
- try {
46
- await execa('curl', [
47
- '-fsSL',
48
- `https://deb.nodesource.com/setup_${REQUIRED_MAJOR}.x`,
49
- '-o',
50
- setupScript,
51
- ]);
52
- } catch (error) {
53
- throw new Error(
54
- `Failed to download NodeSource setup script. Check your internet connection.\n${error.stderr || error.message}`,
55
- );
56
- }
57
-
58
- subtask.output = 'Running NodeSource setup...';
59
- try {
60
- await execa('bash', [setupScript]);
61
- } catch (error) {
62
- throw new Error(`NodeSource setup script failed.\n${error.stderr || error.message}`);
27
+ const version = await installedNodeVersion();
28
+ if (version === null) {
29
+ subtask.output = `Node.js not found at ${NODE_BIN}, installing ${NODE_MAJOR}.x`;
30
+ } else if (nodeVersionSupported(version)) {
31
+ ctx.nodeAlreadyInstalled = true;
32
+ subtask.output = `Node.js ${version} found, skipping install`;
33
+ } else if (nodeVersionTooNew(version)) {
34
+ throw nodeTooNewError(version);
35
+ } else {
36
+ subtask.output = `Node.js ${version} found (below ${MIN_NODE_VERSION}), will upgrade`;
63
37
  }
64
-
65
- await unlink(setupScript).catch(() => {});
66
- subtask.output = 'NodeSource repository added';
67
38
  },
68
39
  rendererOptions: { persistentOutput: true },
69
40
  },
70
41
  {
71
- title: `Installing Node.js ${REQUIRED_MAJOR} LTS`,
72
- skip: () => ctx.nodeAlreadyInstalled && `Node.js ${REQUIRED_MAJOR}+ already installed`,
42
+ title: `Installing Node.js ${NODE_MAJOR} LTS`,
43
+ skip: () => ctx.nodeAlreadyInstalled && `Node.js ${MIN_NODE_VERSION}+ already installed`,
73
44
  task: async (_ctx, subtask) => {
74
- subtask.output = 'Running apt-get install nodejs...';
75
- try {
76
- await execa('apt-get', ['install', '-y', 'nodejs']);
77
- } catch (error) {
78
- throw new Error(
79
- `Failed to install Node.js. Try running 'apt-get update' manually and retrying.\n${error.stderr || error.message}`,
80
- );
81
- }
45
+ await installNodeFromNodeSource((line) => {
46
+ subtask.output = line;
47
+ });
82
48
  subtask.output = 'Node.js installed';
83
49
  },
84
50
  rendererOptions: { persistentOutput: true },
@@ -86,23 +52,16 @@ export function nodeTasks(ctx, task) {
86
52
  {
87
53
  title: 'Verifying Node.js installation',
88
54
  task: async (_ctx, subtask) => {
89
- const { stdout: nodeVersion } = await execa('node', ['--version']);
55
+ const nodeVersion = await installedNodeVersion();
90
56
  const { stdout: npmVersion } = await execa('npm', ['--version']);
91
-
92
- const trimmedNode = nodeVersion.trim();
93
- const trimmedNpm = npmVersion.trim();
94
- const major = parseInt(trimmedNode.replace(/^v/, ''), 10);
95
-
96
- if (major < REQUIRED_MAJOR) {
57
+ if (nodeVersion === null || !nodeVersionSupported(nodeVersion)) {
97
58
  throw new Error(
98
- `Expected Node.js v${REQUIRED_MAJOR}+, got ${trimmedNode}. Installation may have failed.`,
59
+ `Expected Node.js ${MIN_NODE_VERSION}+ at ${NODE_BIN}, got ${nodeVersion ?? 'none'}. Installation may have failed.`,
99
60
  );
100
61
  }
101
-
102
- ctx.nodeVersion = trimmedNode;
103
- ctx.npmVersion = trimmedNpm;
104
-
105
- subtask.output = `Node.js ${trimmedNode}, npm ${trimmedNpm}`;
62
+ ctx.nodeVersion = nodeVersion;
63
+ ctx.npmVersion = npmVersion.trim();
64
+ subtask.output = `Node.js ${nodeVersion}, npm ${ctx.npmVersion}`;
106
65
  },
107
66
  rendererOptions: { persistentOutput: true },
108
67
  },
@@ -12,6 +12,15 @@ import { generateCertHelpPage } from '../lib/cert-help-page.js';
12
12
  import { ensureRootOwnedInstallDir, removeTree, writeFileNoFollow } from '../lib/ownership.js';
13
13
  import { provisionRelayServices } from '../lib/relay-services.js';
14
14
  import { installSudoersFile, installSudoersWrappers } from '../lib/wrappers.js';
15
+ import {
16
+ MIN_NODE_VERSION,
17
+ NODE_BIN,
18
+ installNodeFromNodeSource,
19
+ installedNodeVersion,
20
+ nodeTooNewError,
21
+ nodeVersionSupported,
22
+ nodeVersionTooNew,
23
+ } from '../lib/node-version.js';
15
24
  import { deployGatekeeperPackage } from './gatekeeper.js';
16
25
 
17
26
  const GATEKEEPER_SERVICE = 'lamalibre-lamaste-gatekeeper';
@@ -115,6 +124,33 @@ export function redeployTasks(ctx, task) {
115
124
  },
116
125
  rendererOptions: { persistentOutput: true },
117
126
  },
127
+ {
128
+ title: 'Checking Node.js',
129
+ task: async (_ctx, subtask) => {
130
+ // NodeSource's 22.x line moves on its own; the relay needs at least
131
+ // MIN_NODE_VERSION (the hook runner's sandbox uses the stable
132
+ // permission model). A full install checks this too; a redeploy is
133
+ // where an existing relay gets the upgrade.
134
+ const version = await installedNodeVersion();
135
+ if (version !== null && nodeVersionSupported(version)) {
136
+ subtask.output = `Node.js ${version}`;
137
+ return;
138
+ }
139
+ if (version !== null && nodeVersionTooNew(version)) throw nodeTooNewError(version);
140
+ await execa('apt-get', ['update']);
141
+ await installNodeFromNodeSource((line) => {
142
+ subtask.output = line;
143
+ });
144
+ const upgraded = await installedNodeVersion();
145
+ if (upgraded === null || !nodeVersionSupported(upgraded)) {
146
+ throw new Error(
147
+ `Expected Node.js ${MIN_NODE_VERSION}+ at ${NODE_BIN} after the upgrade, got ${upgraded ?? 'none'}`,
148
+ );
149
+ }
150
+ subtask.output = `Node.js upgraded to ${upgraded}`;
151
+ },
152
+ rendererOptions: { persistentOutput: true },
153
+ },
118
154
  {
119
155
  title: 'Updating serverd',
120
156
  task: async (_ctx, subtask) => {
@@ -55,14 +55,14 @@ SSH into a fresh Ubuntu droplet and run one command:
55
55
 
56
56
  ```bash
57
57
  apt install -y npm
58
- npx @lamalibre/create-lamaste@4.2.0
58
+ npx @lamalibre/create-lamaste@4.3.1
59
59
  ```
60
60
 
61
61
  This installs everything the relay needs: nginx, Node.js, firewall rules, certificates, and the management panel. At the end, it prints a client certificate file and password. You download the certificate, import it into your browser, and disconnect SSH. You never SSH in again.
62
62
 
63
63
  **Option B: Desktop app (cloud provisioning)**
64
64
 
65
- Install the desktop app (`npx @lamalibre/create-lamaste-desktop@4.2.0`) and use the Servers tab to create a DigitalOcean droplet. The app handles everything — droplet creation, Lamaste installation, certificate download, and connection — without you ever touching SSH. Cloud API tokens are stored in your OS credential store (macOS Keychain or Linux libsecret), never in plaintext. See the [Cloud Provisioning guide](../02-guides/cloud-provisioning.md) for a step-by-step walkthrough.
65
+ Install the desktop app (`npx @lamalibre/create-lamaste-desktop@4.3.1`) and use the Servers tab to create a DigitalOcean droplet. The app handles everything — droplet creation, Lamaste installation, certificate download, and connection — without you ever touching SSH. Cloud API tokens are stored in your OS credential store (macOS Keychain or Linux libsecret), never in plaintext. See the [Cloud Provisioning guide](../02-guides/cloud-provisioning.md) for a step-by-step walkthrough.
66
66
 
67
67
  **Phase 2: Onboarding (10 minutes, browser)**
68
68
 
@@ -59,7 +59,7 @@ Once connected:
59
59
 
60
60
  ```bash
61
61
  apt install -y npm
62
- npx @lamalibre/create-lamaste@4.2.0
62
+ npx @lamalibre/create-lamaste@4.3.1
63
63
  ```
64
64
 
65
65
  The installer displays a confirmation banner showing what it will do:
@@ -253,7 +253,7 @@ Every tunnel is carried by one **agent** — the machine that runs the app. Enro
253
253
  Install the Lamaste Desktop app, which provides a GUI with automatic service discovery and one-click tunnel creation:
254
254
 
255
255
  ```bash
256
- npx @lamalibre/create-lamaste-desktop@4.2.0
256
+ npx @lamalibre/create-lamaste-desktop@4.3.1
257
257
  ```
258
258
 
259
259
  The app auto-detects local services (Ollama, ComfyUI, PostgreSQL, Docker containers, etc.) and lets you expose them with one click. See the [Desktop App Setup](../02-guides/desktop-app-setup.md) guide for details.
@@ -263,7 +263,7 @@ The app auto-detects local services (Ollama, ComfyUI, PostgreSQL, Docker contain
263
263
  In the panel, go to **Certificates → Enrollment Token** and create a token for a label such as `laptop`. Then, on the machine that runs your app, install the agent globally and run setup:
264
264
 
265
265
  ```bash
266
- npm install -g @lamalibre/lamaste-agent@4.2.0
266
+ npm install -g @lamalibre/lamaste-agent@4.3.1
267
267
  LAMALIBRE_LAMASTE_ENROLLMENT_TOKEN=<token> \
268
268
  lamaste-agent setup --label laptop --panel-url https://203.0.113.42:9292
269
269
  ```
@@ -345,7 +345,7 @@ Your Mac DigitalOcean Droplet Internet
345
345
  The installer accepts optional flags:
346
346
 
347
347
  ```bash
348
- npx @lamalibre/create-lamaste@4.2.0 [flags]
348
+ npx @lamalibre/create-lamaste@4.3.1 [flags]
349
349
  ```
350
350
 
351
351
  | Flag | Effect |
@@ -435,7 +435,7 @@ In development mode (`NODE_ENV=development` or `NODE_ENV` unset), the panel serv
435
435
  # 2. SSH in and install
436
436
  ssh root@203.0.113.42
437
437
  apt install -y npm
438
- npx @lamalibre/create-lamaste@4.2.0
438
+ npx @lamalibre/create-lamaste@4.3.1
439
439
 
440
440
  # 3. Download cert (from your local machine)
441
441
  scp root@203.0.113.42:/etc/lamalibre/lamaste/pki/client.p12 .
@@ -36,7 +36,7 @@ Lamaste solves the problem of exposing local services to the internet securely.
36
36
  **Option A: Manual setup (SSH)**
37
37
 
38
38
  1. Create a fresh Ubuntu 24.04 droplet on DigitalOcean
39
- 2. SSH in once and run `npx @lamalibre/create-lamaste@4.2.0`
39
+ 2. SSH in once and run `npx @lamalibre/create-lamaste@4.3.1`
40
40
  3. Download the client certificate it prints
41
41
  4. Import the certificate into your browser
42
42
  5. Open `https://<droplet-ip>:9292` — you are in the admin panel
@@ -46,7 +46,7 @@ Lamaste solves the problem of exposing local services to the internet securely.
46
46
 
47
47
  **Option B: Desktop app cloud provisioning**
48
48
 
49
- 1. Install the desktop app (`npx @lamalibre/create-lamaste-desktop@4.2.0`)
49
+ 1. Install the desktop app (`npx @lamalibre/create-lamaste-desktop@4.3.1`)
50
50
  2. Go to the Servers tab and click "Create a new server"
51
51
  3. Enter your DigitalOcean API token — the app validates scopes and stores it securely in your OS credential store
52
52
  4. Pick a region (the app measures latency to each) and a domain name
@@ -93,14 +93,14 @@ All backend services bind `127.0.0.1` only — nginx is the sole public-facing s
93
93
 
94
94
  State is stored in JSON files (`/etc/lamalibre/lamaste/panel.json`, `tunnels.json`) with atomic writes (write to temp → fsync → rename). No database is needed at this scale.
95
95
 
96
- The installer (`npx @lamalibre/create-lamaste@4.2.0`) is completely non-interactive — it detects the environment, provisions everything, and prints a summary. All configuration happens through the browser-based onboarding wizard after installation.
96
+ The installer (`npx @lamalibre/create-lamaste@4.3.1`) is completely non-interactive — it detects the environment, provisions everything, and prints a summary. All configuration happens through the browser-based onboarding wizard after installation.
97
97
 
98
98
  ## Quick Reference
99
99
 
100
100
  | Item | Value |
101
101
  | -------------------- | ------------------------------- |
102
102
  | **Minimum VPS** | 512MB RAM, Ubuntu 24.04 |
103
- | **Install command** | `npx @lamalibre/create-lamaste@4.2.0` |
103
+ | **Install command** | `npx @lamalibre/create-lamaste@4.3.1` |
104
104
  | **Admin panel** | `https://<ip>:9292` (mTLS) |
105
105
  | **Auth for admin** | Client certificate (zero-login) |
106
106
  | **Auth for apps** | TOTP 2FA via Authelia |
@@ -108,5 +108,5 @@ The installer (`npx @lamalibre/create-lamaste@4.2.0`) is completely non-interact
108
108
  | **TLS certificates** | Let's Encrypt (auto-renewing) |
109
109
  | **State storage** | JSON files (no database) |
110
110
  | **RAM usage** | ~245MB total (all services) |
111
- | **npm package** | `@lamalibre/create-lamaste@4.2.0` |
111
+ | **npm package** | `@lamalibre/create-lamaste@4.3.1` |
112
112
  | **License** | Polyform Noncommercial 1.0.0 |
@@ -289,10 +289,11 @@ location /authelia {
289
289
  proxy_pass http://127.0.0.1:9091/api/verify?rd=https://auth.example.com/;
290
290
  proxy_pass_request_body off;
291
291
  proxy_set_header Content-Length "";
292
- proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
292
+ # This vhost's own hostname, never $http_host (see below)
293
+ proxy_set_header X-Original-URL https://app.example.com$request_uri;
293
294
  proxy_set_header X-Forwarded-Method $request_method;
294
295
  proxy_set_header X-Forwarded-Proto $scheme;
295
- proxy_set_header X-Forwarded-Host $http_host;
296
+ proxy_set_header X-Forwarded-Host app.example.com;
296
297
  proxy_set_header X-Forwarded-Uri $request_uri;
297
298
  proxy_set_header X-Forwarded-For $remote_addr;
298
299
  }
@@ -314,9 +315,11 @@ location / {
314
315
  }
315
316
 
316
317
  # Unauthenticated users are redirected to Authelia
317
- error_page 401 =302 https://auth.example.com/?rd=$scheme://$http_host$request_uri;
318
+ error_page 401 =302 https://auth.example.com/?rd=https://app.example.com$request_uri;
318
319
  ```
319
320
 
321
+ Authelia decides by the URL it is asked about, so the vhost writes its own hostname into it. `$http_host` — the client's `Host` header — would let a client send the request line of this vhost (`GET https://app.example.com/ HTTP/1.1`, which nginx uses to select the vhost) with the `Host` of a hostname its rules allow, and be let in here. Lamaste's generated vhosts do this; see [nginx Configuration](../03-architecture/nginx-configuration.md#authorization-subrequests-name-their-own-hostname).
322
+
320
323
  The flow works like this:
321
324
 
322
325
  ```
@@ -95,25 +95,37 @@ Gatekeeper watches its state files (`groups.json`, `access-grants.json`) for cha
95
95
 
96
96
  ### Architecture
97
97
 
98
- Gatekeeper runs as an independent Fastify service on `127.0.0.1:9294`. nginx uses `auth_request` to delegate authorization decisions to it, similar to how Authelia handles authentication via the same pattern.
98
+ Gatekeeper runs as an independent Fastify service on `127.0.0.1:9294`. The vhost of every `authenticated` and `restricted` tunnel delegates each request to it with an nginx `auth_request` to `/authz/tunnel/<fqdn>` — the vhost names its tunnel in the path; Gatekeeper validates the Authelia session itself and decides.
99
99
 
100
100
  The request flow for a restricted tunnel:
101
101
 
102
102
  ```
103
103
  1. Visitor requests staging.example.com
104
- 2. nginx sends auth_request to Authelia (127.0.0.1:9091)
105
- 3. Authelia validates session cookie:
106
- a. No valid session -> 401 -> nginx redirects to auth.example.com
107
- b. Valid session -> 200 with Remote-User/Remote-Groups headers
108
- 4. nginx sends auth_request to Gatekeeper (127.0.0.1:9294)
109
- - Forwards Remote-User, Remote-Groups, and Host headers
110
- 5. Gatekeeper checks for a matching grant:
111
- a. Grant exists (user or group match) -> 200
104
+ 2. nginx sends auth_request to Gatekeeper
105
+ (127.0.0.1:9294/authz/tunnel/staging.example.com) with
106
+ - X-Original-URL https://staging.example.com<request URI> and
107
+ Host staging.example.com — the vhost's own hostname, from its configuration
108
+ - X-Original-Method and the visitor's Cookie header
109
+ 3. Gatekeeper takes the tunnel named in the path (X-Original-URL and Host
110
+ must agree) and validates the Authelia session cookie with Authelia for
111
+ that URL:
112
+ a. No valid session -> 401 -> nginx redirects to
113
+ auth.example.com/?rd=https://staging.example.com<request URI>
114
+ b. Valid session -> the user (cached, see Session caching)
115
+ 4. Gatekeeper checks for a matching grant:
116
+ a. Grant exists (user or group match) -> 200 with Remote-User, Remote-Groups,
117
+ Remote-Name and Remote-Email
112
118
  b. No grant -> 403 with access request HTML page
113
- 6. If both return 200: nginx proxies to the tunnel backend
119
+ 5. On 200 nginx proxies to the tunnel backend with those identity headers
114
120
  ```
115
121
 
116
- For public tunnels, nginx skips both auth_request directives. For authenticated tunnels, nginx uses only the Authelia auth_request and skips Gatekeeper.
122
+ Public tunnel vhosts have no `auth_request`. For authenticated tunnels Gatekeeper answers 200 for any valid session without looking for a grant.
123
+
124
+ ### Which tunnel is authorized
125
+
126
+ Gatekeeper authorizes the tunnel the vhost names, never one the client names. nginx selects the vhost from the request line when a request is in absolute form (`GET https://staging.example.com/ HTTP/1.1`) and from `Host` otherwise, so a client can send the request line of one tunnel with the `Host` header of another. Vhosts written before 4.3 called `/authz/check` with `X-Original-URL` and `Host` built from the client's `Host` header (`$http_host`): an Authelia user allowed on any tunnel — and every user is allowed on an `authenticated` one — reached any `restricted` tunnel, app or plugin, by naming the allowed tunnel in `Host`, without a grant, and Authelia evaluated that other URL as well.
127
+
128
+ Generated vhosts now name their tunnel in the subrequest's path, `/authz/tunnel/<fqdn>`, as the agent-panel vhost names its agent, and write the same hostname into `X-Original-URL`, `Host` and the login redirect. Gatekeeper authorizes the tunnel in the path and refuses a check whose `X-Original-URL` or `Host` names another hostname, or has no `Host` (401), before asking Authelia. It refuses `/authz/check` outright (403): a vhost written before 4.3 that the panel has not rewritten closes its tunnel instead of opening it. The panel rewrites tunnel vhosts when it starts. Authelia-protected static sites, which ask Authelia directly, had the same flaw towards Authelia's per-site rules and are rewritten the same way; the auth portal's subrequest is rewritten too, as hardening. See [nginx Configuration](../03-architecture/nginx-configuration.md#authorization-subrequests-name-their-own-hostname).
117
129
 
118
130
  ### Grant model
119
131
 
@@ -170,7 +182,7 @@ State files use the same atomic write pattern as the rest of Lamaste (temp file
170
182
  When Gatekeeper denies a request (no matching grant), it returns a 403 response with an HTML page instead of a bare status code. The page is generated from templates in `packages/lamaste-gatekeeper/src/lib/templates.ts` and includes:
171
183
 
172
184
  - The username (from `Remote-User` header)
173
- - The resource they attempted to access (from `Host` header)
185
+ - The resource they attempted to access (the tunnel's hostname, from `X-Original-URL` — written by the vhost)
174
186
  - A human-readable timestamp
175
187
  - Pre-filled message templates for email (mailto link), Slack, Teams, WhatsApp, and a generic plain-text format
176
188
 
@@ -212,11 +224,11 @@ The HTML is self-contained (inline CSS, no external dependencies) so it renders
212
224
 
213
225
  ### Access modes
214
226
 
215
- | Mode | Authelia | Gatekeeper | Who can access |
216
- | ------------- | -------- | ---------- | --------------------------- |
217
- | Public | No | No | Anyone on the internet |
218
- | Authenticated | Yes | No | Any logged-in Authelia user |
219
- | Restricted | Yes | Yes | Users with a matching grant |
227
+ | Mode | Authelia | Gatekeeper | Who can access |
228
+ | ------------- | -------- | -------------------- | --------------------------- |
229
+ | Public | No | No | Anyone on the internet |
230
+ | Authenticated | Yes | Yes (session only) | Any logged-in Authelia user |
231
+ | Restricted | Yes | Yes (session, grant) | Users with a matching grant |
220
232
 
221
233
  ### Cache TTLs
222
234
 
@@ -29,7 +29,7 @@ For the IP-based admin panel (`https://<ip>:9292`), there is no domain to verify
29
29
 
30
30
  ### When certificates are issued
31
31
 
32
- - **Self-signed + mTLS certs** — created during `npx @lamalibre/create-lamaste@4.2.0` installation
32
+ - **Self-signed + mTLS certs** — created during `npx @lamalibre/create-lamaste@4.3.1` installation
33
33
  - **Core Let's Encrypt certs** — issued during onboarding for `panel`, `auth`, and `tunnel` subdomains
34
34
  - **App Let's Encrypt certs** — issued when you create a new tunnel or a managed static site
35
35
  - **Custom-domain site certs** — issued when a custom-domain site's DNS is verified: one certificate lineage, named after the site's domain, covering the domain and every redirect alias (re-issued when aliases change)
@@ -82,7 +82,7 @@ The most critical scenario is the mTLS CA expiring, but it has a 10-year validit
82
82
 
83
83
  ### Let's Encrypt implementation
84
84
 
85
- Lamaste uses certbot with the nginx plugin for certificate issuance. The implementation lives in `@lamalibre/lamaste@4.2.0/server` (`packages/core/lib/src/server/certbot.ts`), wrapped by `packages/server/daemon/src/lib/certbot.js`.
85
+ Lamaste uses certbot with the nginx plugin for certificate issuance. The implementation lives in `@lamalibre/lamaste@4.3.1/server` (`packages/core/lib/src/server/certbot.ts`), wrapped by `packages/server/daemon/src/lib/certbot.js`.
86
86
 
87
87
  The panel runs as the unprivileged `lamaste` user and never runs `certbot` or `openssl` on Let's Encrypt files directly. It calls two root-owned wrapper scripts through sudo:
88
88
 
@@ -193,7 +193,7 @@ This three-step check prevents unnecessary certificate issuance and respects rat
193
193
 
194
194
  #### Custom-domain site certificate issuance
195
195
 
196
- A custom-domain static site may have redirect aliases (e.g. `www.myblog.net`). `issueSiteCert(fqdn, aliases, email)` in `@lamalibre/lamaste@4.2.0/server` keeps them on **one** lineage named after the primary domain:
196
+ A custom-domain static site may have redirect aliases (e.g. `www.myblog.net`). `issueSiteCert(fqdn, aliases, email)` in `@lamalibre/lamaste@4.3.1/server` keeps them on **one** lineage named after the primary domain:
197
197
 
198
198
  ```bash
199
199
  sudo /usr/local/sbin/lamaste-certbot issue admin@example.com myblog.net myblog.net www.myblog.net
@@ -25,7 +25,7 @@ You encounter mTLS during two moments:
25
25
 
26
26
  ### Importing the certificate
27
27
 
28
- After running `npx @lamalibre/create-lamaste@4.2.0`, the installer prints something like:
28
+ After running `npx @lamalibre/create-lamaste@4.3.1`, the installer prints something like:
29
29
 
30
30
  ```
31
31
  ╔══════════════════════════════════════════════════════════════╗
@@ -101,7 +101,7 @@ When connecting a machine to Lamaste using `lamaste-agent`, you should **not** u
101
101
  2. Click "Generate" and enter a label (e.g., `macbook-pro`)
102
102
  3. Save the displayed password — it cannot be retrieved later
103
103
  4. Download the `.p12` file
104
- 5. Share the `.p12` file and password with the agent machine through a secure channel, then install the agent there (`npm install -g @lamalibre/lamaste-agent@4.2.0`) and run `lamaste-agent setup` (an enrollment token avoids moving the key at all — see [Agent Setup](../02-guides/agent-setup.md))
104
+ 5. Share the `.p12` file and password with the agent machine through a secure channel, then install the agent there (`npm install -g @lamalibre/lamaste-agent@4.3.1`) and run `lamaste-agent setup` (an enrollment token avoids moving the key at all — see [Agent Setup](../02-guides/agent-setup.md))
105
105
 
106
106
  Agent certificates have capability-based access. By default, a new agent can only read tunnel configuration. Admins can grant additional capabilities per-agent from the panel UI:
107
107
 
@@ -253,9 +253,14 @@ server {
253
253
  location /internal/lamaste/authz {
254
254
  internal;
255
255
 
256
- proxy_pass http://127.0.0.1:9294/authz/check;
256
+ # The tunnel is named in the path, and the same hostname in the headers —
257
+ # never $http_host, the client's Host header, which an absolute-form
258
+ # request line can set to another tunnel
259
+ proxy_pass http://127.0.0.1:9294/authz/tunnel/myapp.example.com;
257
260
  proxy_pass_request_body off;
258
- # ... X-Original-URL, X-Original-Method, Cookie headers ...
261
+ proxy_set_header X-Original-URL https://myapp.example.com$request_uri;
262
+ proxy_set_header Host myapp.example.com;
263
+ # ... X-Original-Method, Cookie headers ...
259
264
  }
260
265
 
261
266
  location / {
@@ -289,7 +294,7 @@ server {
289
294
  proxy_send_timeout 86400s;
290
295
 
291
296
  # Not logged in -> Authelia portal
292
- error_page 401 =302 https://auth.example.com/?rd=$scheme://$http_host$request_uri;
297
+ error_page 401 =302 https://auth.example.com/?rd=https://myapp.example.com$request_uri;
293
298
  # Restricted only: no grant -> Gatekeeper's access-request page
294
299
  error_page 403 = /internal/lamaste/authz;
295
300
  }