@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.
- package/package.json +2 -2
- package/scripts/bundle-vendor.js +13 -8
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +8 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/panel-pin.js +41 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +110 -35
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
- package/vendor/docs/02-guides/desktop-app-setup.md +11 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +8 -4
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +3 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +195 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +25 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/panel-pin.js +40 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +277 -34
- package/vendor/server/src/panel-identity.js +165 -0
- package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
package/src/tasks/gatekeeper.js
CHANGED
|
@@ -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{}
|
|
147
|
+
# Scope: the location{} a tunnel vhost's auth_request targets.
|
|
143
148
|
proxy_cache lamaste_authz;
|
|
144
|
-
proxy_cache_key "$cookie_authelia_session
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 ${
|
|
72
|
-
skip: () => ctx.nodeAlreadyInstalled && `Node.js ${
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
59
|
+
`Expected Node.js ${MIN_NODE_VERSION}+ at ${NODE_BIN}, got ${nodeVersion ?? 'none'}. Installation may have failed.`,
|
|
99
60
|
);
|
|
100
61
|
}
|
|
101
|
-
|
|
102
|
-
ctx.
|
|
103
|
-
|
|
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
|
},
|
package/src/tasks/redeploy.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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`.
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
119
|
+
5. On 200 nginx proxies to the tunnel backend with those identity headers
|
|
114
120
|
```
|
|
115
121
|
|
|
116
|
-
|
|
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 `
|
|
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
|
|
216
|
-
| ------------- | -------- |
|
|
217
|
-
| Public | No | No
|
|
218
|
-
| Authenticated | Yes |
|
|
219
|
-
| Restricted | Yes | Yes
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|