@lamalibre/create-lamaste 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { chmod, chown, lstat, mkdir, open, readdir, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Filesystem operations create-lamaste performs as root on paths the lamaste
|
|
9
|
+
* user can write to (its config directory, the web root, /etc/authelia).
|
|
10
|
+
*
|
|
11
|
+
* The panel may be compromised, and it can trigger create-lamaste itself
|
|
12
|
+
* (self-update), so anything it controls may be a trap: a symlink where a
|
|
13
|
+
* file is expected, planted between a check and a use. These helpers never
|
|
14
|
+
* follow a symlink the lamaste user could have placed:
|
|
15
|
+
* - new files are created with O_EXCL | O_NOFOLLOW under a random name and
|
|
16
|
+
* renamed into place (rename replaces a symlink, it does not follow it);
|
|
17
|
+
* - ownership and mode are changed through the open file descriptor
|
|
18
|
+
* (fchown/fchmod), never by path;
|
|
19
|
+
* - recursive changes use coreutils (chown -R, rm -rf), which do not follow
|
|
20
|
+
* symlinks and are safe against a tree changing underneath them.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const NOFOLLOW_READ = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Atomically write `content` to `path`, owned by uid:gid with `mode`.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} path
|
|
29
|
+
* @param {string | Uint8Array} content
|
|
30
|
+
* @param {{ mode: number, uid: number, gid: number }} options
|
|
31
|
+
*/
|
|
32
|
+
export async function writeFileNoFollow(path, content, { mode, uid, gid }) {
|
|
33
|
+
const tmp = join(dirname(path), `.${basename(path)}.${crypto.randomBytes(8).toString('hex')}`);
|
|
34
|
+
const fh = await open(
|
|
35
|
+
tmp,
|
|
36
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
37
|
+
0o600,
|
|
38
|
+
);
|
|
39
|
+
try {
|
|
40
|
+
await fh.writeFile(content);
|
|
41
|
+
await fh.chown(uid, gid);
|
|
42
|
+
await fh.chmod(mode);
|
|
43
|
+
await fh.sync();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
await fh.close();
|
|
46
|
+
await rm(tmp, { force: true });
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
await fh.close();
|
|
50
|
+
try {
|
|
51
|
+
await rename(tmp, path);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
await rm(tmp, { force: true });
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set owner and mode of the regular file at `path` through a descriptor.
|
|
60
|
+
* A symlink, a missing file or anything but a regular file is left alone.
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise<boolean>} whether the file exists and was checked
|
|
63
|
+
*/
|
|
64
|
+
export async function setFileOwnership(path, { uid, gid, mode }) {
|
|
65
|
+
let fh;
|
|
66
|
+
try {
|
|
67
|
+
fh = await open(path, NOFOLLOW_READ);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err.code === 'ENOENT' || err.code === 'ELOOP') return false;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const st = await fh.stat();
|
|
74
|
+
if (!st.isFile()) return false;
|
|
75
|
+
if (st.uid !== uid || st.gid !== gid) await fh.chown(uid, gid);
|
|
76
|
+
if ((st.mode & 0o7777) !== mode) await fh.chmod(mode);
|
|
77
|
+
return true;
|
|
78
|
+
} finally {
|
|
79
|
+
await fh.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set owner and mode of the directory at `path` through a descriptor,
|
|
85
|
+
* creating it (root-owned parent assumed) if it does not exist.
|
|
86
|
+
*/
|
|
87
|
+
export async function ensureDirectory(path, { uid, gid, mode }) {
|
|
88
|
+
await mkdir(path, { recursive: true, mode });
|
|
89
|
+
const fh = await open(path, NOFOLLOW_READ | constants.O_DIRECTORY);
|
|
90
|
+
try {
|
|
91
|
+
const st = await fh.stat();
|
|
92
|
+
if (st.uid !== uid || st.gid !== gid) await fh.chown(uid, gid);
|
|
93
|
+
if ((st.mode & 0o7777) !== mode) await fh.chmod(mode);
|
|
94
|
+
} finally {
|
|
95
|
+
await fh.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `rm -rf` without following symlinks or crossing filesystems. */
|
|
100
|
+
export async function removeTree(path) {
|
|
101
|
+
await execa('rm', ['-rf', '--one-file-system', '--', path]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `chown -R` without following symlinks (coreutils' default for -R). */
|
|
105
|
+
export async function chownTree(path, owner) {
|
|
106
|
+
await execa('chown', ['-R', '-P', '--', owner, path]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Make the install directory (code the panel runs, and the lamaste-server
|
|
111
|
+
* CLI that administrators run as root) writable by root only. Earlier
|
|
112
|
+
* versions handed it to the lamaste user: a compromised panel could then
|
|
113
|
+
* rewrite code that root executes. Its contents are not reused — the tree
|
|
114
|
+
* may hold symlinks that would redirect the redeploy's writes — so a tree
|
|
115
|
+
* not already owned by root is moved aside, a fresh root-owned directory
|
|
116
|
+
* takes its place, and the caller redeploys every component into it and
|
|
117
|
+
* then removes the old tree.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} installDir
|
|
120
|
+
* @returns {Promise<{ previous: string | null }>} the moved-aside tree, if any
|
|
121
|
+
*/
|
|
122
|
+
export async function ensureRootOwnedInstallDir(installDir) {
|
|
123
|
+
let needsReset = false;
|
|
124
|
+
try {
|
|
125
|
+
const st = await lstat(installDir);
|
|
126
|
+
if (!st.isDirectory() || st.uid !== 0) {
|
|
127
|
+
needsReset = true;
|
|
128
|
+
} else {
|
|
129
|
+
for (const entry of await readdir(installDir)) {
|
|
130
|
+
if ((await lstat(join(installDir, entry))).uid !== 0) {
|
|
131
|
+
needsReset = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err.code !== 'ENOENT') throw err;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let previous = null;
|
|
141
|
+
if (needsReset) {
|
|
142
|
+
previous = `${installDir}.lamaste-owned-${Date.now()}`;
|
|
143
|
+
await rename(installDir, previous);
|
|
144
|
+
}
|
|
145
|
+
await ensureDirectory(installDir, { uid: 0, gid: 0, mode: 0o755 });
|
|
146
|
+
return { previous };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hand the web root to the lamaste user, readable by nginx through the
|
|
151
|
+
* www-data group: directories 2750 (new files inherit the group), files
|
|
152
|
+
* 0640. Earlier versions kept it www-data-owned and let the panel write it
|
|
153
|
+
* through wildcard sudo rules. Modes are fixed while the tree is still
|
|
154
|
+
* owned by www-data, before the lamaste user can touch it.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} webRoot
|
|
157
|
+
* @param {{ lamasteUid: number, wwwDataGid: number }} ids
|
|
158
|
+
* @returns {Promise<{ migrated: boolean }>}
|
|
159
|
+
*/
|
|
160
|
+
export async function migrateWebRoot(webRoot, { lamasteUid, wwwDataGid }) {
|
|
161
|
+
let st = null;
|
|
162
|
+
try {
|
|
163
|
+
st = await lstat(webRoot);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.code !== 'ENOENT') throw err;
|
|
166
|
+
}
|
|
167
|
+
if (st && st.isDirectory() && st.uid === lamasteUid) {
|
|
168
|
+
await ensureDirectory(webRoot, { uid: lamasteUid, gid: wwwDataGid, mode: 0o2750 });
|
|
169
|
+
return { migrated: false };
|
|
170
|
+
}
|
|
171
|
+
if (st && !st.isDirectory()) throw new Error(`${webRoot} is not a directory`);
|
|
172
|
+
if (st) {
|
|
173
|
+
await execa('find', [webRoot, '-xdev', '-type', 'd', '-exec', 'chmod', '2750', '{}', '+']);
|
|
174
|
+
await execa('find', [webRoot, '-xdev', '-type', 'f', '-exec', 'chmod', '0640', '{}', '+']);
|
|
175
|
+
await chownTree(webRoot, `${lamasteUid}:${wwwDataGid}`);
|
|
176
|
+
}
|
|
177
|
+
await ensureDirectory(webRoot, { uid: lamasteUid, gid: wwwDataGid, mode: 0o2750 });
|
|
178
|
+
return { migrated: st !== null };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Give /etc/authelia to the lamaste user (the panel writes Authelia's
|
|
183
|
+
* configuration) and its group to the Authelia account (which reads the
|
|
184
|
+
* configuration and keeps its database and notification file there).
|
|
185
|
+
* Earlier versions ran Authelia as root with every file root-owned. Files
|
|
186
|
+
* are re-owned while the directory still belongs to root, so the lamaste
|
|
187
|
+
* user cannot interfere; the directory itself is handed over last.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} dir
|
|
190
|
+
* @param {{ lamasteUid: number, lamasteGid: number, autheliaUid: number, autheliaGid: number }} ids
|
|
191
|
+
* @returns {Promise<{ migrated: boolean }>}
|
|
192
|
+
*/
|
|
193
|
+
export async function migrateAutheliaDir(dir, ids) {
|
|
194
|
+
const { lamasteUid, lamasteGid, autheliaUid, autheliaGid } = ids;
|
|
195
|
+
let st = null;
|
|
196
|
+
try {
|
|
197
|
+
st = await lstat(dir);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err.code !== 'ENOENT') throw err;
|
|
200
|
+
}
|
|
201
|
+
if (st && st.isDirectory() && st.uid === lamasteUid) {
|
|
202
|
+
await ensureDirectory(dir, { uid: lamasteUid, gid: autheliaGid, mode: 0o2770 });
|
|
203
|
+
return { migrated: false };
|
|
204
|
+
}
|
|
205
|
+
if (st && !st.isDirectory()) throw new Error(`${dir} is not a directory`);
|
|
206
|
+
|
|
207
|
+
if (st) {
|
|
208
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
209
|
+
if (!entry.isFile()) continue;
|
|
210
|
+
const path = join(dir, entry.name);
|
|
211
|
+
const name = entry.name;
|
|
212
|
+
if (name === '.secrets.json') {
|
|
213
|
+
// The panel's own record of the secrets; Authelia reads them from its config.
|
|
214
|
+
await chown(path, lamasteUid, lamasteGid);
|
|
215
|
+
await chmod(path, 0o600);
|
|
216
|
+
} else if (name.startsWith('db.sqlite3') || name === 'notifications.txt') {
|
|
217
|
+
await chown(path, autheliaUid, autheliaGid);
|
|
218
|
+
await chmod(path, 0o600);
|
|
219
|
+
} else if (name === 'users.yml') {
|
|
220
|
+
// Authelia rewrites it in place when a user changes their password.
|
|
221
|
+
await chown(path, lamasteUid, autheliaGid);
|
|
222
|
+
await chmod(path, 0o660);
|
|
223
|
+
} else {
|
|
224
|
+
await chown(path, lamasteUid, autheliaGid);
|
|
225
|
+
await chmod(path, 0o640);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await ensureDirectory(dir, { uid: lamasteUid, gid: autheliaGid, mode: 0o2770 });
|
|
230
|
+
return { migrated: st !== null };
|
|
231
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ensureAutheliaUser, ensureChiselGroup, lookupId } from './accounts.js';
|
|
4
|
+
import { installPinnedAuthelia, installPinnedChisel } from './binaries.js';
|
|
5
|
+
import {
|
|
6
|
+
chownTree,
|
|
7
|
+
ensureDirectory,
|
|
8
|
+
migrateAutheliaDir,
|
|
9
|
+
migrateWebRoot,
|
|
10
|
+
setFileOwnership,
|
|
11
|
+
} from './ownership.js';
|
|
12
|
+
import {
|
|
13
|
+
AUTHELIA_CONFIG_DIR,
|
|
14
|
+
AUTHELIA_LOG_DIR,
|
|
15
|
+
AUTHELIA_UNIT_PATH,
|
|
16
|
+
AUTHELIA_USER,
|
|
17
|
+
CHISEL_GROUP,
|
|
18
|
+
CHISEL_UNIT_PATH,
|
|
19
|
+
WEB_ROOT,
|
|
20
|
+
generateAutheliaUnit,
|
|
21
|
+
generateChiselUnit,
|
|
22
|
+
} from './service-config.js';
|
|
23
|
+
|
|
24
|
+
async function writeIfChanged(path, content) {
|
|
25
|
+
let current = null;
|
|
26
|
+
try {
|
|
27
|
+
current = await readFile(path, 'utf8');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code !== 'ENOENT') throw err;
|
|
30
|
+
}
|
|
31
|
+
if (current === content) return false;
|
|
32
|
+
await writeFile(path, content, { mode: 0o644 });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Everything the relay's services need from root, for a fresh install and
|
|
38
|
+
* for every redeploy: service accounts, the pinned chisel and Authelia
|
|
39
|
+
* binaries, their systemd units, and the ownership of the directories the
|
|
40
|
+
* panel writes without privileges. Idempotent.
|
|
41
|
+
*
|
|
42
|
+
* Does not start, stop or restart anything: the caller runs
|
|
43
|
+
* `systemctl daemon-reload` and restarts what changed. Onboarding enables
|
|
44
|
+
* and starts chisel and Authelia on a fresh install.
|
|
45
|
+
*
|
|
46
|
+
* @param {{ configDir: string, pkiDir: string }} ctx
|
|
47
|
+
* @param {(line: string) => void} report
|
|
48
|
+
* @returns {Promise<{ chiselChanged: boolean, autheliaChanged: boolean }>}
|
|
49
|
+
* whether the binary or unit of each changed (a running service needs a
|
|
50
|
+
* restart to pick it up)
|
|
51
|
+
*/
|
|
52
|
+
export async function provisionRelayServices(ctx, report) {
|
|
53
|
+
report(`Ensuring the ${CHISEL_GROUP} group and the ${AUTHELIA_USER} account...`);
|
|
54
|
+
await ensureChiselGroup();
|
|
55
|
+
await ensureAutheliaUser();
|
|
56
|
+
|
|
57
|
+
const lamasteUid = await lookupId('user', 'lamaste');
|
|
58
|
+
const lamasteGid = await lookupId('group', 'lamaste');
|
|
59
|
+
const wwwDataGid = await lookupId('group', 'www-data');
|
|
60
|
+
const chiselGid = await lookupId('group', CHISEL_GROUP);
|
|
61
|
+
const autheliaUid = await lookupId('user', AUTHELIA_USER);
|
|
62
|
+
const autheliaGid = await lookupId('group', AUTHELIA_USER);
|
|
63
|
+
|
|
64
|
+
report('Installing the pinned chisel release...');
|
|
65
|
+
const chisel = await installPinnedChisel();
|
|
66
|
+
report('Installing the pinned Authelia release...');
|
|
67
|
+
const authelia = await installPinnedAuthelia();
|
|
68
|
+
if (authelia.keptNewer) {
|
|
69
|
+
report(`Keeping the installed Authelia ${authelia.version} (newer than the pinned release)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
report('Setting ownership of the web root, Authelia and chisel files...');
|
|
73
|
+
await migrateWebRoot(WEB_ROOT, { lamasteUid, wwwDataGid });
|
|
74
|
+
const autheliaDir = await migrateAutheliaDir(AUTHELIA_CONFIG_DIR, {
|
|
75
|
+
lamasteUid,
|
|
76
|
+
lamasteGid,
|
|
77
|
+
autheliaUid,
|
|
78
|
+
autheliaGid,
|
|
79
|
+
});
|
|
80
|
+
await ensureDirectory(AUTHELIA_LOG_DIR, { uid: autheliaUid, gid: autheliaGid, mode: 0o750 });
|
|
81
|
+
await chownTree(AUTHELIA_LOG_DIR, `${autheliaUid}:${autheliaGid}`);
|
|
82
|
+
// chisel runs as nobody:lamaste-chisel; the panel creates the key (as
|
|
83
|
+
// lamaste, group lamaste-chisel). Earlier versions made it nobody:nogroup 0400.
|
|
84
|
+
await setFileOwnership(join(ctx.configDir, 'chisel-server.key'), {
|
|
85
|
+
uid: lamasteUid,
|
|
86
|
+
gid: chiselGid,
|
|
87
|
+
mode: 0o640,
|
|
88
|
+
});
|
|
89
|
+
// The panel is the CA: the PKI directory is its own. Files earlier
|
|
90
|
+
// versions created through sudo may still be root-owned.
|
|
91
|
+
await chownTree(ctx.pkiDir, `${lamasteUid}:${lamasteGid}`).catch((err) => {
|
|
92
|
+
if (!/No such file/.test(err.stderr ?? '')) throw err;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
report('Writing the chisel and Authelia units...');
|
|
96
|
+
const chiselUnitChanged = await writeIfChanged(CHISEL_UNIT_PATH, generateChiselUnit(ctx));
|
|
97
|
+
const autheliaUnitChanged = await writeIfChanged(AUTHELIA_UNIT_PATH, generateAutheliaUnit());
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
chiselChanged: chisel.changed || chiselUnitChanged,
|
|
101
|
+
autheliaChanged: authelia.changed || autheliaUnitChanged || autheliaDir.migrated,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a URL-safe random password string.
|
|
5
|
+
* @param {number} [length=24] Number of random bytes (output is base64url-encoded, so longer).
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function generatePassword(length = 24) {
|
|
9
|
+
return randomBytes(length).toString('base64url');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a hex-encoded random secret.
|
|
14
|
+
* @param {number} [length=32] Number of random bytes.
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function generateHex(length = 32) {
|
|
18
|
+
return randomBytes(length).toString('hex');
|
|
19
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared systemd unit and sudoers content generators.
|
|
3
|
+
* Used by both the full installer (panel.js) and the redeploy flow (redeploy.js).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate the lamalibre-lamaste-serverd systemd service unit content.
|
|
8
|
+
*
|
|
9
|
+
* @param {{ installDir: string, configDir: string }} ctx
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function generateServiceUnit(ctx) {
|
|
13
|
+
return `[Unit]
|
|
14
|
+
Description=Lamaste Panel Server
|
|
15
|
+
After=network.target
|
|
16
|
+
|
|
17
|
+
[Service]
|
|
18
|
+
Type=simple
|
|
19
|
+
User=lamaste
|
|
20
|
+
Group=lamaste
|
|
21
|
+
# Lets the panel hand the chisel authfile to the group chisel runs as.
|
|
22
|
+
SupplementaryGroups=lamaste-chisel
|
|
23
|
+
WorkingDirectory=${ctx.installDir}/serverd
|
|
24
|
+
ExecStart=/usr/bin/node src/index.js
|
|
25
|
+
Environment=NODE_ENV=production
|
|
26
|
+
Environment=CONFIG_FILE=${ctx.configDir}/panel.json
|
|
27
|
+
Restart=always
|
|
28
|
+
RestartSec=5
|
|
29
|
+
StandardOutput=journal
|
|
30
|
+
StandardError=journal
|
|
31
|
+
SyslogIdentifier=lamalibre-lamaste-serverd
|
|
32
|
+
|
|
33
|
+
# Security hardening
|
|
34
|
+
# Note: NoNewPrivileges is intentionally omitted — the panel uses sudo for
|
|
35
|
+
# the fixed commands and root-owned helpers listed in /etc/sudoers.d/lamaste
|
|
36
|
+
# (systemctl, nginx -t, lamaste-priv, lamaste-certbot, lamaste-cert-info).
|
|
37
|
+
ProtectHome=true
|
|
38
|
+
ReadWritePaths=${ctx.configDir} /var/www/lamaste
|
|
39
|
+
PrivateTmp=true
|
|
40
|
+
|
|
41
|
+
[Install]
|
|
42
|
+
WantedBy=multi-user.target
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate the lamaste sudoers file content.
|
|
48
|
+
*
|
|
49
|
+
* The panel runs as `lamaste`; this file is everything it may do as root.
|
|
50
|
+
* Every rule is a fixed command line or a root-owned program that validates
|
|
51
|
+
* its own arguments — no rule ends in, or contains, a wildcard. (A sudoers
|
|
52
|
+
* `*` matches spaces, so a wildcard rule such as `mv /tmp/x-* /var/www/*`
|
|
53
|
+
* also accepts `mv /tmp/x-a -t /etc/sudoers.d /var/www/y`.)
|
|
54
|
+
*
|
|
55
|
+
* What the panel does NOT need root for, and therefore has no rule for:
|
|
56
|
+
* - its PKI (/etc/lamalibre/lamaste/pki, owned by lamaste — the panel is the CA),
|
|
57
|
+
* - static sites (/var/www/lamaste, owned lamaste:www-data),
|
|
58
|
+
* - Authelia's configuration (/etc/authelia, owned lamaste:lamaste-authelia),
|
|
59
|
+
* - the chisel authfile and key (group lamaste-chisel, of which lamaste is a member).
|
|
60
|
+
* Binaries, systemd units and service accounts are installed by create-lamaste,
|
|
61
|
+
* which runs as root; the panel never writes a unit or a binary.
|
|
62
|
+
*
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function generateSudoersContent() {
|
|
66
|
+
return `# Lamaste serverd sudo rules — generated by create-lamaste; do not edit.
|
|
67
|
+
# Every rule is a fixed command line or a root-owned program that validates its
|
|
68
|
+
# own arguments. There are no wildcards: a sudoers \`*\` matches spaces too.
|
|
69
|
+
|
|
70
|
+
# --- systemctl: managed services ---
|
|
71
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start nginx
|
|
72
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop nginx
|
|
73
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
|
|
74
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx
|
|
75
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start chisel
|
|
76
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop chisel
|
|
77
|
+
# Chisel reloads its authfile by itself. The panel restarts it only when a
|
|
78
|
+
# change revokes access (a removed or rotated credential, a withdrawn port
|
|
79
|
+
# grant) — \`try-restart\`, so a chisel stopped on purpose stays stopped.
|
|
80
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart chisel
|
|
81
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl try-restart chisel
|
|
82
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl enable chisel
|
|
83
|
+
# Disabled (with stop) when startup reconciliation cannot secure chisel, so a
|
|
84
|
+
# reboot does not start it on a stale authfile; re-enabled once it succeeds.
|
|
85
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl disable chisel
|
|
86
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start authelia
|
|
87
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop authelia
|
|
88
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart authelia
|
|
89
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl reload authelia
|
|
90
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl enable authelia
|
|
91
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl enable certbot.timer
|
|
92
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start certbot.timer
|
|
93
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start lamalibre-lamaste-serverd
|
|
94
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop lamalibre-lamaste-serverd
|
|
95
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart lamalibre-lamaste-serverd
|
|
96
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl start lamalibre-lamaste-gatekeeper
|
|
97
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl stop lamalibre-lamaste-gatekeeper
|
|
98
|
+
lamaste ALL=(root) NOPASSWD: /usr/bin/systemctl restart lamalibre-lamaste-gatekeeper
|
|
99
|
+
|
|
100
|
+
# --- nginx config test ---
|
|
101
|
+
lamaste ALL=(root) NOPASSWD: /usr/sbin/nginx -t
|
|
102
|
+
|
|
103
|
+
# --- root-owned programs that validate their own arguments ---
|
|
104
|
+
# lamaste-priv: install/enable/remove the panel's nginx vhosts (every directive
|
|
105
|
+
# checked against an allow-list — nginx opens the files a vhost names as
|
|
106
|
+
# root), store a TOTP secret with Authelia's CLI as the Authelia user, and
|
|
107
|
+
# start a self-update (create-lamaste@<version>) in a transient unit.
|
|
108
|
+
lamaste ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-priv
|
|
109
|
+
# Let's Encrypt: certbot and openssl with fixed flags, hostnames and email
|
|
110
|
+
# validated. See scripts/lamaste-certbot, scripts/lamaste-cert-info.
|
|
111
|
+
lamaste ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-certbot
|
|
112
|
+
lamaste ALL=(root) NOPASSWD: /usr/local/sbin/lamaste-cert-info
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate the lamalibre-lamaste-gatekeeper systemd service unit content.
|
|
118
|
+
*
|
|
119
|
+
* @param {{ installDir: string, configDir: string }} ctx
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function generateGatekeeperServiceUnit(ctx) {
|
|
123
|
+
return `[Unit]
|
|
124
|
+
Description=Lamaste Gatekeeper — tunnel authorization service
|
|
125
|
+
After=network.target authelia.service
|
|
126
|
+
|
|
127
|
+
[Service]
|
|
128
|
+
Type=simple
|
|
129
|
+
User=lamaste
|
|
130
|
+
Group=lamaste
|
|
131
|
+
WorkingDirectory=${ctx.installDir}/gatekeeper
|
|
132
|
+
ExecStart=/usr/bin/node dist/server/index.js
|
|
133
|
+
Environment=NODE_ENV=production
|
|
134
|
+
Environment=LAMALIBRE_LAMASTE_DATA_DIR=${ctx.configDir}
|
|
135
|
+
Restart=always
|
|
136
|
+
RestartSec=5
|
|
137
|
+
StandardOutput=journal
|
|
138
|
+
StandardError=journal
|
|
139
|
+
SyslogIdentifier=lamalibre-lamaste-gatekeeper
|
|
140
|
+
|
|
141
|
+
# Security hardening
|
|
142
|
+
ProtectHome=true
|
|
143
|
+
ReadWritePaths=${ctx.configDir}
|
|
144
|
+
PrivateTmp=true
|
|
145
|
+
NoNewPrivileges=true
|
|
146
|
+
|
|
147
|
+
[Install]
|
|
148
|
+
WantedBy=multi-user.target
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Root-owned sudoers wrapper scripts shipped in this package's scripts/
|
|
154
|
+
* directory and installed to /usr/local/sbin. The sudoers rules reference
|
|
155
|
+
* these absolute paths; each script validates its arguments and runs its
|
|
156
|
+
* command with a fixed argument vector.
|
|
157
|
+
*/
|
|
158
|
+
export const SUDOERS_WRAPPERS = [
|
|
159
|
+
{ name: 'lamaste-priv', dest: '/usr/local/sbin/lamaste-priv', interpreter: 'node' },
|
|
160
|
+
{ name: 'lamaste-certbot', dest: '/usr/local/sbin/lamaste-certbot', interpreter: 'bash' },
|
|
161
|
+
{ name: 'lamaste-cert-info', dest: '/usr/local/sbin/lamaste-cert-info', interpreter: 'bash' },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Wrappers earlier versions installed that no sudoers rule references any
|
|
166
|
+
* more; create-lamaste removes them on upgrade. The panel signs agent CSRs
|
|
167
|
+
* and renames PKI files itself: it owns the PKI directory and the CA key.
|
|
168
|
+
*/
|
|
169
|
+
export const RETIRED_SUDOERS_WRAPPERS = [
|
|
170
|
+
'/usr/local/sbin/lamaste-sign-csr',
|
|
171
|
+
'/usr/local/sbin/lamaste-pki-rename',
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The group the chisel server runs as. The 0640 chisel authfile (every
|
|
176
|
+
* agent's tunnel password) belongs to it; the lamaste user is a member so the
|
|
177
|
+
* panel can hand the file to the group without privileges. Must match
|
|
178
|
+
* CHISEL_AUTHFILE_GROUP in @lamalibre/lamaste/server.
|
|
179
|
+
*/
|
|
180
|
+
export const CHISEL_GROUP = 'lamaste-chisel';
|
|
181
|
+
|
|
182
|
+
/** The system account Authelia runs as (see accounts.js). */
|
|
183
|
+
export const AUTHELIA_USER = 'lamaste-authelia';
|
|
184
|
+
|
|
185
|
+
export const CHISEL_BIN = '/usr/local/bin/chisel';
|
|
186
|
+
export const AUTHELIA_BIN = '/usr/local/bin/authelia';
|
|
187
|
+
export const CHISEL_UNIT_PATH = '/etc/systemd/system/chisel.service';
|
|
188
|
+
export const AUTHELIA_UNIT_PATH = '/etc/systemd/system/authelia.service';
|
|
189
|
+
export const AUTHELIA_CONFIG_DIR = '/etc/authelia';
|
|
190
|
+
export const AUTHELIA_LOG_DIR = '/var/log/authelia';
|
|
191
|
+
export const WEB_ROOT = '/var/www/lamaste';
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The chisel server unit. The authfile carries per-agent credentials and
|
|
195
|
+
* port grants; chisel reloads it on change. The process runs as `nobody`
|
|
196
|
+
* with group {@link CHISEL_GROUP}, the only group that can read the 0640
|
|
197
|
+
* authfile and key the panel writes. `--keyfile` keeps the SSH host identity
|
|
198
|
+
* stable across restarts.
|
|
199
|
+
*
|
|
200
|
+
* Installed by create-lamaste only; onboarding enables and starts it.
|
|
201
|
+
*
|
|
202
|
+
* @param {{ configDir: string }} ctx
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
export function generateChiselUnit(ctx) {
|
|
206
|
+
return `[Unit]
|
|
207
|
+
Description=Chisel Tunnel Server
|
|
208
|
+
After=network.target
|
|
209
|
+
|
|
210
|
+
[Service]
|
|
211
|
+
Type=simple
|
|
212
|
+
User=nobody
|
|
213
|
+
Group=${CHISEL_GROUP}
|
|
214
|
+
ExecStart=${CHISEL_BIN} server --reverse --port 9090 --host 127.0.0.1 --keyfile ${ctx.configDir}/chisel-server.key --authfile ${ctx.configDir}/chisel-users
|
|
215
|
+
Restart=always
|
|
216
|
+
RestartSec=5
|
|
217
|
+
StandardOutput=journal
|
|
218
|
+
StandardError=journal
|
|
219
|
+
SyslogIdentifier=chisel
|
|
220
|
+
|
|
221
|
+
NoNewPrivileges=yes
|
|
222
|
+
CapabilityBoundingSet=
|
|
223
|
+
ProtectSystem=strict
|
|
224
|
+
ProtectHome=yes
|
|
225
|
+
PrivateTmp=yes
|
|
226
|
+
PrivateDevices=yes
|
|
227
|
+
ProtectKernelTunables=yes
|
|
228
|
+
ProtectKernelModules=yes
|
|
229
|
+
ProtectControlGroups=yes
|
|
230
|
+
RestrictNamespaces=yes
|
|
231
|
+
LockPersonality=yes
|
|
232
|
+
|
|
233
|
+
[Install]
|
|
234
|
+
WantedBy=multi-user.target
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The Authelia unit, after upstream's packaged unit: a dedicated
|
|
240
|
+
* unprivileged account, files it creates readable by its group only, and a
|
|
241
|
+
* read-only view of the system except its configuration directory (the
|
|
242
|
+
* SQLite database and notification file live there) and its log directory.
|
|
243
|
+
*
|
|
244
|
+
* Installed by create-lamaste only; onboarding writes the configuration,
|
|
245
|
+
* then enables and starts it.
|
|
246
|
+
*
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
export function generateAutheliaUnit() {
|
|
250
|
+
return `[Unit]
|
|
251
|
+
Description=Authelia Authentication Server
|
|
252
|
+
After=network.target
|
|
253
|
+
|
|
254
|
+
[Service]
|
|
255
|
+
Type=simple
|
|
256
|
+
User=${AUTHELIA_USER}
|
|
257
|
+
Group=${AUTHELIA_USER}
|
|
258
|
+
UMask=027
|
|
259
|
+
Environment=AUTHELIA_SERVER_DISABLE_HEALTHCHECK=true
|
|
260
|
+
ExecStart=${AUTHELIA_BIN} --config ${AUTHELIA_CONFIG_DIR}/configuration.yml
|
|
261
|
+
Restart=always
|
|
262
|
+
RestartSec=5
|
|
263
|
+
StandardOutput=journal
|
|
264
|
+
StandardError=journal
|
|
265
|
+
SyslogIdentifier=authelia
|
|
266
|
+
|
|
267
|
+
NoNewPrivileges=yes
|
|
268
|
+
CapabilityBoundingSet=
|
|
269
|
+
ProtectSystem=strict
|
|
270
|
+
ReadWritePaths=${AUTHELIA_CONFIG_DIR} ${AUTHELIA_LOG_DIR}
|
|
271
|
+
ProtectHome=yes
|
|
272
|
+
PrivateTmp=yes
|
|
273
|
+
PrivateDevices=yes
|
|
274
|
+
ProtectKernelTunables=yes
|
|
275
|
+
ProtectKernelModules=yes
|
|
276
|
+
ProtectControlGroups=yes
|
|
277
|
+
RestrictNamespaces=yes
|
|
278
|
+
LockPersonality=yes
|
|
279
|
+
SystemCallArchitectures=native
|
|
280
|
+
SystemCallFilter=@system-service
|
|
281
|
+
SystemCallErrorNumber=EPERM
|
|
282
|
+
|
|
283
|
+
[Install]
|
|
284
|
+
WantedBy=multi-user.target
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** sites-available name of the port-80 catch-all. */
|
|
289
|
+
export const HTTP_REDIRECT_SITE = 'lamalibre-lamaste-http-redirect';
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The port-80 catch-all: every plain-HTTP request is redirected to HTTPS on
|
|
293
|
+
* the same host and path. Without it nothing listens on :80 and `http://`
|
|
294
|
+
* links to the relay's hostnames are refused.
|
|
295
|
+
*
|
|
296
|
+
* It does not get in the way of Let's Encrypt: certbot's nginx authenticator
|
|
297
|
+
* (`certonly --nginx`, and renewals) clones this default_server block per
|
|
298
|
+
* hostname for the length of an HTTP-01 challenge, answers the challenge
|
|
299
|
+
* path ahead of the redirect, and restores the file afterwards — verified
|
|
300
|
+
* on Ubuntu 24.04 (nginx 1.24, certbot 2.9) for new names, names with an
|
|
301
|
+
* existing 443 block, multi-name lineages, and forced renewals.
|
|
302
|
+
*/
|
|
303
|
+
export function generateHttpRedirectVhost() {
|
|
304
|
+
return `# Managed by Lamaste — plain HTTP is redirected to HTTPS.
|
|
305
|
+
server {
|
|
306
|
+
listen 80 default_server;
|
|
307
|
+
server_name _;
|
|
308
|
+
return 301 https://$host$request_uri;
|
|
309
|
+
}
|
|
310
|
+
`;
|
|
311
|
+
}
|