caddyserver/caddy · error

client leaf certificate failed validation

Error message

client leaf certificate failed validation

What it means

The leaf verifier parsed the client's certificate successfully but found no exact match (x509.Certificate.Equal) among the configured trustedLeafCerts, so the mTLS handshake is rejected: this is the expected 'untrusted client' outcome of leaf-pinned client authentication.

Source

Thrown at modules/caddytls/connpolicy.go:1045

	}
	return nil
}

func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
	if len(rawCerts) == 0 {
		return fmt.Errorf("no client certificate provided")
	}

	remoteLeafCert, err := x509.ParseCertificate(rawCerts[0])
	if err != nil {
		return fmt.Errorf("can't parse the given certificate: %s", err.Error())
	}

	if slices.ContainsFunc(l.trustedLeafCerts, remoteLeafCert.Equal) {
		return nil
	}

	return fmt.Errorf("client leaf certificate failed validation")
}

// PublicKeyAlgorithm is a JSON-unmarshalable wrapper type.
type PublicKeyAlgorithm x509.PublicKeyAlgorithm

// UnmarshalJSON satisfies json.Unmarshaler.
func (a *PublicKeyAlgorithm) UnmarshalJSON(b []byte) error {
	algoStr := strings.ToLower(strings.Trim(string(b), `"`))
	algo, ok := publicKeyAlgorithms[algoStr]
	if !ok {
		return fmt.Errorf("unrecognized public key algorithm: %s (expected one of %v)",
			algoStr, publicKeyAlgorithms)
	}
	*a = PublicKeyAlgorithm(algo)
	return nil
}

// ConnectionMatcher is a type which matches TLS handshakes.

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Confirm which cert the client sends (openssl x509 -in client.pem -noout -fingerprint -sha256) and compare with the server's loaded leaf(s)
  2. If the client cert was rotated, update the server's leaf_cert_file/inline list and reload Caddy (config reload picks up new files)
  3. Prefer CA-based verification (trusted_ca_cert_file / ca module) over leaf pinning unless you specifically need pinning — it survives client cert renewal
  4. Check you pinned the leaf certificate, not the CA/intermediate

Example fix

# before: pinning an intermediate (handshake always fails)
verifier leaf {
  leaf_cert_file /etc/caddy/intermediate.pem
}

# after: pin the exact client leaf, or switch to CA verification
verifier leaf {
  leaf_cert_file /etc/caddy/client-leaf.pem
}
Defensive patterns

Strategy: validation

Validate before calling

// Server-side: before reload, confirm every trusted leaf fingerprint matches intent
func fingerprint(path string) (string, error) {
	b, _ := os.ReadFile(path)
	blk, _ := pem.Decode(b)
	c, err := x509.ParseCertificate(blk.Bytes)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%X", sha256.Sum256(c.Raw)), nil
}
// compare against the client cert actually issued today

Prevention

When it happens

Trigger: Client presents a certificate that is valid X.509 but not byte-for-byte one of the loaded trust anchors; client cert was renewed/reissued and the server still pins the old one; wrong trust file loaded (dev cert in prod).

Common situations: Certificate rotation without updating the server's trusted leaf list; environments where clients have many certs and the server pins one; accidental trust of an intermediate instead of the actual leaf.

Understand the failure class

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/a6e29aee9bbe4fed. Report an issue: GitHub.