@lamalibre/create-lamaste 4.5.0 → 4.7.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 +20 -3
- package/scripts/lamaste-priv +56 -5
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -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 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- 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 +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/02-guides/static-sites.md +4 -0
- 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 +80 -12
- package/vendor/docs/03-architecture/overview.md +5 -5
- 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 +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +9 -0
- package/vendor/docs/04-api-reference/tunnels.md +35 -28
- package/vendor/docs/04-api-reference/users.md +41 -29
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- 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/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +56 -11
- 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 +325 -55
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +175 -14
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +18 -1
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/index-Cb-ADsbB.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 +50 -19
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +576 -131
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +82 -9
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +29 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +77 -31
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
- package/vendor/serverd/src/lib/web-surface.js +783 -0
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +6 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +188 -71
- package/vendor/serverd/src/routes/management/tunnels.js +37 -22
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamalibre/create-lamaste",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "One-command setup for secure reverse tunnels with a management dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"mtls"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@lamalibre/lamaste": "^4.
|
|
46
|
+
"@lamalibre/lamaste": "^4.7.0",
|
|
47
47
|
"chalk": "^5.3.0",
|
|
48
48
|
"execa": "^9.0.0",
|
|
49
49
|
"listr2": "^8.0.0"
|
package/scripts/bundle-vendor.js
CHANGED
|
@@ -13,6 +13,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { assertProductionBundle } from '../../../../scripts/assert-production-bundle.js';
|
|
16
|
+
import { workspaceVersions, writePinnedManifest } from './pin-release.js';
|
|
16
17
|
|
|
17
18
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const packageRoot = join(thisDir, '..');
|
|
@@ -24,6 +25,10 @@ async function main() {
|
|
|
24
25
|
await rm(vendorDir, { recursive: true, force: true });
|
|
25
26
|
await mkdir(vendorDir, { recursive: true });
|
|
26
27
|
|
|
28
|
+
// Each vendored manifest names this release's own packages exactly
|
|
29
|
+
// (pin-release.js), so the relay's `npm install` never mixes releases.
|
|
30
|
+
const versions = await workspaceVersions(monorepoRoot);
|
|
31
|
+
|
|
27
32
|
// --- lamaste-serverd: package.json + src/ ---
|
|
28
33
|
const serverSrc = join(monorepoRoot, 'packages', 'server', 'daemon');
|
|
29
34
|
if (!existsSync(serverSrc)) {
|
|
@@ -32,7 +37,11 @@ async function main() {
|
|
|
32
37
|
|
|
33
38
|
const serverDest = join(vendorDir, 'serverd');
|
|
34
39
|
await mkdir(serverDest, { recursive: true });
|
|
35
|
-
await
|
|
40
|
+
await writePinnedManifest(
|
|
41
|
+
join(serverSrc, 'package.json'),
|
|
42
|
+
join(serverDest, 'package.json'),
|
|
43
|
+
versions,
|
|
44
|
+
);
|
|
36
45
|
await cp(join(serverSrc, 'src'), join(serverDest, 'src'), {
|
|
37
46
|
recursive: true,
|
|
38
47
|
});
|
|
@@ -49,7 +58,11 @@ async function main() {
|
|
|
49
58
|
}
|
|
50
59
|
const serverCliDest = join(vendorDir, 'server');
|
|
51
60
|
await mkdir(serverCliDest, { recursive: true });
|
|
52
|
-
await
|
|
61
|
+
await writePinnedManifest(
|
|
62
|
+
join(serverCliSrc, 'package.json'),
|
|
63
|
+
join(serverCliDest, 'package.json'),
|
|
64
|
+
versions,
|
|
65
|
+
);
|
|
53
66
|
await cp(join(serverCliSrc, 'bin'), join(serverCliDest, 'bin'), { recursive: true });
|
|
54
67
|
await cp(join(serverCliSrc, 'src'), join(serverCliDest, 'src'), { recursive: true });
|
|
55
68
|
console.error('Bundled vendor/server from lamaste-server CLI (package.json + bin/ + src/)');
|
|
@@ -81,7 +94,11 @@ async function main() {
|
|
|
81
94
|
}
|
|
82
95
|
const gatekeeperDest = join(vendorDir, 'gatekeeper');
|
|
83
96
|
await mkdir(gatekeeperDest, { recursive: true });
|
|
84
|
-
await
|
|
97
|
+
await writePinnedManifest(
|
|
98
|
+
join(gatekeeperSrc, 'package.json'),
|
|
99
|
+
join(gatekeeperDest, 'package.json'),
|
|
100
|
+
versions,
|
|
101
|
+
);
|
|
85
102
|
await cp(gatekeeperDist, join(gatekeeperDest, 'dist'), { recursive: true });
|
|
86
103
|
console.error('Bundled vendor/gatekeeper (package.json + dist/)');
|
|
87
104
|
|
package/scripts/lamaste-priv
CHANGED
|
@@ -81,21 +81,37 @@ class UsageError extends Error {}
|
|
|
81
81
|
// ---------------------------------------------------------------------------
|
|
82
82
|
|
|
83
83
|
const LABEL = '[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?';
|
|
84
|
+
/** A full hostname, lower case, two labels at least — what names a vhost since the multi-domain relay. */
|
|
85
|
+
const HOST = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+';
|
|
84
86
|
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
85
87
|
|
|
86
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Vhosts the panel writes: the panel's and chisel's (primary domain), one
|
|
90
|
+
* auth portal per domain, a tunnel's by its full hostname, a site's by id.
|
|
91
|
+
*/
|
|
87
92
|
const PANEL_SITE_RE = new RegExp(
|
|
88
|
-
`^lamalibre-lamaste-(?:panel-domain|auth|
|
|
93
|
+
`^lamalibre-lamaste-(?:panel-domain|tunnel|auth-${HOST}|app-${HOST}|agent-panel-${HOST}|site-${UUID})$`,
|
|
89
94
|
);
|
|
90
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Vhost names before the multi-domain relay — the auth portal and tunnels
|
|
98
|
+
* by subdomain alone. The panel renames them at start; it may still link,
|
|
99
|
+
* unlink, back up, restore and remove them, but never write one.
|
|
100
|
+
*/
|
|
101
|
+
const LEGACY_SITE_RE = new RegExp(`^lamalibre-lamaste-(?:auth|app-${LABEL}|agent-panel-${LABEL})$`);
|
|
102
|
+
|
|
91
103
|
/** Vhosts create-lamaste writes that the panel may only switch on and off. */
|
|
92
104
|
const TOGGLE_ONLY_SITES = new Set(['lamalibre-lamaste-panel-ip']);
|
|
93
105
|
|
|
106
|
+
/** The longest site name: a file name, with room for its `.bak`. */
|
|
107
|
+
const MAX_SITE_NAME = 250;
|
|
108
|
+
|
|
94
109
|
function assertSiteName(name, action) {
|
|
95
|
-
if (typeof name !== 'string' || name.length >
|
|
110
|
+
if (typeof name !== 'string' || name.length > MAX_SITE_NAME) {
|
|
96
111
|
throw new UsageError('invalid site name');
|
|
97
112
|
}
|
|
98
113
|
if (PANEL_SITE_RE.test(name)) return;
|
|
114
|
+
if (action !== 'write' && LEGACY_SITE_RE.test(name)) return;
|
|
99
115
|
if ((action === 'enable' || action === 'disable') && TOGGLE_ONLY_SITES.has(name)) return;
|
|
100
116
|
throw new UsageError(`site name not allowed for ${action}: ${name}`);
|
|
101
117
|
}
|
|
@@ -251,6 +267,12 @@ const WEBROOT_RE = /^\/var\/www\/lamaste\/[A-Za-z0-9._/-]*$/;
|
|
|
251
267
|
const HEADER_NAME_RE = /^[A-Za-z0-9-]{1,64}$/;
|
|
252
268
|
const VARIABLE_RE = /^\$[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
253
269
|
const URI_PATH_RE = /^\/[A-Za-z0-9._~/-]*$/;
|
|
270
|
+
/** A named location (`location @previous`), the target of a try_files fallback. */
|
|
271
|
+
const NAMED_LOCATION_RE = /^@[a-z][a-z0-9_]{0,31}$/;
|
|
272
|
+
const TRY_FILES_ARG_RE = /^[A-Za-z0-9._/$=-]{1,256}$/;
|
|
273
|
+
/** A `types` entry: a media type, then the extensions served with it. */
|
|
274
|
+
const MEDIA_TYPE_RE = /^[a-z]+\/[a-z0-9.+-]{1,64}$/;
|
|
275
|
+
const EXTENSION_RE = /^[a-z0-9]{1,16}$/;
|
|
254
276
|
|
|
255
277
|
function hasDotDotSegment(p) {
|
|
256
278
|
return p.split('/').some((seg) => seg === '..' || seg === '.');
|
|
@@ -317,18 +339,27 @@ const RULES = {
|
|
|
317
339
|
error_page: { args: errorPageArgs },
|
|
318
340
|
return: { args: argc(1, 2) },
|
|
319
341
|
rewrite: { args: rewriteArgs },
|
|
342
|
+
// A host whose every location asks the same authorization (a plugin's
|
|
343
|
+
// web surface): a URI of this server, never a file.
|
|
344
|
+
auth_request: { args: (a) => a.length === 1 && (a[0] === 'off' || URI_PATH_RE.test(a[0])) },
|
|
320
345
|
location: { block: 'location', args: locationArgs },
|
|
321
346
|
},
|
|
322
347
|
location: {
|
|
323
348
|
location: { block: 'location', args: locationArgs },
|
|
324
349
|
if: { block: 'if', args: argc(1, 5) },
|
|
350
|
+
// The content types of a plugin's web surface, named by the vhost
|
|
351
|
+
// rather than the host's mime.types (entries checked below).
|
|
352
|
+
types: { block: 'types', args: argc(0, 0) },
|
|
325
353
|
internal: { args: argc(0, 0) },
|
|
354
|
+
// The type of a body the location writes itself (a user surface's JSON
|
|
355
|
+
// refusal): a media type, never a file.
|
|
356
|
+
default_type: { args: (a) => a.length === 1 && /^[a-z]+\/[a-z0-9.+-]{1,64}$/.test(a[0]) },
|
|
326
357
|
return: { args: argc(1, 2) },
|
|
327
358
|
rewrite: { args: rewriteArgs },
|
|
328
359
|
root: { args: (a) => a.length === 1 && webrootPath(a[0]) },
|
|
329
360
|
alias: { args: (a) => a.length === 1 && webrootPath(a[0]) },
|
|
330
361
|
index: { args: each(/^[A-Za-z0-9._-]{1,64}$/) },
|
|
331
|
-
try_files: { args:
|
|
362
|
+
try_files: { args: tryFilesArgs },
|
|
332
363
|
add_header: {
|
|
333
364
|
args: (a) =>
|
|
334
365
|
(a.length === 2 || (a.length === 3 && a[2] === 'always')) && HEADER_NAME_RE.test(a[0]),
|
|
@@ -386,8 +417,20 @@ function rewriteArgs(a) {
|
|
|
386
417
|
);
|
|
387
418
|
}
|
|
388
419
|
|
|
420
|
+
/** Files to try under the location's root; the last may instead name a location. */
|
|
421
|
+
function tryFilesArgs(a) {
|
|
422
|
+
if (a.length === 0) return false;
|
|
423
|
+
const last = a[a.length - 1];
|
|
424
|
+
return (
|
|
425
|
+
a.slice(0, -1).every((v) => TRY_FILES_ARG_RE.test(v)) &&
|
|
426
|
+
(TRY_FILES_ARG_RE.test(last) || NAMED_LOCATION_RE.test(last))
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
389
430
|
function locationArgs(a) {
|
|
390
|
-
if (a.length === 1)
|
|
431
|
+
if (a.length === 1) {
|
|
432
|
+
return (a[0].length <= 512 && a[0].startsWith('/')) || NAMED_LOCATION_RE.test(a[0]);
|
|
433
|
+
}
|
|
391
434
|
return a.length === 2 && ['=', '~', '~*', '^~'].includes(a[0]) && a[1].length <= 512;
|
|
392
435
|
}
|
|
393
436
|
|
|
@@ -407,6 +450,14 @@ function validateDirectives(directives, context) {
|
|
|
407
450
|
}
|
|
408
451
|
continue;
|
|
409
452
|
}
|
|
453
|
+
if (context === 'types') {
|
|
454
|
+
// types entries are "media/type ext...;" — no block, no include.
|
|
455
|
+
if (d.block !== null) throw new UsageError(`vhost line ${d.line}: block inside types`);
|
|
456
|
+
if (!MEDIA_TYPE_RE.test(d.name) || !each(EXTENSION_RE)(d.args)) {
|
|
457
|
+
throw new UsageError(`vhost line ${d.line}: malformed types entry`);
|
|
458
|
+
}
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
410
461
|
const rule = rules[d.name];
|
|
411
462
|
if (!rule) {
|
|
412
463
|
throw new UsageError(
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins a vendored package's dependencies on this monorepo's packages to the
|
|
3
|
+
* exact versions it was built with.
|
|
4
|
+
*
|
|
5
|
+
* create-lamaste vendors serverd, the server CLI and Gatekeeper, then runs
|
|
6
|
+
* `npm install` for them on the relay. Their manifests name the core
|
|
7
|
+
* library with a caret range (`^4.6.0`), so an install of release X made
|
|
8
|
+
* after X+1 shipped resolved X+1's core — and 4.6.0's serverd on 4.7.0's
|
|
9
|
+
* core did not start. Pinned, create-lamaste@X always installs the core,
|
|
10
|
+
* tickets and identity libraries of release X; third-party dependencies
|
|
11
|
+
* keep their ranges.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The name → version of every workspace package (`packages/<group>/<name>`).
|
|
22
|
+
*
|
|
23
|
+
* @param {string} monorepoRoot
|
|
24
|
+
* @returns {Promise<Map<string, string>>}
|
|
25
|
+
*/
|
|
26
|
+
export async function workspaceVersions(monorepoRoot) {
|
|
27
|
+
const versions = new Map();
|
|
28
|
+
const packagesDir = join(monorepoRoot, 'packages');
|
|
29
|
+
for (const group of await readdir(packagesDir, { withFileTypes: true })) {
|
|
30
|
+
if (!group.isDirectory()) continue;
|
|
31
|
+
for (const entry of await readdir(join(packagesDir, group.name), { withFileTypes: true })) {
|
|
32
|
+
const manifestPath = join(packagesDir, group.name, entry.name, 'package.json');
|
|
33
|
+
if (!entry.isDirectory() || !existsSync(manifestPath)) continue;
|
|
34
|
+
const { name, version } = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
35
|
+
if (typeof name === 'string' && typeof version === 'string') versions.set(name, version);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return versions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The manifest with each dependency on a workspace package set to that
|
|
43
|
+
* package's exact version. Pure; throws on a workspace dependency whose
|
|
44
|
+
* range the workspace's version does not satisfy as a caret or exact range
|
|
45
|
+
* (the vendored code would then be pinned to something it was not written
|
|
46
|
+
* against).
|
|
47
|
+
*
|
|
48
|
+
* @param {Record<string, unknown>} manifest
|
|
49
|
+
* @param {Map<string, string>} versions
|
|
50
|
+
* @returns {Record<string, unknown>}
|
|
51
|
+
*/
|
|
52
|
+
export function pinWorkspaceDependencies(manifest, versions) {
|
|
53
|
+
const pinned = { ...manifest };
|
|
54
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
55
|
+
const deps = manifest[field];
|
|
56
|
+
if (deps === null || typeof deps !== 'object') continue;
|
|
57
|
+
const next = { ...deps };
|
|
58
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
59
|
+
const version = versions.get(name);
|
|
60
|
+
if (version === undefined) continue;
|
|
61
|
+
if (range !== version && !caretAdmits(range, version)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${manifest.name}: ${field}.${name} is "${range}", the workspace has ${version}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
next[name] = version;
|
|
67
|
+
}
|
|
68
|
+
pinned[field] = next;
|
|
69
|
+
}
|
|
70
|
+
return pinned;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether `^a.b.c` admits `version` (same major, not lower). */
|
|
74
|
+
function caretAdmits(range, version) {
|
|
75
|
+
const match = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(String(range));
|
|
76
|
+
const have = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
77
|
+
if (!match || !have) return false;
|
|
78
|
+
const [want, got] = [match.slice(1).map(Number), have.slice(1).map(Number)];
|
|
79
|
+
if (want[0] !== got[0]) return false;
|
|
80
|
+
for (let i = 1; i < 3; i++) if (got[i] !== want[i]) return got[i] > want[i];
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Copy `source` (a package.json) to `dest` with its workspace dependencies
|
|
86
|
+
* pinned.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} source
|
|
89
|
+
* @param {string} dest
|
|
90
|
+
* @param {Map<string, string>} versions
|
|
91
|
+
*/
|
|
92
|
+
export async function writePinnedManifest(source, dest, versions) {
|
|
93
|
+
const manifest = JSON.parse(await readFile(source, 'utf8'));
|
|
94
|
+
await writeFile(
|
|
95
|
+
dest,
|
|
96
|
+
`${JSON.stringify(pinWorkspaceDependencies(manifest, versions), null, 2)}\n`,
|
|
97
|
+
);
|
|
98
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync } from 'node:fs';
|
|
|
12
12
|
import { Listr } from 'listr2';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import { execa } from 'execa';
|
|
15
|
-
import { detectOS, detectIP, checkRoot } from './lib/env.js';
|
|
15
|
+
import { detectOS, detectIP, checkRoot, installedPanelIp } from './lib/env.js';
|
|
16
16
|
import { printSummary } from './lib/summary.js';
|
|
17
17
|
import { readPanelKeyPin } from './lib/panel-pin.js';
|
|
18
18
|
import { hardenTasks } from './tasks/harden.js';
|
|
@@ -485,6 +485,13 @@ export async function main() {
|
|
|
485
485
|
{
|
|
486
486
|
title: 'Detecting IP address',
|
|
487
487
|
task: async (_ctx, subtask) => {
|
|
488
|
+
// A redeploy keeps the installed relay's IP (installedPanelIp).
|
|
489
|
+
const installedIp = flags.forceFull ? null : await installedPanelIp();
|
|
490
|
+
if (installedIp) {
|
|
491
|
+
ctx.ip = installedIp;
|
|
492
|
+
subtask.output = `${installedIp} (the installed relay's, from panel.json)`;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
488
495
|
ctx.ip = await detectIP({ allowPrivate: flags.dev });
|
|
489
496
|
if (flags.dev) {
|
|
490
497
|
subtask.output = `${ctx.ip} (dev mode — private IP accepted)`;
|
package/src/lib/env.js
CHANGED
|
@@ -123,6 +123,25 @@ function isPublicIP(ip) {
|
|
|
123
123
|
* Verify the process is running as root.
|
|
124
124
|
* @throws {Error} if not root
|
|
125
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* The IP an installed relay was set up with (`panel.json` `ip`), or null —
|
|
128
|
+
* no install, no readable file, or no valid IPv4 in it. A redeploy keeps
|
|
129
|
+
* it: the panel's IP certificate, agents and admins know the relay by it,
|
|
130
|
+
* and detecting again could change it (or fail, where the installer's
|
|
131
|
+
* public-IP check does not hold — a dev install on a private address).
|
|
132
|
+
*
|
|
133
|
+
* @param {string} [panelJsonPath]
|
|
134
|
+
* @returns {Promise<string | null>}
|
|
135
|
+
*/
|
|
136
|
+
export async function installedPanelIp(panelJsonPath = '/etc/lamalibre/lamaste/panel.json') {
|
|
137
|
+
try {
|
|
138
|
+
const { ip } = JSON.parse(await readFile(panelJsonPath, 'utf8'));
|
|
139
|
+
return typeof ip === 'string' && isValidIPv4(ip) ? ip : null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
126
145
|
export function checkRoot() {
|
|
127
146
|
if (process.getuid() !== 0) {
|
|
128
147
|
throw new Error(
|
|
@@ -265,12 +265,19 @@ WantedBy=multi-user.target
|
|
|
265
265
|
* Installed by create-lamaste only; onboarding writes the configuration,
|
|
266
266
|
* then enables and starts it.
|
|
267
267
|
*
|
|
268
|
+
* No start rate limit (`StartLimitIntervalSec=0`): the panel restarts
|
|
269
|
+
* Authelia for every change of membership, domains or OIDC clients, and
|
|
270
|
+
* systemd's default (5 starts in 10 s) refused the sixth of a quick series
|
|
271
|
+
* of admin changes — Authelia then stayed down on every domain until
|
|
272
|
+
* something started it again. A crash loop stays bounded by `RestartSec=5`.
|
|
273
|
+
*
|
|
268
274
|
* @returns {string}
|
|
269
275
|
*/
|
|
270
276
|
export function generateAutheliaUnit() {
|
|
271
277
|
return `[Unit]
|
|
272
278
|
Description=Authelia Authentication Server
|
|
273
279
|
After=network.target
|
|
280
|
+
StartLimitIntervalSec=0
|
|
274
281
|
|
|
275
282
|
[Service]
|
|
276
283
|
Type=simple
|
package/src/tasks/redeploy.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
nodeVersionTooNew,
|
|
25
25
|
} from '../lib/node-version.js';
|
|
26
26
|
import { deployGatekeeperPackage } from './gatekeeper.js';
|
|
27
|
+
import { autheliaReady } from '@lamalibre/lamaste/server';
|
|
27
28
|
|
|
28
29
|
const GATEKEEPER_SERVICE = 'lamalibre-lamaste-gatekeeper';
|
|
29
30
|
|
|
@@ -447,6 +448,13 @@ export function redeployTasks(ctx, task) {
|
|
|
447
448
|
if (ctx.autheliaWasActive) {
|
|
448
449
|
subtask.output = 'Starting Authelia...';
|
|
449
450
|
await execa('systemctl', ['start', 'authelia']);
|
|
451
|
+
// systemctl returns when the process is spawned; the panel, which
|
|
452
|
+
// starts next, talks to Authelia at once.
|
|
453
|
+
if (!(await autheliaReady(execa))) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
'Authelia is not serving requests after the redeploy — see journalctl -u authelia',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
450
458
|
}
|
|
451
459
|
if (ctx.gatekeeperWasActive) {
|
|
452
460
|
subtask.output = 'Starting gatekeeper...';
|
|
@@ -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.7.0
|
|
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.7.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.
|
|
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.7.0
|
|
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.7.0
|
|
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.7.0
|
|
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.7.0 [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.7.0
|
|
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.7.0`
|
|
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.7.0`)
|
|
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.7.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.
|
|
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.7.0` |
|
|
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.5.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.7.0` |
|
|
112
112
|
| **License** | Polyform Noncommercial 1.0.0 |
|
|
@@ -40,7 +40,7 @@ From the Users page in the management panel, you can:
|
|
|
40
40
|
1. Navigate to the Users page in the management panel
|
|
41
41
|
2. Click "Add User"
|
|
42
42
|
3. Enter a username, display name, email, and password
|
|
43
|
-
4. Lamaste creates the user in `users.yml
|
|
43
|
+
4. Lamaste creates the user in `users.yml`; Authelia reloads it in place
|
|
44
44
|
5. Share the credentials with the user — they enroll in TOTP on first login
|
|
45
45
|
|
|
46
46
|
### TOTP enrollment flow
|
|
@@ -221,7 +221,7 @@ users:
|
|
|
221
221
|
|
|
222
222
|
Lamaste manages this file through the panel server API. All writes are atomic: content is written to a temporary file first, then moved into place. This prevents Authelia from reading a partially written file.
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
Authelia watches `users.yml` (`authentication_backend.file.watch: true`) and reloads it in place; with the watch it re-reads a user's details at each of their requests, so a change applies at the user's next request without a restart and without ending anyone else's session. It skips a reload within 0.5 s of the previous one, so the panel writes the file once more 0.6 s after its last write.
|
|
225
225
|
|
|
226
226
|
### Creating users programmatically
|
|
227
227
|
|
|
@@ -420,6 +420,10 @@ Key choices:
|
|
|
420
420
|
|
|
421
421
|
See [Registry Credential and OIDC Clients](../04-api-reference/registry-and-oidc.md) for the manifest fields and the API.
|
|
422
422
|
|
|
423
|
+
### Several domains
|
|
424
|
+
|
|
425
|
+
On a relay with several domains Authelia carries one `session.cookies` entry per active domain (each `authelia_url: https://auth.<domain>`), so each domain's sign-in sets and reads its own `authelia_session` cookie — same name, scoped by the browser. `access_control` admits each domain's active members on the domain and every host under it (protected sites' rules narrowed to members), then denies; `default_policy` is `deny`. OIDC sign-ins are gated by per-domain `authorization_policies`, since `access_control` does not apply to them, and each domain is an issuer of its own (`https://auth.<domain>`). The panel renders all of it from the directory's memberships ([Domains API](../04-api-reference/domains.md)); on a one-domain relay every user is the domain's member, which admits whom it did before. TOTP is per person, not per domain.
|
|
426
|
+
|
|
423
427
|
### Safety rule: never delete the last user
|
|
424
428
|
|
|
425
429
|
The panel server API prevents deleting the last Authelia user. If only one user exists and you try to delete them, the API returns an error. This prevents a lockout scenario where no one can authenticate to reach the tunneled apps.
|
|
@@ -102,6 +102,10 @@ From the Gatekeeper dashboard in the admin panel:
|
|
|
102
102
|
|
|
103
103
|
Gatekeeper reads groups and grants from its SQLite database (`gatekeeper.db`) on every check, so a group, member or grant created through the admin panel takes effect on the next request — no service restart required. The tunnel list (the panel's `tunnels.json`) and Gatekeeper's settings (`gatekeeper.json`) are watched and reloaded when they change. Changes through the panel also recompute the audiences of agent-hosted plugins at once; edits made with Gatekeeper's own CLI are picked up by the panel within a minute.
|
|
104
104
|
|
|
105
|
+
### Domains and membership
|
|
106
|
+
|
|
107
|
+
On a relay with several domains, membership of a host's domain comes before every access mode: `authenticated` means "any signed-in member of this domain", and `restricted` still needs a grant on top. Membership is rendered into Authelia and checked again by Gatekeeper from the panel's `domains.json`; a signed-in non-member gets a "not a member of this domain" page — with the admin contact and a sign-out link — rather than the sign-in page. Members are managed per domain ([Domains API](../04-api-reference/domains.md)); the admin certificate manages every domain, and Authelia's `admins` group needs membership like everyone else (inside a domain it still passes `restricted` sites).
|
|
108
|
+
|
|
105
109
|
## For Developers
|
|
106
110
|
|
|
107
111
|
### Architecture
|
|
@@ -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.7.0` 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.7.0/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.7.0/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
|
|
@@ -260,6 +260,15 @@ Once set during onboarding, the domain is used throughout the system for constru
|
|
|
260
260
|
| `packages/lamaste-serverd/src/lib/nginx.js` | Vhost creation with domain-based server_name |
|
|
261
261
|
| `packages/lamaste-serverd/src/lib/state.js` | Domain storage in panel.json |
|
|
262
262
|
|
|
263
|
+
## Multiple domains
|
|
264
|
+
|
|
265
|
+
One relay can serve several base domains: the **primary** (set at onboarding — `panel.`, `tunnel.` and agent panels live there) and **added** domains, each with its own sign-in at `auth.<domain>` and its own member list. People have one account across all of them; a person reaches a domain's protected hosts only as a member, and signs in once per domain (browsers never share cookies between domains).
|
|
266
|
+
|
|
267
|
+
- **DNS for an added domain:** an A record for `auth.<domain>`, and either a wildcard `*.<domain>` or one A record per hostname, all to the relay. Explicit records (apex, `www`, mail) keep precedence over a wildcard. Steps: [Adding a Second Domain](../02-guides/second-domain.md).
|
|
268
|
+
- **Domains never overlap:** `a.samplepark.tr` cannot be added beside `samplepark.tr` — a domain's cookies reach every host under it.
|
|
269
|
+
- **Hostnames:** a tunnel or managed site is `<label>.<domain>`, unique as a whole; the reserved labels hold on every domain; the domain of a hostname is the one it is under (never stored separately). A domain's apex and `www.` may be a custom site or an alias.
|
|
270
|
+
- **Records** live in the panel's `state.db` (`domains`, `domain_members`); `panel.json` `domain` stays the primary.
|
|
271
|
+
|
|
263
272
|
## Quick Reference
|
|
264
273
|
|
|
265
274
|
### Required DNS records (onboarding)
|
|
@@ -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.7.0`, 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.7.0`) 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
|
|