Skip to content

Commit 40b8cfe

Browse files
committed
Merge branch 'master' into releases/1.6.x
2 parents 93911b9 + 3775a72 commit 40b8cfe

File tree

5 files changed

+85
-22
lines changed

5 files changed

+85
-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

pkg/apiserver/papi.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type Papi struct {
7070
consoleConfig *csconfig.ConsoleConfig
7171
Logger *log.Entry
7272
apic *apic
73+
stopChan chan struct{}
7374
}
7475

7576
type PapiPermCheckError struct {
@@ -118,6 +119,7 @@ func NewPAPI(apic *apic, dbClient *database.Client, consoleConfig *csconfig.Cons
118119
apic: apic,
119120
consoleConfig: consoleConfig,
120121
Logger: logger.WithFields(log.Fields{"interval": SyncInterval.Seconds(), "source": "papi"}),
122+
stopChan: make(chan struct{}),
121123
}
122124

123125
return papi, nil
@@ -316,8 +318,11 @@ func (p *Papi) Pull(ctx context.Context) error {
316318
}
317319

318320
logger.Debugf("set last timestamp to %s", newTime)
321+
case <-p.stopChan:
322+
cancel()
319323
}
320324
}
325+
321326
}
322327

323328
func (p *Papi) SyncDecisions(ctx context.Context) error {
@@ -398,5 +403,9 @@ func (p *Papi) SendDeletedDecisions(ctx context.Context, cacheOrig *models.Decis
398403
func (p *Papi) Shutdown() {
399404
p.Logger.Infof("Shutting down PAPI")
400405
p.syncTomb.Kill(nil)
406+
select {
407+
case p.stopChan <- struct{}{}: // Cancel any HTTP request still in progress
408+
default:
409+
}
401410
p.Client.Stop()
402411
}

pkg/longpollclient/client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ func (c *LongPollClient) doQuery(ctx context.Context) (*http.Response, error) {
6868
req.Header.Set("Accept", "application/json")
6969
resp, err := c.httpClient.Do(req)
7070
if err != nil {
71+
if errors.Is(err, context.Canceled) {
72+
return nil, ctx.Err() // Don't log
73+
}
7174
logger.Errorf("failed to execute request: %s", err)
7275
return nil, err
7376
}
@@ -175,6 +178,10 @@ func (c *LongPollClient) pollEvents(ctx context.Context) error {
175178
close(c.c)
176179
return err
177180
}
181+
if errors.Is(err, context.Canceled) {
182+
c.logger.Debug("context canceled, stopping polling")
183+
return nil
184+
}
178185
c.logger.Errorf("failed to poll: %s, retrying in %s", err, currentBackoff)
179186
select {
180187
case <-c.t.Dying():

0 commit comments

Comments
 (0)