Stop Single-Host Gateway Outages: Practical keepalived VRRP Floating IPs on Linux
A service IP that lives on only one box is a single point of failure.
When that host reboots, freezes, or loses its uplink, every client that hard-coded 192.0.2.10 goes dark — even if an identical standby is sitting next to it with a warm cache and healthy disks.
keepalived implements the Virtual Router Redundancy Protocol (VRRP) on Linux so two or more hosts can share a floating virtual IP (VIP). One node owns the address as MASTER; the others stay BACKUP and take over when advertisements stop or a tracked check fails.
This is host-level IP failover, not NIC bonding on a single machine and not full application clustering. Different failure domain, different tool.
What you get (and what you do not)
You get:
- A stable service address clients can keep forever
- Automatic VIP migration when the current master dies or is demoted
- Optional health coupling (
vrrp_script,vrrp_track_process,track_interface) so “host up but nginx dead” still fails over - Gratuitous ARP / unsolicited Neighbor Advertisements so L2 neighbors relearn the VIP quickly
This article does not cover:
- Linux Ethernet bonding / LACP (link aggregation on one host)
- Device Mapper multipath (storage path HA)
- Full LVS/IPVS load-balancer farms (keepalived can do that too; different problem)
- Pacemaker/Corosync multi-resource clusters
- DNS-based failover (TTLs and client caches behave differently)
If you need two cables on one server, bond first. If you need one IP that survives a whole server dying, keep reading.
How VRRP actually works (short version)
VRRP (RFC 5798 for version 3; RFC 3768 for the older v2 story) elects one Master per virtual router ID (VRID) on a LAN:
- Routers periodically send advertisements (default destination for IPv4 multicast is
224.0.0.18, protocol 112). - The highest priority healthy router becomes Master and owns the virtual IPvX address(es).
- Backups listen. If advertisements stop for long enough, a backup promotes itself.
- On becoming Master, the new owner sends gratuitous ARP (IPv4) or unsolicited NA (IPv6) so switches and hosts update their neighbor caches.
keepalived is the common Linux implementation: it programs VIPs via netlink, runs optional track scripts/processes, and can use a VMAC (use_vmac) so the virtual router MAC stays stable across failovers.
Important keepalived note from the man page: VRRPv2 authentication (auth_type PASS / AH) was removed from the VRRPv2 specification by RFC 3768. PASS sends a cleartext password on the wire. Treat it as a misconfiguration guard, not real security. Prefer network isolation (dedicated VLAN, firewall) for VRRP traffic.
Lab topology
Two Debian/Ubuntu-style hosts on the same L2 segment:
| Host | Real IP (management) | Role intent | VRRP priority |
|---|---|---|---|
gw-a |
192.0.2.11/24 |
Preferred master | 150 |
gw-b |
192.0.2.12/24 |
Standby | 100 |
| VIP | 192.0.2.10/24 |
Floating service IP | — |
Clients (or upstream routes) use 192.0.2.10 only. Never point production traffic at the real host IPs if you want transparent failover.
Replace interface names (eth0 / enp1s0 / bond0) with whatever ip -br link shows. If the uplink is a bond, put VRRP on bond0 — bonding and VRRP compose cleanly and cover different failures.
Install and enable
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y keepalived
# Fedora/RHEL family
# sudo dnf install -y keepalived
sudo systemctl enable keepalived.service
Config path: /etc/keepalived/keepalived.conf
Logs: journalctl -u keepalived -f
Runtime state: ip -br addr, ip -d link, and keepalived’s own logs on state transitions.
Baseline: two-node VIP with intentional preemption
Prefer the healthier, higher-priority node when it is online. Both nodes share the same virtual_router_id and VIP; only priority (and optional tracks) differ.
Host A (gw-a) — higher priority
/etc/keepalived/keepalived.conf:
global_defs {
router_id gw-a
# Prefer a non-root script user when you add track/notify scripts later
script_user keepalived_script
enable_script_security
# GARP tuning after becoming MASTER (modern switches rarely need a storm)
vrrp_garp_master_refresh 60
vrrp_garp_master_refresh_repeat 1
}
vrrp_instance VI_GATEWAY {
# Initial state before advertisements settle. Priority still wins the election.
state BACKUP
interface eth0
virtual_router_id 51 # 1-255, unique per VIP set on this LAN
priority 150 # higher = preferred master
advert_int 1 # seconds (fractional values allowed)
# VRRPv3 is the RFC 5798 path; IPv6 instances use v3 anyway
version 3
# Optional: keep VIP MAC stable via macvlan VMAC (good with picky switches)
# use_vmac vrrp51
# vmac_xmit_base
authentication {
auth_type PASS
auth_pass chg-me-8c # first 8 chars matter; same on all peers
}
virtual_ipaddress {
192.0.2.10/24 dev eth0
}
# Demote if the uplink itself disappears
track_interface {
eth0
}
}
Host B (gw-b) — lower priority
Same file with:
global_defs {
router_id gw-b
script_user keepalived_script
enable_script_security
vrrp_garp_master_refresh 60
vrrp_garp_master_refresh_repeat 1
}
vrrp_instance VI_GATEWAY {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
version 3
authentication {
auth_type PASS
auth_pass chg-me-8c
}
virtual_ipaddress {
192.0.2.10/24 dev eth0
}
track_interface {
eth0
}
}
Create the script user if your package did not:
sudo useradd -r -s /usr/sbin/nologin keepalived_script 2>/dev/null || true
Apply:
sudo keepalived -t -l -f /etc/keepalived/keepalived.conf # config test
sudo systemctl restart keepalived.service
sudo systemctl --no-pager --full status keepalived.service
Verify ownership
On both hosts:
ip -br addr show eth0
ip addr show eth0 | grep 192.0.2.10 || echo "no VIP here"
journalctl -u keepalived -n 50 --no-pager
Expect:
- Exactly one host shows
192.0.2.10/24(secondary address) - That host’s journal says transition to MASTER
- The other stays BACKUP
- From a third machine:
ping -c3 192.0.2.10succeeds
Quick ownership check you can script:
#!/bin/bash
VIP=192.0.2.10
if ip -4 -o addr show | awk '{print $4}' | grep -q "^${VIP}/"; then
echo "MASTER holds $VIP on $(hostname -s)"
exit 0
fi
echo "BACKUP without $VIP on $(hostname -s)"
exit 1
Make failover mean “service healthy,” not just “kernel still boots”
A VIP on a host whose reverse proxy is dead is a polished outage. Couple VRRP priority to real health.
Track a process (simple, low overhead)
vrrp_track_process track_nginx {
process nginx # exact match semantics (not a loose pgrep regex)
# weight omitted => instance goes to FAULT when process is gone after delay
delay 1
}
vrrp_instance VI_GATEWAY {
# ...same as before...
track_process {
track_nginx
}
}
Track a script (port/HTTP check)
vrrp_script chk_https {
script "/usr/lib/keepalived/check_https.sh"
interval 2
timeout 2
weight -30 # subtract from priority while failing; 0 weight => FAULT
fall 2 # failures before down
rise 2 # successes before up
}
vrrp_instance VI_GATEWAY {
# ...
track_script {
chk_https
}
}
Example check script (mode 0755, owned by root, not world-writable — enable_script_security cares):
/usr/lib/keepalived/check_https.sh:
#!/bin/bash
# Exit 0 = healthy, non-zero = unhealthy
exec curl -fsS --max-time 1 -o /dev/null "http://127.0.0.1:80/healthz"
sudo install -o root -g root -m 0755 /tmp/check_https.sh /usr/lib/keepalived/check_https.sh
sudo keepalived -t -l -f /etc/keepalived/keepalived.conf
sudo systemctl reload keepalived.service
Weight semantics (from keepalived.conf(5)):
-
weight 0(default for scripts): monitoring failure drives the instance to FAULT afterfallfailures - Non-zero weight: adjust effective priority up/down so another node can win without a hard FAULT
Use FAULT when the node must not hold the VIP at all. Use negative weight when you want soft preference (“prefer the node with a warm cache, but either can serve”).
nopreempt: stop flapping when the old master returns
By default, a recovering higher-priority node snatches the VIP back. That is correct for “always prefer gw-a,” and painful for long-lived TCP sessions if gw-a reboots every patch night.
nopreempt keeps the current master until it fails:
vrrp_instance VI_GATEWAY {
state BACKUP # REQUIRED: nopreempt is ignored if initial state is MASTER
nopreempt
priority 150
# ...
}
Apply on both nodes (with different priorities). Document the tradeoff:
| Mode | Behavior | Good for |
|---|---|---|
| Preempt (default) | Highest healthy priority always owns VIP | Deterministic “primary DC node” |
nopreempt |
Winner stays until it fails | Fewer failbacks, calmer TCP |
You can also delay preemption with preempt_delay when you want eventual return to the preferred node without an instant bounce.
Unicast peers when multicast is blocked
Cloud security groups, some Wi-Fi AP isolation modes, and locked-down switches break 224.0.0.18. keepalived can speak VRRP over unicast:
vrrp_instance VI_GATEWAY {
state BACKUP
interface eth0
virtual_router_id 51
priority 150
advert_int 1
version 3
unicast_src_ip 192.0.2.11
unicast_peer {
192.0.2.12
}
virtual_ipaddress {
192.0.2.10/24
}
}
On gw-b, swap unicast_src_ip / peer addresses. Man page warning: unicast mode without peers is invalid — configure real peers explicitly.
If you combine use_vmac with unicast, set vmac_xmit_base as documented so advertisements leave the underlying interface correctly.
Firewall notes
Allow VRRP between peers on the VRRP interface:
# nftables sketch — adjust interface and policy to your base table
sudo nft add rule inet filter input iifname "eth0" ip protocol 112 accept
sudo nft add rule inet filter input iifname "eth0" ip daddr 224.0.0.18 accept
# unicast mode: allow protocol 112 between peer unicast addresses instead
Also permit whatever your track scripts probe (localhost health checks usually need nothing extra).
If you use use_vmac or no_accept, modern keepalived prefers nftables helpers to manage its small firewall table — do not randomly flush all nft tables on those hosts.
Controlled failover test
Do this once before you trust the VIP in DNS or upstream static routes.
- From a client:
ping -i 0.2 192.0.2.10 - Confirm VIP on preferred node:
ip addr show eth0 | grep 192.0.2.10 - Stop keepalived on the master:
sudo systemctl stop keepalived(or pull its cable / kill nginx if you track the process) - Watch ping: a small blip is normal; multi-second black holes are not
- On the standby: VIP should appear; journal should show MASTER
- Start keepalived on the original node again
- Confirm preemption vs
nopreemptmatches your design
Optional packet view (on systems with tcpdump):
sudo tcpdump -ni eth0 'ip proto 112 or host 224.0.0.18'
You should see regular advertisements from the master and a burst of gratuitous ARP when ownership changes (tcpdump -ni eth0 arp).
Operational pitfalls
Duplicate virtual_router_id for unrelated VIPs on the same LAN.
VRIDs must be unique per virtual router on that broadcast domain. Collisions produce split-brain weirdness.
VIP still configured statically in netplan/networkd/NM.
If both keepalived and your network manager permanently own 192.0.2.10, failover becomes a fight. Let keepalived add/remove the address.
Authentication as security theater.
auth_type PASS is cleartext and non-compliant with modern VRRPv2 expectations. Isolate VRRP; do not rely on the password against a hostile L2 neighbor.
Scripts writable by non-root.
With enable_script_security, keepalived refuses unsafe root scripts. Good. Fix ownership/mode instead of disabling the guard.
Initial state MASTER + nopreempt.
The man page is explicit: for nopreempt to work, initial state must not be MASTER. Use BACKUP on all nodes and let priority decide.
Forgetting track failures are sticky without rise.
Tune fall/rise so a single slow health check does not thrash the VIP.
Assuming VRRP replaces backups.
Failover preserves reachability. It does not replicate disk state. Pair with real data replication for anything stateful.
Minimal systemd-friendly health timer (optional)
If you already alert from node exporters, scrape whether the VIP is local:
/etc/systemd/system/check-vip.service:
[Unit]
Description=Check floating VIP presence
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/check-vip.sh
/etc/systemd/system/check-vip.timer:
[Unit]
Description=Periodic VIP ownership check
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
Unit=check-vip.service
[Install]
WantedBy=timers.target
Alert on “VIP missing on all nodes” (bad) separately from “VIP not on preferred node” (info under nopreempt).
Quick chooser
-
Two gateways, prefer A, fail to B, fail back to A: priorities
150/100, default preempt, track uplink + service -
Two gateways, minimize failback flaps: both
state BACKUP,nopreempt, track service hard (weight 0) -
Multicast blocked:
unicast_src_ip+unicast_peer { ... } -
Picky switch MAC learning: consider
use_vmac -
Need L4 load balancing across many real servers: that is IPVS/
virtual_server— adjacent keepalived feature, not this VIP recipe -
Need two NICs on one box: bonding first, then VRRP on
bond0
References
- RFC 5798 — VRRP Version 3 for IPv4 and IPv6
- RFC 3768 — VRRP Version 2 (historical; authentication removed from the spec)
- keepalived.conf(5) — Debian man page (exhaustive keyword reference maintained with the project)
- ArchWiki: Keepalived — compact master/backup and track_process examples
- keepalived configuration synopsis
- IANA VRRP: IPv4 multicast
224.0.0.18, IP protocol number112
Wrap-up
Floating IPs are one of the highest-leverage HA upgrades you can give a pair of Linux hosts. Keep the recipe boring:
- Same
virtual_router_idand VIP on every peer - Different priorities (and honest health tracks)
- Config test with
keepalived -t, then restart/reload - Prove ownership with
ip addr+ a third-party ping - Fail the master on purpose once before DNS points at the VIP
Do that, and a single host outage stops being “the gateway is down” and becomes a short, tested cutover your clients barely notice.
Top comments (0)