quickwit-oss/quickwit · error
private key in `{key_path}` does not match certificate in `{
Error message
private key in `{key_path}` does not match certificate in `{cert_path}` What it means
After assembling a rustls CertifiedKey, quickwit explicitly calls `keys_match()` to verify the private key's public half equals the certificate's public key. A definite KeyMismatch is fatal — bail with this message. Inconclusive results (key type can't expose its public key) are only logged as a warning, not fatal. This guards against serving TLS with a cert/key pair from different rotations.
Source
Thrown at quickwit/quickwit-transport/src/tls.rs:108
let certs = load_certs(cert_path)?;
if certs.is_empty() {
anyhow::bail!("no certificate found in `{cert_path}`");
}
let key = load_private_key(key_path)?;
let crypto_provider = rustls::crypto::CryptoProvider::get_default()
.context("no default rustls crypto provider is installed")?;
let signing_key = crypto_provider
.key_provider
.load_private_key(key)
.with_context(|| format!("private key in `{key_path}` is not usable"))?;
let certified_key = CertifiedKey::new(certs, signing_key);
// Guard against swapping in a mismatched cert/key pair, e.g. if the reload task reads the two
// files mid-rotation. A definite mismatch is fatal; an inconclusive result (key type that
// cannot expose its public key) is tolerated since we cannot do better.
match certified_key.keys_match() {
Ok(()) => {}
Err(rustls::Error::InconsistentKeys(rustls::InconsistentKeys::KeyMismatch)) => {
anyhow::bail!(
"private key in `{key_path}` does not match certificate in `{cert_path}`"
);
}
Err(error) => {
warn!("could not verify that private key matches certificate: {error}");
}
}
Ok(certified_key)
}
/// A cert resolver whose certificate can be swapped atomically at runtime. rustls calls `resolve`
/// on each handshake, so the latest stored certificate is always served. The same resolver works
/// for both server handshakes ([`ResolvesServerCert`]) and client handshakes
/// ([`ResolvesClientCert`], for gRPC mTLS client identities).
pub(crate) struct ReloadableCertResolver {
cert_path: String,
key_path: String,
certified_key: ArcSwap<CertifiedKey>,View on GitHub (pinned to a39730c5cd)
Solutions
- Redeploy a matching cert/key pair (both from the same issuance).
- Make rotation atomic: write both files to temp names, validate, then rename — so reload never sees a mixed pair.
- If rotation is the cause, reload again once both files are updated.
Example fix
// before: cp new-cert.pem cert.pem; cp new-key.pem key.pem (non-atomic) // after: install both atomically cp new-cert.pem cert.pem.tmp && cp new-key.pem key.pem.tmp mv cert.pem.tmp cert.pem && mv key.pem.tmp key.pem
Defensive patterns
Strategy: try-catch
Try / catch
match tls_config.load() {
Err(e) if e.to_string().contains("does not match certificate") => {
// cert/key pair out of sync — re-deploy matching pair and reload
}
other => other?,
} Prevention
- Rotate cert and key atomically so reload never sees a mixed pair.
- Verify pairs match offline with openssl before deploying.
- Always issue cert and key together from the same CA/issuance.
When it happens
Trigger: TLS reload (reload_and_compare) or initial load where cert_path and key_path files hold mismatched pairs — e.g. the reload task reads the two files mid-rotation and picks up new cert with old key.
Common situations: Certificate rotation replacing cert.pem and key.pem non-atomically; copying a renewed certificate without renewing the key; misconfigured paths pointing to two different services' pairs.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- no certificate found in `{cert_path}`
- TLS CA certificate path (`tls.ca_path`) is not set; it is re
- no CA certificate found in `{ca_path}`
- `tls.cert_poll_interval` must be greater than zero, got `{}`
- registering a signal handler for SIGHUP should not fail
AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08).
Data as JSON: /api/errors/f6cb136f58fabeb0.
Report an issue: GitHub.