temporalio/temporal · error · ErrTLSConfig

unable to read client ca file

Error message

unable to read client ca file

What it means

parseCAs returns this error when CaFile is set but os.ReadFile fails to read the CA PEM file. The original os error is chained in, so the message includes the underlying reason (missing file, permissions, etc.) and everything is wrapped with ErrTLSConfig.

Source

Thrown at common/auth/tls_config_helper.go:146

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

View on GitHub (pinned to bde624efd1)

Solutions

  1. Verify the path in CaFile exists and is readable by the service user (ls -l; fix permissions/ownership).
  2. Correct the path in config — use an absolute path and confirm the mount location inside the container.
  3. Check the chained os error in the message: ENOENT means wrong path, EACCES means permissions.
  4. If the CA is intended inline, remove CaFile and set base64-encoded CaData instead.

Example fix

// before
tls:
  caFile: "/etc/temporal/certs/ca.cr t"
// after
tls:
  caFile: "/etc/temporal/certs/ca.pem"
Defensive patterns

Strategy: validation

Validate before calling

func checkCaFileReadable(path string) error {
	info, err := os.Stat(path)
	if err != nil {
		return fmt.Errorf("caFile not accessible: %w", err)
	}
	if info.IsDir() {
		return fmt.Errorf("caFile is a directory: %s", path)
	}
	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("caFile not readable: %w", err)
	}
	return f.Close()
}

Try / catch

tlsCfg, err := auth.NewTLSConfig(cfg)
if err != nil {
	if errors.Is(err, auth.ErrTLSConfig) {
		logger.Error("TLS configuration invalid", tag.Key, err) // inspect chained os error
		return err
	}
	return err
}

Prevention

When it happens

Trigger: NewTLSConfig -> parseCAs with CaFile pointing to a path that does not exist, is a directory, or is unreadable by the process (permission denied).

Common situations: Typo in the CA path; config mounted into the container at a different path than referenced; file permissions after mounting a Kubernetes secret (non-root user cannot read); relative path resolved against a different working directory than expected in the service.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


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