UFW Advanced Usage¶
Custom Chains¶
When to Use Custom Chains¶
- Organize rules logically
- Share rules between multiple entry points
- Implement complex matching
Creating Custom Chains in before.rules¶
# /etc/ufw/before.rules (in *filter section)
# Define custom chain
:ufw-custom-ssh - [0:0]
# Populate custom chain
-A ufw-custom-ssh -s 192.168.1.0/24 -j ACCEPT
-A ufw-custom-ssh -s 10.0.0.0/8 -j ACCEPT
-A ufw-custom-ssh -m recent --name ssh-block --rcheck --seconds 60 --hitcount 4 -j DROP
-A ufw-custom-ssh -m recent --name ssh-block --set
-A ufw-custom-ssh -j ACCEPT
# Jump to custom chain from before-input
-A ufw-before-input -p tcp --dport 22 -j ufw-custom-ssh
Rate Limiting Deep Dive¶
Built-in Limit¶
UFW's built-in limit is simple:
This creates: - 6 connections per 30 seconds - No per-IP tracking
Custom Rate Limiting¶
More sophisticated limiting in before.rules:
# Per-IP rate limiting
-A ufw-before-input -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --name SSH --set
-A ufw-before-input -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --name SSH --update --seconds 60 --hitcount 4 -j DROP
# Using hashlimit for per-IP limits
-A ufw-before-input -p tcp --dport 80 -m hashlimit \
--hashlimit-name http-limit \
--hashlimit-mode srcip \
--hashlimit-above 50/second \
--hashlimit-burst 100 \
--hashlimit-srcmask 32 \
-j DROP
Connection Limits¶
# Limit concurrent connections per IP
-A ufw-before-input -p tcp --dport 80 -m connlimit \
--connlimit-above 50 --connlimit-mask 32 -j DROP
Port Knocking¶
Simple Port Knock¶
# /etc/ufw/before.rules
# Create sets for tracking
# (Requires manual iptables, UFW doesn't support sets directly)
# Alternative: Use recent module
-N ufw-port-knock
-A ufw-before-input -j ufw-port-knock
# Stage 1: Knock on port 7000
-A ufw-port-knock -p tcp --dport 7000 -m recent --name KNOCK1 --set -j DROP
# Stage 2: Knock on port 8000 (must have knocked 7000)
-A ufw-port-knock -p tcp --dport 8000 -m recent --name KNOCK1 --rcheck \
-m recent --name KNOCK2 --set -j DROP
# Stage 3: Knock on port 9000 (must have knocked 8000)
-A ufw-port-knock -p tcp --dport 9000 -m recent --name KNOCK2 --rcheck \
-m recent --name KNOCK3 --set -j DROP
# Allow SSH after completing knock sequence
-A ufw-port-knock -p tcp --dport 22 -m recent --name KNOCK3 --rcheck \
--seconds 30 -j ACCEPT
Using knockd (Better Approach)¶
Install knockd for proper port knocking:
sudo apt install knockd
# /etc/knockd.conf
[options]
UseSyslog
[openSSH]
sequence = 7000,8000,9000
seq_timeout = 5
command = /usr/sbin/ufw allow from %IP% to any port 22
tcpflags = syn
[closeSSH]
sequence = 9000,8000,7000
seq_timeout = 5
command = /usr/sbin/ufw delete allow from %IP% to any port 22
tcpflags = syn
GeoIP Blocking¶
UFW doesn't natively support GeoIP, but you can use xtables-addons:
# Install xtables-geoip
sudo apt install xtables-addons-common libtext-csv-xs-perl
# Download GeoIP database
sudo /usr/lib/xtables-addons/xt_geoip_dl
sudo /usr/lib/xtables-addons/xt_geoip_build
Add to before.rules:
IP Sets Integration¶
Why Use IP Sets¶
- Efficient for large lists (thousands of IPs)
- O(1) lookup vs O(n) for rule chains
- Dynamic updates without rule reload
Setup¶
# Install ipset
sudo apt install ipset
# Create set
sudo ipset create blocklist hash:net
# Add entries
sudo ipset add blocklist 10.0.0.0/8
sudo ipset add blocklist 192.168.50.0/24
Use in before.rules¶
Persist IP Sets¶
# Save
sudo ipset save > /etc/ipset.rules
# Restore (before UFW starts)
# Create /etc/systemd/system/ipset-restore.service
[Unit]
Description=Restore ipset rules
Before=ufw.service
[Service]
Type=oneshot
ExecStart=/usr/sbin/ipset restore -f /etc/ipset.rules
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Fail2ban Integration¶
How Fail2ban Works with UFW¶
Fail2ban monitors logs and adds temporary firewall rules:
Configure for UFW¶
# /etc/fail2ban/jail.local
[DEFAULT]
banaction = ufw
banaction_allports = ufw
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
UFW Ban Action¶
# /etc/fail2ban/action.d/ufw.conf
[Definition]
actionban = ufw insert 1 deny from <ip> to any
actionunban = ufw delete deny from <ip> to any
Traffic Shaping¶
UFW doesn't do traffic shaping, but you can add tc rules:
# Mark traffic with iptables
-A ufw-before-output -p tcp --dport 80 -j MARK --set-mark 1
# Use tc for shaping (separate from UFW)
tc qdisc add dev eth0 root handle 1: htb default 10
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc filter add dev eth0 parent 1: protocol ip handle 1 fw flowid 1:1
Multi-Path Routing¶
Mark-Based Routing¶
# /etc/ufw/before.rules (in *mangle section - add before *filter)
*mangle
:PREROUTING ACCEPT [0:0]
# Mark traffic from specific source
-A PREROUTING -s 192.168.1.0/24 -j MARK --set-mark 1
COMMIT
Then configure policy routing:
# /etc/iproute2/rt_tables
100 isp2
# Routing rules
ip rule add fwmark 1 table isp2
ip route add default via 10.0.0.1 table isp2
Transparent Proxy¶
Redirect HTTP to Proxy¶
# /etc/ufw/before.rules (in *nat section)
*nat
# Redirect port 80 to local proxy
-A PREROUTING -i eth1 -p tcp --dport 80 -j REDIRECT --to-port 3128
COMMIT
With Squid¶
Connection Tracking Tuning¶
Timeouts¶
# /etc/ufw/sysctl.conf (or /etc/sysctl.d/99-conntrack.conf)
# TCP established (default 5 days, reduce for busy servers)
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
# TCP time-wait
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
# UDP
net.netfilter.nf_conntrack_udp_timeout = 30
Table Size¶
# Increase for busy servers
net.netfilter.nf_conntrack_max = 524288
# Hash table (conntrack_max / 4)
net.netfilter.nf_conntrack_buckets = 131072
Logging Configuration¶
Log Levels¶
# /etc/ufw/ufw.conf
LOGLEVEL=low # Match only logging rules
LOGLEVEL=medium # Log rate-limited and more
LOGLEVEL=high # Log even more
LOGLEVEL=full # Log everything
Custom Logging Rules¶
# /etc/ufw/before.rules
# Log specific traffic
-A ufw-before-input -p tcp --dport 22 -j LOG --log-prefix "[UFW SSH] "
# Log with rate limit
-A ufw-before-input -m limit --limit 3/min -j LOG --log-prefix "[UFW WARN] "
Log to Separate File¶
Stateless Rules¶
By default, UFW is stateful (uses conntrack). For high-performance scenarios:
# /etc/ufw/before.rules
# Add to *raw table (before *nat)
*raw
:PREROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
# Don't track high-volume traffic
-A PREROUTING -p udp --dport 53 -j NOTRACK
-A OUTPUT -p udp --sport 53 -j NOTRACK
COMMIT
Then add stateless rules in *filter:
# Must explicitly allow both directions for untracked
-A ufw-before-input -p udp --dport 53 -j ACCEPT
-A ufw-before-output -p udp --sport 53 -j ACCEPT
Dynamic Rule Updates¶
API-Driven Rules¶
Use ufw commands in scripts:
#!/bin/bash
# Add rule based on API response
NEW_IP=$(curl -s https://api.example.com/allowed-ip)
ufw allow from $NEW_IP to any port 443
Webhook-Triggered¶
# Simple webhook endpoint (using netcat)
while true; do
IP=$(echo -e "HTTP/1.1 200 OK\n\n" | nc -l -p 9999 | grep "X-Real-IP" | cut -d: -f2)
ufw allow from $IP to any port 22 comment "Dynamic allow"
done
Testing Rules¶
Dry Run (Sort of)¶
# Check what rules will be added
ufw --dry-run allow 80
# Note: This still modifies user.rules but doesn't apply
Test Environment¶
# Create network namespace for testing
sudo ip netns add firewall-test
sudo ip netns exec firewall-test iptables -L
# Test rules there before applying to production