aaif-goose/goose · error

Failed to read client certificate: {}

Error message

Failed to read client certificate: {}

What it means

Part of goose's mTLS support: when a provider config supplies a client certificate pair, TlsConfig::load_identity reads the certificate PEM from cert_path with std::fs::read_to_string. Any I/O failure — missing file, wrong path, permission denied, non-UTF8 bytes — surfaces as 'Failed to read client certificate: {io_error}'.

Source

Thrown at crates/goose-providers/src/api_client.rs:92

            key_path,
        });
        self
    }

    pub fn with_ca_cert(mut self, path: PathBuf) -> Self {
        self.ca_cert_path = Some(path);
        self
    }

    pub fn is_configured(&self) -> bool {
        self.client_identity.is_some() || self.ca_cert_path.is_some()
    }

    #[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
    fn load_identity(&self) -> Result<Option<Identity>> {
        if let Some(cert_key_pair) = &self.client_identity {
            let cert_pem = read_to_string(&cert_key_pair.cert_path)
                .map_err(|e| anyhow::anyhow!("Failed to read client certificate: {}", e))?;
            let key_pem = read_to_string(&cert_key_pair.key_path)
                .map_err(|e| anyhow::anyhow!("Failed to read client private key: {}", e))?;

            #[cfg(not(feature = "native-tls"))]
            let identity = {
                let combined_pem = format!("{}\n{}", cert_pem, key_pem);
                Identity::from_pem(combined_pem.as_bytes()).map_err(|e| {
                    anyhow::anyhow!("Failed to create identity from cert and key: {}", e)
                })?
            };

            #[cfg(feature = "native-tls")]
            let identity = {
                let pkcs8_key_pem = convert_key_to_pkcs8_pem(&key_pem)?;
                Identity::from_pkcs8_pem(cert_pem.as_bytes(), pkcs8_key_pem.as_bytes()).map_err(
                    |e| anyhow::anyhow!("Failed to create identity from cert and key: {}", e),
                )?
            };

View on GitHub (pinned to 3810898a74)

Solutions

  1. Verify the path exists and is readable by the goose process: ls -l /path/to/cert.pem
  2. Use an absolute path in the provider config to avoid working-directory issues
  3. If the file is DER, convert to PEM: openssl x509 -inform der -in cert.der -out cert.pem
  4. Fix permissions so the running user can read it (chmod 640 + right group)

Example fix

# before
client_identity:
  cert_path: ./certs/client.crt   # relative, or DER binary
  key_path: ./certs/client.key

# after
client_identity:
  cert_path: /etc/goose/tls/client-cert.pem   # absolute, PEM format
  key_path: /etc/goose/tls/client-key.pem
Defensive patterns

Strategy: validation

Validate before calling

fn readable_utf8_file(path: &std::path::Path) -> bool {
    std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)
        && std::fs::read_to_string(path).is_ok()
}

if !readable_utf8_file(std::path::Path::new(&cert_path)) {
    anyhow::bail!("client cert missing/unreadable: {cert_path}");
}

Type guard

fn is_pem_text(path: &std::path::Path) -> bool {
    std::fs::read_to_string(path)
        .map(|s| s.starts_with("-----BEGIN"))
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Configuring a provider with a client cert (client_identity) where cert_path does not exist in the filesystem, is unreadable by the goose process (ownership/mode), is a directory, or contains binary DER instead of PEM text. Fires when the TLS config is first loaded, i.e. when building the HTTP client for that provider.

Common situations: Relative paths resolved from a different working directory (cron, systemd, desktop app); cert mounted at a different path in a container; cert generated as DER (.crt/.der) instead of PEM; file owned by root with mode 600 while goose runs as a user.

Understand the failure class

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/1a762c4ca112fde3. Report an issue: GitHub.