opentofu/opentofu · error
cannot load client certificate: %w
Error message
cannot load client certificate: %w
What it means
With both client_certificate_pem and client_private_key_pem set, configureTLS calls tls.X509KeyPair on the two PEM blobs and wraps any failure as 'cannot load client certificate'. X509KeyPair fails when either blob is not a well-formed PEM certificate/key block, or when the private key does not correspond to the certificate.
Source
Thrown at internal/backend/remote-state/http/backend.go:201
var tlsConfig tls.Config
client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig
if skipCertVerification {
// ignores TLS verification
tlsConfig.InsecureSkipVerify = true
}
if clientCACertificatePem != "" {
// trust servers based on a CA
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) {
return errors.New("failed to append certs")
}
}
if clientCertificatePem != "" && clientPrivateKeyPem != "" {
// attach a client certificate to the TLS handshake (aka mTLS)
certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem))
if err != nil {
return fmt.Errorf("cannot load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{certificate}
}
return nil
}
func (b *Backend) configure(ctx context.Context) error {
data := schema.FromContextBackendConfig(ctx)
address := data.Get("address").(string)
updateURL, err := url.Parse(address)
if err != nil {
return fmt.Errorf("failed to parse address URL: %w", err)
}
if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
return fmt.Errorf("address must be HTTP or HTTPS")
}View on GitHub (pinned to 3561785c48)
Solutions
- Check the pair matches: 'openssl x509 -in client.crt -noout -modulus | openssl md5' and 'openssl rsa -in client.key -noout -modulus | openssl md5' must print the same hash
- Confirm both files are PEM with proper -----BEGIN CERTIFICATE----- / -----BEGIN PRIVATE KEY----- fences and no interleaved text or truncation
- Re-issue the cert/key pair together if they came from different CSAs and redeploy
Example fix
# before (cert and key from different issuages)
client_certificate_pem = file("new-client.crt")
client_private_key_pem = file("old-client.key")
# after
client_certificate_pem = file("new-client.crt")
client_private_key_pem = file("new-client.key") Defensive patterns
Strategy: validation
Validate before calling
// Verify the PEM pair loads as an X509KeyPair before tofu init
certPEM, errCert := os.ReadFile("client.crt")
keyPEM, errKey := os.ReadFile("client.key")
if errCert == nil && errKey == nil {
if _, err := tls.X509KeyPair(certPEM, keyPEM); err != nil {
log.Fatalf("bad mTLS pair: %v", err)
}
} Prevention
- Verify cert/key modulus hashes match after any rotation (openssl md5 of the modulus)
- Keep PEM fences intact; do not wrap base64 lines when pasting through ticketing systems
- Rotate certificate and key together, never independently
When it happens
Trigger: PEM data missing its -----BEGIN/END----- fences, cert and key files swapped between the two attributes, a key generated for a different certificate, or extraneous text interleaved with the base64 body.
Common situations: file() reading the wrong path or a truncated file; passphrase-encrypted keys the loader cannot parse; rotating the cert but not the key; PEMs mangled by templating or copy-paste line wrapping.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- client_certificate_pem is set but client_private_key_pem is
- client_private_key_pem is set but client_certificate_pem is
- failed to append certs
- %s "%s" name must not be empty and only contain A-Za-z0-9-_
- %s "%s" value must not be empty and only contain ascii chara
AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15).
Data as JSON: /api/errors/101d347d84f848d8.
Report an issue: GitHub.