temporalio/temporal · error · ErrTLSConfig

unable to read client certificate file

Error message

unable to read client certificate file

What it means

parseClientCert returns this error when CertFile is set but os.ReadFile fails to read the client certificate PEM file. The underlying os error is chained into the message and the whole thing is wrapped with ErrTLSConfig, so mTLS setup fails fast at config load.

Source

Thrown at common/auth/tls_config_helper.go:195

		}
		if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
			continue
		}

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

View on GitHub (pinned to bde624efd1)

Solutions

  1. Check the chained os error: fix the path (ENOENT) or permissions (EACCES) accordingly.
  2. Use an absolute path and confirm the cert is actually mounted/installed at that location in the deployment.
  3. Correct the CertFile value in config or, if the cert is meant to be inline, remove CertFile and set base64 CertData.
  4. Ensure rotation processes replace the file atomically so it never disappears mid-read.

Example fix

// before
tls:
  certFile: "/etc/temporal/certs/client.pem"   # file not mounted in container
// after (configmap/secret mount added, e.g. k8s):
//   volumeMounts:
//     - name: temporal-certs
//       mountPath: /etc/temporal/certs
tls:
  certFile: "/etc/temporal/certs/client.pem"
Defensive patterns

Strategy: validation

Validate before calling

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

Try / catch

tlsCfg, err := auth.NewTLSConfig(cfg)
if err != nil {
	if errors.Is(err, auth.ErrTLSConfig) {
		// message includes chained os error: ENOENT vs EACCES
		logger.Error("client cert file could not be read", tag.Key, err)
	}
	return err
}

Prevention

When it happens

Trigger: NewTLSConfig -> parseClientCert with CertFile pointing to a missing, deleted, or unreadable file (wrong path, wrong mount, permission denied, file is a directory).

Common situations: Typo in cert path; cert secret not mounted into the pod; permission issues for a non-root service user; relative path whose working directory differs in container vs local dev; file removed after a failed cert rotation.

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/124b3e18fa4abf37. Report an issue: GitHub.