aaif-goose/goose · error

Failed to read client private key: {}

Error message

Failed to read client private key: {}

What it means

Companion to the certificate read: TlsConfig::load_identity also reads the client private key PEM from key_path. Any I/O failure on that file (missing, unreadable, non-UTF8) is reported as 'Failed to read client private key: {io_error}' before any identity parsing is attempted.

Source

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

        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),
                )?
            };

            Ok(Some(identity))

View on GitHub (pinned to 3810898a74)

Solutions

  1. Check the exact key_path in the config and ls -l it from the goose process context
  2. Convert DER keys to PEM: openssl pkey -inform der -in key.der -out key.pem
  3. Use absolute paths and readable permissions for the goose user
  4. Confirm the file is a PEM text starting with -----BEGIN ... PRIVATE KEY-----

Example fix

# before
client_identity:
  cert_path: /etc/goose/tls/client-cert.pem
  key_path: /etc/goose/tls/client-key   # DER binary or typo'd name

# after
client_identity:
  cert_path: /etc/goose/tls/client-cert.pem
  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(&key_path)) {
    anyhow::bail!("client key missing/unreadable: {key_path}");
}

Type guard

fn key_file_is_pem(path: &std::path::Path) -> bool {
    std::fs::read_to_string(path)
        .map(|s| s.contains("-----BEGIN") && s.contains("PRIVATE KEY-----"))
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Provider config includes client_identity whose key_path is wrong/unreadable, or the key file is binary DER rather than PEM text. Triggered at client build time; the cert read (previous line) must have succeeded, so the cert path was fine and only the key is at fault.

Common situations: Cert and key in different directories and only the cert path updated; key generated by openssl genrsa outputting PKCS#8 binary without -outform PEM; Kubernetes secret mounted with the key at a slightly different name; file permission 600 owned by another user.

Related errors


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