Add authentication between bouncers and waf

This commit is contained in:
alteredCoder 2023-11-16 18:19:32 +01:00
parent 9db48e2110
commit 9864d2c459
3 changed files with 105 additions and 14 deletions

View file

@ -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

View file

@ -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

View file

@ -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 {