Skip to content

Commit 5fc0b2a

Browse files
authored
appsec: do not query LAPI multiple times when checking auth (#3678)
1 parent 5539cda commit 5fc0b2a

File tree

3 files changed

+69
-22
lines changed

3 files changed

+69
-22
lines changed

pkg/acquisition/modules/appsec/appsec.go

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
2626
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
27+
"github.com/crowdsecurity/crowdsec/pkg/apiclient/useragent"
2728
"github.com/crowdsecurity/crowdsec/pkg/appsec"
2829
"github.com/crowdsecurity/crowdsec/pkg/appsec/allowlists"
2930
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@@ -36,6 +37,11 @@ const (
3637
OutOfBand = "outofband"
3738
)
3839

40+
var (
41+
errMissingAPIKey = errors.New("missing API key")
42+
errInvalidAPIKey = errors.New("invalid API key")
43+
)
44+
3945
var DefaultAuthCacheDuration = (1 * time.Minute)
4046

4147
// configuration structure of the acquis for the application security engine
@@ -69,6 +75,7 @@ type AppsecSource struct {
6975
AppsecRunners []AppsecRunner // one for each go-routine
7076
appsecAllowlistClient *allowlists.AppsecAllowlist
7177
lapiCACertPool *x509.CertPool
78+
authMutex sync.Mutex
7279
}
7380

7481
// Struct to handle cache of authentication
@@ -98,6 +105,12 @@ func (ac *AuthCache) Get(apiKey string) (time.Time, bool) {
98105
return expiration, exists
99106
}
100107

108+
func (ac *AuthCache) Delete(apiKey string) {
109+
ac.mu.Lock()
110+
delete(ac.APIKeys, apiKey)
111+
ac.mu.Unlock()
112+
}
113+
101114
// @tko + @sbl : we might want to get rid of that or improve it
102115
type BodyResponse struct {
103116
Action string `json:"action"`
@@ -454,14 +467,15 @@ func (w *AppsecSource) Dump() interface{} {
454467
return w
455468
}
456469

457-
func (w *AppsecSource) IsAuth(ctx context.Context, apiKey string) bool {
470+
func (w *AppsecSource) isValidKey(ctx context.Context, apiKey string) (bool, error) {
458471
req, err := http.NewRequestWithContext(ctx, http.MethodHead, w.lapiURL, http.NoBody)
459472
if err != nil {
460473
w.logger.Errorf("Error creating request: %s", err)
461-
return false
474+
return false, err
462475
}
463476

464477
req.Header.Add("X-Api-Key", apiKey)
478+
req.Header.Add("User-Agent", useragent.AppsecUserAgent())
465479

466480
client := &http.Client{
467481
Timeout: 200 * time.Millisecond,
@@ -478,12 +492,53 @@ func (w *AppsecSource) IsAuth(ctx context.Context, apiKey string) bool {
478492
resp, err := client.Do(req)
479493
if err != nil {
480494
w.logger.Errorf("Error performing request: %s", err)
481-
return false
495+
return false, err
482496
}
483497

484498
defer resp.Body.Close()
485499

486-
return resp.StatusCode == http.StatusOK
500+
return resp.StatusCode == http.StatusOK, nil
501+
}
502+
503+
func (w *AppsecSource) checkAuth(ctx context.Context, apiKey string) error {
504+
505+
if apiKey == "" {
506+
return errMissingAPIKey
507+
}
508+
509+
w.authMutex.Lock()
510+
defer w.authMutex.Unlock()
511+
512+
expiration, exists := w.AuthCache.Get(apiKey)
513+
now := time.Now()
514+
515+
if !exists { // No key in cache, require a valid check
516+
isAuth, err := w.isValidKey(ctx, apiKey)
517+
if err != nil || !isAuth {
518+
if err != nil {
519+
w.logger.Errorf("Error checking auth for API key: %s", err)
520+
}
521+
return errInvalidAPIKey
522+
}
523+
// Cache the valid API key
524+
w.AuthCache.Set(apiKey, now.Add(*w.config.AuthCacheDuration))
525+
return nil
526+
}
527+
528+
if now.After(expiration) { // Key is expired, recheck the value OR keep it if we cannot contact LAPI
529+
isAuth, err := w.isValidKey(ctx, apiKey)
530+
if isAuth {
531+
w.AuthCache.Set(apiKey, now.Add(*w.config.AuthCacheDuration))
532+
} else if err != nil { // General error when querying LAPI, consider the key still valid
533+
w.logger.Errorf("Error checking auth for API key: %s, extending cache duration", err)
534+
w.AuthCache.Set(apiKey, now.Add(*w.config.AuthCacheDuration))
535+
} else { // Key is not valid, remove it from cache
536+
w.AuthCache.Delete(apiKey)
537+
return errInvalidAPIKey
538+
}
539+
}
540+
541+
return nil
487542
}
488543

489544
// should this be in the runner ?
@@ -495,27 +550,12 @@ func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
495550
clientIP := r.Header.Get(appsec.IPHeaderName)
496551
remoteIP := r.RemoteAddr
497552

498-
if apiKey == "" {
499-
w.logger.Errorf("Unauthorized request from '%s' (real IP = %s)", remoteIP, clientIP)
553+
if err := w.checkAuth(ctx, apiKey); err != nil {
554+
w.logger.Errorf("Unauthorized request from '%s' (real IP = %s): %s", remoteIP, clientIP, err)
500555
rw.WriteHeader(http.StatusUnauthorized)
501-
502556
return
503557
}
504558

505-
expiration, exists := w.AuthCache.Get(apiKey)
506-
// if the apiKey is not in cache or has expired, just recheck the auth
507-
if !exists || time.Now().After(expiration) {
508-
if !w.IsAuth(ctx, apiKey) {
509-
rw.WriteHeader(http.StatusUnauthorized)
510-
w.logger.Errorf("Unauthorized request from '%s' (real IP = %s)", remoteIP, clientIP)
511-
512-
return
513-
}
514-
515-
// apiKey is valid, store it in cache
516-
w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration))
517-
}
518-
519559
// parse the request only once
520560
parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger)
521561
if err != nil {

pkg/apiclient/useragent/useragent.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ import (
77
func Default() string {
88
return "crowdsec/" + version.String() + "-" + version.System
99
}
10+
11+
func AppsecUserAgent() string {
12+
return "appsec/" + version.String() + "-" + version.System
13+
}

pkg/apiserver/middlewares/v1/api_key.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ type APIKey struct {
2929
TlsAuth *TLSAuth
3030
}
3131

32-
//
3332
func GenerateAPIKey(n int) (string, error) {
3433
bytes := make([]byte, n)
3534
if _, err := rand.Read(bytes); err != nil {
@@ -129,6 +128,10 @@ func (a *APIKey) authPlain(c *gin.Context, logger *log.Entry) *ent.Bouncer {
129128
logger.Errorf("while fetching bouncer info: %s", err)
130129
return nil
131130
}
131+
if len(bouncer) == 0 {
132+
logger.Debugf("no bouncer found with this key")
133+
return nil
134+
}
132135
return bouncer[0]
133136
}
134137

0 commit comments

Comments
 (0)