argoproj/argo-workflows · error

client certificate authentication requires both clientCert a

Error message

client certificate authentication requires both clientCert and clientKey

What it means

GetClientTLSConfig builds a client TLS config and enforces that mutual-TLS credentials are complete: clientCert and clientKey must both be set or both be empty. Exactly one was provided, so it refuses to build a half-configured mTLS client. This guard prevents silently connecting without client auth when the user thinks it is enabled.

Source

Thrown at util/tls/tls.go:164

	return &tls.Config{
		Certificates: []tls.Certificate{cert},
		MinVersion:   tlsMinVersion,
		NextProtos:   []string{"h2"},
	}, nil
}

// GetClientTLSConfig creates a TLS 1.2 or newer configuration for client connections.
// Client certificate authentication requires both clientCert and clientKey. If caCert is provided,
// the certificate authority is used instead of the system roots to verify the server certificate.
// The insecureSkipVerify parameter controls whether the server's certificate is verified.
func GetClientTLSConfig(clientCert, clientKey, caCert string, insecureSkipVerify bool) (*tls.Config, error) {
	tlsConfig := &tls.Config{
		InsecureSkipVerify: insecureSkipVerify,
		MinVersion:         tls.VersionTLS12,
	}
	if (clientCert == "") != (clientKey == "") {
		return nil, fmt.Errorf("client certificate authentication requires both clientCert and clientKey")
	}
	if caCert != "" {
		caPEM, err := os.ReadFile(caCert)
		if err != nil {
			return nil, fmt.Errorf("failed to read certificate authority: %w", err)
		}
		certPool := x509.NewCertPool()
		if ok := certPool.AppendCertsFromPEM(caPEM); !ok {
			return nil, fmt.Errorf("failed to parse certificate authority %q", caCert)
		}
		tlsConfig.RootCAs = certPool
	}
	if clientCert != "" && clientKey != "" {
		cert, err := tls.LoadX509KeyPair(clientCert, clientKey)
		if err != nil {
			return nil, err
		}
		tlsConfig.Certificates = []tls.Certificate{cert}

View on GitHub (pinned to 35bff19146)

Solutions

  1. Provide both the client certificate and the client key paths (or files) together
  2. If mTLS is not intended, unset the single credential so both are empty
  3. Check your Helm values / CLI flags / env vars to confirm both --client-cert and --client-key (or equivalents) are set
  4. If using a k8s secret mount, verify both tls.crt and tls.key are mounted at the expected paths

Example fix

// before
config, err := tls.GetClientTLSConfig("/certs/client.crt", "", "", false)
// after
config, err := tls.GetClientTLSConfig("/certs/client.crt", "/certs/client.key", "", false)
Defensive patterns

Strategy: validation

Validate before calling

func validateClientTLSArgs(cert, key string) error {
    if (cert == "") != (key == "") {
        return fmt.Errorf("clientCert and clientKey must both be set or both empty (got cert=%q key=%q)", cert != "", key != "")
    }
    return nil
}

Try / catch

if err := validateClientTLSArgs(clientCert, clientKey); err != nil {
    return fmt.Errorf("misconfigured mTLS: %w", err)
}
config, err := tls.GetClientTLSConfig(clientCert, clientKey, caCert, insecure)

Prevention

When it happens

Trigger: Calling tls.GetClientTLSConfig with a non-empty clientCert but empty clientKey (or vice versa) — e.g. the argo CLI/server flags --client-cert / --client-key or env-derived paths where only one path is configured.

Common situations: Setting ARGOPROJ/CLI TLS env vars or config for client auth and forgetting the key (or vice versa); mounting only one of two secret files in a pod; typo in one file path yielding an empty string after defaulting.

Understand the failure class

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/ad992893f1749add. Report an issue: GitHub.