SSH brute-force detecteren en blokkeren: sshd-config, fail2ban, en wat fail2ban niet ziet
Hoe je in twee minuten ziet wie er op je SSH-poort inhamert, welke vijf sshd-regels 99 % van de aanvallen kansloos maken, en hoe je fail2ban op Ubuntu 24.04 correct instelt (de systemd-backend-valkuil). Daarna: de drie aanvalspatronen waar fail2ban blind voor is.
Inhoud
Zet een server met een publiek IP online en binnen een uur probeert iemand root met 123456. Dat is geen aanval op jou, het is het achtergrondgeluid van het internet. Het wordt pas een probleem als je wachtwoord-login toestaat, als een van je gebruikers een zwak wachtwoord heeft, of als je niet ziet dat het patroon verandert van "iedereen probeert root" naar "iemand probeert jouw gebruikersnamen". Dit artikel zet de detectie en de blokkade op met wat er al op de server staat.
Stap 1: kijk wie er nu aan de deur staat
Op Ubuntu 24.04 en Debian 12 logt sshd naar de journal. /var/log/auth.log bestaat alleen nog als rsyslog geïnstalleerd is — op cloud-images vaak niet.
# Mislukte logins vandaag, aantal
journalctl -u ssh --since today --no-pager | grep -c 'Failed password'
# Debian noemt de unit ssh.service ook; op sommige systemen sshd.service
journalctl -u sshd --since today --no-pager | grep -c 'Failed password'
# Top-10 bron-IP's
journalctl -u ssh --since today --no-pager | grep 'Failed password' \
| grep -oE 'from [0-9a-f.:]+' | sort | uniq -c | sort -rn | head
# Welke gebruikersnamen proberen ze? Onderscheid bestaand (Failed password for USER)
# van niet-bestaand (invalid user USER)
journalctl -u ssh --since today --no-pager | grep -oE 'invalid user [^ ]+' | sort | uniq -c | sort -rn | head
journalctl -u ssh --since today --no-pager | grep -E 'Failed password for [^i]' | grep -oE 'for [^ ]+' | sort | uniq -c | sort -rn | head
# En: wie is er WEL binnengekomen, vanwaar?
journalctl -u ssh --since "7 days ago" --no-pager | grep -E 'Accepted (publickey|password)' \
| awk '{print $1, $2, $3, $9, $11}' | sort -k4
Het laatste commando is het belangrijkste en het minst gedraaide. Duizend mislukte pogingen zijn ruis; één Accepted publickey for deploy from 185.x.x.x op een IP dat je niet kent, is een incident.
Stap 2: vijf sshd-regels die het spel veranderen
Alles in /etc/ssh/sshd_config.d/10-hardening.conf (Ubuntu en Debian lezen die map; laat het hoofdbestand met rust):
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
# 1. Geen wachtwoorden. Brute-force op keys is zinloos.
PasswordAuthentication no
KbdInteractiveAuthentication no
# 2. root logt nooit rechtstreeks in. sudo laat een spoor na, root niet.
PermitRootLogin no
# 3. Alleen deze accounts mogen via SSH binnen. Alles wat hier niet staat, bestaat niet voor sshd.
AllowUsers deploy jeroen
# 4. Minder pogingen, korter venster.
MaxAuthTries 3
LoginGraceTime 20
# 5. Geen ongebruikte kanalen.
X11Forwarding no
AllowAgentForwarding no
EOF
# Syntaxcheck VOOR je herlaadt — een fout hier sluit je buiten.
sudo sshd -t && sudo systemctl reload ssh
Hou je huidige sessie open en test in een tweede terminal of je nog binnenkomt. Pas daarna de eerste sluiten.
Wat er bewust niet in staat: een andere poort. Port 2222 vermindert het lawaai in je logs, niet de aanval. Scanners vinden de poort in seconden, en je verliest de mogelijkheid om SSH-verkeer op poort 22 te onderscheiden in firewalls en netflow. Doe het alleen voor de rust in je logs, en noem het geen security.
Stap 3: rate-limit op de firewall
Nog voor fail2ban: laat de kernel het grove werk doen. Met ufw:
sudo ufw limit 22/tcp comment 'ssh rate-limit'
# = max 6 nieuwe verbindingen per 30 s per bron-IP, daarna drop
Of direct in nftables, als je geen ufw gebruikt:
sudo nft add rule inet filter input tcp dport 22 ct state new limit rate over 6/minute burst 6 packets drop
Dit kost niets, heeft geen daemon nodig en stopt de domme, snelle brute-force volledig.
Stap 4: fail2ban — met de juiste backend
fail2ban leest logs, telt mislukkingen per IP en zet tijdelijk een firewall-regel. De valkuil op Ubuntu 24.04 en Debian 12: de standaardconfiguratie zoekt /var/log/auth.log, en dat bestand bestaat niet als rsyslog ontbreekt. Dan start de jail wel maar bant nooit. Forceer de systemd-backend.
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
backend = systemd
bantime = 1h
findtime = 10m
maxretry = 5
# Escalatie: wie na een ban terugkomt, blijft langer weg
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 1w
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8
[sshd]
enabled = true
mode = aggressive # telt ook "invalid user" en pre-auth disconnects
EOF
sudo systemctl enable --now fail2ban
# Werkt het?
sudo fail2ban-client status sshd
sudo fail2ban-client status sshd | grep 'Currently banned'
Als "Currently failed" na een dag nog op 0 staat terwijl journalctl -u ssh vol mislukkingen staat, is de backend fout. fail2ban-client get sshd logpath moet leeg zijn (systemd) of naar een bestaand bestand wijzen.
Handmatig bannen en ontbannen:
sudo fail2ban-client set sshd banip 203.0.113.7
sudo fail2ban-client set sshd unbanip 203.0.113.7
Stap 5: alert op wat ertoe doet
Mislukte logins hoeven je niet wakker te maken; fail2ban en ufw handelen die af. Deze drie dingen wel, en daar heb je een klein script voor nodig:
sudo tee /usr/local/sbin/ssh-watch.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
NTFY="https://ntfy.example.be/security"
HOST=$(hostname -s)
KNOWN=/etc/ssh-watch.known # één IP of prefix per regel die je vertrouwt
ALERTS=()
touch "$KNOWN"
# 1. Geslaagde login vanaf een onbekend IP (laatste 15 min)
while read -r user ip; do
grep -q "^${ip%.*}" "$KNOWN" || ALERTS+=("login $user from UNKNOWN $ip")
done < <(journalctl -u ssh --since "15 min ago" --no-pager | grep -E 'Accepted (publickey|password)' | awk '{print $9, $11}')
# 2. Pogingen op ECHTE gebruikersnamen (niet 'invalid user') — iemand kent je accounts
real=$(journalctl -u ssh --since "15 min ago" --no-pager | grep -E 'Failed password for [^i]' | wc -l)
[ "$real" -gt 20 ] && ALERTS+=("$real failed attempts on EXISTING users in 15 min")
# 3. Trage, gespreide brute-force: > 50 unieke bron-IP's in een uur, elk < 5 pogingen
uniq_ips=$(journalctl -u ssh --since "1 hour ago" --no-pager | grep -oE 'from [0-9a-f.:]+' | sort | uniq -c | awk '$1<5' | wc -l)
[ "$uniq_ips" -gt 50 ] && ALERTS+=("distributed: $uniq_ips low-rate source IPs in 1 h")
if [ ${#ALERTS[@]} -gt 0 ]; then
printf '%s\n' "${ALERTS[@]}" | curl -s -H "Title: ssh@$HOST" -H "Priority: urgent" --data-binary @- "$NTFY" >/dev/null
fi
EOF
sudo chmod 0755 /usr/local/sbin/ssh-watch.sh
echo '*/15 * * * * root /usr/local/sbin/ssh-watch.sh' | sudo tee /etc/cron.d/ssh-watch
Vul /etc/ssh-watch.known met de eerste drie octetten van je kantoor- en VPN-ranges. Ja, dat is grof. Het is ook binnen een uur werkend en vangt de alert die je écht wilt: iemand is binnen vanaf een plek die jij niet kent.
Valkuilen
- Jezelf buitensluiten. Altijd
sshd -tvóórreload, altijd een tweede sessie open, en bij cloud-VPS'en: weet waar de console in het paneel zit vóór je begint. - fail2ban bant je eigen kantoor. Een collega die drie keer een verkeerd key-bestand kiest, triggert
maxretry. Zet je eigen ranges inignoreip. - IPv6 vergeten.
ufw limit 22/tcpdekt v4 én v6; handgeschreven nftables-regels vaak niet. Controleer metss -tn state established '( sport = :22 )'of er v6-verbindingen zijn. AllowUsersen automation. Een backup- of deploy-account dat niet in de lijst staat, faalt stil. Voeg het toe, of gebruikAllowGroups ssh-usersen beheer de groep.- Journal-rotatie.
journalctl --since "7 days ago"werkt alleen als de journal zo lang bewaard wordt. ZetSystemMaxUse=enMaxRetentionSec=30dayinjournald.conf; anders zie je bij een incident niets ouder dan gisteren.
Wat je hiermee nog niet hebt
fail2ban en het script zien één host en één log. Drie patronen glippen erdoor:
- Geografie. Een geslaagde login van een IP dat je niet in
knownhebt staan, is een alert — maar "deze gebruiker heeft nog nooit vanuit dit land ingelogd" is een veel scherpere. Daarvoor heb je GeoIP nodig, en bij voorkeur zonder MaxMind-licentie (de RIR-bestanden van RIPE, ARIN, APNIC, AFRINIC en LACNIC zijn gratis). - Fleet-correlatie. Hetzelfde IP dat op vijf van je servers één poging doet, bant nergens en valt nergens op. Dat is precies hoe gespreide aanvallen werken.
- Wat er ná de login gebeurt. Een geslaagde SSH-login gevolgd door het lezen van
/root/.ssh/id_rsa.backupbinnen twee minuten is geen sysadmin, dat is een inbreker. Zie de how-to over honeypot-bestanden.
Zo doet monsys het
De monsys-agent doet de detectie op de host zelf — er gaat geen logregel naar buiten, alleen het signaal: ssh_bruteforce (per bron-IP en per gebruiker, met venster), invalid_user_enumeration, new_country_login (GeoIP via RIR-data, geen licentie) en geo_blocked_country. De hub correleert over alle hosts van een tenant, en de lateral movement-detectie koppelt een honeypot-trip aan een interne SSH-login binnen vijf minuten. Blokkeren blijft een beslissing van een mens, via een ondertekende Emergency Action Token — monsys grijpt nooit zelf in.
FAQ
Is het veilig om wachtwoord-login uit te zetten?
Ja, op voorwaarde dat je eerst je publieke sleutel in ~/.ssh/authorized_keys hebt gezet en getest hebt dat key-login werkt. Doe dat in een tweede sessie voor je de eerste sluit. Sinds PasswordAuthentication no is brute-force op wachtwoorden per definitie kansloos.
Waarom bant fail2ban niets op Ubuntu 24.04?
Meestal omdat /var/log/auth.log niet bestaat (geen rsyslog) en fail2ban standaard dat bestand leest. Zet backend = systemd in jail.local en herstart fail2ban. Controleer met fail2ban-client status sshd.
Helpt het om SSH op een andere poort te zetten?
Het vermindert het aantal logregels, niet het risico. Poortscanners vinden een alternatieve poort binnen seconden. Combineer het hooguit met key-only login en rate-limiting; op zichzelf is het geen beveiligingsmaatregel.
Geschreven door het monsys-team — sysadmins die dit dagelijks doen.
Zelf gedaan? Laat monsys het bijhouden.
Alles uit deze how-to draait in monsys als doorlopende check, met historie, alerts en audit-bewijs. 5 servers gratis, EU-gehost in België, geïnstalleerd in 60 seconden.