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

  1. Redeploy a matching cert/key pair (both from the same issuance).
  2. Make rotation atomic: write both files to temp names, validate, then rename — so reload never sees a mixed pair.
  3. 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

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

Related errors


AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08). Data as JSON: /api/errors/f6cb136f58fabeb0. Report an issue: GitHub.