Setup and configuration

Configuring SSH protection

Set brute-force thresholds and allowed sources, and avoid locking yourself out.

  • Level: Basics
  • About 35 min
  • 2026-11-12

#Two settings that guard SSH

Linux Server guards SSH in two ways.

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.

Practice in a disposable environment

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.

Create a test user and an outside source

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-server

Create 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 -2
Output
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.087/0.087/0.087/0.000 ms

Look at the guard's state.

sudo roamswitch bans
Output
=====================================================
  🛑 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.

Which one should ban: fail2ban or RoamSwitch

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 ignoreip too
  • You want RoamSwitch to ban: stop fail2ban. You list and lift bans with roamswitch bans and unban, and exempt your own addresses with ssh_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).

Log in again and again with a wrong password

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
Output
  State     : detects and bans
  Log       : journal
  Ports     : 22

Try 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 $?"
done
Excerpt (the outside source)
labuser@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 255

The 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
Output
=====================================================
  🛑 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 address

Read 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
  • SSH in 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:34 is 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"
Excerpt
  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.

Sources that are never banned

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.

unban, and a new threshold

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'
Output
  ✓ 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 22

Logging 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 0
Output
6
  ✓ 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
Excerpt
  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
Excerpt
  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 restart

The keys are below. All of them live in /etc/roamswitch/server.conf, and a change needs a restart.

KeyDefaultMeaning
ssh_guard_enabledtrueRun the guard
ssh_guard_block_enabledtrueBan (false = detect and notify only)
ssh_guard_max_failures6Failures before a ban (2 to 1000)
ssh_guard_window_secs600Time window for counting failures (seconds)
ssh_guard_ban_secs3600Length of the first ban (seconds)
ssh_guard_ban_max_secs604800Longest ban (seconds)
ssh_guard_allowemptyAddresses and CIDRs never banned
ssh_guard_log_sourceautoLog 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
Check the safety checks, then turn it on

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
Output
=====================================================
  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
Output
  ✓ 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
Output
  ✓ 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'
Output
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 22

Only 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
Output
  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
Output
  ✓ 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                  : off

The 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.

Before turning it on, count your ways in

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.

Taking your own source out of the list, then disconnecting

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.

What happens when only sshd gets a new port

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'
Output
  Ports     : 22, 2222
		tcp dport { 22, 80, 443 } accept
ssh: connect to host 10.99.0.1 port 2222: Connection timed out

The 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
Output
[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 restart

The 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'
Output
  ✓ 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 2222

Once 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 restart
The order for moving sshd to a new port

Move sshd to a new port in this order. The other way round, the new port is dropped and you cannot get in.

  1. Put both the old and the new port in ssh_ports, then sudo roamswitch server restart
  2. Add the new port to sshd and, without closing your current session, check that you can log in on it
  3. Once that works, take the old port out of sshd and ssh_ports, and restart again

#What you cannot see

What was checked, and what was not

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_ports and sshd's ports shows up as a warning in doctor run with sudo. After changing ports, check with sudo roamswitch doctor and nft list chain inet roamswitch_server input
Check yourself

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.

Check yourself

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
Features covered

Related techniques (ATT&CK): T1110.001

What was verified

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.