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
- Verify the path exists and is readable by the goose process: ls -l /path/to/cert.pem
- Use an absolute path in the provider config to avoid working-directory issues
- If the file is DER, convert to PEM: openssl x509 -inform der -in cert.der -out cert.pem
- 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
- Use absolute paths for all TLS material in provider configs
- Verify files from the goose process context: sudo -u goose cat /path/cert.pem
- Keep PEM files as UTF-8 text with proper armor; convert DER with openssl x509
- Mount secrets read-only at stable paths in containers
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Failed to read client private key: {}
- Failed to read CA certificate: {}
- Failed to create identity from cert and key: {}
- Failed to create directory: {}
- Provider '{}' has dynamic_models: false but no static models
AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16).
Data as JSON: /api/errors/1a762c4ca112fde3.
Report an issue: GitHub.