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