← Back to Wiki
Security / Networking

Restricting SSH to Your Own IP Is How You Lock Yourself Out

Allowlisting SSH to your own public IP is good advice. It is also the single easiest way to lose a remote host permanently, because the address you think you connect from and the address you actually connect from are two different facts. On one host here, 17 of 17 successful logins had arrived from a jump host and not one from the home IP the rule was about to be written for. Writing that rule would have locked the box out completely, with no console and no way back.

Share on X

The check nobody does first

One command. Run it before you touch a firewall:

sudo grep "Accepted" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn

That prints every source address that has ever successfully logged in, with a count. On a host you have run for a while it is a complete map of your real access paths. Compare it against the address you were about to allowlist.

If your distro uses the journal rather than a file:

sudo journalctl -u ssh --no-pager | grep "Accepted" | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn
BE WARNED: the answer is often not the address you expect. A jump host, a VPN exit, a tunnel, a CI runner, a phone on cellular. Any of those puts a different source address on the connection. On this host, direct connections from home were blocked upstream when it was built, so the documented access path had quietly become a ProxyJump through an unrelated relay. Everything worked. Nobody had reason to notice which address that produced, right up until it was about to become the deciding factor in a firewall rule.

Allow every path you actually use, and say which one is load-bearing

The fix is not to pick the right single address. It is to allow all of them, and to write down which one you cannot afford to lose:

# the path with a proven track record in the log
sudo ufw allow from <jump-host-ip> to any port 22 proto tcp \
  comment 'LOAD-BEARING: every real login arrives here, do not remove'

# the path you believe you use
sudo ufw allow from <home-ip> to any port 22 proto tcp \
  comment 'home egress, may change with ISP'

The comment is not decoration. Six months from now, a tidy-up pass looks at two rules for the same port and deletes the one that looks redundant. The comment is what stops that being the one keeping you in.

Add before you delete, always

Never replace an access rule in one action. Add the new rule, verify it carries a real connection, and only then remove the old one. That ordering costs you one extra minute and removes the entire class of accident where the new rule has a typo in it.

The same applies to the default policy. Turn ufw on with the allow rules already in place, not before.

Verify the block from somewhere that is not allowlisted

A rule you have not tested from the outside is a guess. Get a shell somewhere else and try:

# from a non-allowlisted host. Should hang and time out, not refuse
timeout 5 bash -c "echo > /dev/tcp/<host>/22" && echo REACHED || echo BLOCKED

# and confirm you did not take the service down with it
curl -s -o /dev/null -w '%{http_code}\n' https://<host>/

A silent timeout is the correct result for a dropped packet. An immediate connection refused means something is answering and rejecting, which is a different configuration than you think you have. The second command matters just as much. It proves you restricted SSH and not everything.

The ISP address will change, and it will look like something else

A residential IP is not stable. When it moves, this rule fails, and it fails at the same moment as every other place you hardcoded that address. The symptoms arrive together and look unrelated to each other, which is what makes it confusing rather than obvious.

Two things make that survivable. Keep one allowlist entry that an ISP change cannot affect, which is what the jump-host rule is doing above. And stop hardcoding the address in more places. There is a longer write-up of that second half in one source of truth for a dynamic home IP.

When this isn't your problem

The one-line version: grep Accepted out of your auth log before you allowlist anything, because the source address of your real logins is frequently not the one you would have guessed. Allow every path you use, comment the load-bearing one, add before you delete, and test the block from a host that is not on the list.