From 9864d2c4594bb42018b0dbd9ba52380dceb9c5db Mon Sep 17 00:00:00 2001 From: alteredCoder Date: Thu, 16 Nov 2023 18:19:32 +0100 Subject: [PATCH] Add authentication between bouncers and waf --- pkg/acquisition/modules/waap/waap.go | 102 ++++++++++++++++++++++++--- pkg/csconfig/config.go | 8 +++ pkg/waf/request.go | 9 +-- 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/pkg/acquisition/modules/waap/waap.go b/pkg/acquisition/modules/waap/waap.go index 92363b2e3..cf9fd2ad6 100644 --- a/pkg/acquisition/modules/waap/waap.go +++ b/pkg/acquisition/modules/waap/waap.go @@ -5,6 +5,10 @@ import ( "encoding/json" "fmt" "net/http" + "sync" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration" "github.com/crowdsecurity/crowdsec/pkg/types" @@ -23,16 +27,21 @@ const ( OutOfBand = "outofband" ) +var ( + DefaultAuthCacheDuration = (1 * time.Minute) +) + // configuration structure of the acquis for the Waap type WaapSourceConfig struct { - ListenAddr string `yaml:"listen_addr"` - ListenPort int `yaml:"listen_port"` - CertFilePath string `yaml:"cert_file"` - KeyFilePath string `yaml:"key_file"` - Path string `yaml:"path"` - Routines int `yaml:"routines"` - WaapConfig string `yaml:"waap_config"` - WaapConfigPath string `yaml:"waap_config_path"` + ListenAddr string `yaml:"listen_addr"` + ListenPort int `yaml:"listen_port"` + CertFilePath string `yaml:"cert_file"` + KeyFilePath string `yaml:"key_file"` + Path string `yaml:"path"` + Routines int `yaml:"routines"` + WaapConfig string `yaml:"waap_config"` + WaapConfigPath string `yaml:"waap_config_path"` + AuthCacheDuration *time.Duration `yaml:"auth_cache_duration"` configuration.DataSourceCommonCfg `yaml:",inline"` } @@ -46,12 +55,38 @@ type WaapSource struct { outChan chan types.Event InChan chan waf.ParsedRequest WaapRuntime *waf.WaapRuntimeConfig - WaapConfigs map[string]waf.WaapConfig - + lapiURL string + AuthCache AuthCache WaapRunners []WaapRunner //one for each go-routine } +// Struct to handle cache of authentication +type AuthCache struct { + APIKeys map[string]time.Time + mu sync.RWMutex +} + +func NewAuthCache() AuthCache { + return AuthCache{ + APIKeys: make(map[string]time.Time, 0), + mu: sync.RWMutex{}, + } +} + +func (ac *AuthCache) Set(apiKey string, expiration time.Time) { + ac.mu.Lock() + ac.APIKeys[apiKey] = expiration + ac.mu.Unlock() +} + +func (ac *AuthCache) Get(apiKey string) (time.Time, bool) { + ac.mu.RLock() + expiration, exists := ac.APIKeys[apiKey] + ac.mu.RUnlock() + return expiration, exists +} + // @tko + @sbl : we might want to get rid of that or improve it type BodyResponse struct { Action string `json:"action"` @@ -97,6 +132,11 @@ func (wc *WaapSource) UnmarshalConfig(yamlConfig []byte) error { if wc.config.WaapConfig == "" && wc.config.WaapConfigPath == "" { return fmt.Errorf("waap_config or waap_config_path must be set") } + + csConfig := csconfig.GetConfig() + wc.lapiURL = fmt.Sprintf("%sv1/decisions/stream", csConfig.API.Client.Credentials.URL) + wc.AuthCache = NewAuthCache() + return nil } @@ -118,6 +158,11 @@ func (w *WaapSource) Configure(yamlConfig []byte, logger *log.Entry) error { w.logger.Tracef("WAF configuration: %+v", w.config) + if w.config.AuthCacheDuration == nil { + w.config.AuthCacheDuration = &DefaultAuthCacheDuration + w.logger.Infof("Cache duration for auth not set, using default: %v", *w.config.AuthCacheDuration) + } + w.addr = fmt.Sprintf("%s:%d", w.config.ListenAddr, w.config.ListenPort) w.mux = http.NewServeMux() @@ -251,8 +296,44 @@ func (w *WaapSource) Dump() interface{} { return w } +func (w *WaapSource) IsAuth(apiKey string) bool { + client := &http.Client{} + req, err := http.NewRequest("HEAD", w.lapiURL, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return false + } + req.Header.Add("X-Api-Key", apiKey) + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error performing request:", err) + return false + } + defer resp.Body.Close() + + return resp.StatusCode == http.StatusOK + +} + // should this be in the runner ? func (w *WaapSource) waapHandler(rw http.ResponseWriter, r *http.Request) { + apiKey := r.Header.Get(waf.APIKeyHeaderName) + if apiKey == "" { + rw.WriteHeader(http.StatusUnauthorized) + return + } + expiration, exists := w.AuthCache.Get(apiKey) + // if the apiKey is not in cache or has expired, just recheck the auth + if !exists || time.Now().After(expiration) { + if !w.IsAuth(apiKey) { + rw.WriteHeader(http.StatusUnauthorized) + return + } + + // apiKey is valid, store it in cache + w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration)) + } + // parse the request only once parsedRequest, err := waf.NewParsedRequestFromRequest(r) if err != nil { @@ -260,6 +341,7 @@ func (w *WaapSource) waapHandler(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusInternalServerError) return } + w.InChan <- parsedRequest response := <-parsedRequest.ResponseChannel diff --git a/pkg/csconfig/config.go b/pkg/csconfig/config.go index ce8f09bc0..9e90c20cc 100644 --- a/pkg/csconfig/config.go +++ b/pkg/csconfig/config.go @@ -21,6 +21,8 @@ var defaultConfigDir = "/etc/crowdsec" // defaultDataDir is the base path to all data files, to be overridden in the Makefile */ var defaultDataDir = "/var/lib/crowdsec/data/" +var globalConfig = Config{} + // Config contains top-level defaults -> overridden by configuration file -> overridden by CLI flags type Config struct { //just a path to ourselves :p @@ -89,9 +91,15 @@ func NewConfig(configFile string, disableAgent bool, disableAPI bool, quiet bool return nil, "", err } + globalConfig = cfg + return &cfg, configData, nil } +func GetConfig() Config { + return globalConfig +} + // XXX: We must not have a different behavior with an empty vs a missing configuration file. // XXX: For this reason, all defaults have to come from NewConfig(). The following function should // XXX: be replaced diff --git a/pkg/waf/request.go b/pkg/waf/request.go index 5a7cc04a3..f7b7a11b7 100644 --- a/pkg/waf/request.go +++ b/pkg/waf/request.go @@ -11,10 +11,11 @@ import ( ) const ( - URIHeaderName = "X-Crowdsec-Waf-Uri" - VerbHeaderName = "X-Crowdsec-Waf-Verb" - HostHeaderName = "X-Crowdsec-Waf-Host" - IPHeaderName = "X-Crowdsec-Waf-Ip" + URIHeaderName = "X-Crowdsec-Waf-Uri" + VerbHeaderName = "X-Crowdsec-Waf-Verb" + HostHeaderName = "X-Crowdsec-Waf-Host" + IPHeaderName = "X-Crowdsec-Waf-Ip" + APIKeyHeaderName = "X-Crowdsec-Waf-Api-Key" ) // type ResponseRequest struct {