@lamalibre/create-lamaste 4.3.0 → 4.4.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/package.json +2 -2
- package/scripts/bundle-vendor.js +13 -8
- package/src/index.js +4 -0
- package/src/lib/panel-pin.js +41 -0
- package/src/lib/service-config.js +6 -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/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +50 -2
- 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 +6 -6
- 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 +12 -11
- 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/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +7 -7
- 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 +393 -33
- package/vendor/docs/04-api-reference/app-host.md +87 -38
- package/vendor/docs/04-api-reference/audit-and-notifications.md +10 -10
- package/vendor/docs/04-api-reference/certificates.md +22 -19
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +33 -11
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -135
- package/vendor/docs/04-api-reference/registry-and-oidc.md +1 -1
- package/vendor/docs/04-api-reference/tunnels.md +14 -10
- package/vendor/docs/05-operations/backup-and-restore.md +4 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +6 -6
- package/vendor/docs/06-reference/config-files.md +94 -84
- 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 +8 -8
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +2 -2
- 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/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +11 -1
- package/vendor/server/package.json +4 -3
- package/vendor/server/src/commands/panel-pin.js +40 -0
- package/vendor/server/src/commands/plugin-settings.js +541 -0
- package/vendor/server/src/commands/plugins.js +94 -27
- package/vendor/server/src/panel-api.js +281 -34
- package/vendor/server/src/panel-identity.js +165 -0
- package/vendor/server/src/plugin-approval.js +78 -0
- package/vendor/server/src/plugin-settings.js +699 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +1 -0
- package/vendor/server-ui/dist/assets/index-CESyyK8V.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +9 -0
- package/vendor/serverd/src/lib/agent-presence.js +106 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +20 -4
- package/vendor/serverd/src/lib/migrations/state/0012_plugin_settings_and_presence.sql +62 -0
- package/vendor/serverd/src/lib/mtls.js +44 -4
- package/vendor/serverd/src/lib/plugin-hooks.js +120 -20
- package/vendor/serverd/src/lib/plugin-hosting.js +35 -14
- package/vendor/serverd/src/lib/plugin-lifecycle.js +267 -83
- package/vendor/serverd/src/lib/plugin-settings.js +1101 -0
- package/vendor/serverd/src/lib/plugins.js +73 -22
- package/vendor/serverd/src/lib/tunnel-hooks.js +11 -0
- package/vendor/serverd/src/routes/management/plugins.js +166 -19
- package/vendor/serverd/src/routes/management/tunnels.js +21 -1
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +0 -1
- package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +0 -84
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel's IP listener, as `lamaste-server` verifies it.
|
|
3
|
+
*
|
|
4
|
+
* The CLI runs as root on the relay and sends the panel requests that carry
|
|
5
|
+
* the registry token, plugin settings and secrets, and the admin client
|
|
6
|
+
* certificate. It reaches the panel through nginx's IP vhost on port 9292
|
|
7
|
+
* (`lamalibre-lamaste-panel-ip`), which presents the self-signed certificate
|
|
8
|
+
* create-lamaste generated for the server's address
|
|
9
|
+
* (`<PKI_DIR>/self-signed.pem`; its key is 0600). Port 9292 is not a
|
|
10
|
+
* privileged port: whenever nginx does not hold it — panel 2FA switches the
|
|
11
|
+
* IP vhost off, nginx is stopped or failed to start — any local user can
|
|
12
|
+
* listen there. So the CLI never trusts whatever answers on it:
|
|
13
|
+
*
|
|
14
|
+
* - before connecting, it refuses when panel 2FA is on or the IP vhost is not
|
|
15
|
+
* enabled, since nginx then does not listen on 9292 (`assertPanelListener`);
|
|
16
|
+
* - every request verifies the listener against exactly that certificate,
|
|
17
|
+
* read from disk as root (`readPanelIdentity`, curl `--cacert`), never
|
|
18
|
+
* `-k`. The TLS handshake fails before the CLI presents its client
|
|
19
|
+
* certificate or sends a byte of the request (see `buildPanelCurlRequest`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { X509Certificate, createHash } from 'node:crypto';
|
|
23
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
24
|
+
import net from 'node:net';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { PKI_DIR } from './config.js';
|
|
27
|
+
|
|
28
|
+
/** The port of nginx's IP vhost for the panel. */
|
|
29
|
+
export const PANEL_IP_PORT = 9292;
|
|
30
|
+
|
|
31
|
+
/** The certificate the IP vhost presents (create-lamaste, `tasks/nginx.js`). */
|
|
32
|
+
export const PANEL_IP_CERT_PATH = path.join(PKI_DIR, 'self-signed.pem');
|
|
33
|
+
|
|
34
|
+
/** The IP vhost's link in nginx's enabled sites (panel 2FA removes it). */
|
|
35
|
+
export const PANEL_IP_SITE_ENABLED = '/etc/nginx/sites-enabled/lamalibre-lamaste-panel-ip';
|
|
36
|
+
|
|
37
|
+
/** Where the CLI connects: the panel's own machine. */
|
|
38
|
+
export const PANEL_LOOPBACK = '127.0.0.1';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The IPv4 address a certificate is issued for: its first `IP Address`
|
|
42
|
+
* subject alternative name. create-lamaste (and the restore procedure in the
|
|
43
|
+
* docs) issue the IP vhost's certificate with `subjectAltName=IP:<address>`;
|
|
44
|
+
* curl checks the request's host against it. Pure, exported for tests.
|
|
45
|
+
*
|
|
46
|
+
* @param {X509Certificate} cert
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function certifiedAddress(cert) {
|
|
50
|
+
const names = (cert.subjectAltName ?? '').split(', ');
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
if (!name.startsWith('IP Address:')) continue;
|
|
53
|
+
const address = name.slice('IP Address:'.length).trim();
|
|
54
|
+
if (net.isIPv4(address)) return address;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
'The panel certificate names no IPv4 address (subjectAltName IP:<address>), so it cannot be verified',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The certificate's public-key pin: base64 SHA-256 of its SubjectPublicKeyInfo
|
|
63
|
+
* — curl's `--pinnedpubkey sha256//<pin>` form, the one agents and the
|
|
64
|
+
* desktop app store for the panel. Pure, exported for tests.
|
|
65
|
+
*
|
|
66
|
+
* @param {X509Certificate} cert
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export function publicKeyPin(cert) {
|
|
70
|
+
const spki = cert.publicKey.export({ type: 'spki', format: 'der' });
|
|
71
|
+
return createHash('sha256').update(spki).digest('base64');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read the IP vhost's certificate: the identity every panel request is
|
|
76
|
+
* verified against. A link or a file others could write is refused — the
|
|
77
|
+
* CLI only trusts a certificate as much as the file that holds it. The file
|
|
78
|
+
* is read on every request, so a certificate regenerated by a full
|
|
79
|
+
* reinstall (or the restore procedure) is picked up as soon as nginx serves
|
|
80
|
+
* it.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} [certPath]
|
|
83
|
+
* @returns {Promise<{ certPath: string, address: string, port: number, pin: string }>}
|
|
84
|
+
*/
|
|
85
|
+
export async function readPanelIdentity(certPath = PANEL_IP_CERT_PATH) {
|
|
86
|
+
let st;
|
|
87
|
+
try {
|
|
88
|
+
st = await lstat(certPath);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Cannot read the panel's TLS certificate ${certPath} (${err.code ?? err.message}); ` +
|
|
92
|
+
'lamaste-server verifies the panel against it and sends nothing without it',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!st.isFile()) {
|
|
96
|
+
throw new Error(`The panel's TLS certificate ${certPath} is not a regular file`);
|
|
97
|
+
}
|
|
98
|
+
if ((st.mode & 0o022) !== 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`The panel's TLS certificate ${certPath} is writable by group or others; refusing to trust it`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
let cert;
|
|
104
|
+
try {
|
|
105
|
+
cert = new X509Certificate(await readFile(certPath));
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error(`${certPath} does not hold a certificate`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
certPath,
|
|
111
|
+
address: certifiedAddress(cert),
|
|
112
|
+
port: PANEL_IP_PORT,
|
|
113
|
+
pin: publicKeyPin(cert),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Why nothing verified listens on the IP vhost's port, or null when nginx
|
|
119
|
+
* should hold it. Pure, exported for tests.
|
|
120
|
+
*
|
|
121
|
+
* @param {{ panel2faEnabled: boolean, siteEnabled: boolean }} state
|
|
122
|
+
* @returns {string | null}
|
|
123
|
+
*/
|
|
124
|
+
export function panelListenerRefusal({ panel2faEnabled, siteEnabled }) {
|
|
125
|
+
if (panel2faEnabled) {
|
|
126
|
+
return (
|
|
127
|
+
'Panel 2FA is enabled: it switches off the panel listener on port 9292 that lamaste-server ' +
|
|
128
|
+
'talks to, and every admin request then needs a 2FA session the CLI cannot hold. Nothing was ' +
|
|
129
|
+
'sent. Use the panel in the browser, or turn 2FA off in its Settings to use this command.'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (!siteEnabled) {
|
|
133
|
+
return (
|
|
134
|
+
'The panel listener on port 9292 is not enabled (nginx site lamalibre-lamaste-panel-ip), so ' +
|
|
135
|
+
'nginx does not hold the port and nothing was sent to it. Re-enable the site or re-run ' +
|
|
136
|
+
'create-lamaste.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Refuse before connecting when nginx does not listen on the IP vhost's
|
|
144
|
+
* port: whatever answered there could only be someone else's listener.
|
|
145
|
+
*
|
|
146
|
+
* @param {Record<string, unknown> | null} config panel.json
|
|
147
|
+
* @param {string} [siteEnabledPath]
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
export async function assertPanelListener(config, siteEnabledPath = PANEL_IP_SITE_ENABLED) {
|
|
151
|
+
let siteEnabled = true;
|
|
152
|
+
try {
|
|
153
|
+
// The link itself, not what it points to: nginx follows it.
|
|
154
|
+
await lstat(siteEnabledPath);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err.code !== 'ENOENT') throw err;
|
|
157
|
+
siteEnabled = false;
|
|
158
|
+
}
|
|
159
|
+
const twofa = config?.panel2fa;
|
|
160
|
+
const refusal = panelListenerRefusal({
|
|
161
|
+
panel2faEnabled: typeof twofa === 'object' && twofa !== null && twofa.enabled === true,
|
|
162
|
+
siteEnabled,
|
|
163
|
+
});
|
|
164
|
+
if (refusal) throw new Error(refusal);
|
|
165
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lamaste-server plugins hooks|update|approve-hooks`: why an update waits
|
|
3
|
+
* for approval, the command that approves it, and the arguments that
|
|
4
|
+
* command takes. Pure — no output — so they are tested as they are.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { terminalText } from './plugin-settings.js';
|
|
8
|
+
|
|
9
|
+
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
10
|
+
const VERSION_RE = /^[0-9A-Za-z.+-]{1,64}$/;
|
|
11
|
+
/** The reason an update turns secret settings into plain ones. */
|
|
12
|
+
const SETTINGS_SECRET_TO_PLAIN = 'settings.secret-to-plain';
|
|
13
|
+
|
|
14
|
+
/** How `approve-hooks` is called. */
|
|
15
|
+
export const APPROVE_HOOKS_USAGE =
|
|
16
|
+
'Usage: lamaste-server plugins approve-hooks <name> --digest <sha256> [--version <v>] (what `hooks <name>` shows)';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Why a pending update waits, as the terminal shows it: each reason of
|
|
20
|
+
* `approvalRequired` (`[{ code, reason }]`; a secret turned plain is a
|
|
21
|
+
* warning), and the sentences of `autoApproveWithheld` that do not repeat
|
|
22
|
+
* one of them — what auto-approve, when on, could not cover beyond that.
|
|
23
|
+
* Every text as {@link terminalText}.
|
|
24
|
+
*
|
|
25
|
+
* @param {{ approvalRequired?: unknown, autoApproveWithheld?: unknown }} update
|
|
26
|
+
* @returns {{ required: Array<{ text: string, warning: boolean }>, notCovered: string[] }}
|
|
27
|
+
*/
|
|
28
|
+
export function pendingUpdateReasons(update) {
|
|
29
|
+
const required = (Array.isArray(update?.approvalRequired) ? update.approvalRequired : [])
|
|
30
|
+
.filter((entry) => entry !== null && typeof entry === 'object')
|
|
31
|
+
.map((entry) => ({
|
|
32
|
+
text: terminalText(entry.reason),
|
|
33
|
+
warning: entry.code === SETTINGS_SECRET_TO_PLAIN,
|
|
34
|
+
}));
|
|
35
|
+
const said = new Set(required.map((line) => line.text));
|
|
36
|
+
const notCovered = (Array.isArray(update?.autoApproveWithheld) ? update.autoApproveWithheld : [])
|
|
37
|
+
.map((reason) => terminalText(reason))
|
|
38
|
+
.filter((text) => !said.has(text));
|
|
39
|
+
return { required, notCovered };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The command that approves what plugin `name` brings: the installed
|
|
44
|
+
* version by its digest (`{ digest }`), or a pending update by its version
|
|
45
|
+
* and — when it has one — its digest (`{ digest, version }`).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @param {{ digest?: string | null, version?: string | null }} target
|
|
49
|
+
*/
|
|
50
|
+
export function approveHooksCommand(name, { digest = null, version = null }) {
|
|
51
|
+
return [
|
|
52
|
+
`lamaste-server plugins approve-hooks ${terminalText(name)}`,
|
|
53
|
+
...(digest ? [`--digest ${terminalText(digest)}`] : []),
|
|
54
|
+
...(version ? [`--version ${terminalText(version)}`] : []),
|
|
55
|
+
].join(' ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The arguments of `approve-hooks <name>` after the name: `--digest
|
|
60
|
+
* <sha256>` (the installed version's, or a pending update's) and
|
|
61
|
+
* `--version <v>` (a pending update's) — at least one; `--version` alone
|
|
62
|
+
* approves an update that brings neither relay hooks nor an agent half.
|
|
63
|
+
*
|
|
64
|
+
* @param {string[]} rest
|
|
65
|
+
* @returns {{ digest: string | null, version: string | null } | { error: string }}
|
|
66
|
+
*/
|
|
67
|
+
export function parseApproveArgs(rest) {
|
|
68
|
+
const valueOf = (flag) => {
|
|
69
|
+
const idx = rest.indexOf(flag);
|
|
70
|
+
return idx === -1 ? null : (rest[idx + 1] ?? '');
|
|
71
|
+
};
|
|
72
|
+
const digest = valueOf('--digest');
|
|
73
|
+
const version = valueOf('--version');
|
|
74
|
+
if (digest === null && version === null) return { error: APPROVE_HOOKS_USAGE };
|
|
75
|
+
if (digest !== null && !DIGEST_RE.test(digest)) return { error: APPROVE_HOOKS_USAGE };
|
|
76
|
+
if (version !== null && !VERSION_RE.test(version)) return { error: APPROVE_HOOKS_USAGE };
|
|
77
|
+
return { digest, version };
|
|
78
|
+
}
|