To block outbound traffic from an Ubuntu server to one IP address, insert a REJECT rule at the top of the OUTPUT chain: sudo iptables -I OUTPUT 1 -d 203.0.113.10 -j REJECT. The server can no longer open connections to that address, and applications get an immediate error instead of hanging. Check it with sudo iptables -L OUTPUT -n -v --line-numbers, remove it with sudo iptables -D OUTPUT -d 203.0.113.10 -j REJECT, and save it if it has to survive a reboot. The rest of this guide covers the choices that trip people up: REJECT or DROP, append or insert, IPv6, ufw, nftables and persistence.
This comes straight from my own runbook for hosting support work. The commands are the ones I keep there; the explanations are the parts I wish every copy-pasted firewall answer included.
When you need to block outbound traffic
Most firewall guides are about keeping traffic out. Outbound blocking is the other direction: stopping the server itself from talking to something. The usual reasons:
- Containment during a malware cleanup. Infected sites often phone home to a command-and-control address. Blocking that address buys you time to clean without the site pulling fresh payloads. I wrote about one of these families in the Steam profile malware that targets WordPress sites.
- A misbehaving integration. A plugin or script keeps hammering a third-party API, or an endpoint it should never reach, and you need it to stop now while the real fix is made in code.
- Testing failure handling. You want to see what an application does when a dependency is unreachable, without touching the dependency.
In all three cases you want something fast, precise (one destination, nothing else) and easy to undo.
Block the IP with iptables
On most Ubuntu servers, iptables is the quickest tool. On current releases the iptables command is the nf_tables backend (iptables -V prints something like iptables v1.8.10 (nf_tables) on Ubuntu 24.04), but the syntax is the same.
The simplest form of the rule is:
sudo iptables -A OUTPUT -d 203.0.113.10 -j REJECT
Replace 203.0.113.10 with the address you want to block. -A OUTPUT appends the rule to the OUTPUT chain, which handles packets the server itself generates. -d matches the destination address, and -j REJECT sends back an error for every matching packet.
Insert, don’t just append
iptables checks rules in order and stops at the first rule that decides a packet’s fate. -A puts your rule at the end of the chain. If an earlier rule already accepts the traffic, your REJECT never runs. That is common on servers where a firewall manager has added its own chains, or where there is an early rule accepting established connections.
So on any server that already has firewall rules, insert the block at the top instead:
sudo iptables -I OUTPUT 1 -d 203.0.113.10 -j REJECT
-I OUTPUT 1 places it at position 1, the head of the chain. A side effect worth knowing: at the top, the rule also catches packets on connections that were already open to that address, so a long-lived connection to the blocked IP breaks too. That is usually what you want during containment.
To block a whole range, use CIDR notation: -d 203.0.113.0/24.
REJECT or DROP?
When I need to block a server’s outbound traffic, I use REJECT, not DROP. With REJECT, the kernel answers the connection attempt with an ICMP error (port unreachable by default for IPv4), so the application fails straight away. PHP, curl or whatever made the call logs an error, and the request that triggered it finishes quickly.
DROP discards the packet silently:
sudo iptables -I OUTPUT 1 -d 203.0.113.10 -j DROP
For a TCP connection, the application keeps retrying until its own timeout expires. On a web server that means PHP workers stuck waiting on a destination that will never answer, which can turn one blocked IP into a slow site. Silent dropping makes sense for inbound traffic from attackers, where you don’t want to confirm anything exists. For traffic your own server starts, a fast, visible failure is kinder to the application and easier to debug.
Verify the rule
List the OUTPUT chain with rule numbers and packet counters:
sudo iptables -L OUTPUT -n -v --line-numbers
You should see the destination IP with target REJECT. -n stops iptables from doing reverse DNS lookups on every address (which is slow and can hang if DNS is part of the problem), and -v shows the pkts and bytes counters. Those counters are the useful part: if they climb, something on the server is still trying to reach that address, which tells you the block is doing work.
Then test it the way the application would:
curl --connect-timeout 5 http://203.0.113.10
With REJECT, curl fails immediately. With DROP, it waits the full five seconds and then times out. If curl connects successfully, the rule is not matching, and the first thing to check is its position in the chain.
To test for a rule in a script without listing everything, use -C (check), which uses the same matching logic as delete but changes nothing:
sudo iptables -C OUTPUT -d 203.0.113.10 -j REJECT && echo "blocked"
It exits 0 if the rule exists, which also makes it easy to avoid adding the same rule twice.
Remove the block
Delete by repeating the rule with -D in place of -A or -I:
# REJECT rule
sudo iptables -D OUTPUT -d 203.0.113.10 -j REJECT
# DROP rule
sudo iptables -D OUTPUT -d 203.0.113.10 -j DROP
The rule specification has to match exactly. If you added the rule with extra options (a protocol, a port), include them in the delete too. The alternative is to delete by position:
sudo iptables -L OUTPUT -n --line-numbers
sudo iptables -D OUTPUT <rule-number>
Deleting by number is convenient but easy to get wrong: numbers shift whenever a rule above yours is added or removed, so always list immediately before deleting, and never reuse a number from earlier in your terminal history.
Don’t forget IPv6
iptables rules only apply to IPv4. If the destination also has an IPv6 address and the server has IPv6 connectivity, applications will happily connect over IPv6 and walk straight past your block. Add the matching rule with ip6tables:
sudo ip6tables -I OUTPUT 1 -d 2001:db8::10 -j REJECT
This matters most when you are blocking a hostname’s addresses rather than a single raw IP. Look up both record types (dig +short A example.com and dig +short AAAA example.com) and block each one.
On the subject of hostnames: iptables will accept a hostname in -d, but the iptables man page is explicit that hostnames are resolved once, before the rule is submitted to the kernel, and it calls using a name that needs a remote DNS query “a really bad idea”. If the name later points somewhere else, your rule keeps blocking the old address. Block IPs, and re-check them if the destination moves.
Make the rule persistent
Rules added with the iptables command live in the kernel only. A reboot wipes them. On Ubuntu, the iptables-persistent package saves them and restores them at boot:
sudo apt install iptables-persistent
sudo netfilter-persistent save
During installation it asks whether to save the current IPv4 and IPv6 rules. netfilter-persistent save writes the live rules to /etc/iptables/rules.v4 and /etc/iptables/rules.v6. To confirm the block was actually saved, check the file, not the running ruleset:
sudo grep 203.0.113.10 /etc/iptables/rules.v4
sudo iptables-save is still useful, but it prints the rules currently loaded in the kernel, not what will come back after a reboot.
If you later remove the block, run sudo netfilter-persistent save again, or the rule returns on the next boot.
If the server uses ufw, stop here
This is the trap. On Ubuntu 24.04 the ufw package declares that it breaks iptables-persistent and netfilter-persistent, so installing iptables-persistent on a ufw-managed server removes ufw. Check first:
sudo ufw status
If it says Status: active, let ufw own the rule. It stores its rules itself and restores them at boot:
sudo ufw reject out to 203.0.113.10
ufw turns that into -A ufw-user-output -d 203.0.113.10 -j REJECT in its own user chain. ufw also evaluates its rules in order, so if you already have allow out rules that could match this address, use sudo ufw insert 1 reject out to 203.0.113.10 to put the block first. Remove it later with sudo ufw delete reject out to 203.0.113.10. A raw iptables rule added next to ufw is not saved by ufw and disappears at the next reboot, so on a ufw server, stay inside ufw.
The nftables equivalent
Some servers manage their firewall directly with nftables and a config file, with no iptables layer. Look at what exists before adding anything:
sudo nft list ruleset
If there is an inet filter table with an output chain, insert the block at the top of it:
sudo nft insert rule inet filter output ip daddr 203.0.113.10 reject
Do not assume that table and chain exist on every server; table and chain names are whatever the administrator chose, so use the names from the ruleset you just listed. In an inet table you can block IPv6 in the same chain with ip6 daddr 2001:db8::10 reject.
Verify, and show rule handles so you can remove it later:
sudo nft -a list chain inet filter output
sudo nft delete rule inet filter output handle <handle>
Rules added with nft on the command line are not persistent either. On Ubuntu the nftables service loads /etc/nftables.conf at boot, so add the rule to that file as well, then check the file’s syntax with sudo nft -c -f /etc/nftables.conf before you reload anything.
Outbound is not inbound
An OUTPUT rule blocks connections from your server to the destination. It does not stop that address from connecting to your server. If you also want to refuse inbound traffic from it, that is the INPUT chain and a source match:
sudo iptables -A INPUT -s 203.0.113.10 -j DROP
Here DROP is the right choice: an attacker gets no confirmation that anything is listening. The same insert-versus-append logic applies, so use -I INPUT 1 if the chain already has accept rules. Blocking inbound at the edge (a CDN or cloud firewall) is often better still, because the traffic never reaches the server.
Where AI fits in
The hard part of containment is rarely typing the iptables rule; it is finding the address worth blocking. Malware usually hides its callback address in obfuscated PHP. That is where AI-assisted triage earns its place: a model can read hundreds of suspicious files quickly and pull out hardcoded domains and IPs for a human to confirm. I described that workflow in AI-powered malware triage with Gemini. The rule itself should still be added by a person who has checked the address, because blocking the wrong IP (a payment gateway, a license server, your own CDN) creates a new outage. If you are responding to an active exploit, pair outbound blocking with patching; my WordPress RCE mitigation guide covers that side.
Quick reference
# Block outbound (insert at the top of OUTPUT)
sudo iptables -I OUTPUT 1 -d IP_ADDRESS -j REJECT
sudo ip6tables -I OUTPUT 1 -d IPV6_ADDRESS -j REJECT
# Check rules and counters
sudo iptables -L OUTPUT -n -v --line-numbers
sudo iptables -C OUTPUT -d IP_ADDRESS -j REJECT
# Remove the block
sudo iptables -D OUTPUT -d IP_ADDRESS -j REJECT
# Persist (not on ufw servers)
sudo netfilter-persistent save
sudo grep IP_ADDRESS /etc/iptables/rules.v4
# ufw servers
sudo ufw reject out to IP_ADDRESS
# nftables
sudo nft insert rule inet filter output ip daddr IP_ADDRESS reject
FAQ
Does blocking an IP with iptables survive a reboot?
No. Rules added with the iptables command live only in the kernel. Save them with netfilter-persistent save (from the iptables-persistent package), or use ufw or /etc/nftables.conf if that is how the server’s firewall is managed.
Should I use REJECT or DROP for outbound traffic?
REJECT. The application gets an immediate error instead of waiting for a TCP timeout, which keeps web workers from piling up and makes the block obvious in logs. Keep DROP for inbound traffic from hosts you don’t want to acknowledge.
Why is my iptables block not working?
The usual causes are rule order (you appended with -A after a rule that already accepts the traffic, so insert with -I OUTPUT 1), IPv6 (the destination is reached over IPv6, so add an ip6tables rule), or a reboot that wiped a rule you never saved.
For every option used above, the iptables man page for Ubuntu 24.04 is the reference, and the nftables wiki covers the nft syntax in depth.