Skip to content

Ca certificate reload#3568

Open
afreof wants to merge 6 commits intoeclipse-mosquitto:masterfrom
afreof:ca-certificate-reload
Open

Ca certificate reload#3568
afreof wants to merge 6 commits intoeclipse-mosquitto:masterfrom
afreof:ca-certificate-reload

Conversation

@afreof
Copy link
Copy Markdown

@afreof afreof commented Apr 15, 2026

Fix stale CA store and CRL not reloaded on SIGHUP certificate reload

listeners__reload_all_certificates() called only net__load_certificates()
on the existing SSL_CTX, without calling net__tls_load_verify(). This meant
that cafile/capath and crlfile were silently ignored on SIGHUP: CA cert
rotations required a full broker restart to take effect, and each reload
leaked an X509_LOOKUP instance into the store's internal lookup-methods stack.

The fix recreates the SSL_CTX from scratch (net__tls_server_ctx() followed by
net__tls_load_verify()) so that all four TLS options — certfile, keyfile,
cafile/capath, and crlfile — are fully reloaded on every SIGHUP. Existing
connections are unaffected because SSL objects hold their own reference to
the old SSL_CTX until they close.

The man page is updated to document that cafile, capath, and crlfile are now
reloaded on SIGHUP (only certfile had this note before).

Tests added:

  • 08-ssl-hup-ca-rotation.py: rotates cafile to an unrelated CA via an atomic
    symlink swap and verifies that client.crt is rejected after SIGHUP.
  • 08-ssl-hup-reload.py: verifies valid connections still work and revoked certs
    are still rejected across 3 SIGHUP cycles (CRL survives SSL_CTX recreation).
  • 08-ssl-hup-chain.py: verifies the server certificate chain identity is stable
    (correct leaf + intermediate fingerprints) across SIGHUP reloads.

Then please check the following list of things we ask for in your pull request:

  • Have you signed the Eclipse Contributor Agreement, using the same email address as you used in your commits?
  • Do each of your commits have a "Signed-off-by" line, with the correct email address? Use "git commit -s" to generate this line for you.
  • If you are contributing a new feature, is your work based off the develop branch?
  • If you are contributing a bugfix, is your work based off the fixes branch?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you successfully run make test with your changes locally?

Adrian Freihofer added 6 commits April 15, 2026 21:43
OpenSSL 3.x on some distributions (e.g. Fedora 42) does not ship
engine.h as part of openssl-devel because the ENGINE API is deprecated.
When the header is absent, the OpenSSL configuration header defines
OPENSSL_NO_ENGINE to signal that the ENGINE API is unavailable.

Three files in lib/ included <openssl/engine.h> unconditionally inside
#ifdef WITH_TLS blocks, causing a fatal build error on those systems:

  lib/tls_mosq.h
  lib/net_mosq.c
  lib/options.c

src/net.c already guarded its engine usage correctly with

Apply the same OPENSSL_NO_ENGINE guard to the three lib/ files.
In options.c, where no other OpenSSL header was included before the
engine.h line, add <openssl/opensslconf.h> first so that the macro is
defined before it is tested.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
listeners__reload_all_certificates() called only net__load_certificates()
on the existing SSL_CTX without calling net__tls_load_verify(). As a
result, any rotation of the CA bundle (cafile/capath) had no effect until
a full broker restart: clients whose certificate was signed by the new CA
were rejected, and clients whose certificate was signed by a revoked CA
continued to be accepted.

Fix by recreating the SSL_CTX from scratch: net__tls_server_ctx() frees
and reallocates a fresh context, then net__tls_load_verify() loads CA
certs and CRLs into it.

As a secondary benefit this also stops a resource leak: net__load_crl_file()
calls X509_STORE_add_lookup() on every reload, which appends a new
X509_LOOKUP_file() entry to the store's internal lookup-methods stack.
There is no public API to remove entries, so each SIGHUP grew the stack
by one. (CRL objects themselves are not duplicated because X509_STORE_add_crl()
deduplicates by issuer name.)

Existing TLS connections are unaffected: SSL objects hold a reference
to their parent SSL_CTX, so the old context is kept alive until the
last connection derived from it is closed.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
Without the SSL_CTX recreation fix, swapping the cafile symlink and
sending SIGHUP leaves the broker using the old CA store.Clients whose
certificate is signed by the old CA continue to connect successfully
even though the cafile now points at an entirely unrelated root CA.

The test atomically replaces a cafile symlink with one pointing at
test-fake-root-ca.crt (a self-signed root with no relation to the
test PKI used to issue client.crt) and then confirms that a subsequent
TLS handshake with client.crt is rejected.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
Add 08-ssl-hup-reload.py to cover the two scenarios that had no
dedicated test:

1. After SIGHUP, new TLS connections with a valid client certificate
   are still accepted. The existing 08-ssl-hup-disconnect.py only
   verifies that an already-established connection is not dropped;
   it does not exercise a fresh TLS handshake against the reloaded
   SSL_CTX.

2. After SIGHUP, revoked certificates (crlfile) are still rejected.
   The existing CRL tests (08-ssl-connect-cert-auth-crl.py,
   08-ssl-connect-cert-auth-revoked.py) never send a SIGHUP, so
   they do not verify that the CRL store survives the reload.

The test repeats the SIGHUP cycle three times to act as a regression
test against any accumulation behaviour that could degrade or break
subsequent reloads.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
…eloads

Configure a certfile containing both the leaf (server.crt) and the
intermediate signing CA (test-signing-ca.crt). After each SIGHUP the
test uses openssl s_client -showcerts to extract every certificate sent
by the broker and compares their SHA-256 fingerprints against the
expected values:

  - chain[0] must be server.crt (correct leaf, not stale or wrong cert)
  - chain[1] must be test-signing-ca.crt (correct intermediate)
  - no further certificates (no accumulation of duplicate intermediates)

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
Before the SSL_CTX recreation fix, only certfile and keyfile were
effectively reloaded on SIGHUP. The mosquitto.conf(5) man page
reflected this: cafile, capath, and crlfile had no reload note, while
certfile mentioned only itself.

Now that net__tls_load_verify() is called on every reload, all four
options take effect on SIGHUP. Update the man page accordingly:
- cafile, capath, crlfile: add 'Reloaded when Mosquitto receives a
  SIGHUP signal.'
- certfile: broaden the existing note to mention keyfile, cafile, and
  crlfile alongside certfile.

Signed-off-by: Adrian Freihofer <adrian.freihofer@siemens.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant