Mahbubur Riad
Back to blog
Hosting & Server 7 min read

Disaster Recovery Blueprint: Automatic Failover for Self‑Hosted Cloudflare Tunnel Alternatives Using Keepalived and HAProxy

Jun 19, 2026 · Mahbubur Riad

Step‑by‑step guide to build a resilient Zero‑Trust tunnel with Keepalived and HAProxy, delivering automatic failover for self‑hosted Cloudflare Tunnel replacements.

On this page

Introduction

Zero‑Trust networking has become the default for remote work, SaaS integrations, and edge‑centric workloads. Cloudflare Tunnel (formerly Argo Tunnel) does an excellent job of exposing internal services without opening inbound ports, but many organisations prefer a self‑hosted alternative for cost, compliance, or custom‑logic reasons.

Running a single tunnel server is a single point of failure. In a production environment you need a disaster‑recovery blueprint that automatically redirects traffic when a node goes down—without manual intervention and without breaking existing client sessions.

In this tutorial we’ll wire together two battle‑tested Linux tools:

  • Keepalived – provides VRRP‑based virtual IP failover.
  • HAProxy – does the actual TCP/HTTP load‑balancing and health‑checking of tunnel back‑ends.

By the end of the guide you’ll have:

  • Two identical tunnel nodes (e.g., tunnel‑01 and tunnel‑02).
  • A floating virtual IP (VIP) that always points to the active node.
  • HAProxy listening on the VIP, routing traffic to the healthy tunnel daemon.
  • Automatic promotion of the standby node when the primary fails.

The result is a high‑availability, zero‑trust tunnel that survives host crashes, network blips, or even full‑site outages.


Prerequisites

Item Minimum Recommended
OS Ubuntu 20.04 LTS or Debian 11 Ubuntu 22.04 LTS
CPUs 2 vCPU 4 vCPU
RAM 2 GB 4 GB
Network Two NICs (one for management, one for client traffic) Separate VLANs for management and data
Packages keepalived, haproxy, curl, git keepalived, haproxy, jq, net-tools
Access Root or sudo on both nodes Password‑less sudo for the admin user
Tunnel software Any Cloudflare‑Tunnel‑compatible daemon (e.g., cloudflared, inlets, caddy with tls proxy) Same as left

Both tunnel nodes must be identical in hardware, OS version, and installed packages. Keep them in the same layer‑2 domain (or use a layer‑2 overlay like VXLAN) so that the VRRP advertisement can be exchanged without additional routing tricks.


Architecture Overview

Text
+-------------------+          +-------------------+
|   Client / Edge   |   TCP    |   HAProxy (VIP)   |
|  (Browser, API)  | <------> |  10.0.0.100 (VRRP) |
+-------------------+          +-------------------+
           ^                             ^
           |                             |
   (Internet)                     +-----------+
                                   | Keepalived|
                                   +-----------+
                                        |
          +-------------------+-------------------+
          |                                   |
   +-------------+                     +-------------+
   | tunnel-01   |                     | tunnel-02   |
   | (active)    |                     | (standby)   |
   +-------------+                     +-------------+
  • HAProxy binds to the VIP (10.0.0.100) and forwards inbound traffic to the tunnel daemon on the local host (127.0.0.1:8443).
  • Keepalived runs VRRP on both nodes. The master claims the VIP; the backup monitors the master and takes over when it disappears.
  • The tunnel daemon (e.g., cloudflared) establishes an outbound connection to the Zero‑Trust controller, exposing internal services.

Step 1 – Install Keepalived

Run the following on both nodes:

Bash
sudo apt update
sudo apt install -y keepalived

Verify the service is active:

Bash
sudo systemctl status keepalived

If the service fails to start, check /etc/keepalived/keepalived.conf for syntax errors (the file doesn’t exist yet, so you’ll create it next).


Step 2 – Configure VRRP with Keepalived

Create /etc/keepalived/keepalived.conf on both nodes. The only difference will be the priority value.

Master (tunnel-01)

CONF
vrrp_instance VI_1 {
    state MASTER
    interface eth0          # management NIC
    virtual_router_id 51
    priority 150            # higher = master
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 3cR3tK3y
    }
    virtual_ipaddress {
        10.0.0.100/24 dev eth0 label eth0:vip
    }
    track_interface {
        eth0
    }
}

Backup (tunnel-02)

CONF
vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 100            # lower than master
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 3cR3tK3y
    }
    virtual_ipaddress {
        10.0.0.100/24 dev eth0 label eth0:vip
    }
    track_interface {
        eth0
    }
}

Key points

Parameter Meaning
virtual_router_id Must be identical on both nodes; 1‑255 range.
priority Determines which node becomes master.
advert_int VRRP advertisement interval (seconds).
auth_pass Simple shared secret; keep it strong.
track_interface If the interface disappears, the node drops out of the VRRP cluster.

Reload Keepalived:

Bash
sudo systemctl restart keepalived
sudo systemctl enable keepalived

Check that the VIP is present on the master:

Bash
ip addr show eth0 | grep 10.0.0.100

On the backup you should not see the VIP. If you do, there is a priority clash—double‑check the config.


Step 3 – Install HAProxy

Again, on both nodes:

Bash
sudo apt install -y haproxy

We will configure HAProxy to listen on the VIP only when it is present. HAProxy can be started on both nodes; the one without the VIP will simply bind to 0.0.0.0:0 (i.e., not listen), which is harmless.

HAProxy Global Settings

Create /etc/haproxy/haproxy.cfg (replace existing content):

CONF
global
    log /dev/log    local0
    log /dev/log    local1 notice
    daemon
    maxconn 2000
    tune.ssl.default-dh-param 2048

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5s
    timeout client  30s
    timeout server  30s

Frontend & Backend for the Tunnel

CONF
frontend tunnel_front
    bind 10.0.0.100:443   # VIP, HTTPS port
    default_backend tunnel_back

backend tunnel_back
    # Health check: connect to the local tunnel daemon
    option tcp-check
    tcp-check connect port 8443
    tcp-check send PING\r\n
    tcp-check expect string PONG
    server tunnel01 127.0.0.1:8443 check fall 3 rise 2 weight 10

Explanation

  • The frontend binds only to the VIP. If the node does not own the VIP, HAProxy will fail to start—this is fine because the backup will never own it.
  • The backend points to the tunnel daemon running locally on port 8443. Adjust if your daemon listens elsewhere.
  • The health check uses a simple TCP ping. Replace with an HTTP request (option httpchk GET /health) if your tunnel offers an HTTP health endpoint.

Reload HAProxy:

Bash
sudo systemctl restart haproxy
sudo systemctl enable haproxy

Verify the listener:

Bash
sudo ss -ltnp | grep 443

You should see HAProxy bound to 10.0.0.100:443 on the master node.


Step 4 – Deploy the Self‑Hosted Tunnel Daemon

The tutorial is agnostic to the specific tunnel implementation. Below we use cloudflared as an example, but you can swap inlets-pro or any compatible binary.

Bash
# Install cloudflared (Ubuntu example)
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb

Create a service file /etc/systemd/system/cloudflared.service:

INI
[Unit]
Description=Cloudflare Tunnel Daemon
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/cloudflared tunnel run my-tunnel
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Replace my-tunnel with your tunnel UUID or name. Enable and start:

Bash
sudo systemctl daemon-reload
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

Confirm it’s listening on 8443 (or the port you configured in HAProxy):

Bash
sudo ss -ltnp | grep 8443

Tip: If you use a daemon that only accepts inbound connections (e.g., inlets-pro server), flip the HAProxy direction accordingly: HAProxy becomes the client to the tunnel server.


Step 5 – Test Automatic Failover

  1. Baseline – From a client machine, curl the VIP:

    Bash
    curl -k https://10.0.0.100
    

    You should receive the response from the internal service exposed through the tunnel.

  2. Force Master Failure – Stop Keepalived on the master:

    Bash
    sudo systemctl stop keepalived
    

    Within a few seconds the backup should claim the VIP.

  3. Verify – On the backup node:

    Bash
    ip addr show eth0 | grep 10.0.0.100
    

    HAProxy should now be listening on the VIP:

    Bash
    sudo ss -ltnp | grep 443
    
  4. Client Test Again – Re‑run the curl from step 1. The request should succeed, proving that traffic seamlessly switched to the standby node.

  5. Return to Normal – Restart Keepalived on the original master. By default, it will regain the VIP (pre‑empt mode). If you prefer manual takeover, set nopreempt in the VRRP config.


Step 6 – Monitoring & Alerting

Tool What it monitors Example config
keepalived logs VRRP state changes journalctl -u keepalived -f
haproxy stats socket Backend health, connection counts listen stats 0.0.0.0:8404 in haproxy.cfg
systemd watchdog Service restarts systemctl status haproxy
Prometheus + node_exporter CPU, memory, network latency Scrape /metrics from both nodes

Simple alert via systemd (master node):

Create /etc/systemd/system/keepalived-fail.service:

INI
[Unit]
Description=Alert when Keepalived stops
After=network.target

[Service]
ExecStart=/usr/bin/logger "⚠️ Keepalived stopped on $(hostname)"
Restart=no

[Install]
WantedBy=multi-user.target

Enable a path unit to trigger it when the keepalived service file disappears:

INI
[Unit]
Description=Watch Keepalived unit file

[Path]
PathChanged=/run/systemd/unit/keepalived.service

[Install]
WantedBy=multi-user.target

This is a lightweight way to push a syslog entry to your central logging system.


Comparison: Keepalived + HAProxy vs. Alternative HA Solutions

Feature Keepalived + HAProxy Corosync + Pacemaker Kubernetes Service (LoadBalancer)
Complexity Low – simple config files, familiar tooling Medium – requires quorum, resource agents High – needs a full K8s cluster
VRRP support Native, proven in networking gear Emulated via resources Not applicable (uses kube-proxy)
Health checks HAProxy’s flexible TCP/HTTP checks Pacemaker resource monitors Liveness probes in pods
Failover time 1–3 seconds (advert_int + detection) 2–5 seconds (cluster consensus) 1–2 seconds (IPVS)
Scalability Ideal for 2‑5 nodes Good for large clusters Excellent for many pods, but adds overhead
Learning curve Minimal for sysadmins familiar with iptables Moderate – need to understand CRM concepts High – requires K8s expertise
Licensing BSD (Keepalived) + GPL (HAProxy) GPL (Corosync) + LGPL (Pacemaker) Apache 2.0 (K8s)

For a two‑node tunnel setup, Keepalived + HAProxy remains the sweet spot: quick to deploy, low overhead, and fully transparent to the upstream Zero‑Trust controller.


FAQ

1. Can I use IPv6 for the virtual IP?
Yes. Set virtual_ipaddress { 2001:db8::100/64 dev eth0 label eth0:vip } in the Keepalived config and adjust HAProxy’s bind line accordingly.

2. What happens to existing TCP connections during a failover?
VRRP only moves the IP; existing connections on the failed node are lost. To mitigate, keep session state in a shared cache (Redis) or use a short client‑side retry interval.

3. Do I need to configure nopreempt if I want manual control?
Set nopreempt inside the vrrp_instance block on the backup node. Then the original master will stay master until you manually force a takeover.

4. How do I secure the HAProxy management socket?
Add stats socket /var/run/haproxy.sock level admin expose-fd listeners and restrict file permissions to 640 with ownership haproxy:haproxy. Use socat with Unix socket for local queries.

5. Can I add a third node for extra redundancy?
Absolutely. Just replicate the Keepalived config on the third server, give it a lower priority, and add another server line in the HAProxy backend. VRRP will elect the highest‑priority node as master.


Conclusion

Building a disaster‑recovery‑ready Zero‑Trust tunnel doesn’t have to involve proprietary SaaS. By pairing Keepalived (for fast VRRP failover) with HAProxy (for robust health‑checking and traffic routing), you get a lean, battle‑tested solution that fits neatly into any Linux‑centric stack.

The steps above cover installation, configuration, testing, and monitoring. Adjust the IP ranges, ports, and tunnel daemon to match your environment, and you’ll have an automated failover pipeline that keeps internal services reachable even when a host disappears.

For more deep‑dive articles on resilient networking and cloud‑native tooling, check out the tutorials at mahbuburriad.com. Happy tunneling!

Related

Related posts