fix(hardening): fail2ban — pin the sshd jail to the systemd backend

Verified live against a real Debian 12: the hardening actions work end-to-end
(root + password self-lockout guards refuse correctly, auto-updates and ufw enable,
disabling password auth persists and blocks password logins). One gap surfaced: on
a minimal image without rsyslog the distro-default fail2ban sshd jail (logpath
/var/log/auth.log) fails to start — "Have not found any log file". Pin the sshd jail
to `backend = systemd` (reads the journal) via a jail.d drop-in on enable:
log-source-agnostic and valid on every supported (systemd) target. fail2ban then
comes up active with the sshd jail reading the journal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-25 01:14:06 +02:00
parent d56bdadd14
commit 940c4c80fa
2 changed files with 30 additions and 1 deletions

View File

@ -238,8 +238,15 @@ class HardeningService
return match ($action) {
'ssh_root' => $this->sshDropInScript('PermitRootLogin '.($enable ? 'yes' : 'no')),
'ssh_password' => $this->sshDropInScript('PasswordAuthentication '.($enable ? 'yes' : 'no')),
// Pin the sshd jail to the SYSTEMD backend (reads the journal) instead of the distro
// default /var/log/auth.log — which is absent on minimal/container images without
// rsyslog, where fail2ban would otherwise fail to start. All supported targets are
// systemd, and `apt/dnf install fail2ban` pulls the systemd binding via Recommends.
'fail2ban' => $enable
? $pm->ensureInstalledScript('fail2ban').' && systemctl enable --now fail2ban'
? $pm->ensureInstalledScript('fail2ban')
.' && mkdir -p /etc/fail2ban/jail.d'
.' && printf \'[sshd]\nbackend = systemd\nenabled = true\n\' > /etc/fail2ban/jail.d/00-clusev-sshd.local'
.' && systemctl enable --now fail2ban'
: 'systemctl disable --now fail2ban',
'unattended' => $this->autoUpdateScript($os, $pm, $enable),
default => throw new InvalidArgumentException(__('backend.unknown_hardening', ['action' => $action])),

View File

@ -113,6 +113,28 @@ class HardeningServiceTest extends TestCase
$this->assertSame(__('backend.ssh_root_self_lockout'), $res['output']);
}
public function test_fail2ban_enable_pins_the_sshd_jail_to_the_systemd_backend(): void
{
$detector = Mockery::mock(OsDetector::class);
$detector->shouldReceive('detect')->andReturn($this->debianProfile());
$captured = '';
$fleet = Mockery::mock(FleetService::class);
$fleet->shouldReceive('runPrivileged')->once()
->andReturnUsing(function ($s, $cmd) use (&$captured) {
$captured = $cmd;
return ['ok' => true, 'output' => ''];
});
$service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
$service->apply(new Server, 'fail2ban', true);
// The default /var/log/auth.log backend fails on minimal images — pin systemd (journal).
$this->assertStringContainsString('backend = systemd', $captured);
$this->assertStringContainsString('/etc/fail2ban/jail.d/00-clusev-sshd.local', $captured);
}
public function test_disabling_root_login_proceeds_for_a_non_root_credential(): void
{
$detector = Mockery::mock(OsDetector::class);