This section covers host firewalls on Linux servers. The main page is a Bash script that rebuilds the iptables INPUT chain with a default-deny policy, allows loopback, private networks and established connections, downloads the list of IPv4 blocks that ARIN has allocated to US organisations, and accepts traffic from those blocks only. Below the script you will find how the rule chain is evaluated, an important rule-ordering caveat, a faster ipset version, operational pitfalls, and the related Fail2Ban uninstall guide.
#!/bin/bash
# Enable error checking and debugging
set -e
set -x
# Function to log messages
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check for required tools
for cmd in curl iptables awk grep sort; do
if ! command_exists "$cmd"; then
log_message "Error: Required command '$cmd' not found"
exit 1
fi
done
log_message "Starting firewall configuration..."
# Create temporary files with cleanup trap
TEMP_DIR=$(mktemp -d)
RAW_IPS_FILE="${TEMP_DIR}/raw_ips"
PROCESSED_IPS_FILE="${TEMP_DIR}/processed_ips"
MERGED_IPS_FILE="${TEMP_DIR}/merged_ips"
trap 'rm -rf "${TEMP_DIR}"' EXIT
# Backup existing rules
if [ -d "/etc/iptables" ]; then
cp /etc/iptables/rules.v4 "/etc/iptables/rules.v4.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
fi
log_message "Setting up basic firewall rules..."
# Clear existing rules
iptables -F
iptables -X
# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow all internal network traffic (adjusted to be more inclusive)
iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT # Private network
iptables -A INPUT -s 172.16.0.0/12 -j ACCEPT # Private network
iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT # Private network
# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow specific services (adjust ports as needed)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # SSH
iptables -A INPUT -p tcp --dport 3000 -j ACCEPT
iptables -A INPUT -p tcp --dport 3389 -j ACCEPT # RDP
iptables -A INPUT -p tcp --dport 443 -j ACCEPT # HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # HTTP
iptables -A INPUT -p tcp --dport 8333 -j ACCEPT
iptables -A INPUT -p udp --dport 53 -j ACCEPT # DNS
log_message "Downloading US IP ranges..."
if ! curl -s "https://ftp.ripe.net/pub/stats/arin/delegated-arin-extended-latest" | \
grep "^arin|.*|ipv4|.*|allocated|.*" | \
grep -i "US|" | \
awk -F'|' '{print $4"/"32-log($5)/log(2)}' > "$RAW_IPS_FILE"; then
log_message "Warning: Failed to download IP ranges, using basic configuration"
fi
# Process IP ranges if download was successful
if [ -s "$RAW_IPS_FILE" ]; then
log_message "Processing IP ranges..."
while IFS= read -r ip_range; do
if [[ $ip_range =~ ^([0-9]{1,3}\.[0-9]{1,3})\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
echo "${BASH_REMATCH[1]}.0.0/16"
fi
done < "$RAW_IPS_FILE" | sort -u > "$PROCESSED_IPS_FILE"
# Add US IP ranges
while IFS= read -r ip_range; do
if [[ $ip_range =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
log_message "Adding rule for $ip_range"
iptables -A INPUT -s "$ip_range" -j ACCEPT
fi
done < "$PROCESSED_IPS_FILE"
fi
# Enable ICMP (ping)
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
# Log dropped packets
iptables -A INPUT -j LOG --log-prefix "DROPPED_PACKET: " --log-level 4
# Final drop rule
iptables -A INPUT -j DROP
# Save rules
if [ -d "/etc/iptables" ]; then
iptables-save > /etc/iptables/rules.v4
log_message "Rules saved to /etc/iptables/rules.v4"
else
log_message "Warning: /etc/iptables directory not found, rules not saved permanently"
fi
log_message "Verifying rules..."
iptables -L INPUT -n -v
log_message "Firewall configuration complete."
iptables evaluates the rules of a chain from top to bottom and stops at the first rule whose match succeeds; the chain policy (-P INPUT DROP) applies only if nothing matched. The script therefore builds an ordered list: trusted interfaces and private ranges, return traffic for connections the server opened, the service ports, then one ACCEPT per US network block, then a LOG rule and a final explicit DROP.
The US list is built from ARIN's delegated-arin-extended-latest file, whose lines look like arin|US|ipv4|23.0.0.0|65536|...|allocated|.... The awk step converts the address count to a prefix length with 32 - log2(count), and the following loop widens every block to its enclosing /16 and de-duplicates, which cuts tens of thousands of entries down to a few thousand rules at the cost of allowing some non-US neighbours.
Important: because the service-port rules (22, 80, 443, 3000, 3389, 8333, 53) accept from any source and sit above the country rules, the geo filter does not restrict those ports at all; it only affects other ports. To actually limit SSH or RDP to US addresses, remove those port rules and accept the ports inside the country match instead, as in the ipset version below.
Thousands of individual rules are evaluated linearly for every new connection. An ipset stores all networks in a hash structure that is checked in roughly constant time, and the set can be rebuilt and swapped atomically without touching the iptables rules.
#!/bin/bash
set -euo pipefail
ipset create us_nets hash:net -exist
ipset create us_nets_new hash:net -exist
ipset flush us_nets_new
curl -s https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest |
awk -F'|' '$2=="US" && $3=="ipv4" && ($7=="allocated"||$7=="assigned") {
n=$5; p=32; while (n>1) {n/=2; p--}; print $4"/"p }' |
while read -r net; do ipset add us_nets_new "$net" -exist; done
ipset swap us_nets_new us_nets
ipset destroy us_nets_new
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m set --match-set us_nets src -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT # public web
iptables -A INPUT -j DROP
ipset save > /etc/ipset.conf
Blocks whose size is not a power of two are rounded to the nearest prefix here; a stricter script would split them into exact CIDRs. Persist the set across reboots with ipset save/restore or the ipset-persistent package, loaded before the iptables rules.
INPUT DROP right after flushing, before the SSH and ESTABLISHED rules are added. Run it from a console, or schedule a rollback (at now + 5 minutes restoring the backup) before running it over SSH.ip6tables rules, IPv6 traffic bypasses the filter entirely.iptables -F and -X remove rules created by Docker, Fail2Ban or ufw; restart those services afterwards.-m limit --limit 5/min to the LOG rule.