zeroclaw-labs/zeroclaw · error · anyhow::Error

amqp channel '{}': client_cert is set but client_key is miss

Error message

amqp channel '{}': client_cert is set but client_key is missing

What it means

The AMQP channel was configured for TLS client authentication with client_cert set but client_key missing. lapin's OwnedIdentity needs a cert+key pair to build the PKCS#12 client identity, so build_client_identity refuses the half-pair during connect rather than attempting an anonymous connection against a broker expecting mTLS.

Source

Thrown at crates/zeroclaw-channels/src/amqp.rs:282

        Connection::connect_with_config(
            &self.amqp_url,
            props,
            OwnedTLSConfig {
                identity,
                cert_chain,
            },
        )
        .await
        .map_err(Into::into)
    }

    fn build_client_identity(&self) -> anyhow::Result<Option<OwnedIdentity>> {
        let (cert_path, key_path) = match (&self.client_cert, &self.client_key) {
            (Some(cert), Some(key)) => (cert, key),
            (None, None) => return Ok(None),
            (Some(_), None) => {
                anyhow::bail!(
                    "amqp channel '{}': client_cert is set but client_key is missing",
                    self.alias
                )
            }
            (None, Some(_)) => {
                anyhow::bail!(
                    "amqp channel '{}': client_key is set but client_cert is missing",
                    self.alias
                )
            }
        };

        let cert_pem = std::fs::read(cert_path)?;
        let key_pem = std::fs::read(key_path)?;
        let der = pem_to_pkcs12_der(&cert_pem, &key_pem, &self.alias)?;

        Ok(Some(OwnedIdentity {
            der,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set the matching client_key path in the same channel config block.
  2. Verify both files exist and are readable by the service user before restarting.
  3. If client-certificate auth is not required by the broker, remove client_cert as well — both unset means no client identity.

Example fix

# before
[channels.amqp.prod]
url = "amqps://broker:5671"
client_cert = "/etc/zeroclaw/tls/client.pem"

# after
[channels.amqp.prod]
url = "amqps://broker:5671"
client_cert = "/etc/zeroclaw/tls/client.pem"
client_key = "/etc/zeroclaw/tls/client.key"
Defensive patterns

Strategy: validation

Validate before calling

match (&cfg.client_cert, &cfg.client_key) {
    (Some(_), None) | (None, Some(_)) => {
        anyhow::bail!("client_cert and client_key must be set together");
    }
    _ => {}
}

Type guard

fn has_complete_client_identity(cfg: &AmqpChannelConfig) -> bool {
    cfg.client_cert.is_some() == cfg.client_key.is_some()
}

Prevention

When it happens

Trigger: AmqpChannelConfig (or the amqp channel config block) sets client_cert without client_key; connect() then calls build_client_identity and hits the (Some, None) arm.

Common situations: Secrets distributed as separate files where only the cert path got templated; config snippets copied between environments dropping one line; a key loader that failed silently and left the field unset.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/1df82978d8c2da36. Report an issue: GitHub.