The public door closed again this morning. haproxy exited at parse time on
every host with
'bind *:443' : inconsistencies between private key and certificate
loaded '/usr/local/etc/haproxy/certificates/site.pem'
on a leaf and a private key that openssl confirms are one pair — same public
key, and the same pair certbot has been serving since 2026-08-22. Nothing of
ours had changed: the image was rebuilt at 10:21 by the scheduled build, on a
base that had moved overnight.
The cause is the override this file added two days ago. Measured on testnet2,
same image, same certificate, only the variable changed:
OPENSSL_FIPS=no config check fails at `bind *:443`
OPENSSL_FIPS=yes config check passes, curl 200, TLSv1.3
`ENV OPENSSL_FIPS=no` was right for the 2026-09-02 base, where preferring the
FIPS provider left haproxy offering an X25519MLKEM768 group it could not use
and refusing every modern client. On today's base the same value stops it
loading an ECDSA certificate at all, while the shipped "yes" both loads it and
still negotiates TLS 1.3 — the thing the override existed to protect. So it is
removed rather than inverted; the comment keeps why, because the right value
depends on the base and the next base may want the other one.
Which is the second change, and the one that matters. `FROM bitnami/haproxy`
carries no tag and the build runs daily, so this image took whatever the base
was each morning: two outages in three days, neither with a commit behind it,
both diagnosed from scratch. The base is pinned by digest now — the one
measured working today. It stops moving on its own, and bumping it becomes a
deliberate act with a check behind it.
The public door has been refusing every modern client since 2026-09-02,
with no commit behind it. `FROM bitnami/haproxy` carries no tag and the
scheduled build runs daily, so the image is rebuilt each morning on
whatever base is `:latest` that day. That morning's base arrived with
OpenSSL's FIPS provider preferred.
Photon OS reads the preference straight out of the environment:
[alg_sect]
default_properties = ?fips=$ENV::OPENSSL_FIPS
With "yes", every algorithm fetch prefers the FIPS provider, and that
provider cannot produce an X25519MLKEM768 key — ssl_generate_pkey_group
fails. HAProxy offers the group anyway: OpenSSL 3.5 lists it first and
haproxy.cfg sets no ssl-default-bind-curves, so the server picks a group
it cannot then use and answers a fatal illegal_parameter. Anything that
offers post-quantum key exchange — curl, browsers, httpx — is refused;
only a client asking for a classical group gets in. МС talks to ДС over
HTTPS, so no deal could be created through it either.
Nothing here needs FIPS, and the base image itself provides the switch.
The provider stays loaded; only the preference is dropped.
Verified by building this image on testnet2 and serving the live
certificate from it: curl 200 where it was a TLS connect error, group
X25519MLKEM768 negotiated, ECDHE-ECDSA-CHACHA20-POLY1305 still offered,
TLS 1.1 still refused.
The Notes explain at length that no visitor's IP address reaches the log and
why both loggers are hand-written to keep it that way. Nothing showed it. A
reader had to take the claim on faith or read haproxy.cfg.
Add a "the log" section to Usage in both READMEs: a real
`docker compose logs haproxy` excerpt from a running node, the two log formats
broken down field by field, and the point the sample exists to make — the
first field is the X-Client-Id pseudonym, and it is all the log knows about
who connected.
An empty key switches the pseudonym off: no X-Client-Id is sent, and a
rate limit downstream falls back to one bucket shared by every visitor.
That is the one state nobody chooses on purpose and the easiest to reach
by forgetting a line in a .env — so the entrypoint now fills the key in
with `openssl rand -base64 32` when nothing else did.
Nobody picks this value, nothing outside the container needs to know it,
and no two deployments need the same one, which is what makes generating
it the right default rather than a convenience. A fresh key per container
start costs a reset of the downstream rate-limit buckets — invisible
against a one-minute window — and makes pseudonyms from before and after
unlinkable, which is the property the key exists for rather than a loss.
Passing one explicitly still wins, for whoever wants pseudonyms stable
across restarts or identical on two proxies.
base64 and not hex, deliberately: HAProxy's hmac() decodes the key as
base64, and hex would be accepted and silently decoded into something
else — a usable key, but it would quietly cost the guarantee that a
malformed one stops the container at configuration parsing.
The base image's entrypoint is the haproxy binary with no shell in
between, so the wrapper is the whole chain and execs the same binary with
the same arguments. CMD is restated rather than inherited.
Verified by building the image and checking the config in all three
states: unset (wrapper reports it generated one, config parses), set and
valid (wrapper silent, config parses), set and not base64 (`[ALERT]
invalid args in converter 'hmac' : failed to parse key`, container
refuses to start).
READMEs updated in both languages, and "address" is spelled "IP address"
throughout — it was never anything else.