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

  1. 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
  2. Confirm both files are PEM with proper -----BEGIN CERTIFICATE----- / -----BEGIN PRIVATE KEY----- fences and no interleaved text or truncation
  3. 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

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

Related errors


AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15). Data as JSON: /api/errors/101d347d84f848d8. Report an issue: GitHub.