add support for appsec in crowdsec (#123)

*  add support for appsec in crowdsec

* 🐛 lint

* 🐛 fix lint

* 🐛 fix lint

* 🐛 fix lint

* fix: comments

* 🐛 lint and doc

* 🐛 fix comment and lint

* 📝 Start documentation for appsec with exemple

* 📝 Fix readme typos and update example

* 🚨 Fix Lint

---------

Co-authored-by: Mathieu Hanotaux <mathieu@hanotaux.fr>
This commit is contained in:
maxlerebourg
2024-01-24 14:11:34 +01:00
committed by GitHub
co-authored by Mathieu Hanotaux
parent fc3da2fc2d
commit b68c692ed1
10 changed files with 312 additions and 55 deletions
+97 -19
View File
@@ -22,13 +22,19 @@ import (
)
const (
crowdsecLapiHeader = "X-Api-Key"
crowdsecCapiHeader = "Authorization"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiLogin = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip"
crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri"
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiHost = "api.crowdsec.net"
crowdsecCapiHeader = "Authorization"
crowdsecCapiLoginRoute = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
)
//nolint:gochecknoglobals
@@ -50,6 +56,9 @@ type Bouncer struct {
template *template.Template
enabled bool
appsecEnabled bool
appsecHost string
appsecFailureBlock bool
crowdsecScheme string
crowdsecHost string
crowdsecKey string
@@ -86,9 +95,9 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if config.CrowdsecMode == configuration.AloneMode {
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
config.CrowdsecLapiHost = "api.crowdsec.net"
config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https"
config.UpdateIntervalSeconds = 7200
config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
} else {
@@ -114,6 +123,9 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey,
@@ -212,7 +224,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isBanned {
rw.WriteHeader(http.StatusForbidden)
} else {
bouncer.next.ServeHTTP(rw, req)
handleNextServeHTTP(bouncer, remoteIP, rw, req)
}
return
}
@@ -221,7 +233,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Right here if we cannot join the stream we forbid the request to go on.
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req)
handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else {
logger.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden)
@@ -232,8 +244,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
} else {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
bouncer.next.ServeHTTP(rw, req)
handleNextServeHTTP(bouncer, remoteIP, rw, req)
}
}
}
@@ -266,6 +277,18 @@ type Login struct {
Expire string `json:"expire"`
}
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
if bouncer.appsecEnabled {
err := appsecQuery(bouncer, remoteIP, req)
if err != nil {
logger.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
return
}
}
bouncer.next.ServeHTTP(rw, req)
}
func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false
@@ -342,7 +365,7 @@ func getToken(bouncer *Bouncer) error {
loginURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecCapiLogin,
Path: crowdsecCapiLoginRoute,
}
body, err := crowdsecQuery(bouncer, loginURL.String(), true)
if err != nil {
@@ -423,6 +446,11 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
if err != nil {
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
}
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
if errToken := getToken(bouncer); errToken != nil {
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
@@ -432,11 +460,6 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
}
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}()
body, err := io.ReadAll(res.Body)
if err != nil {
@@ -444,3 +467,58 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
}
return body, nil
}
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.appsecHost,
Path: "/",
}
var req *http.Request
if httpReq.Body != nil && httpReq.ContentLength > 0 {
bodyBytes, err := io.ReadAll(httpReq.Body)
if err != nil {
return fmt.Errorf("appsecQuery:GetBody %w", err)
}
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
} else {
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
}
for key, headers := range httpReq.Header {
for _, value := range headers {
req.Header.Add(key, value)
}
}
req.Header.Set(crowdsecAppsecHeader, bouncer.crowdsecKey)
req.Header.Set(crowdsecAppsecIPHeader, ip)
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.Path)
res, err := bouncer.httpClient.Do(req)
if err != nil {
return fmt.Errorf("appsecQuery %w", err)
}
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
}
}()
if res.StatusCode == http.StatusInternalServerError {
logger.Debug("crowdsecQuery statusCode:500")
if bouncer.appsecFailureBlock {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
}
return nil
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
}
if err != nil {
return fmt.Errorf("appsecQuery:readBody %w", err)
}
return nil
}