HAProxy is a proxy that accepts client connections on a public frontend and forwards them to backend services. This guide helps your AI assistant make sure it is configured securely: terminating TLS and redirecting HTTP, requiring authentication, bounding expensive endpoints, and protecting the stats interface and runtime API.
If an AI assistant is helping you secure a deployment, give it the prompt below:
Read the secureconfig guide at
https://raw.githubusercontent.com/jposluns/secureconfig/main/haproxy.md
in full, including the section for AI assistants at the end. Then, for my
deployment: state which of its recommendations apply and the exact changes
to make; compare the versions the guide was written against with the
releases I am running and with the vendor's current releases, and flag any
guidance a newer release may have changed; and run or adapt the guide's
Verify steps where you can. List separately every item you could not
verify instead of reporting it as passed.
Written against HAProxy 3.0, HAProxy http-after-response minimum 2.2+, OpenSSL password command 3.0, and Certbot deploy hook documentation v2.3.0; 1 further source is pinned to a commit and 1 carries no recorded version. Metadata reviewed 2026-10-05; documentation checked September 2026. The guide records 30 claims, all reasoned from cited sources.
A newer release of any of this software can make version-specific details here outdated. To prioritize an update, open a GitHub issue, send a pull request, add a request under requests/, or reach the maintainer as CONTRIBUTING.md describes. The machine-readable version basis and claim list are in the section for AI assistants at the end of this guide.
HAProxy's public frontend, its stats interface, its runtime API, and any directly reachable backend are separate exposure surfaces; protect and verify each one you enable. These examples target a maintained HAProxy 3.0 installation.
Get a certificate first (free-certificates.md or self-signed.md). This example combines the certificate chain and its matching private key in one PEM file (HAProxy also supports separately loaded keys):
sudo mkdir -p /etc/haproxy/certs && sudo chmod 700 /etc/haproxy/certs
# Write the combined PEM to a root-only temp file (umask 077 keeps it mode 600), then rename it into place,
# so the private key is never briefly world-readable and a failure leaves nothing half-written.
sudo bash -c 'umask 077; tmp=$(mktemp /etc/haproxy/certs/.pem.XXXXXX) || exit 1
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem > "$tmp" || { rm -f "$tmp"; exit 1; }
mv "$tmp" /etc/haproxy/certs/example.com.pem'With Certbot v2.3.0, run the concatenation and reload HAProxy after a successful renewal using certbot renew --deploy-hook /path/to/deploy-hook-script or an executable in /etc/letsencrypt/renewal-hooks/deploy (assuming the /etc/letsencrypt configuration directory, on certbot renew).
global
ssl-default-bind-options ssl-min-ver TLSv1.2
# Preserve the package's service hardening; for a root-started deployment, drop privileges and jail the
# workers (provision the account and an empty root-owned /var/lib/haproxy first).
user haproxy
group haproxy
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock user root group root mode 600 level admin
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
frontend web
bind :80
bind :443 ssl crt /etc/haproxy/certs/example.com.pem
http-request redirect scheme https code 301 unless { ssl_fc }
http-after-response set-header Strict-Transport-Security "max-age=31536000" if { ssl_fc }
default_backend app
backend app
server app1 127.0.0.1:3000 checkssl-min-ver is available on maintained HAProxy 3.0. For explicit cipher lists, ssl-default-bind-ciphers configures TLS 1.2 and ssl-default-bind-ciphersuites configures TLS 1.3; the Mozilla SSL Configuration Generator is a policy source, not the authority for HAProxy syntax. http-after-response (HAProxy 2.2+) sets the HSTS header on HAProxy's own generated responses, such as the 401 and 413 below, which http-response would miss; add includeSubDomains only once every affected subdomain is served over HTTPS. The backend here is plaintext on loopback; for a remote backend, require and verify its certificate, for example server app1 app.internal:443 ssl verify required ca-file /etc/ssl/certs/internal-ca.crt verifyhost app.internal check.
Application-level login is preferable (authentication.md). At the proxy, define a userlist with a crypt(3)-hashed password and demand it:
openssl passwd -6 # prompts, outputs a $6$ SHA-512 crypt hashuserlist admins
user admin password $6$REPLACE_WITH_HASH
backend app
http-request auth realm Restricted unless { http_auth(admins) }
server app1 127.0.0.1:3000 checkopenssl passwd -6 prompts for the password. The prompt keeps the password out of the command text, shell history and shell tracing; the account owner and root can still read it in the openssl process while it runs. Hashed password entries rely on the system's crypt(3); $6$ works on glibc-based Linux. Avoid insecure-password, which stores the password in cleartext in the configuration file. For machine-to-machine access, client certificates are stronger: add verify required ca-file /etc/ssl/certs/internal-ca.crt to the bind :443 line.
Basic authentication here is single-factor. For human-facing sites, add MFA with an Authelia portal (HAProxy is supported through Authelia's Lua module) or by fronting the site with Cloudflare Access and requiring MFA in its governing Access setting (policy over application over organization); options in mfa.md.
The timeouts in section 1 are half of this. Add a concurrency cap with maxconn, which in global is
the maximum per-process concurrent connections and in a frontend caps that frontend.
HAProxy has no single request-body-size directive. The rule below uses the req.body_size fetch,
which the manual defines as the advertised length of the body, so for a request carrying
Content-Length it reads that value and can reject without waiting for the request body. Two limits are worth knowing.
It needs no request buffering; do not add option http-buffer-request for it, because the manual says
that option waits until either the whole body is received or the request buffer is full, which buffers
the upload you are trying to refuse. And for a chunked request, which advertises no length, the manual
says the fetch returns the size of the available data instead, which without buffering is only what has
arrived so far, so this rule does not meaningfully bound a chunked upload. Enforce the real limit at the
application and treat this as a front-door guard against the common case.
# add to the existing global section
maxconn 4096
# add to `frontend web` from section 1. Do not paste a new frontend: that one
# carries bind, the HTTPS redirect, the HSTS header and default_backend.
maxconn 2000
http-request deny deny_status 413 if { req.body_size gt 10485760 }The stats page and the runtime API are separate from proxied traffic and easy to leave open. Inventory both in every loaded configuration, and leave each disabled unless needed.
The stats page is disabled unless a configuration enables it (in a frontend, listen, backend, or defaults), and enabling it authenticates nothing on its own: an unauthenticated page leaks operational detail, and stats admin additionally lets a caller enable, disable, or drain backends. Inventory every loaded configuration, omit stats admin from a monitoring page, keep any admin page private, and reuse the hashed userlist rather than a cleartext stats auth:
listen monitoring
bind 127.0.0.1:8404 ssl crt /etc/haproxy/certs/example.com.pem
mode http
stats enable
stats uri /stats
stats http-request auth unless { http_auth(admins) }The runtime API (the stats socket added to global above) is not enabled by default, and its default command level is operator; level admin grants authority, not authentication. Keep it a root-owned local Unix socket at mode 600 and never expose it as an unauthenticated TCP socket, because HTTP stats authentication does not protect it.
# REASONED: configuration and exposed/fixed checks follow the cited HAProxy documentation;
# no HAProxy runtime is available in the authoring environment.
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
# REASONED, not demonstrated here: no HAProxy runtime in the authoring environment, so the shell was checked
# but the exposed-vs-fixed responses were not. A transport, TLS, or
# DNS error is inconclusive, never a pass.
(
trap - DEBUG RETURN ERR # assumes a clean shell (CONTRIBUTING rule 7): no inherited DEBUG trap, extdebug, function or alias
set +x +a +e # never trace or export the credential read below
{ unset -n pw && unset -v pw; } 2>/dev/null ||
{ echo 'a readonly pw is set in this shell; not probing'; exit 2; }
{ unset -n IFS; } 2>/dev/null || { echo 'a readonly IFS is set in this shell; not probing'; exit 2; }
set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_HOST'
[ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo 'paste the whole block, including its set -- line; not probing'; exit 2; }
shift
[ "$#" -eq 1 ] || { echo 'the set -- line needs exactly 1 value; not probing'; exit 2; }
case "$1" in ''|*REPLACE_WITH_*) echo 'substitute the host; not probing'; exit 2 ;; esac
IFS= read -r -s -p 'admin password: ' pw < /dev/tty || exit 2; echo
case "$pw" in ''|*[[:cntrl:]]*) echo 'supply the admin password; not probing'; exit 2 ;; esac
pw=${pw//\\/\\\\}; pw=${pw//\"/\\\"} # escape \ and " so curl --config parsing keeps the exact password
base=(-sS --noproxy '*' --connect-timeout 5 --max-time 30)
# 1) HTTP must redirect to HTTPS.
curl -q -g "${base[@]}" -D - -o /dev/null -w 'redirect http=%{http_code}\n' "http://$1/" # expect 301 + https Location
# 2) Auth triple against ONE protected resource. No creds and wrong creds must be 401; valid creds the app's
# expected success. The password reaches curl through stdin config (curl --config -), never argv.
# The hidden prompt keeps it out of shell history and set +x keeps it out of set -x tracing. Stdin
# closes the argv channel only: the account owner and root can still read it in the subshell and curl.
curl -q -g "${base[@]}" -o /dev/null -w 'no-creds http=%{http_code}\n' "https://$1/"
printf 'user = "admin:%s"\n' 'definitely-wrong' | curl -q -g "${base[@]}" --config - -o /dev/null -w 'bad-creds http=%{http_code}\n' "https://$1/"
printf 'user = "admin:%s"\n' "$pw" | curl -q -g "${base[@]}" --config - -o /dev/null -w 'valid http=%{http_code}\n' "https://$1/"
# 3) Body-size guard, BOTH cases with valid creds so a 401 cannot masquerade as the result. Payloads live in
# a private temp dir removed on exit.
tmp=$(mktemp -d) || { echo 'no tempdir; not probing'; exit 2; }
trap 'rm -rf "$tmp"' EXIT
head -c 1M /dev/zero > "$tmp/under.bin" || { echo 'payload write failed'; exit 2; }
head -c 11M /dev/zero > "$tmp/over.bin" || { echo 'payload write failed'; exit 2; }
for f in under over; do
printf 'user = "admin:%s"\n' "$pw" | curl -q -g "${base[@]}" --config - --data-binary @"$tmp/$f.bin" \
-o /dev/null -w "$f http=%{http_code}\n" "https://$1/"
done
# under: the app's success, not 413; over: 413. req.body_size reads the advertised Content-Length, which curl
# sets here; a chunked upload advertises none and is not covered. A backend can return 413 too, so to
# attribute the refusal to HAProxy, in an isolated fixture remove ONLY the http-request deny line and
# confirm the over upload then reaches the app.
)
ss -tlnp # inventory: the backend (3000) must be 127.0.0.1 only, never 0.0.0.0 - the checks above pass while
# the backend also answers directly on 3000, bypassing HAProxy's TLS and auth. Confirm the runtime
# socket is a root-owned mode-600 Unix socket (ls -l /run/haproxy/admin.sock), not a TCP listener.REASONED: following block; management-surface comparisons follow the cited HAProxy stats, runtime API and TLS documentation, with no HAProxy runtime or certificate fixtures available; Bash 5.3.9(1)-release passed bash -n on the following fence, and extracted guards with local printf markers rejected empty, placeholder and missing host setup but admitted both proxy.internal and bad host, so hostname syntax is not validated; certificate/key guards skipped placeholders, empty and nonexistent paths with exit 0 and reached the marker for readable plain-text files, proving neither certificate validity nor a successful TLS check; no network command ran. Verify each management surface you enabled, REASONED from the cited HAProxy documentation: the stats listener with
an unauthenticated request (refused) and a valid-credential request (returns the statistics) against that
listener specifically; the runtime socket by its ownership and mode and an unauthorized local identity
(permission denied), since show cli level reporting admin is the authorized level, not proof of access
control; and, where verify required is set, client certificates (no certificate and an untrusted one
rejected at TLS, a trusted one admitted with --cert/--key, server verification kept on).
In this block and the one above, paste the password alone at the hidden prompt: a multi-line paste leaves
every line after the first for your shell to run.
# REASONED: management-surface checks follow the cited HAProxy documentation; no HAProxy runtime is available.
(
trap - DEBUG RETURN ERR # assumes a clean shell (CONTRIBUTING rule 7): no inherited DEBUG trap, extdebug, function or alias
set +x +a +e
{ unset -n sp && unset -v sp; } 2>/dev/null ||
{ echo 'a readonly sp is set in this shell; not probing'; exit 2; }
{ unset -n IFS; } 2>/dev/null || { echo 'a readonly IFS is set in this shell; not probing'; exit 2; }
set -- PASTE_WHOLE_BLOCK 'REPLACE_WITH_HOST'
[ "${1-}" = PASTE_WHOLE_BLOCK ] || { echo 'paste the whole block, including its set -- line; not probing'; exit 2; }
shift
[ "$#" -eq 1 ] || { echo 'the set -- line needs exactly 1 value; not probing'; exit 2; }
case "$1" in ''|*REPLACE_WITH_*) echo 'substitute the host; not probing'; exit 2 ;; esac
base=(-sS --noproxy '*' --connect-timeout 5 --max-time 15)
# Stats listener (run on the HAProxy machine; --resolve keeps the certificate hostname while connecting to
# the loopback bind): no credentials must be refused; valid credentials return the statistics.
curl -q -g "${base[@]}" --resolve "$1:8404:127.0.0.1" -o /dev/null -w 'stats no-creds=%{http_code}\n' "https://$1:8404/stats"
# The stats password reaches curl through stdin config (curl --config -), never argv. The hidden prompt
# keeps it out of shell history and set +x keeps it out of set -x tracing. Stdin closes the argv channel
# only: the account owner and root can still read it in the subshell and curl.
IFS= read -r -s -p 'stats admin password: ' sp < /dev/tty || exit 2; echo
case "$sp" in ''|*[[:cntrl:]]*) echo 'supply the stats password; not probing'; exit 2 ;; esac
sp=${sp//\\/\\\\}; sp=${sp//\"/\\\"}
printf 'user = "admin:%s"\n' "$sp" | curl -q -g "${base[@]}" --resolve "$1:8404:127.0.0.1" --config - -o /dev/null -w 'stats auth=%{http_code}\n' "https://$1:8404/stats"
# mTLS, only if `bind ... verify required`: no cert and an untrusted cert must be rejected at TLS; a trusted
# cert must be admitted (it may still return 401 if Basic auth is also configured). The path guards keep a
# local file error from standing in for a server rejection.
curl -q -g "${base[@]}" -o /dev/null -w 'mtls no-cert=%{http_code} exit=%{exitcode}\n' "https://$1/"
for pair in 'REPLACE_WITH_UNTRUSTED_CRT:REPLACE_WITH_UNTRUSTED_KEY:untrusted' 'REPLACE_WITH_TRUSTED_CRT:REPLACE_WITH_TRUSTED_KEY:trusted'; do
crt=${pair%%:*}; rest=${pair#*:}; key=${rest%%:*}; label=${rest##*:}
case "$crt$key" in *REPLACE_WITH_*) echo "substitute the $label cert and key; not probing"; continue ;; esac
if [ -r "$crt" ] && [ -r "$key" ]; then
curl -q -g "${base[@]}" --cert "$crt" --key "$key" \
-o /dev/null -w "mtls $label=%{http_code} exit=%{exitcode}\n" "https://$1/"
else
echo "$label cert or key not readable; not probing"
fi
done
)
# Runtime socket: root-owned mode-600 Unix socket, and an unauthorized local identity must be denied.
ls -l /run/haproxy/admin.sock # expect srw------- root root
# guard-conventions: allow fixed local HAProxy admin socket /run/haproxy/admin.sock; no reader-substituted target
printf 'show info\n' | sudo socat - /run/haproxy/admin.sock | head -1 # authorized (root): prints info
sudo -u nobody socat - /run/haproxy/admin.sock < /dev/null # unauthorized identity: expect permission denied- Copying only
fullchain.peminto the combined crt file used here; that file needs the matching private key too (HAProxy can instead load a separate adjacent key file, but this example uses the combined form). - Renewing the certificate without rebuilding the combined PEM or reloading HAProxy.
- Backends reachable directly on
0.0.0.0, bypassing the proxy; bind them to127.0.0.1and confirm withss -tlnp.
- HAProxy 3.0 Configuration Manual (
bind ssl crt/verify,ssl-default-bind-options/-ciphers/-ciphersuites,maxconn,req.body_size,http-request deny deny_status,http-after-response,timeout http-request,stats enable/auth/admin,user/group/chroot) (http-after-response: HAProxy 2.2+): https://docs.haproxy.org/3.0/configuration.html - HAProxy 3.0 Management Guide (runtime API over the
stats socket, command levels, reload): https://docs.haproxy.org/3.0/management.html - HAProxy supported releases: https://www.haproxy.org/
- HAProxy statistics dashboard (
stats enable/auth/admincapabilities): https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/ - Authelia HAProxy integration (MFA portal): https://www.authelia.com/integration/proxies/haproxy/
- Mozilla SSL Configuration Generator (TLS policy, not HAProxy syntax): https://ssl-config.mozilla.org/
- OpenSSL 3.0
passwd -6SHA-512 crypt and terminal prompting (checked October 2026): https://docs.openssl.org/3.0/man1/openssl-passwd/ - Certbot v2.3.0 deploy hooks after successful renewal (checked October 2026): https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675
- Cloudflare Access MFA documentation at c896795b8e8bab68d05ae78d64c9cbcda1fdb104, application or policy MFA (checked October 2026): https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81
This generated section holds the recorded versions, claim statuses and source mappings for AI assistants; use the prompt at the top. This guide is guidance to verify against your own deployment, not a certification, an audit, or a guarantee of compliance with any standard.
Version basis
AI assistants must compare the versions listed below with current releases and treat this guide as guidance, re-verifying version-specific defaults when newer releases exist.
DEMONSTRATED refers to historical evidence in this guide; REASONED records source reasoning, not a live demonstration. Unknown means no version recorded.
Components (recorded basis):
- haproxy: HAProxy (3.0)
- hsts: HAProxy http-after-response minimum (2.2+)
- docs: Supporting documentation (unknown)
- openssl-cli: OpenSSL password command (3.0)
- certbot-source: Certbot deploy hook documentation (v2.3.0)
- cloudflare-source: Cloudflare Access MFA documentation (c896795b8e8bab68d05ae78d64c9cbcda1fdb104)
Claims (full source mapping in the JSON block below):
- scope (REASONED; haproxy, docs): Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.
- pem (REASONED; haproxy): Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.
- renewal (REASONED; haproxy, certbot-source): Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).
- tls (REASONED; haproxy): Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.
- ciphers (REASONED; haproxy, docs): ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.
- privileges (REASONED; haproxy): Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.
- timeouts (REASONED; haproxy): Set connect 5s, client/server 30s and http-request 10s timeouts.
- hsts (REASONED; haproxy, hsts): http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.
- backend (REASONED; haproxy): Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.
- remote-backend (REASONED; haproxy): Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.
- basic (REASONED; haproxy, openssl-cli): Use a crypt(3)-hashed userlist and http_auth condition;
$6$ works on glibc Linux, while insecure-password stores plaintext. - mtls (REASONED; haproxy): For machine access, bind verify required with ca-file requires trusted client certificates.
- mfa (REASONED; docs, cloudflare-source): Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).
- maxconn (REASONED; haproxy): Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.
- body-guard (REASONED; haproxy): No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.
- body-buffering (REASONED; haproxy): Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.
- chunked (REASONED; haproxy): Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.
- stats-default (REASONED; haproxy, docs): Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.
- stats-admin (REASONED; haproxy, docs): stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.
- stats-auth (REASONED; haproxy, docs): Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.
- runtime-default (REASONED; haproxy): Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.
- runtime-isolation (REASONED; haproxy): Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.
- verify-config (REASONED; haproxy): haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.
- verify-redirect (REASONED; haproxy): HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.
- verify-auth (REASONED; haproxy): On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.
- verify-size (REASONED; haproxy): With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.
- verify-backend (REASONED; haproxy): Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.
- verify-stats (REASONED; haproxy, docs): Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.
- verify-mtls (REASONED; haproxy): With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.
- verify-runtime (REASONED; haproxy): Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.
{
"schema": 1,
"checked": "2026-10-05",
"documentation_checked": "2026-09",
"body_sha256": "139557576e68c1f8eda2367c1d08c55341e12e0e390f9af7f74d71fa03795507",
"components": {
"haproxy": {
"name": "HAProxy",
"basis": "3.0",
"sources": {
"s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html",
"s9c32621d6493": "https://docs.haproxy.org/3.0/management.html"
}
},
"hsts": {
"name": "HAProxy http-after-response minimum",
"basis": "2.2+",
"sources": {
"s5792e6ff4990": "https://docs.haproxy.org/3.0/configuration.html"
}
},
"docs": {
"name": "Supporting documentation",
"basis": "unknown",
"sources": {
"s2be90eb936bd": "https://www.haproxy.org/",
"sae992a8ac3a7": "https://www.haproxy.com/documentation/haproxy-configuration-tutorials/alerts-and-monitoring/statistics/",
"s192d42dd9feb": "https://www.authelia.com/integration/proxies/haproxy/",
"s529b0eabe2ed": "https://ssl-config.mozilla.org/"
}
},
"openssl-cli": {
"name": "OpenSSL password command",
"basis": "3.0",
"sources": {
"s3b2ccc45875a": "https://docs.openssl.org/3.0/man1/openssl-passwd/"
}
},
"certbot-source": {
"name": "Certbot deploy hook documentation",
"basis": "v2.3.0",
"sources": {
"sd39028a759a7": "https://github.com/certbot/certbot/blob/v2.3.0/certbot/docs/using.rst#L659-L675"
}
},
"cloudflare-source": {
"name": "Cloudflare Access MFA documentation",
"basis": "c896795b8e8bab68d05ae78d64c9cbcda1fdb104",
"sources": {
"sdcac732541da": "https://github.com/cloudflare/cloudflare-docs/blob/c896795b8e8bab68d05ae78d64c9cbcda1fdb104/src/content/docs/cloudflare-one/access-controls/policies/mfa-requirements.mdx#L35-L81"
}
}
},
"claims": {
"scope": {"text": "Examples target a maintained HAProxy 3.0 installation and treat frontend, stats, runtime API and direct backend as separate surfaces.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "docs:s2be90eb936bd"], "status": "REASONED"},
"pem": {"text": "Use matching chain/key in a combined PEM, built via a root-only temporary file and rename; separate key loading is also supported.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"renewal": {"text": "Rebuild the combined PEM and reload HAProxy after successful renewal using Certbot v2.3.0 --deploy-hook or an executable in /etc/letsencrypt/renewal-hooks/deploy (if the configuration directory is /etc/letsencrypt; certbot renew only).", "components": ["haproxy", "certbot-source"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493", "certbot-source:sd39028a759a7"], "status": "REASONED"},
"tls": {"text": "Frontend :443 loads the PEM with ssl and ssl-min-ver TLSv1.2; :80 redirects to HTTPS with 301.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"ciphers": {"text": "ssl-default-bind-ciphers governs TLS 1.2 and -ciphersuites TLS 1.3; Mozilla is policy guidance, not HAProxy syntax authority.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:s529b0eabe2ed"], "status": "REASONED"},
"privileges": {"text": "Preserve service hardening; root-started workers drop to haproxy user/group and chroot into a provisioned root-owned directory.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"timeouts": {"text": "Set connect 5s, client/server 30s and http-request 10s timeouts.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"hsts": {"text": "http-after-response (HAProxy 2.2+) covers generated 401/413 responses that http-response misses; includeSubDomains requires HTTPS on every affected subdomain.", "components": ["haproxy", "hsts"], "sources": ["haproxy:s5792e6ff4990", "hsts:s5792e6ff4990"], "status": "REASONED"},
"backend": {"text": "Example backend is plaintext 127.0.0.1:3000 with health checks; keep it private.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"remote-backend": {"text": "Remote TLS backends require certificate verification, a trusted CA and verifyhost matching the backend name.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"basic": {"text": "Use a crypt(3)-hashed userlist and http_auth condition; $6$ works on glibc Linux, while insecure-password stores plaintext.", "components": ["haproxy", "openssl-cli"], "sources": ["haproxy:s5792e6ff4990", "openssl-cli:s3b2ccc45875a"], "status": "REASONED"},
"mtls": {"text": "For machine access, bind verify required with ca-file requires trusted client certificates.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"mfa": {"text": "Basic is single-factor; Authelia uses its HAProxy Lua integration, or front the site with Cloudflare Access and require MFA in its governing Access setting (policy over application over organization).", "components": ["docs", "cloudflare-source"], "sources": ["docs:s192d42dd9feb", "cloudflare-source:sdcac732541da"], "status": "REASONED"},
"maxconn": {"text": "Global maxconn 4096 caps per-process concurrent connections; frontend maxconn 2000 caps that frontend.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"body-guard": {"text": "No single request-body-size directive: req.body_size reads advertised Content-Length; deny_status 413 rejects values above 10485760 without buffering.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"body-buffering": {"text": "Do not add http-buffer-request for this guard: it waits for a complete body or full request buffer.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"chunked": {"text": "Chunked req.body_size reflects available data, so this unbuffered guard does not meaningfully bound chunked uploads; enforce the real limit in the app.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED"},
"stats-default": {"text": "Stats is disabled until configured and enabling it adds no authentication; inventory every loaded config.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"},
"stats-admin": {"text": "stats admin permits backend enable/disable/drain; omit it for monitoring and keep any admin page private.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"},
"stats-auth": {"text": "Bind the example stats listener to TLS 127.0.0.1:8404 at /stats and use hashed-userlist auth instead of cleartext stats auth.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED"},
"runtime-default": {"text": "Runtime API is not enabled by default; default command level is operator, and level admin grants authority rather than authentication.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"},
"runtime-isolation": {"text": "Keep /run/haproxy/admin.sock a root-owned mode-600 Unix socket; HTTP stats auth does not protect an exposed runtime TCP socket.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED"},
"verify-config": {"text": "haproxy -c precedes reload; the guide records shell checks but no HAProxy runtime or exposed/fixed response demonstration.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]},
"verify-redirect": {"text": "HTTP must return 301 with an HTTPS Location; transport/TLS/DNS errors are inconclusive.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]},
"verify-auth": {"text": "On one protected resource, absent and wrong credentials yield 401 while valid credentials reach the expected app response; credentials travel via stdin.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]},
"verify-size": {"text": "With valid credentials, 1M succeeds and 11M returns 413; remove only the deny rule in isolation to attribute refusal. These Content-Length probes do not test chunked bodies.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [1]},
"verify-backend": {"text": "Inventory all listeners: backend 3000 must be loopback-only even if frontend checks pass, and the runtime endpoint must not be TCP.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990", "haproxy:s9c32621d6493"], "status": "REASONED", "verify": [1]},
"verify-stats": {"text": "Probe the 8404 stats listener itself via loopback with the certificate hostname: no credentials refused, valid credentials return statistics.", "components": ["haproxy", "docs"], "sources": ["haproxy:s5792e6ff4990", "docs:sae992a8ac3a7"], "status": "REASONED", "verify": [2]},
"verify-mtls": {"text": "With verify required, no/untrusted client certificates must fail TLS and trusted ones pass; readable cert/key controls avoid local errors, and Basic may still return 401.", "components": ["haproxy"], "sources": ["haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]},
"verify-runtime": {"text": "Check root/root mode 600 and root show info success versus nobody permission denial; reported admin command level is not proof of access control.", "components": ["haproxy"], "sources": ["haproxy:s9c32621d6493", "haproxy:s5792e6ff4990"], "status": "REASONED", "verify": [2]}
}
}