Enterprise modules
Update a certificate store from a remote source
This page applies to:
- HAProxy Enterprise 3.4r1 and newer
- HAProxy ALOHA 18.5 and newer
The Certificate Store Update module fills a named crt-store with certificates downloaded from one or more URLs, and fetches them again on an interval. A certificate store normally holds the certificates that were loaded from disk when the load balancer started. With this module, the store can also track certificates that live somewhere else.
The module supports fetching certificates from two types of URLs:
- Fetch certificates from a JSON Web Key Set (JWKS) URL.
- Fetch certificates from a URL that hosts PEM files, when the identity provider doesn’t host a JWKS URL.
Fetching certificates from a JWKS URL is appropriate when you need to verify that JSON Web Tokens (JWTs) have been digitally signed by the identity provider that issued them. A JWKS is a JSON document that contains one or more public keys, indexed with unique IDs, that match up with digital signatures. Providing multiple keys in a single document makes it possible to rotate signing keys without disrupting clients still using JWTs signed by an older key. That’s because each JWT embeds the ID of the key it was signed with, so the right key can be looked up in the JWKS document.
Ensure the keys set the x5c field
To be compatible with HAProxy Enterprise’s Certificate Store Update module, a JWKS should include an x5c field for each key, which contains the public key in X.509 certificate form.
The module can also fetch certificates from a URL that returns a single certificate PEM file, in case the identity provider doesn’t host a JWKS URL but lets you download the certificates directly. You wouldn’t have the same ability to safely rotate keys, since you wouldn’t get a document with multiple certificates.
Use this module when verifying the digital signatures of JWTs during an OAuth flow. You don’t need this module when configuring the OpenID Connect module, since that has a built-in feature for fetching keys from a JWKS URL.
Install the Certificate Store Update module Jump to heading
-
Install the Certificate Store Update module according to your platform:
Install the Certificate Store Update modulenixsudo apt-get install hapee-3.4r1-lb-crtstore-updatenixsudo apt-get install hapee-3.4r1-lb-crtstore-updatenixsudo yum install hapee-3.4r1-lb-crtstore-updatenixsudo yum install hapee-3.4r1-lb-crtstore-updatenixsudo zypper install hapee-3.4r1-lb-crtstore-updatenixsudo zypper install hapee-3.4r1-lb-crtstore-updatenixsudo pkg install hapee-3.4r1-lb-crtstore-updatenixsudo pkg install hapee-3.4r1-lb-crtstore-update -
In the
globalsection of your configuration file, add amodule-loaddirective:haproxyglobal...module-load hapee-lb-crtstore-update.sohaproxyglobal...module-load hapee-lb-crtstore-update.soThe default HAProxy Enterprise configuration already sets
module-pathto the module directory, so themodule-loadline is all you need.Try the fetch flow with the sample certificates
The module package includes example JWKS documents, so you can exercise the fetch flow without a real identity provider. To test this:
-
Serve the example JWKS documents from a local web server:
nixcd /etc/hapee-3.4/crtstore_examplespython3 -m http.server 8000nixcd /etc/hapee-3.4/crtstore_examplespython3 -m http.server 8000 -
Put the test configuration in its own file, so your main configuration is untouched:
crtstore-test.cfghaproxygloballog stderr format raw local0 infostats socket /tmp/crtstore-test.sock mode 600 level adminmodule-path /opt/hapee-3.4/modulesmodule-load hapee-lb-crtstore-update.socrt-store sample_storeupdate url http://127.0.0.1:8000/keys.jwk delay 5 retry-delay 5 format jwkcrtstore-test.cfghaproxygloballog stderr format raw local0 infostats socket /tmp/crtstore-test.sock mode 600 level adminmodule-path /opt/hapee-3.4/modulesmodule-load hapee-lb-crtstore-update.socrt-store sample_storeupdate url http://127.0.0.1:8000/keys.jwk delay 5 retry-delay 5 format jwk -
Start the load balancer. The
log stderrline makes each fetch visible:nixsudo /opt/hapee-3.4/sbin/hapee-lb \-f /etc/hapee-3.4/crtstore-test.cfgnixsudo /opt/hapee-3.4/sbin/hapee-lb \-f /etc/hapee-3.4/crtstore-test.cfg -
After a few seconds, check the store in another terminal:
nixecho "show ssl jwt" | sudo socat stdio /tmp/crtstore-test.socknixecho "show ssl jwt" | sudo socat stdio /tmp/crtstore-test.sockThe store lists one entry per key in the document:
outputtext# filename@sample_store/<kid>outputtext# filename@sample_store/<kid>
Three sample documents are installed:
keys.jwkandkeycloak_keys.jwkare working JWKS documents from two providers.invalid.jwkis deliberately not valid JSON. Use it to see the failure path: the fetch returns200, nothing is inserted, and the log shows only an ordinary request line.
A blank line instead of a list means nothing has been fetched yet. Check the
log stderroutput for the fetch, and run the command again after a few seconds. -
Fetch certificates from a JWKS URL Jump to heading
This example demonstrates a portion of an OAuth configuration that verifies bearer tokens that clients present to the load balancer. The identity provider publishes its signing certificates as a JSON Web Key Set (JWKS).
-
Declare a named certificate store and one
updateline per remote source:hapee-lb.cfghaproxycrt-store idp_storeupdate url https://login.example.com/common/discovery/v2.0/keys delay 3600 retry-delay 60 format jwkhapee-lb.cfghaproxycrt-store idp_storeupdate url https://login.example.com/common/discovery/v2.0/keys delay 3600 retry-delay 60 format jwkIn this example:
urlpoints to the provider’s key discovery endpoint. It is required and must come first.delay 3600refetches the URL every hour after a successful fetch.retry-delay 60retries a failed fetch after one minute.format jwkreads the response as a JWKS document and names each certificate by itskid.
-
Address the fetched certificates in your configuration. Each entry of the JWKS is stored under
@<store_name>/<kid>, wherekidis the key identifier from the JWKS document. Read thekidfrom the token’s header to build the name:hapee-lb.cfghaproxyfrontend apibind :443 ssl crt /etc/hapee-3.4/site.pemmode httphttp-request set-var(txn.jwt) http_auth_bearerhttp-request set-var(txn.alg) var(txn.jwt),jwt_header_query('$.alg')http-request set-var(txn.kid) var(txn.jwt),jwt_header_query('$.kid')http-request set-var(txn.crt) str(),concat('@idp_store/',txn.kid)http-request deny content-type 'text/html' string 'Invalid token' unless { var(txn.jwt),jwt_verify_cert(txn.alg,txn.crt) -m int 1 }hapee-lb.cfghaproxyfrontend apibind :443 ssl crt /etc/hapee-3.4/site.pemmode httphttp-request set-var(txn.jwt) http_auth_bearerhttp-request set-var(txn.alg) var(txn.jwt),jwt_header_query('$.alg')http-request set-var(txn.kid) var(txn.jwt),jwt_header_query('$.kid')http-request set-var(txn.crt) str(),concat('@idp_store/',txn.kid)http-request deny content-type 'text/html' string 'Invalid token' unless { var(txn.jwt),jwt_verify_cert(txn.alg,txn.crt) -m int 1 }In this example:
http_auth_bearerreads the token from theAuthorizationheader and stores it in thetxn.jwtvariable.jwt_header_query('$.alg')extracts the signing algorithm from the token header and stores it in thetxn.algvariable.jwt_header_query('$.kid')extracts the key identifier from the token header and stores it in thetxn.kidvariable.str(),concat('@idp_store/',txn.kid)builds the certificate name in the store from the store name and the token’s ownkid, so a token signed with a newly rotated key resolves to the certificate that the store fetched for it.jwt_verify_cert(txn.alg,txn.crt)verifies the token signature against that certificate, and thehttp-request denyline rejects the request unless the converter returns1.
-
Confirm that the fetched certificates are in the store. The module marks them for token validation, so they appear in the list that
show ssl jwtreturns:nixecho "show ssl jwt" | \sudo socat stdio unix-connect:/var/run/hapee-3.4/hapee-lb.socknixecho "show ssl jwt" | \sudo socat stdio unix-connect:/var/run/hapee-3.4/hapee-lb.sockoutputtext# filename@idp_store/<kid>outputtext# filename@idp_store/<kid>Tip
A certificate that’s missing from this list hasn’t been fetched yet. Wait a few seconds and run the command again, or check the
urlargument.
Fetch a PEM certificate Jump to heading
The pem format accommodates certificate sources that publish a single X.509 certificate directly, rather than exposing a JWKS document. An internal or legacy token issuer, for example, can publish its current signing certificate at a stable URL. The load balancer refreshes that certificate after each key rotation, without external distribution automation, and without a JWKS endpoint that exists only to distribute one public certificate.
Each argument follows from what the source publishes. The remote resource is already a certificate, so format pem inserts it as is. The document has no kid, so you must pass name, and setting name with format jwk is rejected. There is no key set to enumerate, so one URL supplies exactly one certificate.
hapee-lb.cfghaproxycrt-store tls_certsupdate url https://pki.example.com/edge.pem delay 86400 retry-delay 600 format pem name edge.pem
hapee-lb.cfghaproxycrt-store tls_certsupdate url https://pki.example.com/edge.pem delay 86400 retry-delay 600 format pem name edge.pem
Info
Setting name with format jwk isn’t valid and the load balancer will reject it.
In this example, the certificate is available as @tls_certs/edge.pem, and show ssl jwt lists it under that name, alongside any certificates that came from a JWKS document. Reference that name where the load balancer looks up a certificate at runtime, such as in the jwt_verify_cert converter:
hapee-lb.cfghaproxyfrontend apibind :443 ssl crt /etc/hapee-3.4/site.pemmode httphttp-request set-var(txn.jwt) http_auth_bearerhttp-request set-var(txn.alg) var(txn.jwt),jwt_header_query('$.alg')http-request deny content-type 'text/html' string 'Invalid token' unless { var(txn.jwt),jwt_verify_cert(txn.alg,'@tls_certs/edge.pem') -m int 1 }
hapee-lb.cfghaproxyfrontend apibind :443 ssl crt /etc/hapee-3.4/site.pemmode httphttp-request set-var(txn.jwt) http_auth_bearerhttp-request set-var(txn.alg) var(txn.jwt),jwt_header_query('$.alg')http-request deny content-type 'text/html' string 'Invalid token' unless { var(txn.jwt),jwt_verify_cert(txn.alg,'@tls_certs/edge.pem') -m int 1 }
Caution
You can’t use a fetched certificate as the server certificate on a bind line. A certificate fetched this way carries no private key, and a bind line resolves its certificates when the configuration is parsed, before the first fetch.
Reference Jump to heading
The update directive configures the remote sources that fill a certificate store. The converters below read certificates from that store, and the sections that follow describe how refreshes work and what the module cannot do.
update Jump to heading
The update directive fills and refreshes the certificate store that contains this line. Declare the line inside a named crt-store section, one line per remote source. A store can hold several update lines, each with its own URL and its own delay and retry-delay.
textupdate url <url>[delay <seconds>][retry-delay <seconds>][format <jwk|pem>][name <certname>]
textupdate url <url>[delay <seconds>][retry-delay <seconds>][format <jwk|pem>][name <certname>]
| Argument | Description |
|---|---|
url <url> |
URL that the load balancer requests with a GET. It’s required, and must appear first. The load balancer resolves the hostname with its internal HTTP client. |
delay <seconds> |
Delay in seconds between two successful fetches of this URL. The default is 86400, or 24 hours. |
retry-delay <seconds> |
Delay in seconds between a failed request and the next attempt against the same URL. The default is 600, or 10 minutes. |
format <jwk|pem> |
Format of the data returned by the remote source. The default is jwk. |
name <certname> |
Name of the certificate. Mandatory when format is pem, and rejected when format is jwk. |
Configuration errors
The load balancer rejects the configuration when:
- a
crt-storesection has no name - an
updateline has nourlargument or a secondurlargument formatis neitherjwknorpemnameis present withformat jwkor missing withformat pem
jwt_verify_cert Jump to heading
The jwt_verify_cert converter verifies the signature of a signed token against a certificate. Pass the algorithm from the token header and the certificate name, such as @idp_store/<kid>. See jwt_verify_cert in the configuration manual.
How certificate refreshes work Jump to heading
-
Soon after the load balancer starts, it requests each URL for the first time. If the remote source is unreachable at that moment, the store stays empty until a fetch succeeds, and the load balancer retries every
retry-delayseconds. -
The load balancer fetches each URL every
delayseconds after a successful fetch, and everyretry-delayseconds after a failed one. A response with a status other than200counts as a failure. -
When a fetch or a parse fails, the certificates already in the store stay in place, and the load balancer retries later.
-
Only one URL is fetched at a time. Entries are ordered by their next update time, so a URL that fails doesn’t hold up the others.
-
A successful fetch replaces the certificate in the store. No reload is needed, and existing uses of the certificate pick up the new one.
-
delayandretry-delayare integers in seconds. Unit suffixes aren’t supported, so1his read as 1 second and a value starting with a letter is read as 0, which requests the URL continuously.Set an interval that suits the source
A short
delayagainst a public endpoint generates steady traffic and may trip rate limits. Size the interval to the rotation schedule you need to track. -
A failed fetch is visible in the logs as a request through an internal proxy named
<MOD-REMOTE-CRTSTORE>, with a status other than200. See Check the fetch in the logs.Check after a rotation
After the source rotates its keys, request a token signed with the new key and send it to the load balancer. If the source is an identity provider, request a fresh token from its token endpoint. If you run the issuer, mint one with the rotated private key. The example frontend returns
403for a token it cannot verify, so a403means the load balancer did not accept the token.Check
show ssl jwtfirst. If the new@<store>/<kid>entry is missing, the fetch has not picked the key up yet. A retired entry can linger in the same list until the load balancer reloads. If the new entry is present, check the algorithm and the certificate name that you pass tojwt_verify_cert. -
The response body shares the load balancer’s buffer, so a large JWKS document or certificate chain may need a larger tune.bufsize value in the
globalsection.
Limitations Jump to heading
- Only JWKs that set a
x5cfield are inserted; anx5cfield represents the public key in an X.509 certificate. A JWK that contains a bare public key isn’t supported, because a public key can’t be stored in acrt-storeyet. - A JWK entry without a
kid, and an entry whosex5cfield can’t be decoded, will cause the whole response to be discarded, so none of the certificates in it are inserted. - A certificate whose
kiddisappears from the JWKS document stays in the store, and remains usable for validation, until the load balancer reloads. To retire a key safely, publish the new key alongside the old one and wait for the store to pick it up, keep both keys in the document until every token signed with the old key has expired, then remove the old key and reload the load balancer. - The module sends a plain GET request. It offers no arguments for a client certificate, an authorization header, or a custom CA file. For
httpsURLs, the load balancer verifies the remote server with the httpclient.ssl.ca-file and httpclient.ssl.verify global settings.
Troubleshooting Jump to heading
Most problems with this module show up in the log line for the fetch or in the contents of the store. Check both, then work through the sections below for the usual causes.
Check the fetch in the logs Jump to heading
The module logs every fetch as an ordinary request through an internal proxy named <MOD-REMOTE-CRTSTORE>. A successful fetch and a failed one differ only in the HTTP status code and the termination state.
A fetch that returns 200:
successful fetchtext-:- [23/Sep/2026:19:07:10.109] <MOD-REMOTE-CRTSTORE> -/- 2/0/1/5/6 200 8290 - - ---- 0/0/0/0/0 0/0 {} "GET http://127.0.0.1:8000/keys.jwk HTTP/1.1"
successful fetchtext-:- [23/Sep/2026:19:07:10.109] <MOD-REMOTE-CRTSTORE> -/- 2/0/1/5/6 200 8290 - - ---- 0/0/0/0/0 0/0 {} "GET http://127.0.0.1:8000/keys.jwk HTTP/1.1"
A source that answers with an error, here a 404:
404 from the sourcetext-:- [29/Sep/2026:15:55:27.321] <MOD-REMOTE-CRTSTORE> -/- 2/0/3/1/4 404 634 - - ---- 0/0/0/0/0 0/0 {} "GET http://127.0.0.1:8000/nosuchfile.jwk HTTP/1.1"
404 from the sourcetext-:- [29/Sep/2026:15:55:27.321] <MOD-REMOTE-CRTSTORE> -/- 2/0/3/1/4 404 634 - - ---- 0/0/0/0/0 0/0 {} "GET http://127.0.0.1:8000/nosuchfile.jwk HTTP/1.1"
A source that can’t be reached, here because nothing listens on that port:
unreachable sourcetext-:- [29/Sep/2026:15:54:46.653] <MOD-REMOTE-CRTSTORE> -/- 2/0/-1/-1/3023 503 0 - - SC-- 0/0/0/0/3 0/0 {} "GET http://127.0.0.1:8099/keys.jwk HTTP/1.1"
unreachable sourcetext-:- [29/Sep/2026:15:54:46.653] <MOD-REMOTE-CRTSTORE> -/- 2/0/-1/-1/3023 503 0 - - SC-- 0/0/0/0/3 0/0 {} "GET http://127.0.0.1:8099/keys.jwk HTTP/1.1"
Read these fields from the log line:
<MOD-REMOTE-CRTSTORE>is the proxy name that the module logs its requests under. Filter the log on that name to see only the fetches.- The status code says whether the fetch worked.
200means the module received a document, and any other status counts as a failure. - The termination state
SC--means the load balancer couldn’t connect to the source at all. It tells an unreachable server apart from one that answered with an error. - The bytes field is the response body plus about 190 bytes of headers. It can be lower than the full body when the load balancer stops reading after a non-
200status, so don’t use it to judge whether a document arrived complete.
A failed fetch produces no ALERT line and no message of its own, so the request line is the only record. An ALERT at startup reports a configuration error, such as a missing url argument. These lines appear only if the load balancer has a logger. To see them on the console, add a line such as log stderr format raw local0 info to the global section.
The store is empty Jump to heading
show ssl jwt lists the certificates in the store. When the command returns only the # filename header, nothing has been inserted. Work through these checks:
- Check the fetch in the log. A status other than
200means the source answered with an error or couldn’t be reached. See Check the fetch in the logs. - Check the timing arguments.
delayandretry-delaytake plain integers in seconds, so1his read as 1 second, and a value that starts with a letter is read as 0. - Wait for the first fetch. It happens soon after startup, but the store stays empty until a fetch succeeds, and a failed fetch is retried every
retry-delayseconds. - Check that the document itself is usable. See The fetch returns 200 but nothing is inserted.
The fetch returns 200 but nothing is inserted Jump to heading
A 200 response that can’t be parsed is logged as an ordinary request line, with nothing to say that the module rejected the document, and the store keeps the certificates already in it. Common causes:
- The document isn’t valid JSON. The packaged
invalid.jwkfile is an example of this, and you can use it to see the behavior. - A key in the document has no
kid, or has anx5cfield that can’t be decoded. Either one discards the whole response, so none of the certificates in it are inserted. - The document holds a bare public key rather than a certificate chain. Only entries with a full chain in the
x5cfield can be stored in acrt-store. - The response is larger than the load balancer’s buffer. A large JWKS document or certificate chain may need a larger tune.bufsize value in the
globalsection.
Compare the store contents against the document when you suspect a parse problem. show ssl jwt lists one entry per key that was inserted, named @<store_name>/<kid>.
See also Jump to heading
- To inspect the certificates that the load balancer can use for token validation, see show ssl jwt.
- To understand certificate stores and how a frontend uses them, see Basics of enabling TLS and Certificate Storage in the configuration manual.
- To manage certificates at runtime, see Manage SSL certificates and set ssl cert.
- To let the single sign-on module retrieve provider keys for its own sign-in flow, see OpenID Connect.
- To refresh ACL and map files from a URL, see Dynamic updates.