Skip to content

Commit ad4c31f

Browse files
luizomfclaude
andcommitted
feat(hardening): add sshd_config hardening page
New /hardening page for generating hardened sshd_config: - Presets: Paranoico (A+), Equilibrado (A), Permissivo (B) - Security score (0-100) with A-F grade and color-coded bar - Warnings: danger/warn/info for risky options - Grouped form: Autenticação, Controle de Acesso, Rede, Segurança, Banners - Live preview of sshd_config with section comments in PT-BR - Download sshd_config and apply script (bash with backup + sshd -t) - Copy to clipboard New libs: sshd-generator.ts, sshd-options.ts (25 new tests, 107 total) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7920118 commit ad4c31f

7 files changed

Lines changed: 1249 additions & 3 deletions

File tree

src/layouts/Base.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const currentPath = Astro.url.pathname;
1010
const navItems = [
1111
{ href: '/', label: 'Home', disabled: false },
1212
{ href: '/tunnels/', label: 'Tunnels', disabled: false },
13-
{ href: '/hardening/', label: 'Hardening', disabled: true },
13+
{ href: '/hardening/', label: 'Hardening', disabled: false },
1414
{ href: '/config/', label: 'Config', disabled: false },
1515
{ href: '/keygen/', label: 'KeyGen', disabled: true },
1616
];
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { generateSshdConfig, generateApplyScript } from '../sshd-generator';
3+
import { getPreset, calculateSecurityScore } from '../sshd-options';
4+
import type { SshdConfig } from '../types';
5+
6+
function getParanoid(): SshdConfig {
7+
return getPreset('paranoid');
8+
}
9+
10+
describe('generateSshdConfig', () => {
11+
it('generates header comments', () => {
12+
const result = generateSshdConfig(getParanoid());
13+
expect(result).toContain('gerado por SSH Toolkit');
14+
expect(result).toContain('sshtoolkit.otaviomiranda.com.br');
15+
});
16+
17+
it('generates paranoid defaults correctly', () => {
18+
const result = generateSshdConfig(getParanoid());
19+
expect(result).toContain('Port 22');
20+
expect(result).toContain('PermitRootLogin no');
21+
expect(result).toContain('PubkeyAuthentication yes');
22+
expect(result).toContain('PasswordAuthentication no');
23+
expect(result).toContain('KbdInteractiveAuthentication no');
24+
expect(result).toContain('ChallengeResponseAuthentication no');
25+
expect(result).toContain('PermitEmptyPasswords no');
26+
expect(result).toContain('AuthenticationMethods publickey');
27+
expect(result).toContain('MaxAuthTries 3');
28+
expect(result).toContain('LoginGraceTime 30');
29+
expect(result).toContain('X11Forwarding no');
30+
expect(result).toContain('AllowAgentForwarding no');
31+
expect(result).toContain('AllowTcpForwarding no');
32+
expect(result).toContain('AllowStreamLocalForwarding no');
33+
expect(result).toContain('GatewayPorts no');
34+
expect(result).toContain('PermitTunnel no');
35+
expect(result).toContain('PermitUserEnvironment no');
36+
expect(result).toContain('PermitUserRC no');
37+
expect(result).toContain('StrictModes yes');
38+
expect(result).toContain('ClientAliveInterval 300');
39+
expect(result).toContain('ClientAliveCountMax 2');
40+
expect(result).toContain('UseDNS no');
41+
expect(result).toContain('UsePAM yes');
42+
expect(result).toContain('LogLevel VERBOSE');
43+
expect(result).toContain('PrintMotd no');
44+
expect(result).toContain('PrintLastLog yes');
45+
expect(result).toContain('MaxSessions 5');
46+
expect(result).toContain('MaxStartups 10:30:60');
47+
});
48+
49+
it('generates balanced preset', () => {
50+
const result = generateSshdConfig(getPreset('balanced'));
51+
expect(result).toContain('AllowTcpForwarding yes');
52+
expect(result).toContain('MaxAuthTries 4');
53+
expect(result).toContain('MaxSessions 10');
54+
});
55+
56+
it('generates permissive preset', () => {
57+
const result = generateSshdConfig(getPreset('permissive'));
58+
expect(result).toContain('PermitRootLogin prohibit-password');
59+
expect(result).toContain('X11Forwarding yes');
60+
expect(result).toContain('AllowAgentForwarding yes');
61+
expect(result).toContain('GatewayPorts clientspecified');
62+
expect(result).toContain('PermitTunnel yes');
63+
expect(result).toContain('PrintMotd yes');
64+
});
65+
66+
it('includes custom port', () => {
67+
const config = { ...getParanoid(), port: 2222 };
68+
const result = generateSshdConfig(config);
69+
expect(result).toContain('Port 2222');
70+
});
71+
72+
it('includes listen addresses', () => {
73+
const config = { ...getParanoid(), listenAddress: '0.0.0.0 ::' };
74+
const result = generateSshdConfig(config);
75+
expect(result).toContain('ListenAddress 0.0.0.0');
76+
expect(result).toContain('ListenAddress ::');
77+
});
78+
79+
it('omits listen address when empty', () => {
80+
const result = generateSshdConfig(getParanoid());
81+
expect(result).not.toContain('ListenAddress');
82+
});
83+
84+
it('includes address family when not any', () => {
85+
const config = { ...getParanoid(), addressFamily: 'inet' as const };
86+
const result = generateSshdConfig(config);
87+
expect(result).toContain('AddressFamily inet');
88+
});
89+
90+
it('omits address family when any', () => {
91+
const result = generateSshdConfig(getParanoid());
92+
expect(result).not.toContain('AddressFamily');
93+
});
94+
95+
it('includes access control directives', () => {
96+
const config = {
97+
...getParanoid(),
98+
allowUsers: 'deploy admin',
99+
allowGroups: 'ssh-users',
100+
denyUsers: 'root',
101+
denyGroups: 'nogroup',
102+
};
103+
const result = generateSshdConfig(config);
104+
expect(result).toContain('AllowUsers deploy admin');
105+
expect(result).toContain('AllowGroups ssh-users');
106+
expect(result).toContain('DenyUsers root');
107+
expect(result).toContain('DenyGroups nogroup');
108+
expect(result).toContain('Controle de Acesso');
109+
});
110+
111+
it('omits access control section when empty', () => {
112+
const result = generateSshdConfig(getParanoid());
113+
expect(result).not.toContain('AllowUsers');
114+
expect(result).not.toContain('Controle de Acesso');
115+
});
116+
117+
it('includes banner when set', () => {
118+
const config = { ...getParanoid(), banner: '/etc/ssh/banner.txt' };
119+
const result = generateSshdConfig(config);
120+
expect(result).toContain('Banner /etc/ssh/banner.txt');
121+
});
122+
123+
it('omits Banner directive when empty', () => {
124+
const result = generateSshdConfig(getParanoid());
125+
expect(result).not.toMatch(/^Banner /m);
126+
});
127+
});
128+
129+
describe('calculateSecurityScore', () => {
130+
it('gives A grade to paranoid preset', () => {
131+
const { score, grade, warnings } = calculateSecurityScore(getParanoid());
132+
expect(grade).toBe('A');
133+
expect(score).toBeGreaterThanOrEqual(90);
134+
// Only info-level warnings for paranoid
135+
const dangerWarnings = warnings.filter((w) => w.severity === 'danger');
136+
expect(dangerWarnings).toHaveLength(0);
137+
});
138+
139+
it('penalizes root login yes', () => {
140+
const config = { ...getParanoid(), permitRootLogin: 'yes' as const };
141+
const { score, warnings } = calculateSecurityScore(config);
142+
expect(score).toBeLessThan(80);
143+
expect(warnings.some((w) => w.message.includes('Root login'))).toBe(true);
144+
});
145+
146+
it('penalizes password authentication', () => {
147+
const config = { ...getParanoid(), passwordAuthentication: true };
148+
const { warnings } = calculateSecurityScore(config);
149+
expect(warnings.some((w) => w.message.includes('senha') && w.severity === 'danger')).toBe(true);
150+
});
151+
152+
it('penalizes disabled pubkey auth', () => {
153+
const config = { ...getParanoid(), pubkeyAuthentication: false };
154+
const { score } = calculateSecurityScore(config);
155+
expect(score).toBeLessThan(75);
156+
});
157+
158+
it('penalizes empty passwords', () => {
159+
const config = { ...getParanoid(), permitEmptyPasswords: true };
160+
const { warnings } = calculateSecurityScore(config);
161+
expect(warnings.some((w) => w.message.includes('vazias'))).toBe(true);
162+
});
163+
164+
it('warns about x11 forwarding', () => {
165+
const config = { ...getParanoid(), x11Forwarding: true };
166+
const { warnings } = calculateSecurityScore(config);
167+
expect(warnings.some((w) => w.message.includes('X11'))).toBe(true);
168+
});
169+
170+
it('warns about gateway ports yes', () => {
171+
const config = { ...getParanoid(), gatewayPorts: 'yes' as const };
172+
const { warnings } = calculateSecurityScore(config);
173+
expect(warnings.some((w) => w.message.includes('GatewayPorts'))).toBe(true);
174+
});
175+
176+
it('gives F grade to worst case', () => {
177+
const config: SshdConfig = {
178+
...getParanoid(),
179+
permitRootLogin: 'yes',
180+
passwordAuthentication: true,
181+
pubkeyAuthentication: false,
182+
permitEmptyPasswords: true,
183+
x11Forwarding: true,
184+
allowAgentForwarding: true,
185+
gatewayPorts: 'yes',
186+
permitTunnel: true,
187+
permitUserEnvironment: true,
188+
maxAuthTries: 10,
189+
};
190+
const { grade } = calculateSecurityScore(config);
191+
expect(grade).toBe('F');
192+
});
193+
194+
it('gives balanced preset a good score', () => {
195+
const { grade } = calculateSecurityScore(getPreset('balanced'));
196+
expect(['A', 'B']).toContain(grade);
197+
});
198+
199+
it('gives permissive preset a lower score', () => {
200+
const { score } = calculateSecurityScore(getPreset('permissive'));
201+
expect(score).toBeLessThan(90);
202+
});
203+
});
204+
205+
describe('getPreset', () => {
206+
it('returns different configs per preset', () => {
207+
const paranoid = getPreset('paranoid');
208+
const balanced = getPreset('balanced');
209+
const permissive = getPreset('permissive');
210+
211+
expect(paranoid.allowTcpForwarding).toBe(false);
212+
expect(balanced.allowTcpForwarding).toBe(true);
213+
expect(permissive.x11Forwarding).toBe(true);
214+
});
215+
});
216+
217+
describe('generateApplyScript', () => {
218+
it('generates a bash script', () => {
219+
const script = generateApplyScript();
220+
expect(script).toContain('#!/bin/bash');
221+
expect(script).toContain('sshd -t');
222+
expect(script).toContain('sshd_config.bak');
223+
expect(script).toContain('systemctl');
224+
expect(script).toContain('launchctl');
225+
});
226+
});

src/lib/sshd-generator.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { SshdConfig } from './types';
2+
3+
function line(key: string, value: string | number | boolean): string {
4+
if (typeof value === 'boolean') {
5+
return `${key} ${value ? 'yes' : 'no'}`;
6+
}
7+
return `${key} ${value}`;
8+
}
9+
10+
export function generateSshdConfig(config: SshdConfig): string {
11+
const lines: string[] = [];
12+
13+
lines.push('# ============================================================');
14+
lines.push('# sshd_config — gerado por SSH Toolkit');
15+
lines.push('# https://sshtoolkit.otaviomiranda.com.br');
16+
lines.push('# ============================================================');
17+
lines.push('');
18+
19+
// Basic Authentication
20+
lines.push('# --- Autenticação ---');
21+
lines.push(line('Port', config.port));
22+
23+
if (config.listenAddress) {
24+
for (const addr of config.listenAddress.split(/[,\s]+/).filter(Boolean)) {
25+
lines.push(line('ListenAddress', addr));
26+
}
27+
}
28+
29+
if (config.addressFamily !== 'any') {
30+
lines.push(line('AddressFamily', config.addressFamily));
31+
}
32+
33+
lines.push(line('PermitRootLogin', config.permitRootLogin));
34+
lines.push(line('PubkeyAuthentication', config.pubkeyAuthentication));
35+
lines.push(line('PasswordAuthentication', config.passwordAuthentication));
36+
lines.push(line('KbdInteractiveAuthentication', config.kbdInteractiveAuthentication));
37+
lines.push(line('ChallengeResponseAuthentication', config.challengeResponseAuthentication));
38+
lines.push(line('PermitEmptyPasswords', config.permitEmptyPasswords));
39+
40+
if (config.authenticationMethods) {
41+
lines.push(line('AuthenticationMethods', config.authenticationMethods));
42+
}
43+
44+
lines.push(line('MaxAuthTries', config.maxAuthTries));
45+
lines.push(line('LoginGraceTime', config.loginGraceTime));
46+
lines.push(line('UsePAM', config.usePAM));
47+
48+
// Access Control
49+
if (config.allowUsers || config.allowGroups || config.denyUsers || config.denyGroups) {
50+
lines.push('');
51+
lines.push('# --- Controle de Acesso ---');
52+
if (config.allowUsers) lines.push(line('AllowUsers', config.allowUsers));
53+
if (config.allowGroups) lines.push(line('AllowGroups', config.allowGroups));
54+
if (config.denyUsers) lines.push(line('DenyUsers', config.denyUsers));
55+
if (config.denyGroups) lines.push(line('DenyGroups', config.denyGroups));
56+
}
57+
58+
// Security
59+
lines.push('');
60+
lines.push('# --- Segurança ---');
61+
lines.push(line('X11Forwarding', config.x11Forwarding));
62+
lines.push(line('AllowAgentForwarding', config.allowAgentForwarding));
63+
lines.push(line('AllowTcpForwarding', config.allowTcpForwarding));
64+
lines.push(line('AllowStreamLocalForwarding', config.allowStreamLocalForwarding));
65+
lines.push(line('GatewayPorts', config.gatewayPorts));
66+
lines.push(line('PermitTunnel', config.permitTunnel));
67+
lines.push(line('PermitUserEnvironment', config.permitUserEnvironment));
68+
lines.push(line('PermitUserRC', config.permitUserRC));
69+
lines.push(line('StrictModes', config.strictModes));
70+
71+
// Sessions & Limits
72+
lines.push('');
73+
lines.push('# --- Sessões e Limites ---');
74+
lines.push(line('MaxSessions', config.maxSessions));
75+
lines.push(line('MaxStartups', config.maxStartups));
76+
lines.push(line('ClientAliveInterval', config.clientAliveInterval));
77+
lines.push(line('ClientAliveCountMax', config.clientAliveCountMax));
78+
lines.push(line('UseDNS', config.useDNS));
79+
lines.push(line('LogLevel', config.logLevel));
80+
81+
// Banners
82+
lines.push('');
83+
lines.push('# --- Banners ---');
84+
lines.push(line('PrintMotd', config.printMotd));
85+
lines.push(line('PrintLastLog', config.printLastLog));
86+
87+
if (config.banner) {
88+
lines.push(line('Banner', config.banner));
89+
}
90+
91+
lines.push('');
92+
return lines.join('\n');
93+
}
94+
95+
export function generateApplyScript(): string {
96+
return `#!/bin/bash
97+
# Script para aplicar o sshd_config gerado
98+
# Execute com: sudo bash apply-sshd.sh
99+
100+
set -e
101+
102+
echo "Fazendo backup do sshd_config atual..."
103+
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
104+
105+
echo "Copiando novo sshd_config..."
106+
sudo cp sshd_config /etc/ssh/sshd_config
107+
108+
echo "Testando configuração..."
109+
sudo sshd -t
110+
111+
if [ $? -eq 0 ]; then
112+
echo "Configuração válida. Reiniciando sshd..."
113+
if command -v systemctl &> /dev/null; then
114+
sudo systemctl restart sshd
115+
elif command -v launchctl &> /dev/null; then
116+
sudo launchctl kickstart -k system/com.openssh.sshd
117+
else
118+
sudo service sshd restart
119+
fi
120+
echo "sshd reiniciado com sucesso!"
121+
else
122+
echo "ERRO: configuração inválida. Restaurando backup..."
123+
sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
124+
echo "Backup restaurado."
125+
exit 1
126+
fi`;
127+
}

0 commit comments

Comments
 (0)