@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,293 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { readFile, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OS hardening subtasks: swap, firewall, fail2ban, SSH hardening, system dependencies.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} ctx Shared installer context.
|
|
9
|
+
* @param {object} task Parent Listr2 task reference.
|
|
10
|
+
* @returns {import('listr2').ListrTask[]}
|
|
11
|
+
*/
|
|
12
|
+
export function hardenTasks(ctx, task) {
|
|
13
|
+
return task.newListr([
|
|
14
|
+
{
|
|
15
|
+
title: 'Creating swap file',
|
|
16
|
+
// Swap is always created — critical for 512MB droplets regardless of hardening
|
|
17
|
+
task: async (_ctx, subtask) => {
|
|
18
|
+
// Check if swap already exists
|
|
19
|
+
const { stdout: swapInfo } = await execa('swapon', ['--show']);
|
|
20
|
+
if (swapInfo.trim().length > 0) {
|
|
21
|
+
subtask.output = 'Swap already active, skipping creation';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
subtask.output = 'Allocating 1GB swap file...';
|
|
26
|
+
await execa('fallocate', ['-l', '1G', '/swapfile']);
|
|
27
|
+
await execa('chmod', ['600', '/swapfile']);
|
|
28
|
+
await execa('mkswap', ['/swapfile']);
|
|
29
|
+
await execa('swapon', ['/swapfile']);
|
|
30
|
+
|
|
31
|
+
// Add to /etc/fstab if not already present
|
|
32
|
+
const fstab = await readFile('/etc/fstab', 'utf8');
|
|
33
|
+
if (!fstab.includes('/swapfile')) {
|
|
34
|
+
await writeFile('/etc/fstab', fstab.trimEnd() + '\n/swapfile none swap sw 0 0\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Set swappiness
|
|
38
|
+
await execa('sysctl', ['vm.swappiness=10']);
|
|
39
|
+
await writeFile('/etc/sysctl.d/99-lamaste.conf', 'vm.swappiness=10\n');
|
|
40
|
+
|
|
41
|
+
subtask.output = 'Swap file created and activated';
|
|
42
|
+
},
|
|
43
|
+
rendererOptions: { persistentOutput: true },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: 'Configuring UFW firewall',
|
|
47
|
+
skip: async () => {
|
|
48
|
+
if (ctx.skipHarden) return 'Skipped via --skip-harden';
|
|
49
|
+
|
|
50
|
+
// Check if UFW is active and all required ports are already allowed
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execa('ufw', ['status']);
|
|
53
|
+
const isActive = stdout.includes('Status: active');
|
|
54
|
+
if (!isActive) return false;
|
|
55
|
+
|
|
56
|
+
const requiredPorts = ['22/tcp', '80/tcp', '443/tcp', '9292/tcp'];
|
|
57
|
+
const allPresent = requiredPorts.every((port) => {
|
|
58
|
+
// Match lines like "22/tcp ALLOW Anywhere"
|
|
59
|
+
const regex = new RegExp(`^${port.replace('/', '\\/')}\\s+ALLOW`, 'm');
|
|
60
|
+
return regex.test(stdout);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (allPresent) return 'UFW active with all required ports already allowed';
|
|
64
|
+
} catch {
|
|
65
|
+
// ufw not installed or not available — proceed with task
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
},
|
|
70
|
+
task: async (_ctx, subtask) => {
|
|
71
|
+
const requiredPorts = ['22/tcp', '80/tcp', '443/tcp', '9292/tcp'];
|
|
72
|
+
|
|
73
|
+
// Determine if UFW is currently active
|
|
74
|
+
let isActive = false;
|
|
75
|
+
let statusOutput = '';
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await execa('ufw', ['status']);
|
|
78
|
+
statusOutput = stdout;
|
|
79
|
+
isActive = stdout.includes('Status: active');
|
|
80
|
+
} catch {
|
|
81
|
+
// ufw not available or inactive
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isActive) {
|
|
85
|
+
// UFW is already active — only add missing port rules
|
|
86
|
+
const actions = [];
|
|
87
|
+
for (const port of requiredPorts) {
|
|
88
|
+
const regex = new RegExp(`^${port.replace('/', '\\/')}\\s+ALLOW`, 'm');
|
|
89
|
+
if (regex.test(statusOutput)) {
|
|
90
|
+
actions.push(`Port ${port} already allowed`);
|
|
91
|
+
} else {
|
|
92
|
+
await execa('ufw', ['allow', port]);
|
|
93
|
+
actions.push(`Added rule for ${port}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
subtask.output = actions.join(', ');
|
|
97
|
+
} else {
|
|
98
|
+
// UFW is not active — set up from scratch (without reset)
|
|
99
|
+
subtask.output = 'Setting UFW defaults...';
|
|
100
|
+
await execa('ufw', ['default', 'deny', 'incoming']);
|
|
101
|
+
await execa('ufw', ['default', 'allow', 'outgoing']);
|
|
102
|
+
|
|
103
|
+
subtask.output = 'Allowing ports 22, 80, 443, 9292...';
|
|
104
|
+
for (const port of requiredPorts) {
|
|
105
|
+
await execa('ufw', ['allow', port]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await execa('ufw', ['--force', 'enable']);
|
|
109
|
+
subtask.output = 'UFW enabled with ports 22, 80, 443, 9292 allowed';
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
rendererOptions: { persistentOutput: true },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
title: 'Installing and configuring fail2ban',
|
|
116
|
+
skip: async () => {
|
|
117
|
+
if (ctx.skipHarden) return 'Skipped via --skip-harden';
|
|
118
|
+
|
|
119
|
+
// Skip if our drop-in config already exists with expected content
|
|
120
|
+
const configPath = '/etc/fail2ban/jail.d/lamaste.conf';
|
|
121
|
+
if (existsSync(configPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const existing = await readFile(configPath, 'utf8');
|
|
124
|
+
if (existing.includes('[sshd]') && existing.includes('[nginx-http-auth]')) {
|
|
125
|
+
const { stdout } = await execa('systemctl', ['is-active', 'fail2ban']);
|
|
126
|
+
if (stdout.trim() === 'active') {
|
|
127
|
+
return 'fail2ban already configured and running';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Config exists but can't be read or fail2ban not running — proceed
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false;
|
|
136
|
+
},
|
|
137
|
+
task: async (_ctx, subtask) => {
|
|
138
|
+
subtask.output = 'Installing fail2ban...';
|
|
139
|
+
try {
|
|
140
|
+
await execa('apt-get', ['install', '-y', 'fail2ban']);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Failed to install fail2ban. Check your network connection and package sources.\n${err.stderr || err.message}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const jailConfig = `[sshd]
|
|
148
|
+
enabled = true
|
|
149
|
+
port = ssh
|
|
150
|
+
filter = sshd
|
|
151
|
+
logpath = /var/log/auth.log
|
|
152
|
+
maxretry = 5
|
|
153
|
+
bantime = 3600
|
|
154
|
+
|
|
155
|
+
[nginx-http-auth]
|
|
156
|
+
enabled = true
|
|
157
|
+
port = http,https
|
|
158
|
+
filter = nginx-http-auth
|
|
159
|
+
logpath = /var/log/nginx/error.log
|
|
160
|
+
maxretry = 5
|
|
161
|
+
bantime = 3600
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
// Write to jail.d/ drop-in instead of overwriting jail.local
|
|
165
|
+
const jailDir = '/etc/fail2ban/jail.d';
|
|
166
|
+
if (!existsSync(jailDir)) {
|
|
167
|
+
await execa('mkdir', ['-p', jailDir]);
|
|
168
|
+
}
|
|
169
|
+
await writeFile(`${jailDir}/lamaste.conf`, jailConfig);
|
|
170
|
+
|
|
171
|
+
subtask.output = 'Restarting fail2ban...';
|
|
172
|
+
await execa('systemctl', ['restart', 'fail2ban']);
|
|
173
|
+
|
|
174
|
+
const { stdout: status } = await execa('systemctl', ['is-active', 'fail2ban']);
|
|
175
|
+
subtask.output = `fail2ban status: ${status.trim()}`;
|
|
176
|
+
},
|
|
177
|
+
rendererOptions: { persistentOutput: true },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
title: 'Hardening SSH configuration',
|
|
181
|
+
skip: () => ctx.skipHarden && 'Skipped via --skip-harden',
|
|
182
|
+
task: async (_ctx, subtask) => {
|
|
183
|
+
const sshdConfigPath = '/etc/ssh/sshd_config';
|
|
184
|
+
const tempPath = '/etc/ssh/sshd_config.lamaste-new';
|
|
185
|
+
const backupPath = '/etc/ssh/sshd_config.pre-lamaste';
|
|
186
|
+
|
|
187
|
+
const originalContent = await readFile(sshdConfigPath, 'utf8');
|
|
188
|
+
|
|
189
|
+
const settings = {
|
|
190
|
+
PasswordAuthentication: 'no',
|
|
191
|
+
PermitRootLogin: 'prohibit-password',
|
|
192
|
+
ChallengeResponseAuthentication: 'no',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Check if all settings are already correctly applied
|
|
196
|
+
const allCorrect = Object.entries(settings).every(([key, value]) => {
|
|
197
|
+
const regex = new RegExp(`^${key}\\s+${value}$`, 'm');
|
|
198
|
+
return regex.test(originalContent);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (allCorrect) {
|
|
202
|
+
subtask.output = 'SSH already hardened, skipping';
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Apply regex modifications to produce new content
|
|
207
|
+
let content = originalContent;
|
|
208
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
209
|
+
// Match both commented and uncommented lines with any spacing
|
|
210
|
+
const regex = new RegExp(`^#?\\s*${key}\\s+.*$`, 'gm');
|
|
211
|
+
if (regex.test(content)) {
|
|
212
|
+
content = content.replace(regex, `${key} ${value}`);
|
|
213
|
+
} else {
|
|
214
|
+
content = content.trimEnd() + `\n${key} ${value}\n`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Write modified content to a temp file for validation
|
|
219
|
+
await writeFile(tempPath, content);
|
|
220
|
+
|
|
221
|
+
subtask.output = 'Validating sshd config...';
|
|
222
|
+
try {
|
|
223
|
+
await execa('sshd', ['-t', '-f', tempPath]);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
// Validation failed — remove temp file, leave original untouched
|
|
226
|
+
await unlink(tempPath).catch(() => {});
|
|
227
|
+
throw new Error(
|
|
228
|
+
`sshd config validation failed, original config is untouched: ${err.message}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Validation passed — back up original (only if no prior backup exists)
|
|
233
|
+
if (!existsSync(backupPath)) {
|
|
234
|
+
await writeFile(backupPath, originalContent);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Atomically move temp file into place
|
|
238
|
+
await execa('mv', [tempPath, sshdConfigPath]);
|
|
239
|
+
|
|
240
|
+
subtask.output = 'Restarting ssh...';
|
|
241
|
+
await execa('systemctl', ['restart', 'ssh']);
|
|
242
|
+
|
|
243
|
+
subtask.output = 'SSH hardened: key-auth only, root with keys only';
|
|
244
|
+
},
|
|
245
|
+
rendererOptions: { persistentOutput: true },
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
title: 'Installing system dependencies',
|
|
249
|
+
task: async (_ctx, subtask) => {
|
|
250
|
+
subtask.output = 'Updating package lists...';
|
|
251
|
+
try {
|
|
252
|
+
await execa('apt-get', ['update']);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Failed to update package lists. Check your network connection and /etc/apt/sources.list.\n${err.stderr || err.message}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
subtask.output = 'Installing curl, openssl, nginx, certbot...';
|
|
260
|
+
try {
|
|
261
|
+
await execa('apt-get', [
|
|
262
|
+
'install',
|
|
263
|
+
'-y',
|
|
264
|
+
'curl',
|
|
265
|
+
'openssl',
|
|
266
|
+
'nginx',
|
|
267
|
+
'certbot',
|
|
268
|
+
'python3-certbot-nginx',
|
|
269
|
+
]);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Failed to install system packages. Check your network connection and package sources.\n${err.stderr || err.message}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const { stdout: nginxVersion } = await execa('nginx', ['-v']);
|
|
277
|
+
subtask.output = `Installed: ${nginxVersion || 'nginx'}`;
|
|
278
|
+
|
|
279
|
+
// Stop nginx for now (will be configured and started later)
|
|
280
|
+
await execa('systemctl', ['stop', 'nginx']);
|
|
281
|
+
|
|
282
|
+
// Disable default nginx site
|
|
283
|
+
const defaultSite = '/etc/nginx/sites-enabled/default';
|
|
284
|
+
if (existsSync(defaultSite)) {
|
|
285
|
+
await unlink(defaultSite);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
subtask.output = 'System dependencies installed, nginx stopped';
|
|
289
|
+
},
|
|
290
|
+
rendererOptions: { persistentOutput: true },
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { writeFile, readFile, access } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { generatePassword } from '../lib/secrets.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* mTLS certificate generation subtasks.
|
|
8
|
+
* Creates a self-signed CA, client certificate, and PKCS12 bundle.
|
|
9
|
+
*
|
|
10
|
+
* Idempotent: if the CA key and client PKCS12 bundle already exist,
|
|
11
|
+
* all generation steps are skipped to avoid invalidating previously
|
|
12
|
+
* imported client certificates.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ctx Shared installer context.
|
|
15
|
+
* @param {object} task Parent Listr2 task reference.
|
|
16
|
+
* @returns {import('listr2').ListrTask[]}
|
|
17
|
+
*/
|
|
18
|
+
export function mtlsTasks(ctx, task) {
|
|
19
|
+
const pkiDir = ctx.pkiDir;
|
|
20
|
+
const alreadyProvisioned = existsSync(`${pkiDir}/ca.key`) && existsSync(`${pkiDir}/client.p12`);
|
|
21
|
+
|
|
22
|
+
if (alreadyProvisioned) {
|
|
23
|
+
return task.newListr([
|
|
24
|
+
{
|
|
25
|
+
title: 'mTLS certificates already exist — skipping generation',
|
|
26
|
+
task: async () => {
|
|
27
|
+
// Read the existing p12 password so the summary can display it.
|
|
28
|
+
ctx.p12Password = await readFile(`${pkiDir}/.p12-password`, 'utf8');
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return task.newListr([
|
|
35
|
+
{
|
|
36
|
+
title: 'Creating PKI directory',
|
|
37
|
+
task: async (_ctx, subtask) => {
|
|
38
|
+
await execa('mkdir', ['-p', pkiDir]);
|
|
39
|
+
await execa('chmod', ['700', pkiDir]);
|
|
40
|
+
subtask.output = `PKI directory: ${pkiDir}`;
|
|
41
|
+
},
|
|
42
|
+
rendererOptions: { persistentOutput: true },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Generating CA private key and certificate',
|
|
46
|
+
task: async (_ctx, subtask) => {
|
|
47
|
+
subtask.output = 'Generating 4096-bit RSA CA key...';
|
|
48
|
+
await execa('openssl', ['genrsa', '-out', `${pkiDir}/ca.key`, '4096']);
|
|
49
|
+
|
|
50
|
+
subtask.output = 'Creating self-signed CA certificate (10 year validity)...';
|
|
51
|
+
await execa('openssl', [
|
|
52
|
+
'req',
|
|
53
|
+
'-x509',
|
|
54
|
+
'-new',
|
|
55
|
+
'-nodes',
|
|
56
|
+
'-key',
|
|
57
|
+
`${pkiDir}/ca.key`,
|
|
58
|
+
'-sha256',
|
|
59
|
+
'-days',
|
|
60
|
+
'3650',
|
|
61
|
+
'-out',
|
|
62
|
+
`${pkiDir}/ca.crt`,
|
|
63
|
+
'-subj',
|
|
64
|
+
'/CN=Lamaste CA/O=Lamaste',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
await execa('chmod', ['600', `${pkiDir}/ca.key`]);
|
|
68
|
+
await execa('chmod', ['644', `${pkiDir}/ca.crt`]);
|
|
69
|
+
|
|
70
|
+
// Verify files exist
|
|
71
|
+
await assertFileExists(`${pkiDir}/ca.key`);
|
|
72
|
+
await assertFileExists(`${pkiDir}/ca.crt`);
|
|
73
|
+
|
|
74
|
+
subtask.output = 'CA key and certificate generated';
|
|
75
|
+
},
|
|
76
|
+
rendererOptions: { persistentOutput: true },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: 'Generating client key and CSR',
|
|
80
|
+
task: async (_ctx, subtask) => {
|
|
81
|
+
subtask.output = 'Generating 4096-bit RSA client key...';
|
|
82
|
+
await execa('openssl', ['genrsa', '-out', `${pkiDir}/client.key`, '4096']);
|
|
83
|
+
|
|
84
|
+
subtask.output = 'Creating certificate signing request...';
|
|
85
|
+
await execa('openssl', [
|
|
86
|
+
'req',
|
|
87
|
+
'-new',
|
|
88
|
+
'-key',
|
|
89
|
+
`${pkiDir}/client.key`,
|
|
90
|
+
'-out',
|
|
91
|
+
`${pkiDir}/client.csr`,
|
|
92
|
+
'-subj',
|
|
93
|
+
'/CN=admin/O=Lamaste',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
await execa('chmod', ['600', `${pkiDir}/client.key`]);
|
|
97
|
+
|
|
98
|
+
await assertFileExists(`${pkiDir}/client.key`);
|
|
99
|
+
await assertFileExists(`${pkiDir}/client.csr`);
|
|
100
|
+
|
|
101
|
+
subtask.output = 'Client key and CSR generated';
|
|
102
|
+
},
|
|
103
|
+
rendererOptions: { persistentOutput: true },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: 'Signing client certificate with CA',
|
|
107
|
+
task: async (_ctx, subtask) => {
|
|
108
|
+
subtask.output = 'Signing client certificate (2 year validity)...';
|
|
109
|
+
await execa('openssl', [
|
|
110
|
+
'x509',
|
|
111
|
+
'-req',
|
|
112
|
+
'-in',
|
|
113
|
+
`${pkiDir}/client.csr`,
|
|
114
|
+
'-CA',
|
|
115
|
+
`${pkiDir}/ca.crt`,
|
|
116
|
+
'-CAkey',
|
|
117
|
+
`${pkiDir}/ca.key`,
|
|
118
|
+
'-CAcreateserial',
|
|
119
|
+
'-out',
|
|
120
|
+
`${pkiDir}/client.crt`,
|
|
121
|
+
'-days',
|
|
122
|
+
'730',
|
|
123
|
+
'-sha256',
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
await execa('chmod', ['644', `${pkiDir}/client.crt`]);
|
|
127
|
+
|
|
128
|
+
await assertFileExists(`${pkiDir}/client.crt`);
|
|
129
|
+
|
|
130
|
+
// Clean up CSR (no longer needed)
|
|
131
|
+
await execa('rm', ['-f', `${pkiDir}/client.csr`]);
|
|
132
|
+
|
|
133
|
+
subtask.output = 'Client certificate signed and CSR removed';
|
|
134
|
+
},
|
|
135
|
+
rendererOptions: { persistentOutput: true },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
title: 'Creating PKCS12 bundle',
|
|
139
|
+
task: async (_ctx, subtask) => {
|
|
140
|
+
const password = generatePassword();
|
|
141
|
+
|
|
142
|
+
subtask.output = 'Creating PKCS12 bundle for browser import...';
|
|
143
|
+
await execa(
|
|
144
|
+
'openssl',
|
|
145
|
+
[
|
|
146
|
+
'pkcs12',
|
|
147
|
+
'-export',
|
|
148
|
+
'-keypbe',
|
|
149
|
+
'PBE-SHA1-3DES',
|
|
150
|
+
'-certpbe',
|
|
151
|
+
'PBE-SHA1-3DES',
|
|
152
|
+
'-macalg',
|
|
153
|
+
'sha1',
|
|
154
|
+
'-out',
|
|
155
|
+
`${pkiDir}/client.p12`,
|
|
156
|
+
'-inkey',
|
|
157
|
+
`${pkiDir}/client.key`,
|
|
158
|
+
'-in',
|
|
159
|
+
`${pkiDir}/client.crt`,
|
|
160
|
+
'-certfile',
|
|
161
|
+
`${pkiDir}/ca.crt`,
|
|
162
|
+
'-passout',
|
|
163
|
+
'stdin',
|
|
164
|
+
],
|
|
165
|
+
{ input: password },
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Save password to file (no trailing newline)
|
|
169
|
+
await writeFile(`${pkiDir}/.p12-password`, password, { mode: 0o600 });
|
|
170
|
+
await execa('chmod', ['600', `${pkiDir}/client.p12`]);
|
|
171
|
+
|
|
172
|
+
await assertFileExists(`${pkiDir}/client.p12`);
|
|
173
|
+
await assertFileExists(`${pkiDir}/.p12-password`);
|
|
174
|
+
|
|
175
|
+
ctx.p12Password = password;
|
|
176
|
+
subtask.output = 'PKCS12 bundle created';
|
|
177
|
+
},
|
|
178
|
+
rendererOptions: { persistentOutput: true },
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Assert that a file exists, throwing a descriptive error if it does not.
|
|
185
|
+
* @param {string} filePath
|
|
186
|
+
*/
|
|
187
|
+
async function assertFileExists(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
await access(filePath);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error(`Expected file was not created: ${filePath}`);
|
|
192
|
+
}
|
|
193
|
+
}
|