Add authentication between bouncers and waf
This commit is contained in:
parent
9db48e2110
commit
9864d2c459
|
@ -5,6 +5,10 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
@ -23,6 +27,10 @@ const (
|
||||||
OutOfBand = "outofband"
|
OutOfBand = "outofband"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultAuthCacheDuration = (1 * time.Minute)
|
||||||
|
)
|
||||||
|
|
||||||
// configuration structure of the acquis for the Waap
|
// configuration structure of the acquis for the Waap
|
||||||
type WaapSourceConfig struct {
|
type WaapSourceConfig struct {
|
||||||
ListenAddr string `yaml:"listen_addr"`
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
|
@ -33,6 +41,7 @@ type WaapSourceConfig struct {
|
||||||
Routines int `yaml:"routines"`
|
Routines int `yaml:"routines"`
|
||||||
WaapConfig string `yaml:"waap_config"`
|
WaapConfig string `yaml:"waap_config"`
|
||||||
WaapConfigPath string `yaml:"waap_config_path"`
|
WaapConfigPath string `yaml:"waap_config_path"`
|
||||||
|
AuthCacheDuration *time.Duration `yaml:"auth_cache_duration"`
|
||||||
configuration.DataSourceCommonCfg `yaml:",inline"`
|
configuration.DataSourceCommonCfg `yaml:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,12 +55,38 @@ type WaapSource struct {
|
||||||
outChan chan types.Event
|
outChan chan types.Event
|
||||||
InChan chan waf.ParsedRequest
|
InChan chan waf.ParsedRequest
|
||||||
WaapRuntime *waf.WaapRuntimeConfig
|
WaapRuntime *waf.WaapRuntimeConfig
|
||||||
|
|
||||||
WaapConfigs map[string]waf.WaapConfig
|
WaapConfigs map[string]waf.WaapConfig
|
||||||
|
lapiURL string
|
||||||
|
AuthCache AuthCache
|
||||||
WaapRunners []WaapRunner //one for each go-routine
|
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
|
// @tko + @sbl : we might want to get rid of that or improve it
|
||||||
type BodyResponse struct {
|
type BodyResponse struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
|
@ -97,6 +132,11 @@ func (wc *WaapSource) UnmarshalConfig(yamlConfig []byte) error {
|
||||||
if wc.config.WaapConfig == "" && wc.config.WaapConfigPath == "" {
|
if wc.config.WaapConfig == "" && wc.config.WaapConfigPath == "" {
|
||||||
return fmt.Errorf("waap_config or waap_config_path must be set")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +158,11 @@ func (w *WaapSource) Configure(yamlConfig []byte, logger *log.Entry) error {
|
||||||
|
|
||||||
w.logger.Tracef("WAF configuration: %+v", w.config)
|
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.addr = fmt.Sprintf("%s:%d", w.config.ListenAddr, w.config.ListenPort)
|
||||||
|
|
||||||
w.mux = http.NewServeMux()
|
w.mux = http.NewServeMux()
|
||||||
|
@ -251,8 +296,44 @@ func (w *WaapSource) Dump() interface{} {
|
||||||
return w
|
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 ?
|
// should this be in the runner ?
|
||||||
func (w *WaapSource) waapHandler(rw http.ResponseWriter, r *http.Request) {
|
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
|
// parse the request only once
|
||||||
parsedRequest, err := waf.NewParsedRequestFromRequest(r)
|
parsedRequest, err := waf.NewParsedRequestFromRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -260,6 +341,7 @@ func (w *WaapSource) waapHandler(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(http.StatusInternalServerError)
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.InChan <- parsedRequest
|
w.InChan <- parsedRequest
|
||||||
|
|
||||||
response := <-parsedRequest.ResponseChannel
|
response := <-parsedRequest.ResponseChannel
|
||||||
|
|
|
@ -21,6 +21,8 @@ var defaultConfigDir = "/etc/crowdsec"
|
||||||
// defaultDataDir is the base path to all data files, to be overridden in the Makefile */
|
// defaultDataDir is the base path to all data files, to be overridden in the Makefile */
|
||||||
var defaultDataDir = "/var/lib/crowdsec/data/"
|
var defaultDataDir = "/var/lib/crowdsec/data/"
|
||||||
|
|
||||||
|
var globalConfig = Config{}
|
||||||
|
|
||||||
// Config contains top-level defaults -> overridden by configuration file -> overridden by CLI flags
|
// Config contains top-level defaults -> overridden by configuration file -> overridden by CLI flags
|
||||||
type Config struct {
|
type Config struct {
|
||||||
//just a path to ourselves :p
|
//just a path to ourselves :p
|
||||||
|
@ -89,9 +91,15 @@ func NewConfig(configFile string, disableAgent bool, disableAPI bool, quiet bool
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalConfig = cfg
|
||||||
|
|
||||||
return &cfg, configData, nil
|
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: 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: For this reason, all defaults have to come from NewConfig(). The following function should
|
||||||
// XXX: be replaced
|
// XXX: be replaced
|
||||||
|
|
|
@ -15,6 +15,7 @@ const (
|
||||||
VerbHeaderName = "X-Crowdsec-Waf-Verb"
|
VerbHeaderName = "X-Crowdsec-Waf-Verb"
|
||||||
HostHeaderName = "X-Crowdsec-Waf-Host"
|
HostHeaderName = "X-Crowdsec-Waf-Host"
|
||||||
IPHeaderName = "X-Crowdsec-Waf-Ip"
|
IPHeaderName = "X-Crowdsec-Waf-Ip"
|
||||||
|
APIKeyHeaderName = "X-Crowdsec-Waf-Api-Key"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type ResponseRequest struct {
|
// type ResponseRequest struct {
|
||||||
|
|
Loading…
Reference in a new issue