Skip to content

SSH Key Authentication and Brute-Force Prevention

SSH Key Authentication and Brute-Force Prevention

Section titled “SSH Key Authentication and Brute-Force Prevention”

Replacing SSH password authentication with public key auth removes the most common attack vector against RouterOS management. Combining key auth with firewall-based brute-force detection and optional port knocking provides layered protection for internet-exposed routers.

  • RouterOS 7.x (RSA and Ed25519 key support confirmed)
  • SSH service enabled: /ip service print shows ssh is active
  • A second terminal session open during configuration — do not lock yourself out

Generate the key pair on your client machine, not on the router. Ed25519 is preferred; use RSA 4096 if your client requires compatibility with older software.

Linux / macOS:

Terminal window
# Ed25519 (preferred)
ssh-keygen -t ed25519 -f ~/.ssh/id_mikrotik
# RSA 4096 (compatibility fallback)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_mikrotik_rsa

Windows (PowerShell with OpenSSH):

Terminal window
# Ed25519 (preferred)
ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\id_mikrotik"
# RSA 4096
ssh-keygen -t rsa -b 4096 -f "$env:USERPROFILE\.ssh\id_mikrotik_rsa"

This produces two files:

  • id_mikrotik — private key (keep secret, never copy to router)
  • id_mikrotik.pub — public key (upload to router)

Supported formats for import: OpenSSH, PEM, PKCS#8.


Step 2 — Upload the Public Key to the Router

Section titled “Step 2 — Upload the Public Key to the Router”

The public key file must be present in router storage before it can be imported.

Option A — SCP (from client):

Terminal window
scp ~/.ssh/id_mikrotik.pub [email protected]:/

Option B — Winbox:

Drag and drop id_mikrotik.pub into the Files panel.

Option C — FTP:

Terminal window
ftp 192.168.88.1
put ~/.ssh/id_mikrotik.pub

Once the file is on the router, associate it with the target user account:

/user ssh-keys import user=admin public-key-file=id_mikrotik.pub

Verify the key was imported:

/user ssh-keys print

Expected output shows the key owner and associated user:

Columns: USER, KEY-OWNER
USER KEY-OWNER
admin user@hostname

Before disabling password login, open a second session and confirm key auth works:

Terminal window
ssh -i ~/.ssh/id_mikrotik [email protected]

Explicitly force password auth to confirm it is still active (before disabling):

Terminal window
ssh -o PreferredAuthentications=password [email protected]

Only proceed to the next step after the key login succeeds in the test session.


Step 5 — Disable SSH Password Authentication

Section titled “Step 5 — Disable SSH Password Authentication”

With key auth confirmed, disable password-based SSH login at the service level:

/ip ssh set always-allow-password-login=no

Verify the setting:

/ip ssh print

Look for always-allow-password-login: no.


Step 6 — Firewall Rules to Block Brute-Force Attempts

Section titled “Step 6 — Firewall Rules to Block Brute-Force Attempts”

Even with password auth disabled, connection noise from brute-force scanners wastes CPU. The staged address-list method escalates repeat offenders into a blacklist that auto-expires after 1 day.

Rule ordering is critical — add rules in this order so blacklist drops fire before escalation rules:

/ip firewall filter
# Optional: whitelist trusted admin IPs (bypasses all SSH rate limiting)
add chain=input action=accept protocol=tcp dst-port=22 \
src-address-list=ssh-trusted \
comment="Allow trusted SSH admins"
# Drop IPs already on the blacklist
add chain=input action=drop protocol=tcp dst-port=22 \
src-address-list=ssh-blacklist \
comment="Drop blacklisted SSH sources"
# Escalation: stage 3 -> blacklist (1 day)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new src-address-list=ssh-stage3 \
address-list=ssh-blacklist address-list-timeout=1d \
comment="SSH stage 3 -> blacklist"
# Escalation: stage 2 -> stage 3 (1 hour)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new src-address-list=ssh-stage2 \
address-list=ssh-stage3 address-list-timeout=1h \
comment="SSH stage 2 -> stage 3"
# Escalation: stage 1 -> stage 2 (15 minutes)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new src-address-list=ssh-stage1 \
address-list=ssh-stage2 address-list-timeout=15m \
comment="SSH stage 1 -> stage 2"
# First new connection attempt -> stage 1 (5 minutes)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new \
address-list=ssh-stage1 address-list-timeout=5m \
comment="SSH new connection -> stage 1"

How it works: after 4 new connection attempts (regardless of whether auth succeeds or fails), the source IP lands in ssh-blacklist and is dropped for 24 hours. Each stage list auto-expires, so an attacker slowing their rate below the timeout threshold resets to stage 1.

This technique can be abused for DoS: an attacker can forge your admin IP as source to get it blacklisted. Always add your management IPs to ssh-trusted first using the accept rule above.

Add trusted admin IPs:

/ip firewall address-list
add list=ssh-trusted address=192.168.1.0/24 comment="Internal management network"
add list=ssh-trusted address=203.0.113.10 comment="Admin home IP"

Monitor the blacklist:

/ip firewall address-list print where list=ssh-blacklist

Manually unblock an IP:

/ip firewall address-list remove [find list=ssh-blacklist address=203.0.113.50]

Port knocking hides SSH entirely from scanners. The router only accepts SSH from source IPs that have sent TCP packets to a specific sequence of ports within defined time windows. Hosts that never knock cannot reach port 22.

/ip firewall filter
# Allow SSH only from the "secured" list
add chain=input action=accept protocol=tcp dst-port=22 \
src-address-list=ssh-secured \
comment="SSH: allow after successful knock"
# Drop SSH from everyone else
add chain=input action=drop protocol=tcp dst-port=22 \
comment="SSH: drop without knock"
# Knock sequence rules (place BEFORE the drop above, or in a pass-through position)
# Knock 1: TCP/1001 -> enter knock-stage1 (30s window)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=1001 \
address-list=knock-stage1 address-list-timeout=30s \
comment="Knock 1"
# Knock 2: TCP/2002 while in knock-stage1 -> enter knock-stage2 (30s window)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=2002 \
src-address-list=knock-stage1 \
address-list=knock-stage2 address-list-timeout=30s \
comment="Knock 2"
# Knock 3: TCP/3003 while in knock-stage2 -> enter secured (15 min)
add chain=input action=add-src-to-address-list protocol=tcp dst-port=3003 \
src-address-list=knock-stage2 \
address-list=ssh-secured address-list-timeout=15m \
comment="Knock 3 -> secured"

Send the knock sequence from the client:

Terminal window
# Linux/macOS — use nc (netcat) or nmap
nc -z 203.0.113.1 1001
nc -z 203.0.113.1 2002
nc -z 203.0.113.1 3003
ssh -i ~/.ssh/id_mikrotik [email protected]

Windows (PowerShell):

Terminal window
function Knock($ip, $port) {
$t = New-Object System.Net.Sockets.TcpClient
try { $t.Connect($ip, $port) } catch {}
$t.Close()
}
Knock "203.0.113.1" 1001
Knock "203.0.113.1" 2002
Knock "203.0.113.1" 3003
ssh -i "$env:USERPROFILE\.ssh\id_mikrotik" [email protected]

For a fully hardened SSH setup combining all steps:

# 1. Import key and disable password auth
/user ssh-keys import user=admin public-key-file=id_mikrotik.pub
/ip ssh set always-allow-password-login=no
# 2. Allow only trusted management IPs (adjust subnets/IPs)
/ip firewall address-list
add list=ssh-trusted address=192.168.1.0/24 comment="Management LAN"
# 3. Staged brute-force blacklist (input chain)
/ip firewall filter
add chain=input action=accept protocol=tcp dst-port=22 \
src-address-list=ssh-trusted comment="Trusted SSH admins"
add chain=input action=drop protocol=tcp dst-port=22 \
src-address-list=ssh-blacklist comment="Drop blacklisted"
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new src-address-list=ssh-stage3 \
address-list=ssh-blacklist address-list-timeout=1d comment="Stage 3 -> blacklist"
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new src-address-list=ssh-stage2 \
address-list=ssh-stage3 address-list-timeout=1h comment="Stage 2 -> stage 3"
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new src-address-list=ssh-stage1 \
address-list=ssh-stage2 address-list-timeout=15m comment="Stage 1 -> stage 2"
add chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \
connection-state=new address-list=ssh-stage1 \
address-list-timeout=5m comment="New SSH -> stage 1"

Key login fails with “Permission denied (publickey)”

Section titled “Key login fails with “Permission denied (publickey)””
  1. Verify the key was imported for the correct user:
    /user ssh-keys print
  2. Check the client is using the right private key:
    Terminal window
    ssh -i ~/.ssh/id_mikrotik -v [email protected]
  3. Confirm the public key file was uploaded intact — re-upload and re-import if in doubt.

Recover via serial console or Winbox (Winbox uses its own auth path):

/ip ssh set always-allow-password-login=yes

Then re-import a working key and retry.

Legitimate IP blocked by brute-force rules

Section titled “Legitimate IP blocked by brute-force rules”

Remove from blacklist manually:

/ip firewall address-list print where list=ssh-blacklist
/ip firewall address-list remove [find list=ssh-blacklist address=<ip>]

Then add the IP to ssh-trusted to prevent future blocking.

  1. Check the address-list entries are being populated:
    /ip firewall address-list print where list~"knock"
  2. Verify the knock rules fire before the SSH drop rule in the input chain:
    /ip firewall filter print chain=input
  3. Confirm all knocks arrive within the timeout window — use a scripted knock rather than manual commands.