-
Notifications
You must be signed in to change notification settings - Fork 486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for centralized allowlists #3355
base: master
Are you sure you want to change the base?
Changes from 29 commits
dbb9adc
804dfc4
5af9706
468c822
3e08ab9
35095f7
198bc91
dc66afd
c87b817
832868d
13a9686
d7f8d2b
28ba0c9
809f4f5
97fac0a
c9bf246
85ab85d
426d6d4
183b28d
4d7d75e
a5beb49
a541363
3baf633
d48ff39
c832ed2
01ea040
37a09e0
e49693f
9f8d286
11cd0d3
4ea0b1d
3953474
a34e802
61a79f0
fb313c0
aea932d
b65da6f
3ff1538
aca0ea6
a692267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
"github.com/crowdsecurity/go-cs-lib/trace" | ||
|
||
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" | ||
"github.com/crowdsecurity/crowdsec/pkg/apiclient" | ||
"github.com/crowdsecurity/crowdsec/pkg/appsec" | ||
"github.com/crowdsecurity/crowdsec/pkg/csconfig" | ||
"github.com/crowdsecurity/crowdsec/pkg/types" | ||
|
@@ -31,6 +32,8 @@ | |
) | ||
|
||
var DefaultAuthCacheDuration = (1 * time.Minute) | ||
var negativeAllowlistCacheDuration = (5 * time.Minute) | ||
var positiveAllowlistCacheDuration = (5 * time.Minute) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like a lot to me, but it depends if SE gets a |
||
|
||
// configuration structure of the acquis for the application security engine | ||
type AppsecSourceConfig struct { | ||
|
@@ -49,18 +52,20 @@ | |
|
||
// runtime structure of AppsecSourceConfig | ||
type AppsecSource struct { | ||
metricsLevel int | ||
config AppsecSourceConfig | ||
logger *log.Entry | ||
mux *http.ServeMux | ||
server *http.Server | ||
outChan chan types.Event | ||
InChan chan appsec.ParsedRequest | ||
AppsecRuntime *appsec.AppsecRuntimeConfig | ||
AppsecConfigs map[string]appsec.AppsecConfig | ||
lapiURL string | ||
AuthCache AuthCache | ||
AppsecRunners []AppsecRunner // one for each go-routine | ||
metricsLevel int | ||
config AppsecSourceConfig | ||
logger *log.Entry | ||
mux *http.ServeMux | ||
server *http.Server | ||
outChan chan types.Event | ||
InChan chan appsec.ParsedRequest | ||
AppsecRuntime *appsec.AppsecRuntimeConfig | ||
AppsecConfigs map[string]appsec.AppsecConfig | ||
lapiURL string | ||
AuthCache AuthCache | ||
AppsecRunners []AppsecRunner // one for each go-routine | ||
allowlistCache allowlistCache | ||
apiClient *apiclient.ApiClient | ||
} | ||
|
||
// Struct to handle cache of authentication | ||
|
@@ -69,6 +74,17 @@ | |
mu sync.RWMutex | ||
} | ||
|
||
// FIXME: auth and allowlist should probably be merged to a common structure | ||
type allowlistCache struct { | ||
mu sync.RWMutex | ||
allowlist map[string]allowlistCacheEntry | ||
} | ||
|
||
type allowlistCacheEntry struct { | ||
allowlisted bool | ||
expiration time.Time | ||
} | ||
|
||
func NewAuthCache() AuthCache { | ||
return AuthCache{ | ||
APIKeys: make(map[string]time.Time, 0), | ||
|
@@ -90,6 +106,30 @@ | |
return expiration, exists | ||
} | ||
|
||
func NewAllowlistCache() allowlistCache { | ||
return allowlistCache{ | ||
allowlist: make(map[string]allowlistCacheEntry, 0), | ||
mu: sync.RWMutex{}, | ||
} | ||
} | ||
|
||
func (ac *allowlistCache) Set(value string, allowlisted bool, expiration time.Time) { | ||
ac.mu.Lock() | ||
ac.allowlist[value] = allowlistCacheEntry{ | ||
allowlisted: allowlisted, | ||
expiration: expiration, | ||
} | ||
ac.mu.Unlock() | ||
} | ||
|
||
func (ac *allowlistCache) Get(value string) (bool, time.Time, bool) { | ||
ac.mu.RLock() | ||
entry, exists := ac.allowlist[value] | ||
ac.mu.RUnlock() | ||
|
||
return entry.allowlisted, entry.expiration, exists | ||
} | ||
|
||
// @tko + @sbl : we might want to get rid of that or improve it | ||
type BodyResponse struct { | ||
Action string `json:"action"` | ||
|
@@ -246,6 +286,12 @@ | |
// We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec | ||
w.mux.HandleFunc(w.config.Path, w.appsecHandler) | ||
|
||
w.apiClient, err = apiclient.GetLAPIClient() | ||
if err != nil { | ||
return fmt.Errorf("unable to get authenticated LAPI client: %w", err) | ||
} | ||
w.allowlistCache = NewAllowlistCache() | ||
|
||
return nil | ||
} | ||
|
||
|
@@ -377,6 +423,33 @@ | |
return resp.StatusCode == http.StatusOK | ||
} | ||
|
||
func (w *AppsecSource) isAllowlisted(ctx context.Context, value string, query bool) bool { | ||
var err error | ||
|
||
allowlisted, expiration, exists := w.allowlistCache.Get(value) | ||
if exists && !time.Now().After(expiration) { | ||
return allowlisted | ||
} | ||
|
||
if !query { | ||
return false | ||
} | ||
|
||
allowlisted, _, err = w.apiClient.Allowlists.CheckIfAllowlisted(ctx, value) | ||
if err != nil { | ||
w.logger.Errorf("unable to check if %s is allowlisted: %s", value, err) | ||
return false | ||
} | ||
|
||
if allowlisted { | ||
w.allowlistCache.Set(value, allowlisted, time.Now().Add(positiveAllowlistCacheDuration)) | ||
} else { | ||
w.allowlistCache.Set(value, allowlisted, time.Now().Add(negativeAllowlistCacheDuration)) | ||
} | ||
|
||
return allowlisted | ||
} | ||
|
||
// should this be in the runner ? | ||
func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) { | ||
w.logger.Debugf("Received request from '%s' on %s", r.RemoteAddr, r.URL.Path) | ||
|
@@ -406,6 +479,25 @@ | |
w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration)) | ||
} | ||
|
||
// check if the client IP is allowlisted | ||
if w.isAllowlisted(r.Context(), clientIP, false) { | ||
w.logger.Infof("%s is allowlisted by LAPI, not processing", clientIP) | ||
statusCode, appsecResponse := w.AppsecRuntime.GenerateResponse(appsec.AppsecTempResponse{ | ||
InBandInterrupt: false, | ||
OutOfBandInterrupt: false, | ||
Action: appsec.AllowRemediation, | ||
}, w.logger) | ||
body, err := json.Marshal(appsecResponse) | ||
if err != nil { | ||
w.logger.Errorf("unable to serialize response: %s", err) | ||
rw.WriteHeader(http.StatusInternalServerError) | ||
return | ||
} | ||
rw.WriteHeader(statusCode) | ||
rw.Write(body) | ||
return | ||
} | ||
|
||
// parse the request only once | ||
parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger) | ||
if err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -205,6 +205,7 @@ func (lc *LokiClient) getURLFor(endpoint string, params map[string]string) strin | |
func (lc *LokiClient) Ready(ctx context.Context) error { | ||
tick := time.NewTicker(500 * time.Millisecond) | ||
url := lc.getURLFor("ready", nil) | ||
lc.Logger.Debugf("Using url: %s for ready check", url) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unrelated change? |
||
for { | ||
select { | ||
case <-ctx.Done(): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated change ?