temporalio/temporal · error · ErrTLSConfig

only one of caData or caFile properties should be specified

Error message

only one of caData or caFile properties should be specified

What it means

This error is returned by validateTemporalTls when the CA certificate is supplied both inline (CaData) and via a file path (CaFile). Like the cert/key checks, the library requires a single CA source so it can deterministically build the x509.CertPool, wrapping the failure with ErrTLSConfig.

Source

Thrown at common/auth/tls_config_helper.go:135

}

func validateTemporalTls(temporalTls *TLS) error {
	if temporalTls.CertData != "" && temporalTls.CertFile != "" {
		return fmt.Errorf("%w: %s", ErrTLSConfig, "only one of certData or certFile properties should be specified")
	}

	if temporalTls.KeyData != "" && temporalTls.KeyFile != "" {
		return fmt.Errorf("%w: %s", ErrTLSConfig, "only one of keyData or keyFile properties should be specified")
	}

	certProvided := temporalTls.CertData != "" || temporalTls.CertFile != ""
	keyProvided := temporalTls.KeyData != "" || temporalTls.KeyFile != ""
	if certProvided != keyProvided {
		return fmt.Errorf("%w: %s", ErrTLSConfig, "cert or key is missing")
	}

	if temporalTls.CaData != "" && temporalTls.CaFile != "" {
		return fmt.Errorf("%w: %s", ErrTLSConfig, "only one of caData or caFile properties should be specified")
	}
	return nil
}

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)
		}
	}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Remove one field: keep caFile for on-disk PEM, or caData with base64-encoded PEM.
  2. Trace the config merge to find where the duplicate CA source is added and unset it.
  3. Fail fast in tests by asserting only one CA source is set per TLS block.

Example fix

// before
tls:
  caData: "LS0tLS1CRUdJTi..."
  caFile: "/etc/temporal/certs/ca.pem"
// after
tls:
  caFile: "/etc/temporal/certs/ca.pem"
Defensive patterns

Strategy: validation

Validate before calling

func validateTLSOneCaSource(t *auth.TLS) error {
	if t.CaData != "" && t.CaFile != "" {
		return fmt.Errorf("set only one of caData or caFile")
	}
	return nil
}

Type guard

func hasExactlyOne(a, b string) bool { return (a != "") != (b != "") }

Prevention

When it happens

Trigger: Calling NewTLSConfig with a *TLS struct where both CaData != "" and CaFile != "" — typically from merged config layers or a templating error.

Common situations: Adding a custom CA via file mount while an inline CA remains from a base config; Helm/environment overlay injecting caData on top of caFile; example-config copy-paste filling both fields.

Related errors


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