— e.g. Docker, Fail2Ban, libvirt —
In real-world environments, we often maintain our own firewall scripts — for example, to handle NAT, DNAT, outbound access control, or time-based restrictions.
However, servers frequently run services such as Docker, Fail2Ban, or libvirt, which also manage their own iptables rules.
If you flush all existing rules using something like iptables -F or iptables -t nat -F, you may accidentally remove rules that these services rely on — breaking containers, SSH, or automatic banning entirely.
This post describes a safe and structured way to manage your own firewall rules —
by creating custom chains that coexist peacefully with other system-managed rules.
🧩 Why Do They Interfere?
Services like Docker or Fail2Ban automatically modify the firewall when they start.
For example:
- Docker adds its own chains:
DOCKER,DOCKER-USER,DOCKER-ISOLATION-STAGE-1 - Fail2Ban creates chains like
f2b-sshd,f2b-postfix, etc. - Many services internally execute
iptables -Foriptables -Xwhen reloading.
If your script also clears entire tables (iptables -t filter -F, iptables -t nat -F), it wipes those rules too — causing:
- Docker containers to lose external connectivity
- Fail2Ban to stop blocking malicious IPs
- All NAT or DNAT rules to disappear
✅ The Principle: Manage Your Own Chains, Don’t Flush Everything
Instead of rewriting built-in chains like INPUT, FORWARD, and OUTPUT,
you only insert a single jump to your own custom chain, such as:
filter:INPUT → FW-INPUT
filter:DOCKER-USER → FW-FORWARD
nat:PREROUTING → FW-PREROUTING
nat:POSTROUTING → FW-POSTROUTING
mangle:FORWARD → FW-FORWARD (for MSS clamp)
That way:
- You can reload your own rules safely — just flush
FW-*chains. - Docker and Fail2Ban rules remain untouched.
- Re-running your script won’t break system-generated chains.
🔧 Implementation Example
Create Custom Chains (Flush Only Yours)
# Create chains (ignore errors if they already exist)
for T in filter nat mangle; do
iptables -t $T -N FW-INPUT 2>/dev/null || true
iptables -t $T -N FW-FORWARD 2>/dev/null || true
iptables -t $T -N FW-PREROUTING 2>/dev/null || true
iptables -t $T -N FW-POSTROUTING 2>/dev/null || true
done
# Flush only your own chains
iptables -t filter -F FW-INPUT
iptables -t filter -F FW-FORWARD
iptables -t nat -F FW-PREROUTING
iptables -t nat -F FW-POSTROUTING
iptables -t mangle -F FW-FORWARD
# Insert jumps only once (if not already present)
iptables -C INPUT -j FW-INPUT 2>/dev/null || iptables -I INPUT -j FW-INPUT
iptables -N DOCKER-USER 2>/dev/null || true
iptables -F DOCKER-USER
iptables -A DOCKER-USER -j FW-FORWARD
iptables -t nat -C PREROUTING -j FW-PREROUTING 2>/dev/null || iptables -t nat -I PREROUTING 1 -j FW-PREROUTING
iptables -t nat -C POSTROUTING -j FW-POSTROUTING 2>/dev/null || iptables -t nat -A POSTROUTING -j FW-POSTROUTING
🔍 Clear Role Separation by Table
| Table | Purpose | Custom Chain |
|---|---|---|
| filter | ACLs (accept/deny traffic) | FW-INPUT, FW-FORWARD (includes whitelist and time rules) |
| nat | Address translation | FW-PREROUTING (DNAT), FW-POSTROUTING (MASQ / hairpin NAT) |
| mangle | Packet modification | FW-FORWARD (TCP MSS clamp) |
Each table handles its own logic — no overlap or conflicts.
⚙️ Example: MSS Clamp in mangle/FORWARD
iptables -t mangle -A FW-FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1452
This avoids packet fragmentation on PPPoE or low-MTU links.
⚙️ Example: Internet Whitelist + Time Control (filter/FORWARD)
We can manage the “who can access the Internet” list using ipset,
then enforce the rule inside filter/FORWARD.
# Create the IP set
ipset create internet_allowed hash:ip -exist
ipset add internet_allowed 192.168.100.2
ipset add internet_allowed 192.168.100.21
# Allow these IPs to go online at any time
iptables -A FW-FORWARD -m set --match-set internet_allowed src -o ppp0 -j ACCEPT
# Allow the entire subnet during specific time windows (UTC)
iptables -A FW-FORWARD -s 192.168.100.0/24 -o ppp0 -m time --timestart 00:00 --timestop 00:30 -j ACCEPT
iptables -A FW-FORWARD -s 192.168.100.0/24 -o ppp0 -m time --timestart 01:00 --timestop 01:30 -j ACCEPT
# ...and other windows as needed
# Drop everything else
iptables -A FW-FORWARD -s 192.168.100.0/24 -o ppp0 -j DROP
These changes take effect immediately — no need to reload iptables.
Update your whitelist dynamically:
ipset add internet_allowed 192.168.100.50
ipset del internet_allowed 192.168.100.21
⚙️ Example: DNAT + Hairpin NAT (nat)
# External 80/443 → internal web server
iptables -t nat -A FW-PREROUTING -p tcp -d 122.116.109.114 --dport 80 -j DNAT --to-destination 192.168.100.2:80
iptables -t nat -A FW-PREROUTING -p tcp -d 122.116.109.114 --dport 443 -j DNAT --to-destination 192.168.100.2:443
# Hairpin NAT: allow internal users to access via public IP
iptables -t nat -A FW-POSTROUTING -s 192.168.100.0/24 -d 192.168.100.2 -o br.eno1.5 -j MASQUERADE
⚙️ Why Docker and Fail2Ban Stay Safe
- Docker’s chains (
DOCKER,DOCKER-USER, etc.) remain untouched — we only jump into them. - Fail2Ban’s
f2b-*chains stay intact underfilter/INPUT. - The script flushes only
FW-*, so all system-generated chains continue working normally.
🧰 ipset Quick Reference
| Action | Command |
|---|---|
| Create a set | ipset create internet_allowed hash:ip |
| Add IP | ipset add internet_allowed 192.168.100.21 |
| Remove IP | ipset del internet_allowed 192.168.100.21 |
| Flush all elements | ipset flush internet_allowed |
| Destroy the set | ipset destroy internet_allowed |
| Save / Restore | ipset save > /etc/firewall/ipset.rules / ipset restore < /etc/firewall/ipset.rules |
Instant effect: ipset works at the kernel level.
iptables simply references the set name — any changes (add/del) apply immediately without reloading rules.
🧠 Why This Design Works Better
| Traditional Approach | Custom Chain Approach |
|---|---|
| Flushes entire tables | Flushes only custom chains |
| System services lose rules | Docker / Fail2Ban unaffected |
| Dozens of whitelist rules | One ipset match — O(1) lookup |
| Need to rerun full script to apply changes | Dynamic add/remove instantly effective |
⚙️ Systemd Integration (Auto-load on Boot)
[Unit]
Description=Firewall rules loader (after Docker)
Wants=docker.service
After=network-online.target docker.service
Requires=network-online.target
[Service]
Type=oneshot
ExecStart=/etc/firewall/firewall.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
✅ Summary
The key mindset is simple:
Manage only your own namespace — don’t take over the entire iptables.
By isolating your rules into dedicated custom chains:
- System-managed rules (Docker, Fail2Ban, etc.) remain intact
- Your firewall logic stays modular and predictable
- Troubleshooting, updates, and re-deployment become much safer
📎 Further Reading
- Netfilter Hook Order (nftables.org)
- Docker and iptables — Official Docs
- Fail2Ban Integration with Firewalls
man ipset,man iptables