- JavaScript 42.7%
- HTML 35.3%
- Shell 20.4%
- Lua 1.1%
- Python 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| builder | ||
| dist | ||
| package | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| README.md | ||
| VERSION | ||
HAProxy Builder
A web tool that builds a complete, secure HAProxy setup: haproxy.cfg,
map files, error pages, certificate handling, security.txt and the scripts
to install and update it on a server. You fill in your sites and backends;
the builder makes sure the result meets the minimum security requirements of
Internet.nl (100% score), SSL Labs (A+), the NCSC TLS guidelines
(as required by the Dutch Forum Standaardisatie) and the relevant
RFCs – with defaults that never break a site.
| Builder version | 1.4.7 (see VERSION, CHANGELOG.md) |
| Author | BvO |
| License | MIT |
| Generates config for | HAProxy 3.4 LTS |
| Tested with | HAProxy 3.4.0 |
| Minimum HAProxy | 3.2 (needed for default-crt and named defaults) |
The builder is one HTML file, dist/haproxy-builder.html.
It runs in any current browser, needs no server and no internet connection,
and keeps your project in the browser until you download it.
Contents
- Quick start
- Minimum requirements
- Installing HAProxy 3.4
- First installation on the server
- Later changes
- Security requirements the builder enforces
- CrowdSec and the WAF
- Importing an existing configuration
- Version numbers
- Repository layout
- Development
- License
Quick start
-
Open
dist/haproxy-builder.htmlin a browser. -
Use Add a site (wizard), or Import your current
haproxy.cfgor a package ZIP. -
Check Compliance and Checks. The download is only possible when there are no errors.
-
Download ZIP, copy it to the server and run:
unzip haproxy-<name>.zip sudo haproxy-<name>/setup.shsetup.shinstalls the package into/usr/local/src/haproxy-<name>and runs from there.
Minimum requirements
To use the builder: a current browser (Chrome, Edge, Firefox, Safari).
On the server:
| Requirement | Why |
|---|---|
| Linux with systemd; Debian 12+ or Ubuntu 22.04+ (default) | the scripts use systemctl and apt-get; RHEL/Rocky/Alma/Fedora work with dnf for the tools and CrowdSec |
| HAProxy 3.2 or newer, 3.4 LTS recommended | features used in the generated config |
| HAProxy built with OpenSSL 1.1.1+ | TLS 1.3 |
| HAProxy built with Lua | security.txt is served by a small Lua script |
| HAProxy built with QUIC | only when HTTP/3 is switched on |
| OpenSSL 3.5+ | only for the post-quantum option (X25519MLKEM768) |
root access (sudo) |
installs into /etc/haproxy |
| ports 80/tcp and 443/tcp open (443/udp for HTTP/3) | plus extra frontend ports you add |
| DNS A and AAAA records pointing to the server | before certificates are requested |
bash, openssl, curl, socat, python3 |
setup.sh installs what is missing; acme.sh and gpg too when needed |
Too old or missing a feature? setup.sh and update.sh first run
scripts/preflight.sh. It reads the package's REQUIREMENTS file and
haproxy -vv, and stops before anything is changed when:
- HAProxy is older than 3.2 (with instructions to install 3.4),
- HTTP/3 is used but HAProxy has no QUIC support,
- security.txt is used but HAProxy has no Lua,
- the post-quantum option is used but OpenSSL is older than 3.5.
A newer HAProxy than the tested version is accepted; haproxy -c then
confirms the configuration. You can run the check by hand:
sudo /usr/local/src/haproxy-<name>/scripts/preflight.sh
Example output on a server that is too old:
==> HAProxy compatibility
ERROR HAProxy 2.8.5 is too old: this configuration needs 3.2 or newer (tested with 3.4.0)
Install a current HAProxy (3.4 LTS) from haproxy.debian.net:
Ubuntu: sudo add-apt-repository ppa:vbernat/haproxy-3.4 && sudo apt install haproxy=3.4.\*
Debian: follow https://haproxy.debian.net (choose your release and 3.4)
-> not compatible: nothing was changed
Installing HAProxy 3.4
Distribution packages are often older than 3.2. Use the packages from haproxy.debian.net (built with OpenSSL, Lua and QUIC):
# Ubuntu
sudo add-apt-repository ppa:vbernat/haproxy-3.4
sudo apt update
sudo apt install haproxy=3.4.\*
# Debian: pick your release and "3.4" on https://haproxy.debian.net
# and follow the commands shown there.
haproxy -vv | head -1 # HAProxy version 3.4.x ...
First installation on the server
scp haproxy-<name>.zip server:
ssh server
unzip haproxy-<name>.zip # anywhere, e.g. your home folder
# own certificates? put them in haproxy-<name>/certs/own/ first
sudo haproxy-<name>/setup.sh
Where it is installed. setup.sh copies the package to
/usr/local/src/haproxy-<name> (that folder exists on every Linux system,
see the Filesystem Hierarchy Standard) and continues from there; the
unpacked folder can then be deleted. Certificates in certs/own/,
certs/credentials.env, crowdsec/crowdsec.env and signed security.txt
files already in that folder are kept; the previous version is kept as
haproxy-<name>.previous. Set HAPROXY_PKG_DIR to use another folder.
setup.sh does everything in one step:
- compatibility check (
scripts/preflight.sh: HAProxy version and build features, operating system), missing tools; - CrowdSec, when it is on in the project: detects the operating system,
adds the CrowdSec repository, installs
crowdsecandcrowdsec-haproxy-spoa-bouncer(apt on Debian/Ubuntu, dnf on the RHEL family), configures them, and checks every dependency – nothing is installed in/etc/haproxyif something is missing; - security.txt, certificates (Let's Encrypt or your own), the statistics password, a test of every backend;
haproxy -c, a backup of/etc/haproxy, a start test, and the restart.
Safe to run again, also with a newer ZIP: existing packages, repositories
and keys are kept, services are only restarted when their configuration
changed, and a configuration that fails haproxy -c or does not start is
never left running – the previous /etc/haproxy is restored and HAProxy
keeps running with it. BUILD-NOTES.md in the package explains the details
of your project.
The WAF is never installed by setup.sh: it is off by default and, when
you switch it on, installed separately on its own VM (see below).
Later changes
-
Change the project in the builder (or import the package ZIP first when you use another browser).
-
Download the ZIP and copy it to the server.
-
Run either of:
sudo /usr/local/src/haproxy-<name>/update.sh ~/haproxy-<name>.zip # or: unzip haproxy-<name>.zip && sudo haproxy-<name>/setup.sh
Both do the same: show the builder versions (old → new), run the
compatibility check, replace the package in /usr/local/src/haproxy-<name>
(keeping certificates and secrets), keep the changes made on the server
(blocklist, maintenance list, extra security.txt domains), show what changes
in haproxy.cfg, ask to confirm, and install with backup and rollback.
Security requirements the builder enforces
The minimum is a 100% score on Internet.nl, plus SSL Labs A+ and the NCSC TLS guidelines. Internet.nl only counts its required subtests for the score; recommended and optional subtests show advice but never lower it. The builder follows the same split:
- Required settings cannot be weakened: weak options are not offered, imports are raised to the minimum, and custom lines that would weaken them are errors that block the download.
- Recommended / optional settings get defaults that never break a site,
and you can change them. The most important one is the CSP: HAProxy sets
no CSP by default, so every site keeps the CSP its own backend sends.
A CSP you add may contain
'unsafe-inline'; Internet.nl then gives advice, but the score stays 100%.
| Area | Setting | Internet.nl level | Builder default | Source |
|---|---|---|---|---|
| TLS | only TLS 1.2 and 1.3 | required | 1.2 + 1.3 | Internet.nl, SSL Labs, NCSC, RFC 8996 |
| TLS | ECDHE with AES-GCM / ChaCha20-Poly1305, server cipher order | required | all "good" suites | Internet.nl, SSL Labs, NCSC, RFC 9325 |
| TLS | X25519, P-256, P-384, X448 (+ optional X25519MLKEM768) | required | X25519, P-256, P-384 | Internet.nl, NCSC |
| TLS | SHA-2 signatures only; no compression, no 0-RTT | required | – | Internet.nl, RFC 9155, RFC 5746 |
| TLS | at least TLS 1.2 to backends | (NCSC) | 1.2 | NCSC |
| HTTP | port 80 redirects to HTTPS | required | on | Internet.nl, Forum Standaardisatie |
| HTTP | HSTS max-age ≥ 1 year | required | 1 year, no includeSubDomains | Internet.nl, SSL Labs A+, RFC 6797 |
| HTTP | X-Content-Type-Options: nosniff | recommended | on | Internet.nl |
| HTTP | Referrer-Policy | recommended | strict-origin-when-cross-origin (browser default) | Internet.nl |
| HTTP | Content-Security-Policy | recommended | none from HAProxy | Internet.nl |
| HTTP | security.txt (Expires < 1 year) | recommended | on | Internet.nl, Forum Standaardisatie, RFC 9116 |
| HTTP | X-Frame-Options | optional | SAMEORIGIN | Internet.nl, RFC 7034 |
| HTTP | Permissions-Policy, COOP, CORP | not tested | off (they can break logins, maps, embeds) | – |
| Network | IPv6 | required | on | Internet.nl, Forum Standaardisatie |
| Outside HAProxy | DNSSEC, RPKI (required), CAA (recommended) | set at your DNS / network provider | Internet.nl |
The Compliance page in the builder shows this list for your project.
Addresses, NAT and certificates
Addresses. The page Addresses & NAT describes how the server is reached: one public IPv4 address behind NAT, several public addresses directly on the server, or a mix – for IPv4 and IPv6 alike. By default HAProxy listens on all addresses (one listener per port for IPv4 and IPv6), so every situation works without further settings; "only these addresses" binds to the listed local addresses instead. Either way IPv4 and IPv6 get exactly the same frontends, routing and headers, so Internet.nl never sees two different websites. The listed addresses are used for:
- the port forwarding table (router) and the firewall ports (IPv4 and
IPv6) in
BUILD-NOTES.md; - the DNS records every host name needs (A and AAAA);
scripts/check-ipv46.sh(run bysetup.sh, also usable from outside): it checks that every A/AAAA record points to this server and that IPv4 and IPv6 give the same status, security headers and page. A record that points to another server, or a port opened for one family only, is the usual reason Internet.nl reports "different content on IPv6".
install.sh stops before restarting when a listed local address is not
configured on the server, and drops IPv6 listeners on a server without IPv6.
Certificates. Choose Let's Encrypt only, own certificates only, or both (for example Let's Encrypt for most names and a purchased wildcard for the rest). Own certificates are not in the ZIP; you add them on the server yourself:
| Where | /usr/local/src/haproxy-<name>/certs/own/ (before the first installation also certs/own/ in the unpacked ZIP) |
| Files | per certificate <name>.pem (certificate + intermediates + key), or <name>.crt + <name>.key (+ <name>.ca-bundle) |
| Rights | chown -R root:root …/certs/own, chmod 700 …/certs/own, chmod 600 …/certs/own/* |
| Installed as | /etc/haproxy/ssl/certs/<first name>.pem, owner root, group haproxy, mode 640 |
| Renewal | put the new files in the same folder and run setup.sh again |
List the names of each own certificate in the builder (Certificates page):
it then checks that every host name is covered, and setup.sh ends with
"STILL TO DO" as long as one is missing.
Mail is passed through by default: TLS untouched (the mail server keeps its own certificates) and the PROXY protocol for the real client address, to one or more mail servers (failover or spread).
CrowdSec and the WAF
CrowdSec (optional) runs on the HAProxy server. When it is on and the
project has no errors, setup.sh installs it during the first installation:
it reads /etc/os-release, uses apt on Debian/Ubuntu (the default) or dnf
on RHEL/Rocky/Alma/Fedora, adds the CrowdSec repository only when it is not
there yet, and installs CrowdSec and the HAProxy SPOA bouncer. On other
systems install those two packages by hand first; preflight.sh warns
before anything changes. Then setup.sh runs
crowdsec/check-crowdsec.sh, which checks every dependency: the packages,
both services, the local API, the crowdsecurity/haproxy collection, the log
acquisition, the bouncer's API key and listening port, and the captcha files.
If anything is missing it prints the fix and installs nothing, because the
configuration depends on the bouncer.
The WAF (Coraza with the OWASP Core Rule Set) is off by default and optional; the builder only shows how to install it. It belongs on a separate VM: it uses a lot of CPU and memory, and a problem in the WAF cannot take HAProxy down. When you switch it on, the builder first explains what you need and shows a checklist: the VM's host name or IP address (optionally with a port), and HAProxy's address as the VM sees it. The download stays blocked until the checklist is complete. Then:
- copy the package's
waf-vm/folder to the VM and runsudo ./setup-waf-vm.shthere; - run
sudo ./setup.shon the HAProxy server; it tests whether the VM answers.
HAProxy only sends requests to the WAF while its health check reaches the VM
(nbsrv(coraza-spoa) gt 0). Until then requests pass uninspected (unless you
chose fail-closed), so a missing or broken VM never takes your sites down.
setup.sh ends with WAF: ACTIVE or WAF: NOT ACTIVE yet. New projects
start in detect mode: switch to block after checking the log.
Importing an existing configuration
Import accepts a package ZIP, a project file (*.haproxy-builder.json),
or loose files (haproxy.cfg, maps, certificates.txt, ...). The import
report has four parts:
- Imported – what was read into the builder;
- Adjusted to meet the minimum requirements – settings that were weaker
than allowed and what they were changed to (for example TLS 1.0 → 1.2,
HSTS 600 s → 1 year, a CSP with
unsafe-inlineno longer set by HAProxy); - Needs your attention – what could not be imported completely, such as a plain-text statistics password;
- Still to do before download – the remaining errors, each with a link to the page where you fix it.
Lines the builder has no field for are kept as custom lines; credentials are never imported. A project made by a newer builder version gives a warning.
Version numbers
The builder follows Semantic Versioning: MAJOR.MINOR.PATCH.
- PATCH (1.0.1): fixes, no change to projects or generated config behaviour;
- MINOR (1.1.0): new options or checks; existing projects still load;
- MAJOR (2.0.0): an incompatible change in projects or in the generated configuration (for example a new minimum HAProxy version).
The version appears top right in the builder, in the header of every
generated haproxy.cfg, in REQUIREMENTS and in the project file:
#=======================================================================
# my-haproxy - HAProxy 3.4 LTS HAProxy Builder 1.4.7
#=======================================================================
# Generated by HAProxy Builder 1.4.7 on 2026-10-31.
# Tested with HAProxy 3.4.0; needs 3.2 or newer (checked by scripts/preflight.sh).
Repository layout
LICENSE MIT, Copyright (c) 2026 BvO
VERSION the builder version (must match core.js)
CHANGELOG.md
dist/haproxy-builder.html the built tool: open this file
builder/
build.py builds dist/haproxy-builder.html
src/core.js generator, import, checks, requirements (also runs in Node.js)
src/ui.html the interface
vendor/jszip.min.js ZIP library (MIT), inlined at build time
package/ scripts and docs copied into every package
scripts/preflight.sh HAProxy version and feature check
scripts/install.sh installs with backup, test and rollback
scripts/check-ipv46.sh DNS points here, IPv4 and IPv6 give the same answer
update.sh installs a newer package ZIP (runs its setup.sh)
... plus a hand-written example config (haproxy.cfg, maps)
tests/
run.sh all tests (build, unit tests, shell syntax, haproxy -c)
test-core.js
fixtures/weak.cfg an old configuration below the requirements
Development
Requirements: Python 3.8+, Node.js 18+, and optionally HAProxy 3.2+ for the
haproxy -c test.
python3 builder/build.py # writes dist/haproxy-builder.html
tests/run.sh # build + all tests
HAPROXY_BIN=/usr/local/sbin/haproxy tests/run.sh
Edit builder/src/* and package/*, never dist/ by hand; commit the
rebuilt dist/haproxy-builder.html together with the source change.
Releasing a new version
- Update
VERSIONandBUILDER_VERSIONinbuilder/src/core.js(build.pyrefuses to build when they differ). When the HAProxy target changes, also updateHAPROXYincore.js. - Add the changes under a new heading in
CHANGELOG.md. tests/run.sh, then commit and tag:git tag -a v1.1.0 -m "HAProxy Builder 1.1.0".
Never commit certificates, private keys, credentials.env,
crowdsec.env or a project with real host names and internal addresses to a
public repository; .gitignore covers the usual files.
License
MIT – Copyright (c) 2026 BvO. Free to use, change and share,
also commercially, as long as the copyright notice stays. The bundled JSZip
is used under its MIT license (see builder/vendor/JSZIP-LICENSE.md).