temporalio/temporal · error

failed to load client ca: %v

Error message

failed to load client ca: %v

What it means

newClientTLSConfig fetches the optional server root CA used by the client (clientProvider.FetchServerRootCAsForClient(isWorker)) for connections to servers whose certs are not in the system trust store. A failure is wrapped as "failed to load client ca: %v". Raised for internode and frontend client TLS config construction.

Source

Thrown at common/rpc/encryption/local_store_tls_provider.go:376

	}
	return auth.NewTLSConfigWithCertsAndCAs(
		clientAuthType,
		[]tls.Certificate{*serverCert},
		clientCaPool,
		logger), nil
}

func newClientTLSConfig(
	clientProvider CertProvider,
	serverName string,
	isAuthRequired bool,
	isWorker bool,
	enableHostVerification bool,
) (*tls.Config, error) {
	// Optional ServerCA for client if not already trusted by host
	serverCa, err := clientProvider.FetchServerRootCAsForClient(isWorker)
	if err != nil {
		return nil, fmt.Errorf("failed to load client ca: %v", err)
	}

	var getCert tlsCertFetcher

	// mTLS enabled, present certificate
	if isAuthRequired {
		getCert = func() (*tls.Certificate, error) {
			cert, err := clientProvider.FetchClientCertificate(isWorker)
			if err != nil {
				return nil, err
			}

			if cert == nil {
				return nil, fmt.Errorf("client auth required, but no certificate provided")
			}
			return cert, nil
		}
	}

View on GitHub (pinned to bde624efd1)

Solutions

  1. Fix the wrapped root cause: ensure the client's server CA file exists, is readable, and is valid PEM.
  2. If the server cert is already trusted by the host's system roots, remove the redundant server CA config.
  3. Re-copy the CA bundle after rotation; verify with openssl x509 -in ca.pem -noout -text.
  4. Check worker-vs-non-worker config split: internode and frontend worker settings each have their own CA paths.

Example fix

// before
clientTLS:
  internode:
    serverName: tls-server
    rootCADataFile: /etc/certs/ca.pem   # not mounted in worker pod
// after
clientTLS:
  internode:
    serverName: tls-server
    rootCADataFile: /etc/temporal/tls/internode-ca.pem   # mounted secret
Defensive patterns

Strategy: validation

Validate before calling

if rootCAPath != "" {
    b, err := os.ReadFile(rootCAPath)
    if err != nil { return fmt.Errorf("client root CA unreadable: %w", err) }
    if _, err := x509.SystemCertPool(); err == nil {
        pool := x509.NewCertPool()
        if !pool.AppendCertsFromPEM(b) { return errors.New("root CA PEM not parseable") }
    }
}

Try / catch

tlsCfg, err := newClientTLSConfig(...)
if err != nil && strings.Contains(err.Error(), "failed to load client ca") {
    return fmt.Errorf("cannot connect over TLS: %w", err)
}

Prevention

When it happens

Trigger: Building a client TLS config (GetInternodeClientConfig / GetFrontendClientConfig / anonymous worker config) where the configured server CA file/data for clients is missing, unreadable, or unparseable.

Common situations: Worker or CLI configured with server CA path that does not exist in its container; self-signed server certs with a stale/mistyped CA path; permission issues after secret mount; typo between serverName and CA file layout.

Related errors


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