|
1 | 1 | import { confirm } from '../prompt.js'; |
2 | 2 | import { runRemoteCommand } from '../runner.js'; |
3 | 3 | import { ui } from '../ui.js'; |
4 | | - |
5 | | -const SUDO = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"'; |
| 4 | +import * as sec from '../../infrastructure/provisioning/security.js'; |
6 | 5 |
|
7 | 6 | export async function cmdHarden(cwd: string, options: { config?: string }): Promise<void> { |
8 | 7 | await runRemoteCommand( |
9 | 8 | cwd, |
10 | 9 | async ({ executor }) => { |
11 | 10 | const changes: string[] = []; |
12 | 11 |
|
13 | | - // ── 1. SSH service check ────────────────────────────────────────────── |
14 | 12 | ui.heading('SSH Hardening'); |
15 | | - |
16 | | - const sshActiveResult = await executor.exec( |
17 | | - `systemctl is-active ssh 2>/dev/null || systemctl is-active sshd 2>/dev/null && echo "active" || echo "inactive"`, |
18 | | - ); |
19 | | - const sshActive = sshActiveResult.stdout.includes('active'); |
| 13 | + const sshActive = (await executor.exec(sec.sshCheckActiveCommand())).stdout.includes('active'); |
20 | 14 | ui.info(`SSH service: ${sshActive ? 'active' : 'inactive'}`); |
21 | | - |
22 | | - const sshdResult = await executor.exec( |
23 | | - `grep -E "^(PermitRootLogin|PasswordAuthentication)" /etc/ssh/sshd_config 2>/dev/null || echo "defaults"`, |
24 | | - ); |
| 15 | + const sshdResult = await executor.exec(sec.sshCheckConfigCommand()); |
25 | 16 | ui.info(`Current SSH config:\n ${sshdResult.stdout.split('\n').join('\n ')}`); |
26 | 17 |
|
27 | | - const hardenSsh = await confirm('Harden SSH?'); |
28 | | - if (hardenSsh) { |
29 | | - // Check for sudo users before disabling root |
30 | | - const sudoUsersResult = await executor.exec( |
31 | | - `getent group sudo 2>/dev/null | cut -d: -f4 | tr ',' '\\n' | grep -v '^$'`, |
32 | | - ); |
33 | | - const sudoUsers = sudoUsersResult.stdout.trim(); |
34 | | - |
| 18 | + if (await confirm('Harden SSH?')) { |
| 19 | + const sudoUsers = (await executor.exec(sec.sshCheckSudoUsersCommand())).stdout.trim(); |
35 | 20 | if (sudoUsers) { |
36 | | - ui.info(`Sudo users found: ${sudoUsers.split('\n').join(', ')}`); |
37 | | - const disableRoot = await confirm('Disable root login?'); |
38 | | - if (disableRoot) { |
39 | | - await executor.exec( |
40 | | - `${SUDO}; $SUDO sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config`, |
41 | | - ); |
42 | | - await executor.exec( |
43 | | - `${SUDO}; $SUDO systemctl restart ssh 2>/dev/null || $SUDO systemctl restart sshd 2>/dev/null || true`, |
44 | | - ); |
| 21 | + ui.info(`Sudo users: ${sudoUsers.split('\n').join(', ')}`); |
| 22 | + if (await confirm('Disable root login?')) { |
| 23 | + await executor.exec(sec.sshDisableRootLoginCommand()); |
| 24 | + await executor.exec(sec.sshRestartCommand()); |
45 | 25 | ui.success('Root login disabled'); |
46 | 26 | changes.push('SSH: PermitRootLogin set to no'); |
47 | 27 | } |
48 | 28 | } else { |
49 | | - ui.warn('No sudo users found — skipping root login disable (would lock you out).'); |
| 29 | + ui.warn('No sudo users — skipping root disable (would lock you out).'); |
50 | 30 | } |
51 | | - |
52 | | - const disablePassAuth = await confirm('Disable password authentication (keys only)?'); |
53 | | - if (disablePassAuth) { |
54 | | - await executor.exec( |
55 | | - `${SUDO}; $SUDO sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config`, |
56 | | - ); |
57 | | - await executor.exec( |
58 | | - `${SUDO}; $SUDO systemctl restart ssh 2>/dev/null || $SUDO systemctl restart sshd 2>/dev/null || true`, |
59 | | - ); |
60 | | - ui.success('Password authentication disabled'); |
| 31 | + if (await confirm('Disable password auth (keys only)?')) { |
| 32 | + await executor.exec(sec.sshDisablePasswordAuthCommand()); |
| 33 | + await executor.exec(sec.sshRestartCommand()); |
| 34 | + ui.success('Password auth disabled'); |
61 | 35 | changes.push('SSH: PasswordAuthentication set to no'); |
62 | 36 | } |
63 | 37 | } |
64 | 38 |
|
65 | | - // ── 2. UFW firewall ─────────────────────────────────────────────────── |
66 | 39 | ui.heading('Firewall (UFW)'); |
67 | | - |
68 | | - const ufwResult = await executor.exec( |
69 | | - `command -v ufw &>/dev/null && ufw status || echo "NOT_INSTALLED"`, |
70 | | - ); |
71 | | - |
| 40 | + const ufwResult = await executor.exec(sec.ufwCheckInstalledCommand()); |
72 | 41 | if (ufwResult.stdout.includes('NOT_INSTALLED')) { |
73 | | - ui.warn('UFW is not installed.'); |
74 | | - const installUfw = await confirm('Install UFW?'); |
75 | | - if (installUfw) { |
76 | | - await executor.exec( |
77 | | - `${SUDO}; $SUDO apt-get install -y ufw`, |
78 | | - ); |
| 42 | + ui.warn('UFW not installed.'); |
| 43 | + if (await confirm('Install UFW?')) { |
| 44 | + await executor.exec(sec.ufwInstallCommand()); |
79 | 45 | ui.success('UFW installed'); |
80 | 46 | changes.push('UFW: installed'); |
81 | 47 | } |
82 | 48 | } else { |
83 | 49 | ui.info(`UFW status:\n ${ufwResult.stdout.split('\n').slice(0, 3).join('\n ')}`); |
84 | 50 | } |
85 | 51 |
|
86 | | - const configureUfw = await confirm('Configure UFW (allow SSH/80/443, deny all else, enable)?'); |
87 | | - if (configureUfw) { |
88 | | - await executor.exec(`${SUDO}; $SUDO ufw default deny incoming`); |
89 | | - await executor.exec(`${SUDO}; $SUDO ufw default allow outgoing`); |
90 | | - await executor.exec(`${SUDO}; $SUDO ufw allow ssh`); |
91 | | - await executor.exec(`${SUDO}; $SUDO ufw allow 80/tcp`); |
92 | | - await executor.exec(`${SUDO}; $SUDO ufw allow 443/tcp`); |
93 | | - await executor.exec(`${SUDO}; $SUDO ufw --force enable`); |
| 52 | + if (await confirm('Configure UFW (allow SSH/80/443, deny all else)?')) { |
| 53 | + for (const cmd of sec.ufwConfigureCommands()) { |
| 54 | + await executor.exec(cmd); |
| 55 | + } |
94 | 56 | ui.success('UFW configured and enabled'); |
95 | | - changes.push('UFW: configured (SSH, 80, 443 allowed; deny incoming by default)'); |
| 57 | + changes.push('UFW: configured (SSH, 80, 443 allowed)'); |
96 | 58 | } |
97 | 59 |
|
98 | | - // ── 3. Fail2ban ─────────────────────────────────────────────────────── |
99 | 60 | ui.heading('Fail2ban'); |
100 | | - |
101 | | - const fail2banResult = await executor.exec( |
102 | | - `command -v fail2ban-server &>/dev/null && systemctl is-active fail2ban && echo "ACTIVE" || echo "NOT_ACTIVE"`, |
103 | | - ); |
104 | | - |
105 | | - if (!fail2banResult.stdout.includes('ACTIVE')) { |
106 | | - ui.warn('Fail2ban is not active.'); |
107 | | - const installFail2ban = await confirm('Install and configure fail2ban?'); |
108 | | - if (installFail2ban) { |
109 | | - await executor.exec(`${SUDO}; $SUDO apt-get install -y fail2ban`); |
110 | | - |
111 | | - const jailConfig = [ |
112 | | - '[DEFAULT]', |
113 | | - 'maxretry = 5', |
114 | | - 'findtime = 600', |
115 | | - 'bantime = 3600', |
116 | | - '', |
117 | | - '[sshd]', |
118 | | - 'enabled = true', |
119 | | - 'port = ssh', |
120 | | - 'filter = sshd', |
121 | | - 'logpath = /var/log/auth.log', |
122 | | - 'maxretry = 5', |
123 | | - ].join('\\n'); |
124 | | - |
125 | | - await executor.exec( |
126 | | - `${SUDO}; printf '${jailConfig}' | $SUDO tee /etc/fail2ban/jail.local > /dev/null`, |
127 | | - ); |
128 | | - await executor.exec(`${SUDO}; $SUDO systemctl enable fail2ban && $SUDO systemctl restart fail2ban`); |
129 | | - ui.success('Fail2ban installed and configured'); |
130 | | - changes.push('Fail2ban: installed, configured (maxretry=5, bantime=3600s), sshd jail enabled'); |
| 61 | + const f2b = (await executor.exec(sec.fail2banCheckActiveCommand())).stdout; |
| 62 | + if (!f2b.includes('ACTIVE')) { |
| 63 | + ui.warn('Fail2ban not active.'); |
| 64 | + if (await confirm('Install and configure fail2ban?')) { |
| 65 | + await executor.exec(sec.fail2banInstallCommand()); |
| 66 | + await executor.exec(sec.fail2banApplyConfigCommand()); |
| 67 | + await executor.exec(sec.fail2banEnableCommand()); |
| 68 | + ui.success('Fail2ban installed'); |
| 69 | + changes.push('Fail2ban: installed, sshd jail enabled'); |
131 | 70 | } |
132 | 71 | } else { |
133 | | - ui.success('Fail2ban is already active'); |
| 72 | + ui.success('Fail2ban already active'); |
134 | 73 | } |
135 | 74 |
|
136 | | - // ── Summary ─────────────────────────────────────────────────────────── |
137 | | - ui.heading('Hardening Summary'); |
138 | 75 | if (changes.length === 0) { |
139 | | - ui.info('No changes were made.'); |
| 76 | + ui.info('No changes made.'); |
140 | 77 | } else { |
141 | | - for (const change of changes) { |
142 | | - ui.success(change); |
143 | | - } |
| 78 | + changes.forEach((c) => ui.success(c)); |
144 | 79 | ui.info(`${changes.length} change(s) applied.`); |
145 | 80 | } |
146 | 81 | }, |
|
0 commit comments