Everybody does this once. A default-deny policy is applied, the rule permitting SSH is not in place yet, and the connection being used to type the command is dropped immediately.
The server is fine. It is simply unreachable, and the only way in is the provider's console.
Schedule a revert first
The single most useful habit, and it takes fifteen seconds.
echo "iptables -F; iptables -P INPUT ACCEPT" | at now + 5 minutes
Or with the firewall your distribution uses, whatever undoes the change. Then make the change.
If you lose the connection, the rules clear themselves five minutes later and you are back in. If everything works, cancel the scheduled job:
atq atrm <job-number>
This turns an irreversible mistake into a five-minute wait, and it is worth doing every single time regardless of how simple the change looks.
Keep a second session open
Open a second SSH connection before starting and leave it idle.
Established connections frequently survive a rule change that blocks new ones, so the second session gives you a way to undo the change even after the first stops responding.
It is not guaranteed (a rule that drops all traffic ends established connections too) which is why it complements the scheduled revert rather than replacing it.
Allow before deny, always
Order the rules so access is permitted before anything is refused:
iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -p tcp --dport 22 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -P INPUT DROP
The policy line comes last. The established-connections rule matters as much as the SSH rule, without it, replies to your own outbound requests are blocked and the machine cannot fetch updates.
With a higher-level tool the same principle applies:
ufw allow 22/tcp ufw allow 80,443/tcp ufw enable
Adding the rule before enabling, not after.
Do not forget the SSH port
If SSH is on a non-standard port, that is the number that must be permitted. Copying a rule that permits 22 onto a server listening on something else locks you out just as completely, and the rule looks correct.
ss -tlnp | grep sshd
Check rather than assume, particularly on a server somebody else configured.
Making rules survive a reboot
Rules applied with iptables directly are lost at reboot. That is occasionally a useful safety property and usually a surprise.
Save them deliberately once the configuration is confirmed working, and confirm it works before saving, so a reboot is your route back in place of the thing that makes the problem permanent.
A firewall that was never made permanent is also a common reason a server is unexpectedly exposed after a restart. For what should be running, see securing your VPS.
Both address families
Rules written for IPv4 do not apply to IPv6, and the two rule sets are separate.
A carefully built firewall with IPv6 wide open is common and invisible, because the machine works normally either way. If the server has IPv6, it needs the same rules again. Understanding IPv6 deals with checking.
Find the console before you need it
Every provider offers console access that reaches the machine without the network. That is exactly the situation a firewall mistake puts you in.
Locate it, and confirm you can log in, on an ordinary afternoon. Hunting for it while locked out: possibly needing a password reset for the provider's panel, is how a five-minute problem becomes an evening.
Diagnosing a server that will not boot goes over using it, and the same console is what recovers every other kind of unreachable machine.
Confirm the rules do what you think
A rule set that looks correct and behaves differently is common, and reading it back is not the same as testing it.
iptables -L INPUT -n -v --line-numbers nft list ruleset 2>/dev/null | head -40 ufw status numbered
The packet and byte counters in the first command are the useful part: a rule with zero packets has never matched, which either means nothing has tried or the rule is unreachable because an earlier one caught the traffic.
Zero counters on a rule you expect to be busy is the signal that ordering is wrong, and it is visible immediately rather than after somebody reports a problem.
Test from outside, not from the machine
Checking a port from the server itself tests the service, not the firewall.
nc -zv -w 3 203.0.113.10 22 80 443 nmap -Pn -p 22,80,443,3306 203.0.113.10 2>/dev/null | grep -E 'open|filtered'
Run these from another machine. The distinction between closed and filtered matters: closed means nothing is listening, filtered means the firewall dropped the packet; two different faults with two different fixes.
The port worth checking specifically is the database: if 3306 is reachable from outside, that is an exposure regardless of how good the password is. Managing MySQL and remote database access sets out closing it properly.
Rate limiting inside the firewall
Beyond permitting and refusing, the firewall can slow down a source that is trying repeatedly, which is cheaper than anything the application can do, because the request never reaches it.
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m recent --set --name SSH iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
Four new connections in a minute is generous for a person and restrictive for automated attempts.
Apply the same shape to any port that attracts repeated attempts. It complements rather than replaces the tools that watch authentication logs. Setting up fail2ban goes into those, which act on what happened rather than on the rate.
Log what you drop, briefly
A firewall that silently discards traffic gives you nothing to diagnose with.
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "DROP: " grep 'DROP: ' /var/log/messages | tail -20
The rate limit on the logging matters as much as the rule: an unlimited log rule under any real traffic fills the disk, which is a self-inflicted outage.
Enable it while diagnosing and turn it off afterwards, or keep it permanently at a low rate: enough to see a pattern, not enough to become a problem. For keeping it bounded, see managing logs and log rotation.
Know what the rules look like when they are correct
Saving a working rule set gives you something to compare against and something to restore, which is more useful than remembering what you did.
iptables-save > /root/fw-$(date +%F).rules nft list ruleset > /root/nft-$(date +%F).rules 2>/dev/null diff /root/fw-known-good.rules <(iptables-save) | head -20
Take the copy when everything works, not when something is wrong. The diff then answers the only question that matters during an incident, which is what changed.
Restoring from that file is also faster and less error prone than reconstructing rules by hand at the point where you are already locked out and working through a console.
Rules are not the only thing that blocks
A connection refused with a correct firewall is a common and confusing state, because several other layers can be responsible.
iptables -L INPUT -n -v | head -5 systemctl is-active fail2ban 2>/dev/null && fail2ban-client status 2>/dev/null | tail -3 grep -c 'DENY' /var/log/messages 2>/dev/null ss -tlnp | grep ':22'
An automated blocking tool, a panel level protection service, a provider side network filter, or the service simply not listening all produce the same symptom.
Work outwards. Confirm the service is listening, then that local rules permit it, then that no automated tool has banned the address, then that the provider is not filtering. Changing firewall rules to fix a problem that is one of the other three is how a working configuration gets broken while chasing something else.