Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions modules/sshd/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ let
fail2ban = {
maxRetry = 3;
findTime = 600;
banTime = 86400;
aggressiveBanTime = 604800;
# Exponential backoff: ban doubles per re-offense from base to cap.
baseBanTime = 300;
maxBanTime = 604800;
};

rateLimiting = {
Expand Down Expand Up @@ -109,6 +110,16 @@ in
enable = true;
maxretry = fail2ban.maxRetry;

# overalljails counts offenses per-IP across jails, not per-filter.
bantime-increment = {
enable = true;
maxtime = toString fail2ban.maxBanTime;
rndtime = "60"; # jitter to avoid synchronized ban expiry
overalljails = true;
};

# Only ignoreIP truly whitelists: it is checked before banning, whereas the
# iptables ACCEPT sits below the f2b jump chain and cannot override a ban.
ignoreIP = [
"127.0.0.1/8"
"::1/128"
Expand All @@ -124,25 +135,37 @@ in
filter = "sshd";
maxretry = fail2ban.maxRetry;
findtime = fail2ban.findTime;
bantime = fail2ban.banTime;
bantime = fail2ban.baseBanTime;
backend = "systemd";
};
};

# maxretry 3 (not 1): one preauth disconnect from a legit client must not
# mean an instant ban. Backoff handles repeat abusers.
sshd-aggressive = {
settings = {
enabled = true;
inherit (ssh) port;
filter = "sshd[mode=aggressive]";
maxretry = 1;
findtime = fail2ban.banTime;
bantime = fail2ban.aggressiveBanTime;
maxretry = fail2ban.maxRetry;
findtime = fail2ban.findTime;
bantime = fail2ban.baseBanTime;
backend = "systemd";
};
};
};
};

# Spare clients that reach the auth phase then drop (e.g. an unconfirmed
# Secretive/Touch-ID prompt) from the aggressive jail. Anonymous floods lack
# the "authenticating user" qualifier, so they are still caught.
environment.etc."fail2ban/filter.d/sshd.local" = lib.mkIf isBastion {
text = ''
[Definition]
ignoreregex = ^(?:Connection closed|Disconnected) by authenticating user \S+ <HOST> port \d+(?: \[preauth\])?\s*$
'';
};

# ========== firewall ==========
# Bastion: SSH exposed to internet with rate limiting
# Non-bastion: SSH only via WireGuard (wg-admin)
Expand Down
Loading