t8y2/dbx · error

Client certificate and key must be provided together

Error message

Client certificate and key must be provided together

What it means

tlsConfigFor builds the TLS configuration and requires mTLS material to be complete. A client certificate without its key, or a key without its certificate, can never form a valid TLS key pair, so the combination is rejected with this error. Cert and key may come from either ClientCertPath/ClientKeyPath or the legacy CertPath/KeyPath aliases.

Source

Thrown at agents/drivers/etcd2-go/client.go:192

}

func tlsConfigFor(connection connectionParams) (*tls.Config, error) {
	tlsConfig := &tls.Config{}
	if ca := strings.TrimSpace(connection.CACertPath); ca != "" {
		authorityPEM, err := os.ReadFile(ca)
		if err != nil {
			return nil, err
		}
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM(authorityPEM) {
			return nil, fmt.Errorf("failed to parse CA certificate at %s", ca)
		}
		tlsConfig.RootCAs = pool
	}
	certPath := firstNonBlank(connection.ClientCertPath, connection.CertPath)
	keyPath := firstNonBlank(connection.ClientKeyPath, connection.KeyPath)
	if (certPath == "") != (keyPath == "") {
		return nil, errors.New("Client certificate and key must be provided together")
	}
	if certPath != "" {
		pair, err := tls.LoadX509KeyPair(certPath, keyPath)
		if err != nil {
			return nil, err
		}
		tlsConfig.Certificates = []tls.Certificate{pair}
	}
	return tlsConfig, nil
}

// probeClient verifies the endpoint speaks the v2 API. It returns the
// connected client and a probe result shaped like the v3 agent's:
// {ok, endpoint, limited?}.
func probeClient(endpoint string, connection connectionParams) (*authenticatedClient, map[string]any, error) {
	httpClient, err := buildHTTPClient(connection)
	if err != nil {
		return nil, nil, err

View on GitHub (pinned to c0390bff16)

Solutions

  1. Provide both certPath and keyPath (each resolves via firstNonBlank from Client*/Cert* and Client*/Key* aliases)
  2. If mTLS is not required, omit both paths so the plain-TLS path is used
  3. Verify both files exist and are readable at the given paths

Example fix

// before
connection.ClientCertPath = "/etc/ssl/client.crt" // key missing
// after
connection.ClientCertPath = "/etc/ssl/client.crt"
connection.ClientKeyPath  = "/etc/ssl/client.key"
Defensive patterns

Strategy: validation

Validate before calling

certPath := firstNonBlank(conn.ClientCertPath, conn.CertPath)
keyPath := firstNonBlank(conn.ClientKeyPath, conn.KeyPath)
if (certPath == "") != (keyPath == "") {
	return errors.New("mTLS config incomplete: provide BOTH client cert and key paths")
}
if certPath != "" {
	if _, err := os.Stat(certPath); err != nil { return err }
	if _, err := os.Stat(keyPath); err != nil { return err }
}

Type guard

func mtlsComplete(c *Connection) bool {
	cert := firstNonBlank(c.ClientCertPath, c.CertPath)
	key := firstNonBlank(c.ClientKeyPath, c.KeyPath)
	return (cert == "") == (key == "")
}

Try / catch

client, err := buildHTTPClient(conn)
if err != nil {
	if strings.Contains(err.Error(), "provided together") {
		return fmt.Errorf("mTLS misconfiguration: set both client cert and key paths")
	}
	return err
}

Prevention

When it happens

Trigger: Setting connection.ClientCertPath (or CertPath) without ClientKeyPath (or KeyPath), or vice versa, when building the HTTP client for an etcd v2 connection.

Common situations: Partial mTLS config in a YAML/JSON file where one path was edited or deleted; secret mounts where only one file was mounted; copying cert path but forgetting the key path in env vars.

Understand the failure class

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/3f0adfc7e5831358. Report an issue: GitHub.