How to Configure High-Availability VPN Endpoints
At first, a VPN endpoint is “just another service.” It starts as a single entry in a firewall rule set, a single IP in a vendor portal, a single box that “has always been up.” Then the organization grows. More remote staff. More third parties. More sites. More compliance pressure. And slowly, that VPN endpoint stops being a convenience and becomes a dependency.
That’s when the real risk shows up: maintenance windows become business events, kernel updates become anxiety, and a routine reboot becomes a potential outage. High-Availability VPN Architecture is how we take that pressure off the team. Not by making the system complicated, but by making it controlled: two endpoints, one stable virtual IP, predictable failover, and a design that survives reboots, upgrades, and human error.
In this guide, we will build a production-grade high-availability VPN endpoint design on Ubuntu using VRRP (Keepalived) for a floating VIP and WireGuard for the VPN tunnel. The result is an enterprise-friendly pattern: clients connect to one stable address, and the active node can change without client reconfiguration.
Architecture overview
What we are building
- Two Ubuntu VPN nodes (active/standby) running WireGuard.
- One floating Virtual IP (VIP) managed by Keepalived (VRRP).
- Firewall and routing that allow VPN traffic and forward it to internal networks.
- Health-aware failover so the VIP only lives on a node that can actually serve VPN traffic.
Why this design works in real environments
- Stable client configuration: clients always connect to the VIP (or a DNS name pointing to it).
- Controlled failover: if the active node fails, the standby takes the VIP and continues service.
- Operational safety: we can patch and reboot one node at a time without taking the VPN offline.
- Enterprise alignment: clear separation of concerns (VIP ownership vs VPN service), auditable configuration, and predictable behavior.
Prerequisites and assumptions
Before we touch any commands, we need to be explicit about the baseline. High availability fails most often when assumptions are implicit.
- OS: Ubuntu Server 22.04 LTS or 24.04 LTS on both nodes. We assume a clean install or a well-controlled baseline (no conflicting VPN stacks, no experimental firewall frameworks).
- Two nodes: vpn-a and vpn-b, in the same L2 network segment for the interface that will hold the VIP (VRRP typically requires L2 adjacency).
- Network:
- One interface facing the client side (internet or edge segment). This is where the VIP will live.
- Optional: a second interface toward internal networks. If we only have one interface, we can still route, but we must be deliberate about firewalling.
- VIP: one unused IP address in the client-facing subnet (example: 203.0.113.50/24). We will not hardcode it in commands; we will set it as a variable.
- WireGuard: UDP port 51820 (we can change it, but we will keep it consistent across both nodes).
- Permissions: we will run administrative commands with
sudo. We assume our account is in thesudogroup. - Time sync: NTP must be working (systemd-timesyncd is fine). Time drift causes confusing authentication and logging issues during incident response.
- Security posture: we will:
- Use least privilege file permissions for private keys.
- Enable IP forwarding explicitly and persistently.
- Apply firewall rules that are explicit and verifiable.
- Use Keepalived authentication to reduce accidental VRRP interference.
We will also avoid fragile “magic” steps. Every major change will be followed by verification so we can prove what changed and why.
Step 1: Establish node identity and capture network facts
We are going to collect the values we must not guess: interface names, node IPs, and the default route interface. This prevents the most common production mistake: applying firewall and VIP configuration to the wrong interface.
hostnamectl
ip -br addr
ip route show default
We have now confirmed the hostname and the interface layout. The default route output tells us which interface is used to reach the outside world (often the client-facing interface). We will store that interface name in a variable so subsequent commands are copy/paste-safe.
EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
echo "EXT_IFACE=${EXT_IFACE}"
We have now captured the external interface name in EXT_IFACE. We will reuse it consistently to avoid hardcoding.
Step 2: Install required packages on both nodes
We are going to install WireGuard for the VPN tunnel and Keepalived for VRRP-based VIP failover. We will also install basic tooling that helps with verification and troubleshooting in production.
sudo apt-get update
sudo apt-get install -y wireguard keepalived ufw iproute2 iptables-persistent jq
We have now installed the VPN stack, the HA component, and firewall tooling. This also ensures we can persist firewall behavior across reboots (a common gap in “it worked yesterday” incidents).
Now we will verify that the services are present and that systemd can manage them.
systemctl status keepalived --no-pager || true
systemctl status wg-quick@wg0 --no-pager || true
At this point, Keepalived may be inactive (expected until configured). WireGuard will also be inactive until we create wg0.
Step 3: Enable persistent IP forwarding
We are going to enable IPv4 forwarding so VPN clients can reach internal networks (or other routed destinations). This must be persistent across reboots, and we must verify it at runtime.
sudo tee /etc/sysctl.d/99-vpn-forwarding.conf >/dev/null <<'EOF'
net.ipv4.ip_forward=1
EOF
sudo sysctl --system
We have now created a dedicated sysctl drop-in and applied it immediately. This is safer than editing monolithic files and makes the change auditable.
Now we will verify the effective value.
sysctl net.ipv4.ip_forward
If the output is net.ipv4.ip_forward = 1, forwarding is active and will persist after reboot.
Step 4: Create WireGuard keys and baseline configuration
We are going to generate WireGuard keys on each node. Each node needs its own private key, and we will keep permissions tight. We will also create a consistent wg0 interface on both nodes so failover is clean: whichever node holds the VIP can immediately serve the same VPN network.
On vpn-a and vpn-b, run the following.
sudo install -d -m 0700 /etc/wireguard
sudo bash -c 'umask 077; wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey'
sudo chmod 600 /etc/wireguard/privatekey
sudo chmod 644 /etc/wireguard/publickey
sudo cat /etc/wireguard/publickey
We have now created a private/public keypair with correct permissions. The private key is readable only by root, which is essential for production hygiene.
Next, we will define the VPN subnet and the WireGuard listen port as variables. We will keep these consistent across both nodes.
VPN_CIDR="10.44.0.0/24"
WG_ADDR_A="10.44.0.2/24"
WG_ADDR_B="10.44.0.3/24"
WG_PORT="51820"
echo "VPN_CIDR=${VPN_CIDR} WG_PORT=${WG_PORT}"
We have now defined a dedicated VPN subnet and per-node tunnel addresses. The VIP will not be the WireGuard interface address; the VIP is on the external interface and is used as the stable endpoint clients connect to.
Configure wg0 on vpn-a
We are going to create /etc/wireguard/wg0.conf on vpn-a. This file defines the interface address, the listen port, and the firewall behavior when the interface comes up and down. We will use UFW-friendly iptables rules for NAT so VPN clients can reach routed networks. In enterprise environments, NAT may be replaced by explicit routing; we will keep NAT as a pragmatic default and call it out clearly.
First, we will safely capture vpn-a’s private key into a variable without printing it to the terminal.
WG_PRIV_A=$(sudo cat /etc/wireguard/privatekey)
echo "Loaded vpn-a private key into memory"
Now we will write the full configuration file. We will use ${EXT_IFACE} for NAT so it matches the actual default route interface.
sudo tee /etc/wireguard/wg0.conf >/dev/null <<EOF
[Interface]
Address = ${WG_ADDR_A}
ListenPort = ${WG_PORT}
PrivateKey = ${WG_PRIV_A}
# NAT for VPN clients to reach external/internal routed networks via this node.
# In environments with end-to-end routing, replace NAT with explicit routes and firewall rules.
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s ${VPN_CIDR} -o ${EXT_IFACE} -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -s ${VPN_CIDR} -o ${EXT_IFACE} -j MASQUERADE
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
We have now created a complete WireGuard interface configuration for vpn-a with secure permissions. The PostUp/PostDown rules ensure forwarding and NAT are applied only when the VPN is active, which reduces accidental exposure.
Configure wg0 on vpn-b
We are going to repeat the same approach on vpn-b, using its own tunnel address and private key. This symmetry is what makes failover predictable.
WG_PRIV_B=$(sudo cat /etc/wireguard/privatekey)
echo "Loaded vpn-b private key into memory"
sudo tee /etc/wireguard/wg0.conf >/dev/null <<EOF
[Interface]
Address = ${WG_ADDR_B}
ListenPort = ${WG_PORT}
PrivateKey = ${WG_PRIV_B}
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s ${VPN_CIDR} -o ${EXT_IFACE} -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -s ${VPN_CIDR} -o ${EXT_IFACE} -j MASQUERADE
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
We have now created the matching WireGuard configuration on vpn-b. Both nodes can serve the VPN network; the VIP will decide which one is active.
Step 5: Define the VIP and configure Keepalived (VRRP)
Now we will implement the HA design: a floating VIP that moves between vpn-a and vpn-b. Clients will connect to the VIP, not to a node IP. Keepalived will manage VIP ownership using VRRP, and we will add a health check so the VIP only stays on a node that has WireGuard up.
First, we will define the VIP and the external interface. We already captured EXT_IFACE. Now we will set the VIP in a variable. We will also detect the node’s primary IP on that interface so we can reason about ARP and routing during troubleshooting.
VIP_CIDR="203.0.113.50/24"
NODE_IP=$(ip -4 -o addr show dev "${EXT_IFACE}" | awk '{print $4}' | head -n1)
echo "VIP_CIDR=${VIP_CIDR}"
echo "NODE_IP=${NODE_IP}"
We have now defined the VIP (including prefix length) and captured the node’s interface IP. In production, we must ensure the VIP is unused and allowed by upstream routing/security controls.
Create a health check script
We are going to create a small script that Keepalived will run. If WireGuard is not up, the script will fail, and Keepalived will reduce priority so the other node can take over. This prevents a subtle failure mode where the VIP is “up” but the VPN service is not.
sudo tee /usr/local/sbin/check_wg0.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
# Return success only if wg0 exists and is up.
ip link show wg0 2>/dev/null | grep -q "state UP"
EOF
sudo chmod 0755 /usr/local/sbin/check_wg0.sh
We have now installed a root-owned, executable health check script. It is intentionally simple and deterministic, which is what we want during failover events.
Configure Keepalived on vpn-a (MASTER)
We are going to configure vpn-a as the preferred active node by giving it a higher VRRP priority. We will also set an authentication password for VRRP to reduce accidental interference on the segment. We will enable unicast VRRP to be explicit about peer communication, which is often more predictable in enterprise networks than multicast.
First, we will capture the peer IP (vpn-b’s IP on the external interface). We will print the local IP so we can confirm we are using the correct interface.
echo "Local node external IP is: ${NODE_IP}"
Now we will set the peer IP as a variable. We must replace the value once, but we will do it safely by prompting ourselves to confirm it before applying. We will not embed a placeholder inside the configuration commands.
PEER_IP="203.0.113.11"
echo "PEER_IP=${PEER_IP}"
Now we will write the full Keepalived configuration for vpn-a. We will use a dedicated VRRP instance name and track the WireGuard health check.
sudo tee /etc/keepalived/keepalived.conf >/dev/null <<EOF
global_defs {
router_id VPN_A
enable_script_security
script_user root
}
vrrp_script chk_wg0 {
script "/usr/local/sbin/check_wg0.sh"
interval 2
timeout 1
fall 2
rise 2
}
vrrp_instance VI_VPN {
state MASTER
interface ${EXT_IFACE}
virtual_router_id 44
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 7c9b2f1a9d3e
}
unicast_src_ip ${NODE_IP}
unicast_peer {
${PEER_IP}
}
virtual_ipaddress {
${VIP_CIDR}
}
track_script {
chk_wg0
}
}
EOF
We have now configured vpn-a to prefer owning the VIP, but only while wg0 is up. The VRRP instance will communicate directly with the peer via unicast, and the VIP will be added/removed automatically on the external interface.
Configure Keepalived on vpn-b (BACKUP)
We are going to configure vpn-b as the standby node with a lower priority. The configuration is nearly identical, but the router_id, state, priority, and peer IP direction will differ. This symmetry is important: it makes failover behavior easy to reason about under pressure.
On vpn-b, we will set the VIP and capture the node IP the same way, then set the peer IP to vpn-a’s external IP.
EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
VIP_CIDR="203.0.113.50/24"
NODE_IP=$(ip -4 -o addr show dev "${EXT_IFACE}" | awk '{print $4}' | head -n1)
PEER_IP="203.0.113.10"
echo "EXT_IFACE=${EXT_IFACE}"
echo "VIP_CIDR=${VIP_CIDR}"
echo "NODE_IP=${NODE_IP}"
echo "PEER_IP=${PEER_IP}"
We have now set the same VIP and identified vpn-b’s external IP and its peer (vpn-a). Now we will write the Keepalived configuration.
sudo tee /etc/keepalived/keepalived.conf >/dev/null <<EOF
global_defs {
router_id VPN_B
enable_script_security
script_user root
}
vrrp_script chk_wg0 {
script "/usr/local/sbin/check_wg0.sh"
interval 2
timeout 1
fall 2
rise 2
}
vrrp_instance VI_VPN {
state BACKUP
interface ${EXT_IFACE}
virtual_router_id 44
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 7c9b2f1a9d3e
}
unicast_src_ip ${NODE_IP}
unicast_peer {
${PEER_IP}
}
virtual_ipaddress {
${VIP_CIDR}
}
track_script {
chk_wg0
}
}
EOF
We have now configured vpn-b as the standby. If vpn-a fails or loses WireGuard, vpn-b will be eligible to take the VIP.
Step 6: Configure firewall rules (UFW) for VPN and VRRP
We are going to apply firewall rules that allow:
- WireGuard UDP on port 51820 to the node (and effectively to the VIP owner).
- VRRP protocol (IP protocol 112) is used for VRRP advertisements. With unicast VRRP, some environments still require allowing protocol 112; others rely on normal IP traffic handling. UFW does not directly support protocol 112 rules cleanly in all cases, so we will focus on allowing peer communication and ensuring Keepalived can operate. We will also verify behavior and provide fixes if VRRP does not transition.
- SSH from trusted networks only (we will keep it simple here and allow OpenSSH; in production we should restrict by source CIDR).
First, we will enable UFW and allow OpenSSH so we do not lock ourselves out. We will then allow WireGuard UDP.
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw --force enable
sudo ufw status verbose
We have now enabled a persistent firewall policy and allowed the minimum ports for management and VPN. The status output confirms the active rules.
Next, we will ensure forwarding is allowed through UFW. By default, UFW may block routed traffic even if iptables rules exist. We will set the default forward policy to ACCEPT and reload UFW.
sudo sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
sudo ufw reload
sudo ufw status verbose
We have now adjusted UFW to allow forwarding, which is required for VPN client traffic to traverse the node. Reloading applies the change without rebooting.
Step 7: Start WireGuard and Keepalived, then verify VIP ownership
We are going to bring up WireGuard first, because Keepalived’s health check depends on it. Then we will start Keepalived and confirm which node owns the VIP.
Start WireGuard on both nodes
We will enable the service so it persists across reboots, then start it immediately.
sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
We have now created the wg0 interface and applied the PostUp firewall rules. The systemd status output should show the service as active.
Now we will verify the interface and WireGuard state.
ip -br link show wg0
sudo wg show
We have confirmed that wg0 exists and is up, and that WireGuard is listening on the expected port. At this stage, peers may not be configured yet, so we may not see handshakes. That is expected.
Start Keepalived on both nodes
Now we will enable Keepalived for persistence and start it. This is the moment the VIP should appear on the preferred node (vpn-a) if everything is healthy.
sudo systemctl enable --now keepalived
sudo systemctl status keepalived --no-pager
We have now started the HA control plane. Keepalived should evaluate the health check and then decide VIP ownership based on priority.
Now we will verify VIP presence. On each node, run:
ip -4 -o addr show dev "${EXT_IFACE}" | awk '{print $2, $4}'
We should see the VIP (203.0.113.50/24 in our example) on exactly one node at a time. If it appears on vpn-a, that confirms the preferred active role is working.
We will also verify that WireGuard is listening on UDP 51820.
sudo ss -lunp | grep -E ':(51820)b' || true
We have now confirmed the process is bound and listening. If the output is empty, WireGuard is not listening and the health check may force failover.
Step 8: Add a sample WireGuard client peer (and keep it consistent across nodes)
High availability only matters if both nodes can accept the same client configuration. We are going to add the same client peer definition to both vpn-a and vpn-b. That way, whichever node owns the VIP can authenticate the client.
First, we will generate a client keypair on a secure admin workstation. If we must generate it on a server temporarily, we will do it in a controlled directory and remove it afterward. Here we will generate it on the node for demonstration, but in production we should treat client private keys as sensitive and avoid leaving them on servers.
umask 077
wg genkey | tee client1_privatekey | wg pubkey > client1_publickey
echo "Client public key:"
cat client1_publickey
We have now generated a client keypair and printed the public key. The private key is in client1_privatekey and must be protected.
Now we will add the client’s public key to both vpn-a and vpn-b. We will assign the client a stable VPN IP (for example, 10.44.0.10/32). We will use wg set for immediate effect and also persist it by appending to wg0.conf.
CLIENT1_PUB=$(cat client1_publickey)
CLIENT1_IP="10.44.0.10/32"
echo "CLIENT1_IP=${CLIENT1_IP}"
We have now captured the client public key and chosen a stable address. Next, on each node, we will apply the peer live and persist it.
sudo wg set wg0 peer "${CLIENT1_PUB}" allowed-ips "${CLIENT1_IP}"
sudo wg show
We have now added the peer to the running interface. The wg show output should list the peer and its allowed IPs.
Now we will persist the peer in the configuration file so it survives reboot. We will append a full peer stanza.
sudo tee -a /etc/wireguard/wg0.conf >/dev/null <<EOF
[Peer]
PublicKey = ${CLIENT1_PUB}
AllowedIPs = ${CLIENT1_IP}
EOF
We have now made the peer configuration persistent. On restart, WireGuard will re-load the peer automatically.
As a final hygiene step for this demonstration, we will restrict the local key files we created and remind ourselves to move the client private key off the server.
chmod 600 client1_privatekey client1_publickey
ls -l client1_privatekey client1_publickey
We have now ensured the generated files are not world-readable. In production, we should securely transfer the client private key to the client device and remove it from the server.
Step 9: Failover validation (controlled, observable, reversible)
We are going to validate failover in a way that is safe in enterprise environments: we will not “pull cables.” We will simulate a service failure by stopping WireGuard on the active node and watching Keepalived move the VIP. This tests the exact condition we care about: “VPN service is not healthy, so the VIP must move.”
First, we will identify which node currently owns the VIP by checking the external interface addresses.
ip -4 -o addr show dev "${EXT_IFACE}" | awk '{print $2, $4}'
We have now confirmed VIP ownership. On the node that currently holds the VIP, we will stop WireGuard.
sudo systemctl stop wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
We have now made the health check fail on that node. Within a few seconds, Keepalived should reduce priority and the peer should take the VIP.
Now we will verify VIP moved by checking both nodes.
ip -4 -o addr show dev "${EXT_IFACE}" | awk '{print $2, $4}'
If the VIP is now present on the other node, failover is working. Next, we will restore WireGuard on the original node and confirm behavior returns to the preferred state (depending on VRRP preemption behavior; by default, higher priority will reclaim VIP when healthy).
sudo systemctl start wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
sudo wg show
We have now restored VPN service on the original node. If preemption is active (typical), the VIP should eventually return to vpn-a when it is healthy and has higher priority. We can observe this by re-checking the interface addresses.
Operational hardening notes (enterprise focus)
DNS and client endpoint stability
We should publish a DNS name (for example, vpn.company.tld) that resolves to the VIP. Clients should use the DNS name, not node IPs. This keeps endpoint identity stable even if the VIP changes later.
Logging and auditability
We should forward logs to a central system (SIEM/syslog) and retain Keepalived and WireGuard logs for incident timelines. On Ubuntu with systemd, we can inspect service logs with:
sudo journalctl -u keepalived --no-pager -n 200
sudo journalctl -u wg-quick@wg0 --no-pager -n 200
We have now pulled the most recent logs for both services, which is essential during failover investigations.
Key management and rotation
WireGuard keys are identity. In enterprise environments, we should treat them like certificates: track ownership, rotate on staff changes, and revoke by removing peers. File permissions we applied are necessary but not sufficient; process matters.
Troubleshooting
Symptom: VIP does not appear on any node
- Likely causes:
- Keepalived is not running or is failing to start due to config syntax.
- Health check script is failing because
wg0is down. - Interface name in Keepalived config is wrong.
- Fix: We will check service status and logs, then validate the interface and health check.
sudo systemctl status keepalived --no-pager
sudo journalctl -u keepalived --no-pager -n 200
ip -br link
sudo /usr/local/sbin/check_wg0.sh && echo "wg0 health OK" || echo "wg0 health FAIL"
We have now confirmed whether Keepalived is healthy and whether the health check is blocking VIP ownership. If the health check fails, we must bring up WireGuard and re-check.
Symptom: VIP appears on both nodes at the same time (split-brain)
- Likely causes:
- VRRP communication between nodes is blocked (network ACLs, host firewall, wrong peer IP).
- Mismatched
virtual_router_idor authentication password. - Nodes are not actually on the same L2 segment for the VIP interface.
- Fix: We will confirm both configs match on VRRP identifiers and that unicast peer IPs are correct, then validate reachability.
sudo grep -E 'virtual_router_id|auth_pass|unicast_src_ip|unicast_peer|interface' -n /etc/keepalived/keepalived.conf
ping -c 3 "${PEER_IP}" || true
sudo journalctl -u keepalived --no-pager -n 200
We have now checked the critical VRRP parameters and basic IP reachability. If ping fails, we must fix routing/ACLs first; VRRP cannot coordinate without peer communication.
Symptom: WireGuard is up, but clients cannot reach internal networks
- Likely causes:
- IP forwarding is disabled.
- UFW forwarding policy is not ACCEPT.
- NAT rules did not apply (PostUp failed) or are overridden by other firewall tooling.
- Missing routes on internal networks back to the VPN subnet (if we are not using NAT).
- Fix: We will verify forwarding, UFW policy, and NAT rules.
sysctl net.ipv4.ip_forward
sudo ufw status verbose
sudo iptables -t nat -S | grep -E 'POSTROUTING.*10.44.0.0/24' || true
sudo iptables -S FORWARD | head -n 50
We have now confirmed whether forwarding is enabled and whether NAT/forward rules exist. If NAT rules are missing, restarting WireGuard will re-apply PostUp rules.
sudo systemctl restart wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
We have now re-applied the interface and its firewall hooks. If the issue persists, we should review whether another firewall manager is overwriting iptables rules.
Symptom: WireGuard is not listening on UDP 51820
- Likely causes:
wg0.confhas invalid syntax or missing PrivateKey.- Port conflict (another process bound to 51820).
- Service failed to start.
- Fix: We will check service logs and socket bindings.
sudo systemctl status wg-quick@wg0 --no-pager
sudo journalctl -u wg-quick@wg0 --no-pager -n 200
sudo ss -lunp | grep -E ':(51820)b' || true
sudo wg show
We have now identified whether the service is failing, whether the port is bound, and whether WireGuard considers the interface valid.
Common mistakes
Mistake: Using the wrong interface for the VIP
Symptom: Keepalived runs, but the VIP never becomes reachable, or it appears on an unexpected interface.
Fix: We will confirm the default route interface and ensure Keepalived’s interface matches it.
ip route show default
sudo grep -n '^ interface' /etc/keepalived/keepalived.conf
We have now validated the interface selection. If it is wrong, we must correct the config and restart Keepalived.
sudo systemctl restart keepalived
sudo systemctl status keepalived --no-pager
Mistake: VIP chosen is already in use
Symptom: Intermittent connectivity, ARP flapping, or another host becomes unreachable when the VIP is assigned.
Fix: We will ARP-scan or check neighbor tables and confirm the VIP is not claimed. In controlled enterprise networks, we should also confirm with IPAM.
ip neigh show | grep -F "203.0.113.50" || true
We have now checked whether the system has seen a neighbor entry for the VIP. If the VIP is in use, we must select a different unused IP and update both Keepalived configs.
Mistake: Peer IPs are reversed or incorrect in unicast VRRP
Symptom: Both nodes believe they should own the VIP, or failover never occurs.
Fix: We will confirm unicast_src_ip matches the node’s external IP and unicast_peer matches the other node.
sudo grep -E 'unicast_src_ip|unicast_peer' -n /etc/keepalived/keepalived.conf
ip -4 -o addr show dev "${EXT_IFACE}" | awk '{print $4}'
We have now validated the unicast VRRP addressing. If it is wrong, we must correct it and restart Keepalived on both nodes.
How do we at NIILAA look at this
This setup is not impressive because it is complex. It is impressive because it is controlled. Every component is intentional. Every configuration has a reason. This is how infrastructure should scale — quietly, predictably, and without drama.
At NIILAA, we help enterprises design and implement HA design patterns like this in production: selecting the right HA boundaries, validating failure modes, hardening security controls, integrating with enterprise networking and monitoring, and maintaining the system through upgrades without downtime surprises. When the VPN endpoint becomes a dependency, we make sure it behaves like one.
- Website: https://www.niilaa.com
- Email: [email protected]
- LinkedIn: https://www.linkedin.com/company/niilaa
- Facebook: https://www.facebook.com/niilaa.llc