opentofu/opentofu · error

client_certificate_pem is set but client_private_key_pem is

Error message

client_certificate_pem is set but client_private_key_pem is not

What it means

configureTLS requires the mTLS pair to be complete: if client_certificate_pem is non-empty while client_private_key_pem is empty, backend configuration aborts with this error before any TLS settings are applied. The two attributes are only meaningful together because tls.X509KeyPair consumes both.

Source

Thrown at internal/backend/remote-state/http/backend.go:176

type Backend struct {
	*schema.Backend
	encryption encryption.StateEncryption

	client *httpClient
}

// configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made.
func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error {
	// If there are no conditions needing to configure TLS, leave the client untouched
	skipCertVerification := data.Get("skip_cert_verification").(bool)
	clientCACertificatePem := data.Get("client_ca_certificate_pem").(string)
	clientCertificatePem := data.Get("client_certificate_pem").(string)
	clientPrivateKeyPem := data.Get("client_private_key_pem").(string)
	if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" {
		return nil
	}
	if clientCertificatePem != "" && clientPrivateKeyPem == "" {
		return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not")
	}
	if clientPrivateKeyPem != "" && clientCertificatePem == "" {
		return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not")
	}

	// TLS configuration is needed; create an object and configure it
	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")

View on GitHub (pinned to 3561785c48)

Solutions

  1. Add the matching client_private_key_pem attribute to the backend block
  2. If you only meant to trust a custom CA, remove client_certificate_pem and keep client_ca_certificate_pem
  3. Check attribute spelling when passing via -backend-config so the value does not silently read as empty

Example fix

# before
backend "http" {
  address = "https://state.example.com"
  client_certificate_pem = file("client.crt")
}
# after
backend "http" {
  address = "https://state.example.com"
  client_certificate_pem = file("client.crt")
  client_private_key_pem = file("client.key")
}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight the mTLS pair before running tofu init
cert := os.Getenv("TF_CLIENT_CERT")
key := os.Getenv("TF_CLIENT_KEY")
if cert != "" && key == "" {
    log.Fatal("client_certificate_pem is set but client_private_key_pem is missing")
}

Prevention

When it happens

Trigger: backend "http" config with client_certificate_pem set but client_private_key_pem omitted; passing the pair via repeated -backend-config flags where the key flag is missing or its attribute name is misspelled (e.g. client_private_key without the _pem suffix).

Common situations: Splitting cert and key into separate files/variables and forgetting the second -backend-config flag; merging config snippets where one side of the pair is dropped; renaming attributes during a migration to the _pem names.

Understand the failure class

Related errors


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