@@ -24,6 +24,7 @@ import (
24
24
25
25
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
26
26
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
27
+ "github.com/crowdsecurity/crowdsec/pkg/apiclient/useragent"
27
28
"github.com/crowdsecurity/crowdsec/pkg/appsec"
28
29
"github.com/crowdsecurity/crowdsec/pkg/appsec/allowlists"
29
30
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@@ -36,6 +37,11 @@ const (
36
37
OutOfBand = "outofband"
37
38
)
38
39
40
+ var (
41
+ errMissingAPIKey = errors .New ("missing API key" )
42
+ errInvalidAPIKey = errors .New ("invalid API key" )
43
+ )
44
+
39
45
var DefaultAuthCacheDuration = (1 * time .Minute )
40
46
41
47
// configuration structure of the acquis for the application security engine
@@ -69,6 +75,7 @@ type AppsecSource struct {
69
75
AppsecRunners []AppsecRunner // one for each go-routine
70
76
appsecAllowlistClient * allowlists.AppsecAllowlist
71
77
lapiCACertPool * x509.CertPool
78
+ authMutex sync.Mutex
72
79
}
73
80
74
81
// Struct to handle cache of authentication
@@ -98,6 +105,12 @@ func (ac *AuthCache) Get(apiKey string) (time.Time, bool) {
98
105
return expiration , exists
99
106
}
100
107
108
+ func (ac * AuthCache ) Delete (apiKey string ) {
109
+ ac .mu .Lock ()
110
+ delete (ac .APIKeys , apiKey )
111
+ ac .mu .Unlock ()
112
+ }
113
+
101
114
// @tko + @sbl : we might want to get rid of that or improve it
102
115
type BodyResponse struct {
103
116
Action string `json:"action"`
@@ -454,14 +467,15 @@ func (w *AppsecSource) Dump() interface{} {
454
467
return w
455
468
}
456
469
457
- func (w * AppsecSource ) IsAuth (ctx context.Context , apiKey string ) bool {
470
+ func (w * AppsecSource ) isValidKey (ctx context.Context , apiKey string ) ( bool , error ) {
458
471
req , err := http .NewRequestWithContext (ctx , http .MethodHead , w .lapiURL , http .NoBody )
459
472
if err != nil {
460
473
w .logger .Errorf ("Error creating request: %s" , err )
461
- return false
474
+ return false , err
462
475
}
463
476
464
477
req .Header .Add ("X-Api-Key" , apiKey )
478
+ req .Header .Add ("User-Agent" , useragent .AppsecUserAgent ())
465
479
466
480
client := & http.Client {
467
481
Timeout : 200 * time .Millisecond ,
@@ -478,12 +492,53 @@ func (w *AppsecSource) IsAuth(ctx context.Context, apiKey string) bool {
478
492
resp , err := client .Do (req )
479
493
if err != nil {
480
494
w .logger .Errorf ("Error performing request: %s" , err )
481
- return false
495
+ return false , err
482
496
}
483
497
484
498
defer resp .Body .Close ()
485
499
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
487
542
}
488
543
489
544
// should this be in the runner ?
@@ -495,27 +550,12 @@ func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
495
550
clientIP := r .Header .Get (appsec .IPHeaderName )
496
551
remoteIP := r .RemoteAddr
497
552
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 )
500
555
rw .WriteHeader (http .StatusUnauthorized )
501
-
502
556
return
503
557
}
504
558
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
-
519
559
// parse the request only once
520
560
parsedRequest , err := appsec .NewParsedRequestFromRequest (r , w .logger )
521
561
if err != nil {
0 commit comments