Skip to content

Commit 29cbe1a

Browse files
committed
refactor(harden): move security commands to infrastructure/provisioning (sprint 3)
1 parent fe72a4f commit 29cbe1a

2 files changed

Lines changed: 112 additions & 100 deletions

File tree

src/cli/commands/harden.ts

Lines changed: 35 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,81 @@
11
import { confirm } from '../prompt.js';
22
import { runRemoteCommand } from '../runner.js';
33
import { ui } from '../ui.js';
4-
5-
const SUDO = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
4+
import * as sec from '../../infrastructure/provisioning/security.js';
65

76
export async function cmdHarden(cwd: string, options: { config?: string }): Promise<void> {
87
await runRemoteCommand(
98
cwd,
109
async ({ executor }) => {
1110
const changes: string[] = [];
1211

13-
// ── 1. SSH service check ──────────────────────────────────────────────
1412
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');
2014
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());
2516
ui.info(`Current SSH config:\n ${sshdResult.stdout.split('\n').join('\n ')}`);
2617

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();
3520
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());
4525
ui.success('Root login disabled');
4626
changes.push('SSH: PermitRootLogin set to no');
4727
}
4828
} 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).');
5030
}
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');
6135
changes.push('SSH: PasswordAuthentication set to no');
6236
}
6337
}
6438

65-
// ── 2. UFW firewall ───────────────────────────────────────────────────
6639
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());
7241
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());
7945
ui.success('UFW installed');
8046
changes.push('UFW: installed');
8147
}
8248
} else {
8349
ui.info(`UFW status:\n ${ufwResult.stdout.split('\n').slice(0, 3).join('\n ')}`);
8450
}
8551

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+
}
9456
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)');
9658
}
9759

98-
// ── 3. Fail2ban ───────────────────────────────────────────────────────
9960
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');
13170
}
13271
} else {
133-
ui.success('Fail2ban is already active');
72+
ui.success('Fail2ban already active');
13473
}
13574

136-
// ── Summary ───────────────────────────────────────────────────────────
137-
ui.heading('Hardening Summary');
13875
if (changes.length === 0) {
139-
ui.info('No changes were made.');
76+
ui.info('No changes made.');
14077
} else {
141-
for (const change of changes) {
142-
ui.success(change);
143-
}
78+
changes.forEach((c) => ui.success(c));
14479
ui.info(`${changes.length} change(s) applied.`);
14580
}
14681
},
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const SUDO = 'SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"';
2+
3+
export function sshCheckActiveCommand(): string {
4+
return `systemctl is-active ssh 2>/dev/null || systemctl is-active sshd 2>/dev/null && echo "active" || echo "inactive"`;
5+
}
6+
7+
export function sshCheckConfigCommand(): string {
8+
return `grep -E "^(PermitRootLogin|PasswordAuthentication)" /etc/ssh/sshd_config 2>/dev/null || echo "defaults"`;
9+
}
10+
11+
export function sshCheckSudoUsersCommand(): string {
12+
return `getent group sudo 2>/dev/null | cut -d: -f4 | tr ',' '\\n' | grep -v '^$'`;
13+
}
14+
15+
export function sshDisableRootLoginCommand(): string {
16+
return `${SUDO}; $SUDO sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config`;
17+
}
18+
19+
export function sshDisablePasswordAuthCommand(): string {
20+
return `${SUDO}; $SUDO sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config`;
21+
}
22+
23+
export function sshRestartCommand(): string {
24+
return `${SUDO}; $SUDO systemctl restart ssh 2>/dev/null || $SUDO systemctl restart sshd 2>/dev/null || true`;
25+
}
26+
27+
export function ufwCheckInstalledCommand(): string {
28+
return `command -v ufw &>/dev/null && ufw status || echo "NOT_INSTALLED"`;
29+
}
30+
31+
export function ufwInstallCommand(): string {
32+
return `${SUDO}; $SUDO apt-get install -y ufw`;
33+
}
34+
35+
export function ufwConfigureCommands(): string[] {
36+
return [
37+
`${SUDO}; $SUDO ufw default deny incoming`,
38+
`${SUDO}; $SUDO ufw default allow outgoing`,
39+
`${SUDO}; $SUDO ufw allow ssh`,
40+
`${SUDO}; $SUDO ufw allow 80/tcp`,
41+
`${SUDO}; $SUDO ufw allow 443/tcp`,
42+
`${SUDO}; $SUDO ufw --force enable`,
43+
];
44+
}
45+
46+
export function fail2banCheckActiveCommand(): string {
47+
return `command -v fail2ban-server &>/dev/null && systemctl is-active fail2ban && echo "ACTIVE" || echo "NOT_ACTIVE"`;
48+
}
49+
50+
export function fail2banInstallCommand(): string {
51+
return `${SUDO}; $SUDO apt-get install -y fail2ban`;
52+
}
53+
54+
export function fail2banJailConfig(): string {
55+
return [
56+
'[DEFAULT]',
57+
'maxretry = 5',
58+
'findtime = 600',
59+
'bantime = 3600',
60+
'',
61+
'[sshd]',
62+
'enabled = true',
63+
'port = ssh',
64+
'filter = sshd',
65+
'logpath = /var/log/auth.log',
66+
'maxretry = 5',
67+
].join('\\n');
68+
}
69+
70+
export function fail2banApplyConfigCommand(): string {
71+
const config = fail2banJailConfig();
72+
return `${SUDO}; printf '${config}' | $SUDO tee /etc/fail2ban/jail.local > /dev/null`;
73+
}
74+
75+
export function fail2banEnableCommand(): string {
76+
return `${SUDO}; $SUDO systemctl enable fail2ban && $SUDO systemctl restart fail2ban`;
77+
}

0 commit comments

Comments
 (0)