Configure a highly available firewall cluster using iptables and keepalived with VRRP for automatic failover. Set up rule synchronization between nodes and implement monitoring for production-grade firewall redundancy.
Prerequisites
- Two Linux servers with network connectivity
- Root or sudo access on both nodes
- Basic understanding of iptables and networking concepts
- SSH access between nodes
What this solves
This tutorial sets up a high availability iptables firewall cluster using keepalived and VRRP (Virtual Router Redundancy Protocol). When your primary firewall fails, the secondary node automatically takes over with synchronized rules, ensuring continuous network security without manual intervention.
Step-by-step configuration
Update system packages
Start by updating your package manager and installing required dependencies on both firewall nodes.
sudo apt update && sudo apt upgrade -y
sudo apt install -y keepalived iptables-persistent rsync
Enable IP forwarding and configure kernel parameters
Enable packet forwarding and configure kernel parameters required for VRRP operation across both nodes.
net.ipv4.ip_forward = 1
net.ipv4.ip_nonlocal_bind = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
sudo sysctl -p
Create base iptables rules
Set up identical base firewall rules on both nodes that will be synchronized. These rules protect the cluster and allow keepalived communication.
# Allow loopback traffic
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
Allow established connections
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Allow SSH (adjust port as needed)
-A INPUT -p tcp --dport 22 -j ACCEPT
Allow VRRP multicast traffic between keepalived nodes
-A INPUT -p vrrp -j ACCEPT
-A OUTPUT -p vrrp -j ACCEPT
Allow keepalived multicast
-A INPUT -d 224.0.0.18/32 -j ACCEPT
-A OUTPUT -d 224.0.0.18/32 -j ACCEPT
Allow HTTP/HTTPS traffic to pass through
-A FORWARD -p tcp -m multiport --dports 80,443 -j ACCEPT
Allow DNS
-A FORWARD -p udp --dport 53 -j ACCEPT
-A FORWARD -p tcp --dport 53 -j ACCEPT
Default policies
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT
Configure keepalived on the primary node
Set up the master keepalived configuration with higher priority. Replace the interface and IP addresses with your network settings.
global_defs {
router_id FIREWALL_MASTER
enable_script_security
script_user keepalived_script
}
vrrp_script chk_iptables {
script "/usr/local/bin/check_iptables.sh"
interval 5
weight -50
fall 2
rise 1
timeout 3
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass firewall_cluster_2024
}
virtual_ipaddress {
203.0.113.100/24
}
track_script {
chk_iptables
}
notify_master "/usr/local/bin/master_notify.sh"
notify_backup "/usr/local/bin/backup_notify.sh"
notify_fault "/usr/local/bin/fault_notify.sh"
}
Configure keepalived on the backup node
Set up the backup node configuration with lower priority. Use the same virtual IP and authentication settings.
global_defs {
router_id FIREWALL_BACKUP
enable_script_security
script_user keepalived_script
}
vrrp_script chk_iptables {
script "/usr/local/bin/check_iptables.sh"
interval 5
weight -50
fall 2
rise 1
timeout 3
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass firewall_cluster_2024
}
virtual_ipaddress {
203.0.113.100/24
}
track_script {
chk_iptables
}
notify_master "/usr/local/bin/master_notify.sh"
notify_backup "/usr/local/bin/backup_notify.sh"
notify_fault "/usr/local/bin/fault_notify.sh"
}
Create the iptables health check script
This script verifies that iptables is running and responsive. Create it on both nodes with identical content.
#!/bin/bash
Check if iptables service is running
if ! systemctl is-active --quiet iptables 2>/dev/null; then
if ! systemctl is-active --quiet netfilter-persistent 2>/dev/null; then
exit 1
fi
fi
Test basic iptables functionality
if ! iptables -L INPUT -n >/dev/null 2>&1; then
exit 1
fi
Check if we can count packets (ensures rules are active)
if ! iptables -L INPUT -n -v >/dev/null 2>&1; then
exit 1
fi
exit 0
sudo chmod 755 /usr/local/bin/check_iptables.sh
Create notification scripts
Set up scripts that handle state transitions and rule synchronization. These ensure rules stay synchronized when roles change.
#!/bin/bash
LOGFILE="/var/log/keepalived-transitions.log"
BACKUP_NODE="203.0.113.101"
RULES_FILE="/etc/iptables/rules.v4"
echo "$(date): Transitioning to MASTER state" >> $LOGFILE
Ensure our iptables rules are loaded
if command -v iptables-restore >/dev/null 2>&1; then
iptables-restore < $RULES_FILE
fi
Synchronize rules to backup node
if ping -c 1 $BACKUP_NODE >/dev/null 2>&1; then
rsync -avz $RULES_FILE root@$BACKUP_NODE:$RULES_FILE
ssh root@$BACKUP_NODE "iptables-restore < $RULES_FILE" 2>/dev/null
fi
echo "$(date): MASTER transition completed" >> $LOGFILE
#!/bin/bash
LOGFILE="/var/log/keepalived-transitions.log"
echo "$(date): Transitioning to BACKUP state" >> $LOGFILE
Ensure iptables rules are still loaded
if [ -f "/etc/iptables/rules.v4" ]; then
iptables-restore < /etc/iptables/rules.v4
fi
echo "$(date): BACKUP transition completed" >> $LOGFILE
#!/bin/bash
LOGFILE="/var/log/keepalived-transitions.log"
echo "$(date): Node entered FAULT state" >> $LOGFILE
Log system status for troubleshooting
echo "$(date): System status during fault:" >> $LOGFILE
systemctl status iptables >> $LOGFILE 2>&1
systemctl status netfilter-persistent >> $LOGFILE 2>&1
echo "$(date): FAULT logging completed" >> $LOGFILE
sudo chmod 755 /usr/local/bin/master_notify.sh
sudo chmod 755 /usr/local/bin/backup_notify.sh
sudo chmod 755 /usr/local/bin/fault_notify.sh
Create keepalived script user
Create a dedicated user for running keepalived scripts with minimal privileges.
sudo useradd -r -s /bin/false keepalived_script
sudo mkdir -p /var/log
sudo touch /var/log/keepalived-transitions.log
sudo chown keepalived_script:keepalived_script /var/log/keepalived-transitions.log
Set up SSH key authentication between nodes
Configure passwordless SSH authentication for rule synchronization. Run this on the primary node first.
sudo ssh-keygen -t ed25519 -f /root/.ssh/firewall_cluster -N ""
sudo ssh-copy-id -i /root/.ssh/firewall_cluster.pub root@203.0.113.101
Then configure SSH to use the key automatically:
Host 203.0.113.101
IdentityFile /root/.ssh/firewall_cluster
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Enable and start services
Start the firewall and keepalived services on both nodes. Enable them to start automatically on boot.
sudo iptables-restore < /etc/iptables/rules.v4
sudo systemctl enable netfilter-persistent
sudo systemctl enable --now keepalived
Create rule synchronization script
Set up automated rule synchronization that can be triggered manually or by cron jobs for ongoing rule management.
#!/bin/bash
PRIMARY_NODE="203.0.113.100"
BACKUP_NODE="203.0.113.101"
RULES_FILE="/etc/iptables/rules.v4"
LOGFILE="/var/log/iptables-sync.log"
Check if we're the current master
if ip addr show | grep -q $PRIMARY_NODE; then
echo "$(date): Syncing rules from master to backup" >> $LOGFILE
# Save current rules
iptables-save > $RULES_FILE
# Sync to backup node
if rsync -avz $RULES_FILE root@$BACKUP_NODE:$RULES_FILE; then
ssh root@$BACKUP_NODE "iptables-restore < $RULES_FILE"
echo "$(date): Rules synchronized successfully" >> $LOGFILE
else
echo "$(date): Failed to synchronize rules" >> $LOGFILE
exit 1
fi
else
echo "$(date): Not master, skipping sync" >> $LOGFILE
fi
sudo chmod 755 /usr/local/bin/sync_iptables.sh
Set up automated monitoring
Create a monitoring script that checks cluster health and logs status information for troubleshooting.
#!/bin/bash
VIP="203.0.113.100"
BACKUP_NODE="203.0.113.101"
LOGFILE="/var/log/cluster-monitor.log"
Check if we have the VIP
if ip addr show | grep -q $VIP; then
ROLE="MASTER"
else
ROLE="BACKUP"
fi
Check keepalived status
KEEPALIVED_STATUS=$(systemctl is-active keepalived)
Check iptables rules count
RULE_COUNT=$(iptables -L | grep -c "^Chain\|^target")
Check if backup node is reachable
if ping -c 1 -W 2 $BACKUP_NODE >/dev/null 2>&1; then
BACKUP_REACHABLE="YES"
else
BACKUP_REACHABLE="NO"
fi
echo "$(date): Role=$ROLE, Keepalived=$KEEPALIVED_STATUS, Rules=$RULE_COUNT, Backup_Reachable=$BACKUP_REACHABLE" >> $LOGFILE
Alert if issues detected
if [ "$KEEPALIVED_STATUS" != "active" ] || [ "$RULE_COUNT" -lt "5" ]; then
echo "$(date): WARNING - Cluster health issues detected" >> $LOGFILE
fi
sudo chmod 755 /usr/local/bin/monitor_cluster.sh
Configure monitoring cron job
Set up automated monitoring that runs every 5 minutes to track cluster health and log important events.
sudo crontab -e
Add this line to the root crontab on both nodes:
/5 * /usr/local/bin/monitor_cluster.sh
Verify your setup
Test the cluster configuration and verify that failover works correctly:
# Check keepalived status on both nodes
sudo systemctl status keepalived
Verify which node has the VIP
ip addr show | grep 203.0.113.100
Check iptables rules are loaded
sudo iptables -L -n
Test VRRP communication
sudo tcpdump -i eth0 vrrp
Monitor cluster logs
sudo tail -f /var/log/keepalived-transitions.log
Test failover by stopping keepalived on the master node:
# On master node - simulate failure
sudo systemctl stop keepalived
On backup node - should see VIP assignment
ip addr show | grep 203.0.113.100
Restore master node
sudo systemctl start keepalived
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| VIP not assigned to any node | VRRP authentication mismatch | Verify identical auth_pass in both configs |
| Both nodes claim master role | Network split-brain condition | Check network connectivity, VRRP multicast |
| Keepalived fails to start | Script user permissions | sudo chown keepalived_script:keepalived_script /usr/local/bin/check_iptables.sh |
| Rules not synchronizing | SSH key authentication failure | Test SSH: ssh root@backup_node whoami |
| Health check script failing | Iptables service not running | sudo systemctl start iptables or netfilter-persistent |
| Failover too slow | Default detection timers | Reduce advert_int to 1 second in keepalived.conf |
Next steps
- Set up keepalived cluster monitoring with Prometheus alerts and Grafana dashboards
- Set up centralized logging with rsyslog and logrotate for security events
- Implement network security with iptables connection tracking and stateful packet filtering
- Configure advanced iptables QoS with DSCP marking and traffic classification
- Set up iptables logging and monitoring with rsyslog for security analysis
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
VIRTUAL_IP=""
INTERFACE=""
NODE_TYPE=""
PEER_IP=""
# Usage function
usage() {
echo "Usage: $0 --virtual-ip <IP> --interface <IF> --node-type <master|backup> --peer-ip <IP>"
echo "Example: $0 --virtual-ip 203.0.113.100 --interface eth0 --node-type master --peer-ip 192.168.1.101"
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--virtual-ip)
VIRTUAL_IP="$2"
shift 2
;;
--interface)
INTERFACE="$2"
shift 2
;;
--node-type)
NODE_TYPE="$2"
shift 2
;;
--peer-ip)
PEER_IP="$2"
shift 2
;;
*)
usage
;;
esac
done
# Validate arguments
if [[ -z "$VIRTUAL_IP" || -z "$INTERFACE" || -z "$NODE_TYPE" || -z "$PEER_IP" ]]; then
usage
fi
if [[ "$NODE_TYPE" != "master" && "$NODE_TYPE" != "backup" ]]; then
echo -e "${RED}Error: node-type must be 'master' or 'backup'${NC}"
exit 1
fi
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root${NC}"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
IPTABLES_SERVICE="netfilter-persistent"
IPTABLES_SAVE_CMD="iptables-save > /etc/iptables/rules.v4"
IPTABLES_RESTORE_CMD="iptables-restore < /etc/iptables/rules.v4"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
IPTABLES_SERVICE="iptables"
IPTABLES_SAVE_CMD="iptables-save > /etc/sysconfig/iptables"
IPTABLES_RESTORE_CMD="iptables-restore < /etc/sysconfig/iptables"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
IPTABLES_SERVICE="iptables"
IPTABLES_SAVE_CMD="iptables-save > /etc/sysconfig/iptables"
IPTABLES_RESTORE_CMD="iptables-restore < /etc/sysconfig/iptables"
;;
*)
echo -e "${RED}Error: Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Error: Cannot detect distribution${NC}"
exit 1
fi
# Error cleanup function
cleanup() {
echo -e "${RED}Installation failed. Cleaning up...${NC}"
systemctl stop keepalived 2>/dev/null || true
rm -f /etc/keepalived/keepalived.conf
rm -f /usr/local/bin/check_iptables.sh
rm -f /usr/local/bin/master_notify.sh
rm -f /usr/local/bin/backup_notify.sh
rm -f /usr/local/bin/fault_notify.sh
}
trap cleanup ERR
echo -e "${GREEN}Starting iptables high availability cluster setup...${NC}"
echo -e "${YELLOW}[1/8] Updating system packages...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
apt update && apt upgrade -y
$PKG_INSTALL keepalived iptables-persistent rsync
else
$PKG_INSTALL epel-release 2>/dev/null || true
$PKG_INSTALL keepalived iptables-services rsync
fi
echo -e "${YELLOW}[2/8] Configuring kernel parameters...${NC}"
cat > /etc/sysctl.d/99-keepalived.conf << EOF
net.ipv4.ip_forward = 1
net.ipv4.ip_nonlocal_bind = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
EOF
sysctl -p /etc/sysctl.d/99-keepalived.conf
echo -e "${YELLOW}[3/8] Creating keepalived script user...${NC}"
useradd -r -s /bin/false keepalived_script 2>/dev/null || true
echo -e "${YELLOW}[4/8] Setting up base iptables rules...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
mkdir -p /etc/iptables
else
mkdir -p /etc/sysconfig
fi
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow VRRP protocol
iptables -A INPUT -p vrrp -j ACCEPT
iptables -A OUTPUT -p vrrp -j ACCEPT
# Allow keepalived multicast
iptables -A INPUT -d 224.0.0.18/32 -j ACCEPT
iptables -A OUTPUT -d 224.0.0.18/32 -j ACCEPT
# Allow communication with peer
iptables -A INPUT -s $PEER_IP -j ACCEPT
iptables -A OUTPUT -d $PEER_IP -j ACCEPT
# Forward rules for common traffic
iptables -A FORWARD -p tcp -m multiport --dports 80,443 -j ACCEPT
iptables -A FORWARD -p udp --dport 53 -j ACCEPT
iptables -A FORWARD -p tcp --dport 53 -j ACCEPT
# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Save rules
eval $IPTABLES_SAVE_CMD
echo -e "${YELLOW}[5/8] Creating health check script...${NC}"
cat > /usr/local/bin/check_iptables.sh << 'EOF'
#!/bin/bash
if ! systemctl is-active --quiet iptables 2>/dev/null; then
if ! iptables -L INPUT -n >/dev/null 2>&1; then
exit 1
fi
fi
exit 0
EOF
chmod 755 /usr/local/bin/check_iptables.sh
chown keepalived_script:keepalived_script /usr/local/bin/check_iptables.sh
echo -e "${YELLOW}[6/8] Creating notification scripts...${NC}"
cat > /usr/local/bin/master_notify.sh << EOF
#!/bin/bash
logger "keepalived: Transitioned to MASTER state"
systemctl reload $IPTABLES_SERVICE
EOF
cat > /usr/local/bin/backup_notify.sh << 'EOF'
#!/bin/bash
logger "keepalived: Transitioned to BACKUP state"
EOF
cat > /usr/local/bin/fault_notify.sh << 'EOF'
#!/bin/bash
logger "keepalived: Transitioned to FAULT state"
EOF
chmod 755 /usr/local/bin/*_notify.sh
chown keepalived_script:keepalived_script /usr/local/bin/*_notify.sh
echo -e "${YELLOW}[7/8] Configuring keepalived...${NC}"
if [[ "$NODE_TYPE" == "master" ]]; then
PRIORITY=110
STATE="MASTER"
ROUTER_ID="FIREWALL_MASTER"
else
PRIORITY=100
STATE="BACKUP"
ROUTER_ID="FIREWALL_BACKUP"
fi
cat > /etc/keepalived/keepalived.conf << EOF
global_defs {
router_id $ROUTER_ID
enable_script_security
script_user keepalived_script
}
vrrp_script chk_iptables {
script "/usr/local/bin/check_iptables.sh"
interval 5
weight -50
fall 2
rise 1
timeout 3
}
vrrp_instance VI_1 {
state $STATE
interface $INTERFACE
virtual_router_id 51
priority $PRIORITY
advert_int 1
authentication {
auth_type PASS
auth_pass firewall_cluster_2024
}
virtual_ipaddress {
$VIRTUAL_IP/24
}
track_script {
chk_iptables
}
notify_master "/usr/local/bin/master_notify.sh"
notify_backup "/usr/local/bin/backup_notify.sh"
notify_fault "/usr/local/bin/fault_notify.sh"
}
EOF
chmod 644 /etc/keepalived/keepalived.conf
echo -e "${YELLOW}[8/8] Enabling and starting services...${NC}"
systemctl enable $IPTABLES_SERVICE
systemctl start $IPTABLES_SERVICE
systemctl enable keepalived
systemctl start keepalived
echo -e "${GREEN}Verifying installation...${NC}"
if systemctl is-active --quiet keepalived; then
echo -e "${GREEN}✓ keepalived is running${NC}"
else
echo -e "${RED}✗ keepalived failed to start${NC}"
exit 1
fi
if systemctl is-active --quiet $IPTABLES_SERVICE; then
echo -e "${GREEN}✓ iptables service is running${NC}"
else
echo -e "${RED}✗ iptables service failed to start${NC}"
exit 1
fi
if iptables -L INPUT -n | grep -q "DROP"; then
echo -e "${GREEN}✓ iptables rules are loaded${NC}"
else
echo -e "${RED}✗ iptables rules not properly loaded${NC}"
exit 1
fi
echo -e "${GREEN}Installation complete!${NC}"
echo -e "${YELLOW}Node configured as: $NODE_TYPE${NC}"
echo -e "${YELLOW}Virtual IP: $VIRTUAL_IP${NC}"
echo -e "${YELLOW}Interface: $INTERFACE${NC}"
echo ""
echo "To check status: systemctl status keepalived"
echo "To view logs: journalctl -u keepalived -f"
Review the script before running. Execute with: bash install.sh