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 validates mTLS material before building the client. A client certificate and its private key are only meaningful as a pair, so supplying exactly one of cert/key is rejected up front with this error rather than failing later inside tls.LoadX509KeyPair. It protects users from half-configured TLS setups.

Source

Thrown at agents/drivers/etcd-go/client.go:289

}

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
		}
		if len(pair.Certificate) > 0 {
			pair.Leaf, err = x509.ParseCertificate(pair.Certificate[0])
			if err != nil {
				return nil, err
			}
		}
		tlsConfig.Certificates = []tls.Certificate{pair}
	}
	return tlsConfig, nil
}

func clientCertificateUsername(config *tls.Config) string {

View on GitHub (pinned to c0390bff16)

Solutions

  1. Set both the client cert path and the client key path in the connection config.
  2. If only a CA is intended (no mTLS), remove the lone cert/key field and keep just the CA config.
  3. Verify secret/file mounts so both PEM files actually exist at the configured paths.
  4. Re-run the connection after fixing; buildClient will then load the pair via tls.LoadX509KeyPair.

Example fix

// before
connection.ClientCertPath = "/certs/client.pem" // key missing
client, err := buildClient(connection) // error
// after
connection.ClientCertPath = "/certs/client.pem"
connection.ClientKeyPath = "/certs/client-key.pem"
client, err := buildClient(connection)
Defensive patterns

Strategy: validation

Validate before calling

cert := firstNonBlank(conn.ClientCertPath, conn.CertPath)
key := firstNonBlank(conn.ClientKeyPath, conn.KeyPath)
if (cert == "") != (key == "") {
	return errors.New("client cert and key must both be set (or both omitted)")
}
if cert != "" {
	if _, err := os.Stat(cert); err != nil { return err }
	if _, err := os.Stat(key); err != nil { return err }
}

Type guard

func hasPairedTLSMaterial(conn connectionParams) bool {
	hasCert := firstNonBlank(conn.ClientCertPath, conn.CertPath) != ""
	hasKey := firstNonBlank(conn.ClientKeyPath, conn.KeyPath) != ""
	return hasCert == hasKey
}

Try / catch

client, err := buildClient(conn)
if err != nil {
	if strings.Contains(err.Error(), "must be provided together") {
		return nil, fmt.Errorf("TLS config incomplete: set both client cert and key paths: %w", err)
	}
	return nil, err
}

Prevention

When it happens

Trigger: Setting connection.ClientCertPath (or CertPath) without ClientKeyPath (or KeyPath), or vice versa, when building the etcd client via buildClient.

Common situations: Partial TLS config in env/files — e.g. only ETCD_CLIENT_CERT_PATH exported; copy-paste config where the key line was dropped; rotating certs and updating only one path; secret mounts where one file failed to mount.

Understand the failure class

Related errors


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