crowdsecurity/crowdsec · error

no verified cert in request

Error message

no verified cert in request

What it means

Even when a peer certificate was presented, ValidateCert requires the TLS handshake to have completed full verification: tls.ConnectionState.VerifiedChains must be non-empty. An empty VerifiedChains means the certificate chain was never validated, so the cert cannot be trusted and auth fails.

Solutions

  1. Set the server's TLS ClientAuth mode to tls.RequireAndVerifyClientCert
  2. Ensure the server trusts the CA that signed client certificates (client_ca certificate loaded correctly)
  3. Restart the API server and reconnect with the client cert

Example fix

// before
tlsConfig.ClientAuth = tls.RequestClientCert
// after
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
tlsConfig.ClientCAs = caPool
Defensive patterns

Strategy: type-guard

Validate before calling

if len(req.TLS.VerifiedChains) == 0 {
    return errors.New("client cert chain was not verified")
}

Type guard

func hasVerifiedChain(req *http.Request) bool {
    return req.TLS != nil && len(req.TLS.VerifiedChains) > 0 && len(req.TLS.VerifiedChains[0]) > 0
}

Try / catch

cn, err := ta.ValidateCert(c)
if err != nil {
    log.Debugf("cert auth rejected: %v", err)
    c.AbortWithStatus(http.StatusUnauthorized)
    return
}

Prevention

When it happens

Trigger: ValidateCert sees len(c.Request.TLS.VerifiedChains) == 0 — typically because the server accepted the connection with client auth set to RequestClientCert (or VerifyClientCertIfGiven without verification succeeding at handshake level), so certs are present but not verified.

Common situations: Go TLS config with ClientAuth: tls.RequestClientCert instead of RequireAndVerifyClientCert; handshake verification errors tolerated; custom TLS wrapper that drops VerifiedChains.

Related errors


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/f039ab275cbf2f8b. Report an issue: GitHub.

Appendix: source

Thrown at pkg/apiserver/middlewares/v1/tls_auth.go:110

	for _, ou := range ous {
		if slices.Contains(ta.AllowedOUs, ou) {
			return nil
		}
	}

	return fmt.Errorf("client certificate OU %v doesn't match expected OU %v", ous, ta.AllowedOUs)
}

func (ta *TLSAuth) ValidateCert(c *gin.Context) (string, error) {
	// Checks cert validity, Returns true + CN if client cert matches requested OU
	var leaf *x509.Certificate

	if c.Request.TLS == nil || len(c.Request.TLS.PeerCertificates) == 0 {
		return "", errors.New("no certificate in request")
	}

	if len(c.Request.TLS.VerifiedChains) == 0 {
		return "", errors.New("no verified cert in request")
	}

	// although there can be multiple chains, the leaf certificate is the same
	// we take the first one
	leaf = c.Request.TLS.VerifiedChains[0][0]

	if err := ta.checkAllowedOU(leaf.Subject.OrganizationalUnit); err != nil {
		return "", err
	}

	if ta.isExpired(leaf) {
		return "", errors.New("client certificate is expired")
	}

	if validErr, cached := ta.revocationCache.Get(leaf); cached {
		if validErr != nil {
			return "", fmt.Errorf("(cache) %w", validErr)
		}

View on GitHub (pinned to 909b515798)