Skip to main content
Version: 3.2.x (beta)

Bundled CA root certificates

certs/ca-bundle.pem is a snapshot of Mozilla's CA root certificate bundle, extracted and packaged by the curl project from the Firefox NSS root store.

The bundle is consumed by core/http_client.lua to verify the certificate chain of any outbound HTTPS request the hub makes (hublist announce, future external feed pulls, future proxy/VPN detection API calls). It is NOT used by the listening TLS port (ssl_ports / ssl_ports_ipv6 use the operator's own certs/servercert.pem + certs/serverkey.pem, and the inbound ssl_params.cafile = certs/cacert.pem is managed by core/cert_bootstrap.lua for the mutual-TLS existence-check use case).

Why ca-bundle.pem and not cacert.pem

cacert.pem is the conventional curl/openssl filename for a CA bundle, but luadch ALREADY uses certs/cacert.pem for an inbound role: core/cert_bootstrap.lua writes the self-signed TLS-listener cert to that path as a satisfy-existence-check for ssl_params.cafile (LuaSec's wrapserver demands the file even when mutual TLS is off).

Two different roles at one path is a latent bug:

  • Inbound (ssl_params.cafile) wants a SINGLE cert (the hub's self-signed or operator-supplied root) used for mutual-TLS client-cert verify. cert_bootstrap.lua manages it.
  • Outbound (http_client.cafile) wants a 186 KB bundle of ~120 trusted public CA roots used to verify remote-server certs on outbound HTTPS. cacert_bootstrap.lua (Precursor 0d) manages it.

The rename to ca-bundle.pem (Precursor 0d of the unified-blocklist arc) splits the two paths cleanly.

Snapshot in this release

FieldValue
Source URLhttps://curl.se/ca/cacert.pem
Snapshot date2026-05-14 (see header lines inside the file)
File size~186 KB
CA count121 trusted roots
SHA-256 (of ca-bundle.pem on disk)86a1f3366afac7c6f8ae9f3c779ac221129328c43f0ab2b8817eb2f362a5025c (re-run sha256sum certs/ca-bundle.pem after every refresh)
SHA-256 (of the certdata.txt source published by Mozilla, embedded in the file header)77130ef91213772844561fbd3aa31d413b25c2ac7f576fea3bc3bbff7ef93489

The two SHA-256s are different by design: the first is the hash of the PEM file shipped in this repo (what sha256sum of the bundle returns); the second is the upstream Mozilla certdata.txt source hash, printed inside the bundle's ## SHA256: header line. Verify both before any refresh - the file hash gates "did we receive the file we expected" and the certdata hash gates "did curl extract from the certdata.txt revision we expected".

Bootstrap: first-boot + upgrade behaviour

On every hub start, core/cacert_bootstrap.lua runs through this matrix:

Runtime stateHub action
certs/ca-bundle.pem missingCopy from lib/luadch/ca-bundle.pem (immutable system path); log INFO
Bundle present + SHA-256 matches lib/luadch/ca-bundle.pemNo log, no action
Bundle present + SHA-256 mismatch + ca_bundle_auto_update = false (default)Log WARN with both hashes; leave file in place. Operator decides if it is a custom bundle (corporate PKI) or an outdated snapshot
Bundle present + SHA-256 mismatch + ca_bundle_auto_update = trueBackup the existing file as certs/ca-bundle.pem.bak-<timestamp> and copy the bundled file; log INFO

For Docker operators with ./certs: volume-mounted from the host: the bundled lib/luadch/ca-bundle.pem is shipped in the image at an immutable path NOT overlaid by the volume mount. The bootstrap copies from there into the host-mounted certs/ directory on first boot. Subsequent image pulls leave the host file alone unless ca_bundle_auto_update = true is set.

For bare-metal operators on cmake --install: the CMake rule installs ca-bundle.pem to BOTH <install>/certs/ (operator- overwriteable; this is the default http_client.cafile) and <install>/lib/luadch/ (immutable source-of-truth). The bootstrap restores the operator-facing copy when missing.

Migration from cacert.pem: existing deployments that have a certs/cacert.pem left over from an older luadch release (or from cert_bootstrap.lua's self-signed copy) are NOT touched. The bundle is at a new path. If you maintained a custom Mozilla bundle at the old name, copy it to certs/ca-bundle.pem.

Cfg keys

KeyDefaultWhat
ca_bundle_pathcerts/ca-bundle.pemRuntime location, also the default http_client.cafile
ca_bundle_source_pathlib/luadch/ca-bundle.pemImmutable system path, source-of-truth bundled with the install
ca_bundle_auto_updatefalseIf true, hub auto-replaces an out-of-date ca_bundle_path with the bundled version (backup file kept)

License

The bundle is dual-licensed by the curl project under MPL-2.0 (carrying Mozilla's original license) or ISC. Both are GPL-3.0 compatible per the FSF's compatibility list. The bundle itself is a collection of public CA certificates; the licensing applies to the extraction tool's output format and the Mozilla source data layout.

Update recipe

CAs change over time - certificates expire, new ones are added, and some get revoked. Refresh the bundle every 3-6 months (Mozilla publishes a new NSS release roughly that often).

One-time refresh (manual)

# 1. Download fresh snapshot + published checksum
curl -fsSL -o examples/certs/ca-bundle.pem.new https://curl.se/ca/cacert.pem
curl -fsSL -o /tmp/cacert.pem.sha256 https://curl.se/ca/cacert.pem.sha256

# 2. Compare published checksum against the downloaded file
( cd /tmp && sha256sum -c cacert.pem.sha256 ) \
&& mv examples/certs/ca-bundle.pem.new examples/certs/ca-bundle.pem \
|| rm examples/certs/ca-bundle.pem.new

# 3. Update the SHA rows in this file with the new on-disk + certdata hashes
sha256sum examples/certs/ca-bundle.pem
grep "^## SHA256:" examples/certs/ca-bundle.pem

# 4. Commit
git add examples/certs/ca-bundle.pem docs/CACERT.md
git commit -m "chore(cacert): refresh Mozilla CA bundle to <date>"

Verifying the running install

# Count CA blocks
grep -c "^-----BEGIN CERTIFICATE-----$" certs/ca-bundle.pem
# Should be around 120-150

# Header date
head -5 certs/ca-bundle.pem

# Hash on disk
sha256sum certs/ca-bundle.pem

Why bundled vs system store

LuaSec (the OpenSSL binding) has no built-in default CA path. Three alternatives, with the trade-offs we considered:

ApproachProConVerdict
Operator-supplied cafile (cfg key)Operator choosesMost ops leave it unset -> verify="none" silently OR the request fails with cryptic LuaSec errorLoses the security benefit by default
OS system CA storeNo bundle to maintainPath varies (Debian/RHEL/Alpine), absent on minimal Docker images, missing on WindowsInconsistent UX across platforms
Bundle in repo + bootstrapOne known path everywhere, fail-closed if missing, smooth upgrade UXWe carry ~186 KB and own the refresh cadencePicked. Consistent behaviour; refresh is one-line / quarterly maintenance

The fail-closed behaviour at core/http_client.lua - refusing the request if cafile is missing rather than silently falling back to verify="none" - is the load-bearing piece. An out-of-the-box deployment authenticates outbound HTTPS or explicitly does not.

Opting out

Callers that need to talk to a self-signed endpoint pass verify = "none" explicitly:

http_client.request {
url = "https://internal.example/api",
verify = "none", -- intentional opt-out; explain WHY in the call site comment
...
}

Callers using their own CA pass a path:

http_client.request {
url = "https://corporate-pki.example/api",
verify = "peer",
cafile = "/etc/corporate-pki/ca-bundle.pem",
...
}

Neither path requires touching certs/ca-bundle.pem - the default just becomes irrelevant for that call.