temporalio/temporal · error · ErrTLSConfig

unable to decode client certificate

Error message

unable to decode client certificate

What it means

parseClientCert returns this error when CertData is set but base64.StdEncoding.DecodeString fails — the value is not valid base64. The decode error is chained and wrapped with ErrTLSConfig so the caller knows their inline client certificate is malformed before any TLS handshake occurs.

Source

Thrown at common/auth/tls_config_helper.go:200

		certBytes := block.Bytes
		return x509.ParseCertificates(certBytes)
	}
	return nil, nil
}

func parseClientCert(temporalTls *TLS) (*tls.Certificate, error) {
	var certBytes []byte
	var keyBytes []byte
	var err error
	if temporalTls.CertFile != "" {
		certBytes, err = os.ReadFile(temporalTls.CertFile)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to read client certificate file", err)
		}
	} else if temporalTls.CertData != "" {
		certBytes, err = base64.StdEncoding.DecodeString(temporalTls.CertData)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to decode client certificate", err)
		}
	}

	if temporalTls.KeyFile != "" {
		keyBytes, err = os.ReadFile(temporalTls.KeyFile)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to read client certificate private key file", err)
		}
	} else if temporalTls.KeyData != "" {
		keyBytes, err = base64.StdEncoding.DecodeString(temporalTls.KeyData)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to decode client certificate private key", err)
		}
	}

	if len(certBytes) > 0 {
		clientCert, err := tls.X509KeyPair(certBytes, keyBytes)
		if err != nil {

View on GitHub (pinned to bde624efd1)

Solutions

  1. Regenerate the value with base64 -w0 client.pem and paste that exact single-line string into CertData.
  2. Confirm it decodes once to PEM text starting with -----BEGIN CERTIFICATE----- (not base64 of base64).
  3. Remove newlines/whitespace introduced by YAML block scalars or templating; use a folded/single-line string.
  4. Switch to CertFile to avoid inline encoding issues entirely.

Example fix

// before
tls:
  certData: "-----BEGIN CERTIFICATE-----\nMIID..."   # raw PEM
// after
tls:
  certData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ..." # base64 -w0 client.pem
Defensive patterns

Strategy: validation

Validate before calling

func checkCertDataIsBase64(certData string) error {
	decoded, err := base64.StdEncoding.DecodeString(certData)
	if err != nil {
		return fmt.Errorf("certData is not valid base64: %w", err)
	}
	if !strings.Contains(string(decoded), "BEGIN CERTIFICATE") {
		return fmt.Errorf("certData does not decode to PEM certificate")
	}
	return nil
}

Prevention

When it happens

Trigger: NewTLSConfig -> parseClientCert with CertData containing raw PEM text, truncated base64, or a string corrupted by whitespace/newlines/escaping during config templating.

Common situations: Pasting the PEM cert directly instead of base64-encoding it; kubectl/secret tooling that already wrapped or altered the base64; YAML multiline strings inserting newlines into the value; double-encoding leaving invalid characters.

Understand the failure class

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/3c1f9433a980fdfd. Report an issue: GitHub.