#Two settings that guard SSH
Linux Server guards SSH in two ways.
- SSH brute-force detection and blocking: a source that keeps failing to log in is kept off the SSH ports for a while. On by default
- Limiting SSH to allowed sources: new SSH connections are limited to the sources in an allow-list. Off by default
Used well, both cut down attacks. Set wrong, they lock you out. In this module you trigger a ban and a lockout on purpose, inside a disposable VM.
The steps in this module change firewall rules. Walk through them in a VM before you touch a real host. The VM here is driven with tart exec, which does not use SSH, so you can keep working while SSH is blocked. On a real host, set up a way in that is not SSH, such as a console, first.
#Prepare the practice setup
To see a ban, you need a source from outside. This host's own addresses are never banned (per the notes in the example settings file). So you create a network namespace inside the VM and connect over SSH from another address.
These steps add a user and network settings (they change the machine). The password is a practice dummy.
sudo useradd -m -s /bin/bash labuser
echo 'labuser:Practice-Pass-123' | sudo chpasswd
sudo apt install -y sshpass
sudo systemctl start roamswitch-serverCreate the namespace outside with the addresses 10.99.0.2 and 10.99.0.3. The VM's side is 10.99.0.1.
sudo ip netns add outside
sudo ip link add veth-host type veth peer name veth-out
sudo ip link set veth-out netns outside
sudo ip addr add 10.99.0.1/24 dev veth-host
sudo ip link set veth-host up
sudo ip netns exec outside ip addr add 10.99.0.2/24 dev veth-out
sudo ip netns exec outside ip addr add 10.99.0.3/24 dev veth-out
sudo ip netns exec outside ip link set veth-out up
sudo ip netns exec outside ping -c1 -W2 10.99.0.1 | tail -21 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.087/0.087/0.087/0.000 msLook at the guard's state.
sudo roamswitch bans=====================================================
🛑 SSH brute-force guard
=====================================================
State : detects only (does not ban)
Reason : fail2ban already guards SSH, so the ban is left to it (ssh_guard_yield_to_fail2ban=true).
Log : journal
Ports : 22
The banning is left to fail2ban. The sources the SSH guard never bans (an open SSH session, a recent public-key login, ssh_guard_allow) are not passed on to fail2ban, so add your own addresses to fail2ban's ignoreip (/etc/fail2ban/jail.local) too. What fail2ban bans: sudo fail2ban-client status sshd
Nothing is banned right now.This VM has fail2ban guarding sshd. So RoamSwitch only detects and notifies, and leaves the ban to fail2ban. To see what fail2ban has banned, use sudo fail2ban-client status sshd, as the guidance line says, not roamswitch bans.
Read the list with sudo. Without it, the firewall tables cannot be read, and it prints "The ban list is readable by root only: sudo roamswitch bans" in place of the list.
To undo: sudo ip netns del outside and sudo userdel -r labuser.
When fail2ban (with an sshd jail) or CrowdSec guards sshd, the RoamSwitch guard only detects and notifies (ssh_guard_yield_to_fail2ban=true). With two tools writing rules, it is harder to tell which one stopped a source.
- You have a tuned fail2ban setup: keep it. When fail2ban bans an SSH source, RoamSwitch sends a notification that names it and gives the command to lift the ban. Add your own addresses to fail2ban's
ignoreiptoo - You want RoamSwitch to ban: stop fail2ban. You list and lift bans with
roamswitch bansandunban, and exempt your own addresses withssh_guard_allow
This module stops fail2ban so you can see the ban.
#Reproduce brute force and see the ban
Failures are counted from sshd's log (journald). By default, a source that fails 6 times (ssh_guard_max_failures) within 600 seconds (ssh_guard_window_secs) is banned for 3600 seconds (ssh_guard_ban_secs).
This stops fail2ban (it changes the machine). The way back is at the end of this exercise.
sudo systemctl stop fail2ban
sudo roamswitch server restart
sudo roamswitch bans | sed -n 5,7p State : detects and bans
Log : journal
Ports : 22Try 8 wrong passwords from each of three sources: 127.0.0.1, the VM's own address (192.168.64.34 on this VM; check it with ip -4 addr), and the outside source 10.99.0.2.
F="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1 -o LogLevel=ERROR -o ConnectTimeout=5"
for i in 1 2 3 4 5 6 7 8; do sshpass -p wrong-pass ssh $F labuser@127.0.0.1 true; done
for i in 1 2 3 4 5 6 7 8; do sshpass -p wrong-pass ssh $F labuser@192.168.64.34 true; done
for i in 1 2 3 4 5 6 7 8; do
sudo ip netns exec outside sshpass -p wrong-pass ssh $F -b 10.99.0.2 labuser@10.99.0.1 true; echo "try $i: exit $?"
donelabuser@10.99.0.1: Permission denied (publickey,password).
try 5: exit 255
try 6: exit 255
ssh: connect to host 10.99.0.1 port 22: Connection timed out
try 7: exit 255
ssh: connect to host 10.99.0.1 port 22: Connection timed out
try 8: exit 255The 6th try was cut before the refusal came back: the ban closes the connections this source had open to SSH. From the 7th try on, nothing reaches sshd. The packets are dropped with no refusal sent back, so the client times out.
sudo roamswitch bans=====================================================
🛑 SSH brute-force guard
=====================================================
State : detects and bans
Log : journal
Ports : 22
Source Kind Left Fails Earlier bans
10.99.0.2 SSH 59m49s 6 0
Lift: sudo roamswitch unban <IP> / --all
Recent records:
2026-11-12T02:23:34 SSH banned 10.99.0.2 (6)
2026-11-12T02:23:07 SSH exempt (never banned) 192.168.64.34 (6) reason: this host's own addressRead it like this.
- 10.99.0.2 is banned, with about 60 minutes left. Private addresses are banned too
- 192.168.64.34 (this host's own address) is "exempt (never banned)", with the reason "this host's own address". Six failures do not get it banned
- 127.0.0.1 was neither banned nor recorded
SSHin the Kind column marks a ban by the brute-force guard. Bans by the web access guard appear in the same list- The times are UTC.
02:23:34is 11:23:34 on 12 November in Japan time (JST)
A ban also sends a notification. Check the notification history.
sudo roamswitch notifications | grep -A8 "source banned" SSH brute force detected, source banned: 10.99.0.2
Host: ubuntu (IP: 192.168.64.34)
Source: 10.99.0.2
Failed logins: 6 within 10 minutes
User names tried: 1
Ban: 60 minutes (earlier bans of this source: 0)
New connections are dropped, and the 1 connection this source already had open to SSH was cut. An authenticated session is never cut.
If this was your own address, lift the ban with sudo roamswitch server unban 10.99.0.2 and add it to ssh_guard_allow in server.conf.
Timestamp: 2026-11-12 02:23:35"1 connection" is the number of connections that were open at the moment of the ban and were actually closed: here, the 6th try. Connections from tries that had already ended are not counted. With none open, the notice says the source had no connection open to SSH.
The history shows these notifications in the reader's language. Here LANG=en_US.UTF-8 was set, while the server's language stayed at its default.
The ban itself lives in the nftables table inet roamswitch_sshguard. Only new connections to the SSH ports are stopped.
To bring fail2ban back, run sudo systemctl start fail2ban, then sudo roamswitch server restart. The state in bans returns to "detects only". Right after it is back, fail2ban may read the recent failures still in the journal and ban the sources used in this lesson. Check with sudo fail2ban-client status sshd and lift them with sudo fail2ban-client set sshd unbanip <IP>.
After it was back, six failures from a source not used before got it banned by fail2ban, and RoamSwitch sent a "fail2ban banned an SSH source" notification. It includes the command to lift the ban.
Per the notes in the example settings file, these are never banned: the gateway, the DNS resolvers, this host's own addresses, whitelist_ips and ssh_guard_allow, a source holding an authenticated session, and a source that logged in with a public key within ssh_guard_trust_key_days (30 days by default).
This VM's gateway is 192.168.64.1 (see ip route). The outside source lives in a namespace because you need an address that none of these rules cover.
#Lift a ban and change the threshold
When your own source is banned, lift it with unban. Change the threshold and other values with roamswitch server config set.
These steps change settings (they change the machine). The way back is at the end of this exercise.
Lift the ban and log in with the right password.
sudo roamswitch unban 10.99.0.2
sudo ip netns exec outside sshpass -p Practice-Pass-123 ssh $F -b 10.99.0.2 labuser@10.99.0.1 'echo ok from $SSH_CLIENT' ✓ Lifted: 10.99.0.2
Its failure count was cleared too. The count of its earlier bans ("Earlier bans" in sudo roamswitch bans) is kept: if the same source is banned again, the ban is longer by that count (at most ssh_guard_ban_max_secs).
ok from 10.99.0.2 57359 22Logging in right after the unban did not raise a "login succeeded after brute force" notification, and no success line was added to the recent records in bans. unban clears the failure count; the count of earlier bans stays.
Set the threshold to 3. A value outside the range is not saved.
sudo roamswitch server config get ssh_guard_max_failures
sudo roamswitch server config set ssh_guard_max_failures 3
sudo roamswitch server config set ssh_guard_max_failures 06
✓ Server configuration updated and saved. (ssh_guard_max_failures = 3)
To apply: sudo systemctl restart roamswitch-server or sudo roamswitch server restart
Error: Must be between 2 and 1000: '0'Saving alone does not apply it. Four failures before the restart did not lead to a ban. Restart, then try from another source, 10.99.0.3.
sudo roamswitch server restart
for i in 1 2 3 4; do sudo ip netns exec outside sshpass -p wrong-pass ssh $F -b 10.99.0.3 labuser@10.99.0.1 true; done
sudo roamswitch bans Source Kind Left Fails Earlier bans
10.99.0.3 SSH 59m50s 3 0
Lift: sudo roamswitch unban <IP> / --all
Recent records:
2026-11-12T02:24:31 SSH banned 10.99.0.3 (3)Three failures were enough. When the same source is banned a second time, the ban is 4 times as long. In this VM, after an unban, the second ban of 10.99.0.3 was 240 minutes (4h00m left in bans, earlier bans: 1). The cap is ssh_guard_ban_max_secs (604800 seconds, 7 days, by default).
Exempt your own source from bans.
sudo roamswitch unban 10.99.0.3
sudo roamswitch server config set ssh_guard_allow 10.99.0.3
sudo roamswitch server restart
for i in 1 2 3 4 5 6 7; do sudo ip netns exec outside sshpass -p wrong-pass ssh $F -b 10.99.0.3 labuser@10.99.0.1 true; done
sudo roamswitch bans | tail -6 Recent records:
2026-11-12T02:25:08 SSH exempt (never banned) 10.99.0.3 (3) reason: it is in whitelist_ips or ssh_guard_allow
2026-11-12T02:24:51 SSH banned 10.99.0.3 (3)Seven failures only produced an "exempt (never banned)" record, with no ban. ssh_guard_allow takes addresses or CIDRs, separated by commas.
Put things back.
sudo roamswitch server config set ssh_guard_max_failures 6
sudo roamswitch server config set ssh_guard_allow ""
sudo roamswitch server restartThe keys are below. All of them live in /etc/roamswitch/server.conf, and a change needs a restart.
| Key | Default | Meaning |
|---|---|---|
ssh_guard_enabled | true | Run the guard |
ssh_guard_block_enabled | true | Ban (false = detect and notify only) |
ssh_guard_max_failures | 6 | Failures before a ban (2 to 1000) |
ssh_guard_window_secs | 600 | Time window for counting failures (seconds) |
ssh_guard_ban_secs | 3600 | Length of the first ban (seconds) |
ssh_guard_ban_max_secs | 604800 | Longest ban (seconds) |
ssh_guard_allow | empty | Addresses and CIDRs never banned |
ssh_guard_log_source | auto | Log to read (auto, journal or a file path) |
Keep ssh_guard_log_source at auto or journal. Per the notes, any local user who may use syslog can write to a log file, so a forged line there can get a third party banned. journald's sender fields cannot be forged.
#Limit SSH to listed sources
roamswitch ssh-allow limits new SSH connections to the sources in the allow-list (ssh_allow_only_sources). Loopback and this host's own addresses always pass. The daemon re-reads the settings every 30 seconds, so no restart is needed.
To keep you from locking yourself out, it is not turned on in two cases.
- The list is empty
- A source with an SSH session open right now is not in the list
These steps change settings (they change the machine). The way back is at the end of this exercise.
Start with an empty list. check changes nothing; it only tells whether turning it on would work.
sudo roamswitch ssh-allow status
sudo roamswitch ssh-allow check; echo "exit=$?"
sudo roamswitch ssh-allow enable=====================================================
SSH allowed sources
=====================================================
ssh_allow_only_enabled : false
ssh_allow_only_sources : -
daemon : off
✗ No allowed source is listed. Add one to ssh_allow_only_sources before turning this on.
exit=2
✗ No allowed source is listed. Add one to ssh_allow_only_sources before turning this on.
Nothing was turned on.Next, keep an SSH session open from 10.99.0.2. Treat it as "you, right now". Put only another address, 192.0.2.10, in the list.
sudo ip netns exec outside setsid sshpass -p Practice-Pass-123 ssh $F -b 10.99.0.2 -N labuser@10.99.0.1 </dev/null >/dev/null 2>&1 &
sudo roamswitch ssh-allow add 192.0.2.10
sudo roamswitch ssh-allow check
sudo roamswitch ssh-allow status | tail -3 ✓ Saved. (ssh_allow_only_sources = 192.0.2.10)
✗ These sources hold an SSH session now and are not in the list: 10.99.0.2. Add them, or end those sessions, before turning this on.
daemon : off
Sources with an SSH session now: 10.99.0.2
The refusal names the source left out, 10.99.0.2. Add your own source, and it can be turned on.
sudo roamswitch ssh-allow add 10.99.0.2
sudo roamswitch ssh-allow check
sudo roamswitch ssh-allow enable
sleep 30
sudo roamswitch ssh-allow status ✓ Saved. (ssh_allow_only_sources = 192.0.2.10,10.99.0.2)
✓ It can be turned on. 2 sources are listed, and every open SSH session is from one of them.
✓ Saved. (ssh_allow_only_enabled = true)
The daemon loads the rule within 30 seconds. `roamswitch ssh-allow status` shows the result.
Open connections stay up. To undo: sudo roamswitch ssh-allow disable
=====================================================
SSH allowed sources
=====================================================
ssh_allow_only_enabled : true
ssh_allow_only_sources : 192.0.2.10, 10.99.0.2
daemon : active — new SSH connections from anything but 2 listed sources are refused (ports [22])
Sources with an SSH session now: 10.99.0.2
In this VM the rule was in place 10 seconds after enable. Try the right password from four sources.
sudo ip netns exec outside sshpass -p Practice-Pass-123 ssh $F -b 10.99.0.3 labuser@10.99.0.1 'echo ok from $SSH_CLIENT'
sudo ip netns exec outside sshpass -p Practice-Pass-123 ssh $F -b 10.99.0.2 labuser@10.99.0.1 'echo ok from $SSH_CLIENT'
sshpass -p Practice-Pass-123 ssh $F labuser@127.0.0.1 'echo ok from $SSH_CLIENT'
sshpass -p Practice-Pass-123 ssh $F labuser@192.168.64.34 'echo ok from $SSH_CLIENT'ssh: connect to host 10.99.0.1 port 22: Connection timed out
ok from 10.99.0.2 59767 22
ok from 127.0.0.1 36432 22
ok from 192.168.64.34 59142 22Only 10.99.0.3, which is not listed, could not get in, even with the right password. The rule is in the nftables table inet roamswitch_sshallow.
Taking a connected source out of the list while the rule is on prints a warning.
sudo roamswitch ssh-allow remove 10.99.0.2 Warning: 10.99.0.2 holds an SSH session now. Taking it out of the list does not cut that session, but a new connection from it is refused once it ends.
✓ Saved. (ssh_allow_only_sources = 192.0.2.10)Put things back. disable removes the rule without waiting for the daemon.
sudo roamswitch ssh-allow add 10.99.0.2
sudo roamswitch ssh-allow disable
sudo nft list table inet roamswitch_sshallow
sudo roamswitch ssh-allow status | sed -n 7p ✓ Saved. (ssh_allow_only_sources = 192.0.2.10,10.99.0.2)
✓ Saved. (ssh_allow_only_enabled = false)
✓ The rule was removed.
Error: No such file or directory
list table inet roamswitch_sshallow
^^^^^^^^^^^^^^^^^^^
daemon : offThe table is gone, and status says "off" right after disable. To empty the list as well, take entries out one by one with ssh-allow remove.
The safety checks protect only the sessions open right now. A way in that is not connected at the moment is not seen. Before turning it on, write these down and add them.
- The source you work from now (the "Sources with an SSH session now" line of
ssh-allow status) - Where you will connect from tomorrow (home, office, VPN exit). If it is not fixed, use a range (CIDR)
- Jump hosts, and monitoring or backup servers
- An IPv6 entry if you connect over IPv6 (IPv4 and IPv6 are judged separately)
Then run check, turn it on, and, without closing your current session, test a new login from another terminal.
If you ssh-allow remove your own source, your current session goes on. Disconnect without noticing, and you cannot get back in. Removing it prints a warning, so read it. ssh_guard_allow and ssh_allow_only_sources are separate keys. A source in the allow-list is not exempt from the brute-force guard.
#Match ssh_ports to sshd's ports
ssh_ports (default 22) is the SSH port that the RoamSwitch firewall lets in. It stays open even after emergency isolation (per the notes in the example settings file). If sshd listens on another port, set it here too.
This changes sshd's settings (it changes the machine). The way back is at the end of this exercise.
Add port 2222 to sshd. Leave ssh_ports at 22.
printf 'Port 22\nPort 2222\n' | sudo tee /etc/ssh/sshd_config.d/10-port.conf
sudo systemctl daemon-reload && sudo systemctl restart ssh.socket
sudo roamswitch server restart
sudo roamswitch bans | grep Ports
sudo nft list chain inet roamswitch_server input | grep dport
sudo ip netns exec outside sshpass -p Practice-Pass-123 ssh $F -p 2222 -b 10.99.0.2 labuser@10.99.0.1 'echo ok port 2222' Ports : 22, 2222
tcp dport { 22, 80, 443 } accept
ssh: connect to host 10.99.0.1 port 2222: Connection timed outThe brute-force guard reads sshd's settings and watches 2222 as well. The rule that lets traffic in, however, covers only ssh_ports and allowed_ports (default 80,443). Connections to 2222 were dropped there.
roamswitch doctor warns about this mismatch.
sudo roamswitch doctor | grep 2222[warn] sshd also listens on port 2222, but ssh_ports in server.conf is 22. The firewall lets in only ssh_ports and allowed_ports, so port 2222 cannot be reached from outside. Anyone who logs in only through it is locked out. To fix: sudo roamswitch server config set ssh_ports 22,2222 then sudo roamswitch server restartThe warning appears while sshd actually listens on 2222. Without sudo, doctor cannot read server.conf, and this warning does not appear.
Match ssh_ports.
sudo roamswitch server config set ssh_ports 22,2222
sudo roamswitch server restart
sudo nft list chain inet roamswitch_server input | grep dport
sudo ip netns exec outside sshpass -p Practice-Pass-123 ssh $F -p 2222 -b 10.99.0.2 labuser@10.99.0.1 'echo ok port 2222' ✓ Server configuration updated and saved. (ssh_ports = 22,2222)
To apply: sudo systemctl restart roamswitch-server or sudo roamswitch server restart
tcp dport { 22, 80, 443, 2222 } accept
ok port 2222Once they match, doctor no longer shows this warning.
Put things back.
sudo rm /etc/ssh/sshd_config.d/10-port.conf
sudo systemctl daemon-reload && sudo systemctl restart ssh.socket
sudo roamswitch server config set ssh_ports 22
sudo roamswitch server restartMove sshd to a new port in this order. The other way round, the new port is dropped and you cannot get in.
- Put both the old and the new port in
ssh_ports, thensudo roamswitch server restart - Add the new port to sshd and, without closing your current session, check that you can log in on it
- Once that works, take the old port out of sshd and
ssh_ports, and restart again
#What you cannot see
In a disposable Ubuntu 24.04 VM, this is what was seen.
- The brute-force guard counts failures in sshd's log. Brute force against other services is not counted by this guard
- This host's own addresses and 127.0.0.1 are never banned. Trying from inside the same machine cannot reproduce a ban
- A new threshold takes effect only after a restart. A change to the allow-list takes effect within 30 seconds
- The allow-list's safety checks look only at sessions open now. Tomorrow's locations and jump hosts are yours to count and add
- A mismatch between
ssh_portsand sshd's ports shows up as a warning in doctor run withsudo. After changing ports, check withsudo roamswitch doctorandnft list chain inet roamswitch_server input
Eight failed logins with ssh labuser@127.0.0.1 from inside the VM led to no ban. Why
Show the explanation
From the outside source, 6 failures led to a ban. To keep you from locking yourself out, the gateway, the resolvers and this host's own addresses are never banned. To test, use a source with another address.
ssh-allow enable refused with "These sources hold an SSH session now and are not in the list: 10.99.0.2". What do you do
Show the explanation
It refused because turning it on as is would keep that source out from then on. If it is yours, add it to the list. If you do not know it, find out what that connection is before turning anything on.
#What to learn next
- What happens when a login succeeds after brute force is covered in "SSH brute force, and when you lock yourself out" (coming soon)
- Attacks on SSH and remote access as a whole are covered in "SSH and remote access" (coming soon)
- Every setting is in the Linux Server operations manual
Related techniques (ATT&CK): T1110.001
Verified on 2026-11-12. Target: Linux 1.17.39. On two disposable Ubuntu 24.04 VMs with the published 1.17.39, wrong passwords were repeated from other addresses in a network namespace to run, top to bottom and in Japanese and English, the ban, unban, the threshold, ssh_guard_allow, ssh-allow, the ssh_ports mismatch and the notification for a fail2ban ban after fail2ban was brought back.
Command output varies with the product version and the environment. If yours differs, check the version first.