{"record":{"id":"f039ab275cbf2f8b","repo":"crowdsecurity/crowdsec","slug":"no-verified-cert-in-request","errorCode":null,"errorMessage":"no verified cert in request","messagePattern":"no verified cert in request","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"pkg/apiserver/middlewares/v1/tls_auth.go","lineNumber":110,"sourceCode":"\tfor _, ou := range ous {\n\t\tif slices.Contains(ta.AllowedOUs, ou) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"client certificate OU %v doesn't match expected OU %v\", ous, ta.AllowedOUs)\n}\n\nfunc (ta *TLSAuth) ValidateCert(c *gin.Context) (string, error) {\n\t// Checks cert validity, Returns true + CN if client cert matches requested OU\n\tvar leaf *x509.Certificate\n\n\tif c.Request.TLS == nil || len(c.Request.TLS.PeerCertificates) == 0 {\n\t\treturn \"\", errors.New(\"no certificate in request\")\n\t}\n\n\tif len(c.Request.TLS.VerifiedChains) == 0 {\n\t\treturn \"\", errors.New(\"no verified cert in request\")\n\t}\n\n\t// although there can be multiple chains, the leaf certificate is the same\n\t// we take the first one\n\tleaf = c.Request.TLS.VerifiedChains[0][0]\n\n\tif err := ta.checkAllowedOU(leaf.Subject.OrganizationalUnit); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif ta.isExpired(leaf) {\n\t\treturn \"\", errors.New(\"client certificate is expired\")\n\t}\n\n\tif validErr, cached := ta.revocationCache.Get(leaf); cached {\n\t\tif validErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"(cache) %w\", validErr)\n\t\t}","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/apiserver/middlewares/v1/tls_auth.go#L92-L128","documentation":"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.","triggerScenarios":"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.","commonSituations":"Go TLS config with ClientAuth: tls.RequestClientCert instead of RequireAndVerifyClientCert; handshake verification errors tolerated; custom TLS wrapper that drops VerifiedChains.","solutions":["Set the server's TLS ClientAuth mode to tls.RequireAndVerifyClientCert","Ensure the server trusts the CA that signed client certificates (client_ca certificate loaded correctly)","Restart the API server and reconnect with the client cert"],"exampleFix":"// before\ntlsConfig.ClientAuth = tls.RequestClientCert\n// after\ntlsConfig.ClientAuth = tls.RequireAndVerifyClientCert\ntlsConfig.ClientCAs = caPool","handlingStrategy":"type-guard","validationCode":"if len(req.TLS.VerifiedChains) == 0 {\n    return errors.New(\"client cert chain was not verified\")\n}","typeGuard":"func hasVerifiedChain(req *http.Request) bool {\n    return req.TLS != nil && len(req.TLS.VerifiedChains) > 0 && len(req.TLS.VerifiedChains[0]) > 0\n}","tryCatchPattern":"cn, err := ta.ValidateCert(c)\nif err != nil {\n    log.Debugf(\"cert auth rejected: %v\", err)\n    c.AbortWithStatus(http.StatusUnauthorized)\n    return\n}","preventionTips":["Use tls.RequireAndVerifyClientCert, not RequestClientCert","Ensure the CA bundle signing client certs is loaded into ClientCAs","Test mTLS with curl --cert/--key before going live"],"tags":["tls","mtls","certificate","verification"],"backgroundTag":"missing-client-certificate","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}