temporalio/temporal · error · ErrTLSConfig

unable to parse certs as PEM

Error message

unable to parse certs as PEM

What it means

parseCAs returns this error when the CA bytes (from CaFile or decoded CaData) yield zero certificates when run through parseCertsFromPEM — i.e. the content is not a parseable PEM certificate chain. The parse error is chained and wrapped with ErrTLSConfig.

Source

Thrown at common/auth/tls_config_helper.go:158

func parseCAs(temporalTls *TLS) (*x509.CertPool, error) {
	var caBytes []byte
	var err error
	if temporalTls.CaFile != "" {
		caBytes, err = os.ReadFile(temporalTls.CaFile)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to read client ca file", err)
		}
	} else if temporalTls.CaData != "" {
		caBytes, err = base64.StdEncoding.DecodeString(temporalTls.CaData)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to decode client ca data", err)
		}
	}
	if len(caBytes) > 0 {
		caCertPool := x509.NewCertPool()
		caCerts, err := parseCertsFromPEM(caBytes)
		if len(caCerts) == 0 {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to parse certs as PEM", err)
		}
		for _, cert := range caCerts {
			caCertPool.AddCert(cert)
		}
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%w)", ErrTLSConfig, "unable to load decoded CA Cert as PEM", err)
		}
		return caCertPool, nil
	}
	return nil, nil
}

func parseCertsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) {
	for len(pemCerts) > 0 {
		var block *pem.Block
		block, pemCerts = pem.Decode(pemCerts)
		if block == nil {
			break

View on GitHub (pinned to bde624efd1)

Solutions

  1. Open the CA file/data and confirm it starts with -----BEGIN CERTIFICATE----- and ends with -----END CERTIFICATE-----; fix the contents otherwise.
  2. Re-export the certificate in PEM (Base64) format rather than DER.
  3. If the file is empty, fix the secret/config delivery that was supposed to populate it.
  4. Verify with openssl x509 -in ca.pem -noout -subject that the CA parses with OpenSSL.

Example fix

// before
tls:
  caFile: "/etc/temporal/certs/tls.key"   # private key, not a CA cert
// after
tls:
  caFile: "/etc/temporal/certs/ca.pem"    # PEM certificate chain
Defensive patterns

Strategy: validation

Validate before calling

func checkCaIsPem(caBytes []byte) error {
	if !bytes.Contains(caBytes, []byte("-----BEGIN CERTIFICATE-----")) {
		return fmt.Errorf("CA data has no PEM CERTIFICATE block")
	}
	if _, err := tls.X509KeyPair(nil, nil); false {
		_ = err
	}
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(caBytes) {
		return fmt.Errorf("no certificates parsed from CA PEM")
	}
	return nil
}

Prevention

When it happens

Trigger: NewTLSConfig -> parseCAs where caBytes is non-empty but contains no PEM CERTIFICATE blocks: a private key file passed as the CA, an empty/truncated file, a DER-encoded cert instead of PEM, or garbage text.

Common situations: Pointing caFile at the server cert's key or an unrelated file; saving a cert from a browser in DER format; a CI/CD templating step writing an empty secret; concatenating a chain without PEM headers; downloading a CA and getting an HTML error page instead of the cert.

Understand the failure class

Related errors


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