RTFM · Networking
HAProxy for MailDragon and webDragon
HAProxy is the world's most widely deployed open-source reverse proxy and Layer-7 load balancer: it accepts connections at an edge address, chooses a healthy backend by rules you write, and shuttles traffic between the two in both directions. It can terminate TLS or pass it through untouched. A VPN joins private networks; HAProxy does not.

What HAProxy is — and what it is not
HAProxy is a reverse proxy. A normal forward proxy works for the client, hiding or serving the outside world on its behalf. A reverse proxy works for the server side: clients connect to HAProxy, which decides which internal — or external — machine actually answers, then relays the whole conversation. It is also a load balancer: one service name can stand over several backend servers, with requests spread by round-robin, least connections, source-hash affinity or plain weights.
Because it understands HTTP, it works at Layer 7 of the OSI model: routing decisions can depend on hostname, path, headers, cookies, methods or status codes rather than just addresses and ports. That is precisely why one public IPv4 address can host dozens of unrelated domains — each request carries its Host header, HAProxy matches it against access control lists (ACLs), and forwards accordingly, no matter whether the target lives at 192.0.2.x next door or at another site entirely. The trade-off cuts both ways: whatever passes through reads both directions of conversation, so treat the proxy itself as trusted ground zero.
| HAProxy does | HAProxy does not |
|---|---|
| Terminate TLS (owning certs) or pass TCP/TLS through byte-for-byte | Create encrypted tunnels between sites — that is WireGuard territory |
| Route by HTTP Host, path, cookies, headers — Layer 7 | Route arbitrary protocols by payload semantics |
| Balance across healthy backends with live checks | Replace your firewall: nftables still decides who may connect |
| Speak HTTP/1.1, HTTP/2 (h2) and HTTP/3 QUIC at the edge | Cache content like a CDN or serve static files like a web server |
How, why, where — and when not
Use HAProxy wherever several services must share scarce edge resources: one IPv4 address serving many names, one certificate owner fronting machines with none, or many backends behind one calm entry point. Skip it when a single service owns a single port outright — a plain firewall forward is fewer moving parts — or when you need encrypted transport between YOUR OWN sites, where WireGuard is the honest answer.
- Why: collapse N certificates, hosts and ports into one audited choke point with logging, checks and statistics in one place.
- Where: the edge of the DMZ, or any host that owns public ports. On Saphira the openvswitch + network.d design typically gives HAProxy host its own dmz-facing bridge with a gateway interface.
- When: hosting multiple domains on one address, publishing mail/web from private ranges, staging blue/green releases by weight.
- When not: pure private-to-private tunnels (VPN), content caching (nginx/varnish-class problem), packet filtering (nftables).
The features worth knowing by name
1. Health checks
Periodic probes (HTTP GET expecting 200, TCP connect, SMTP greeting…) tell HAProxy which backends deserve traffic. Marked-down servers receive nothing; recovery brings them back automatically. A check turns silent failure into visible status.
2. Fallback / backup servers
Any server line may carry the 'backup' keyword. Backups sit idle while primaries pass checks, then absorb the whole load instantly the moment every primary fails — ideal for maintenance pages or cold-standby hosts.
3. SNI — Server Name Indication
In a TLS ClientHello the client announces WHICH hostname it came to see before encryption begins. HAProxy can read that hint and route TCP passthrough by name — terminating nothing. That is how dozens of independently certified domains share one :443 listener even though each keeps its own certificate on its own internal box.
4. ALPN, HTTP/2 and QUIC
'bind … alpn h2,http/1.1' negotiates HTTP/2 alongside legacy HTTP. QUIC is HTTP/3's UDP-based transport folding TLS 1.3 into connection setup; modern HAProxy builds can offer it ('bind … quic'), where kernel/build support allows — verify against your actual package before promising browsers HTTP/3.
5. Rewrite rules
http-request/http-response directives rewrite URLs, redirect schemes, add or strip headers (X-Forwarded-For, X-Forwarded-Proto) — small surgical edits en route.
6. SPOE — Stream Processing Offload Engine
A module protocol delegating decisions (scoring, bot detection, enrichment) to external helper daemons via structured messages. Powerful; rarely needed day one.
The official configuration manual at docs.haproxy.org documents every keyword used below; this chapter teaches shape and intent, the manual holds every flag.
Choose where TLS ends
For webDragon, HAProxy can terminate HTTPS and send HTTP to an internal webDragon listener, or pass encrypted TLS through so webDragon terminates it. For MailDragon, TCP passthrough is usually the least surprising because SMTP, IMAP, and POP3 each have their own TLS and protocol behavior. Do not make a backend parse PROXY protocol unless it is configured to do so.
global
log /dev/log local0
defaults
mode tcp
timeout connect 5s
timeout client 1m
timeout server 1m
frontend smtp_in
bind :25
default_backend mail_smtp
backend mail_smtp
server maildragon 192.168.20.25:25 check
frontend submission_tls
bind :465
default_backend mail_submission_tls
backend mail_submission_tls
server maildragon 192.168.20.25:465 checkwebDragon HTTP mode
global
log /dev/log local0
defaults
mode http
option forwardfor
timeout connect 5s
timeout client 1m
timeout server 1m
frontend https_in
bind :443 ssl crt /etc/haproxy/certs/example.pem
http-request set-header X-Forwarded-Proto https
default_backend webdragon
backend webdragon
server webdragon 192.168.20.30:80 checkThe backend must trust the HAProxy host before using X-Forwarded-For for logs or access control. If TLS terminates at webDragon instead, use TCP passthrough and let webDragon own the certificate.
Real client addresses and mail PTR
A proxy normally becomes the source address seen by a backend. HTTP backends can use X-Forwarded-For. TCP backends can use PROXY protocol only when MailDragon or the relevant service is explicitly configured to parse it. The public IP used for outbound mail is the address whose PTR must point to mail.example.com; it is not automatically the HAProxy backend address.
Do not publish a mail PTR for an internal 192.168.x.x address. Reverse DNS is controlled by the owner of the public address, usually the ISP or hosting provider.
Operate safely
# Validate and reload HAProxy
haproxy -c -f /etc/haproxy/haproxy.cfg
# OpenRC
rc-service haproxy status
rc-service haproxy reload
rc-update add haproxy default
# systemd
systemctl status haproxy.service
systemctl enable --now haproxy.service
systemctl reload haproxy.service
journalctl -u haproxy.serviceBuild a proxy path before exposing it
A proxy is a chain, not a single configuration file. Decide the public address, the listener port, the HAProxy frontend, the private backend address and port, the TLS owner, and the firewall rule at both ends. If HAProxy and the backend run on separate hosts, the backend firewall should normally accept its public-service port only from the proxy network. This prevents someone bypassing HAProxy by discovering the backend address.
1. Prove the backend locally
From HAProxy's network, connect directly to the backend address and port. Fix its service, route, or firewall before involving the proxy.
2. Add one frontend and one health check
Use a named frontend/backend and server ... check. A health check makes a failed backend visible instead of silently accepting a client connection that cannot be completed.
3. Validate without reloading
Run haproxy -c -f /etc/haproxy/haproxy.cfg. A validation failure changes nothing; a reload should happen only after the test succeeds.
4. Test at each boundary
Test backend direct from the proxy, proxy listener from its LAN, then public name from a truly external network. Test the certificate or SMTP TLS negotiation at the public edge.
# Baseline installed configuration and local status endpoint.
# The status listener is deliberately local, not a public dashboard.
frontend status
bind 127.0.0.1:8404
mode http
http-request return status 200 content-type text/plain string "HAProxy is running\n"
# Validate, then reload through the selected service manager.
haproxy -c -f /etc/haproxy/haproxy.cfg
rc-service haproxy reload
curl -fsS http://127.0.0.1:8404/Prove it works: A working proxy is observable
The config validates, HAProxy owns only the intended public ports, the local status endpoint answers on 127.0.0.1, a backend health check is up, and an external client reaches the chosen public hostname with the expected certificate or mail protocol response.
Choose the right tool for the boundary
| Need | Use | Do not assume |
|---|---|---|
| Publish HTTPS or SMTP to the Internet | HAProxy plus firewall and public DNS | A VPN makes a public website or mail MX reachable. |
| Give administrators private access | WireGuard/VPN plus restricted firewall policy | HAProxy protects SSH, databases, or backup ports. |
| Map IPv4 at a home router | Explicit port forwarding or a carefully hardened DMZ Host | An HAProxy frontend bypasses CGNAT or router policy. |
| Control whether packets are allowed | nftables at the host and edge | A HAProxy backend declaration is a firewall rule. |
Worked example: one IPv4, many domains (derived from our live config)
AKADATA runs HAProxy in exactly this pattern: a handful of public IPv4 addresses hosting dozens of domains, proxying to internal hosts across several networks. The snippets below are sanitised rewrites of that live configuration (hosts renamed to example ranges) — use them as starting shapes.
First the simplest possible shape — one listener, one service chain, expressed as a flat listen block:
listen simple_web
bind 198.51.100.10:80
mode http
server app1 172.16.99.20:80 checkFrontend/backend splits the same idea and unlocks routing. The frontend owns listeners and ACL decisions; backends own health checks and server lines. Order matters: first matching use_backend wins, so put specific names above catch-alls:
frontend http_web
bind 198.51.100.10:80
mode http
option forwardfor
# ACLs match the Host header the client sent
acl host_alpha hdr(host) -i alpha.example.org www.alpha.example.org
acl host_beta hdr(host) -i beta.example.net www.beta.example.net
acl host_stats hdr(host) -i stats.example.org stats.example.net
use_backend alpha_http if host_alpha
use_backend beta_http if host_beta
use_backend stats_http if host_stats
default_backend alpha_http # unknown Host values land here
backend alpha_http
mode http
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host alpha.example.org
http-check expect status 200
server alpha 172.16.99.20:80 weight 90 check send-proxy-v2
backend beta_http
mode http
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host beta.example.net
http-check expect status 200
server beta 172.16.0.2:80 checkAt :443 with certificates kept on each internal machine, switch from HTTP mode to TCP passthrough and route by SNI instead of Host headers. The inspect-delay exists because the hostname arrives inside the TLS ClientHello — HAProxy waits briefly to read it before picking a backend:
frontend https_passthru
bind 198.51.100.10:443 alpn h2,http/1.1
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
acl sni_alpha req.ssl_sni -i alpha.example.org www.alpha.example.org
acl sni_beta req.ssl_sni -i beta.example.net www.beta.example.net
use_backend alpha_tls if sni_alpha
use_backend beta_tls if sni_beta
default-server inter 2s fall 3 rise 2 on-marked-down shutdown-sessions
backend alpha_tls
mode tcp
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host alpha.example.org
http-check expect status 200
server alpha 172.16.99.20:443 check check-ssl verify none \
+ sni str(alpha.example.org) alpn h2,http/1.1 send-proxy-v2Health checks travel in the clear here only because each target answers plain-HTTP probes internally; verify none skips cert validation for checking purposes while real traffic stays end-to-end encrypted. Where confidentiality of checks matters, install the CA and drop verify none.
Backup servers and the statistics dashboard
Fallback is one keyword long. While 172.16.99.20 answers checks it takes everything; the instant it stops, requests flow to the backup line until primaries recover:
backend alpha_tls_fallback_demo
balance roundrobin
default-server inter 2s fall 2 rise 2 on-marked-down shutdown-sessions
server alpha-primary 172.16.99.20:443 check check-ssl verify none sni str(alpha.example.org)
server alpha-maint 172.16.99.30:443 check backup # fallback serverHAProxy also ships its own dashboard. Bind it deliberately — public internet access means strong credentials and, better, a firewall or ACL in front:
listen stats
bind 198.51.100.10:7777
mode http
stats enable
stats uri /
stats refresh 10s
stats realm HAProxy\ Statistics
stats auth admin:CHANGE-ME-BEFORE-PUBLISHING
# then prove who may reach it at the firewall layer too:
nft add rule inet filter input ip saddr != { 203.0.113.0/24 } tcp dport 7777 rejectThe dashboard shows every frontend, backend and server with up/down state, request rates and queue depths — the fastest way to see what health checks already decided on your behalf.
Where this chapter draws from
Every keyword above is documented exhaustively in the official HAProxy configuration manual; keep it open beside this page when writing anything beyond these examples.
- Official HAProxy configuration manual (docs.haproxy.org)
- HTTP/2 end-to-end concepts explained by HAProxy Technologies
A runnable reference covering virtual hosting, SNI passthrough, PROXY protocol v2, health checks, backup servers and the stats listener ships with this repository as demo.haproxy.cfg — read alongside this page. Passwords inside have been changed; treat every address as documentation-only.
Getting dirty: external health checks
Built-in checks answer 'is the port up?'. External checks answer any question you can phrase as a program: does this app authenticate, does the CMS render, does the database accept a real query? With option external-check HAProxy runs an arbitrary script — bash, python, perl, a compiled binary — and treats exit code 0 as UP, anything else as down. The options are genuinely endless because the checker is just your own code.
backend alpha_http
mode http
option external-check
external-check command /etc/haproxy/checks/probe.sh
server alpha 172.16.99.20:80 check inter 5s fall 3 rise 2HAProxy hands your script its full context as positional arguments. Anyone who remembers ldirectord or ipvsadm health scripts will recognise $1 through $4 — and the fifth argument is the one people always forget:
| Argument | Meaning |
|---|---|
| $1 | Proxy name — the frontend/backend/listen block running the check |
| $2 | Server name — the exact identifier of the server line being probed |
| $3 | Server IP (IPv4 or IPv6) of the target |
| $4 | Server port being checked |
| $5 | Optional source address/port when a source binding was declared |
A minimal probe needs nothing more than curl and the right variable:
#!/bin/bash
# /etc/haproxy/checks/probe.sh - chmod 755
PROXY_NAME=$1
SERVER_NAME=$2
SERVER_IP=$3
SERVER_PORT=$4
SOURCE_PORT=$5 # present only if a source binding was declared
# Custom logic in full HAProxy context:
curl --silent --fail \
+ "http://${SERVER_IP}:${SERVER_PORT}/health" > /dev/null
# exit 0 = UP; non-zero = DOWN. Add --max-time to avoid hangs.From there it scales up naturally: log into the service over SSH and verify a session, run a Python client against the internal API, POST credentials and expect a specific response code, walk a full application workflow. The same discipline applies to plain TCP services via tcp-check conversation blocks — a Redis ping-pong takes four lines:
backend redis_servers
mode tcp
option tcp-check
tcp-check connect
tcp-check send PING\r\n
tcp-check expect string +PONG
server redis1 192.168.4.10:6379 check inter 5s- tcp-check expect supports string, rstring (regex), status codes and binary patterns.
- Multi-step conversations work too: send AUTH/USER lines first, then probe — that is how you build login-style checks without a helper script.
Two operational traps: HAProxy often runs inside a chroot (our global config uses chroot /var/lib/haproxy), so the interpreter and script must exist INSIDE the chroot tree with their libraries, or use a statically compiled checker. And every external check spawns a process per probe — keep intervals sane (inter 5s or slower) and give scripts explicit timeouts so a hung curl cannot pile up zombies.
Prefer built-in http/tcp checks wherever they suffice, and reserve external checks for questions they cannot ask. Scripts are powerful precisely because they are arbitrary — which also makes them the slowest and most fragile kind of check.