Skip to main content

X.509 — Adding CA Certificates to the Trust Bundle

This guide explains how to take a CA certificate issued by an external PKI (such as a national identity authority), convert it to the correct format, and add it so that voter certificates signed by that CA are accepted.

See also: X.509 Certificate Voter Authentication — full dev and production setup.


1. Identify the Certificate Format

CA certificates are distributed in two common encodings:

FormatDescriptionHow to recognise
PEMBase64-encoded, plain textStarts with -----BEGIN CERTIFICATE-----
DERBinary encodingStarts with hex bytes 30 82 (ASN.1 SEQUENCE)

Common file extensions (.cer, .crt, .der) do not reliably indicate the encoding — always inspect the file content:

# If the output starts with -----BEGIN CERTIFICATE-----, it is already PEM.
head -1 <ca-certificate-file>

# Otherwise check the binary header — 30 82 means DER.
xxd <ca-certificate-file> | head -1

2. Convert DER to PEM (if needed)

openssl x509 -inform DER -in <ca-certificate-file> -out <ca-certificate-file>.pem

Verify the result:

openssl x509 -in <ca-certificate-file>.pem -text -noout | head -20

3. PKCS#7 Bundles

Some authorities distribute certificates as a PKCS#7 file (.p7b / .p7c or a PEM file starting with -----BEGIN PKCS7-----). Extract all certs with:

openssl pkcs7 -print_certs -in <bundle-file> -out extracted.pem

4. Add the CA via the Admin Portal

CA certificates are stored in the database per election event and served by Harvest. This applies to both dev and production.

  1. Log in and navigate to the election event
  2. Open the Certificate Authorities tab
  3. Click Import, upload the PEM file
  4. Confirm

Keycloak picks up the new CA within the next refresh cycle (at most KC_SPI_TRUSTSTORE_URL_REFRESH_INTERVAL_SECONDS, default 1 hour in production, 60 seconds in dev). No Keycloak restart is required.

To force an immediate pickup, reduce the refresh interval temporarily or restart Keycloak.

Permissions required: ca-write on the election event.


5. Update the Cloudflare mTLS Truststore (production only)

In production, Cloudflare terminates the TLS connection and validates the client certificate against its own mTLS truststore before forwarding the request to Keycloak. Cloudflare and Keycloak maintain independent truststores:

TruststoreUpdated byWhen
Keycloak (UrlTruststoreProvider)Admin portal → Postgres → HarvestAutomatic, within refresh cycle
Cloudflare mTLS truststoreOperator, manuallyMust be done separately

When you add or remove a CA via the admin portal, you must also update the Cloudflare mTLS truststore in the Cloudflare dashboard (Access → Service Auth → mTLS). If this step is skipped, Cloudflare will reject voter certificates signed by the new CA at the edge, and they will never reach Keycloak.

Note: in dev, nginx uses optional_no_ca and performs no CA validation, so there is no equivalent manual step — only the admin portal import is needed.


6. Verify End-to-End

After adding the CA, confirm it is trusted:

# 1. Check Keycloak truststore logs
docker compose logs keycloak | grep -i "truststore\|Loading realm-specific\|Using cached"

# 2. Test the full mTLS flow (from inside dev container)
curl -v --cacert .devcontainer/certs/nginx-tls.crt \
--cert <voter-cert.pem> --key <voter-key.pem> \
https://keycloak-nginx:8443/

If the voter certificate is signed by the newly added CA and the Keycloak X.509 authenticator correctly extracts the user identifier, the authentication flow should proceed without error.