mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
575d3a02e5 | ||
|
|
6c183d9231 | ||
|
|
bd71b58f19 |
@@ -36,7 +36,8 @@ There are 4 operating modes (CrowdsecMode) for this plugin:
|
|||||||
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
||||||
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
|
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
|
||||||
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
|
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
|
||||||
| alone | Standalone mode, similar to the streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI. It does not include any localy banned IP, but can work without a crowdsec service. |
|
| alone | Standalone mode, similar to the streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI. It does not include any locally banned IP, but can work without a crowdsec service. |
|
||||||
|
| appsec | Disable Crowdsec IP checking but apply Crowdsec Appsec checking. This mode is intended to be used when Crowdsec IP checking is applied at the Firewall Level. |
|
||||||
|
|
||||||
The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.
|
The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ Only one instance of the plugin is *possible*.
|
|||||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
||||||
- CrowdsecMode
|
- CrowdsecMode
|
||||||
- string
|
- string
|
||||||
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`
|
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
|
||||||
- CrowdsecAppsecEnabled
|
- CrowdsecAppsecEnabled
|
||||||
- bool
|
- bool
|
||||||
- default: false
|
- default: false
|
||||||
|
|||||||
+40
-29
@@ -75,19 +75,20 @@ type Bouncer struct {
|
|||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
cacheClient *cache.Client
|
cacheClient *cache.Client
|
||||||
|
log *logger.Log
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
||||||
logger.Init(config.LogLevel)
|
log := logger.New(config.LogLevel)
|
||||||
err := configuration.ValidateParams(config)
|
err := configuration.ValidateParams(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
|
log.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
|
||||||
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
crowdsecStreamRoute := ""
|
crowdsecStreamRoute := ""
|
||||||
@@ -103,14 +104,14 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
} else {
|
} else {
|
||||||
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
||||||
crowdsecHeader = crowdsecLapiHeader
|
crowdsecHeader = crowdsecLapiHeader
|
||||||
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config)
|
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
log.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||||
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
|
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
|
||||||
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
|
log.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
config.CrowdsecLapiKey = apiKey
|
config.CrowdsecLapiKey = apiKey
|
||||||
@@ -137,6 +138,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
crowdsecHeader: crowdsecHeader,
|
crowdsecHeader: crowdsecHeader,
|
||||||
|
log: log,
|
||||||
serverPoolStrategy: &ip.PoolStrategy{
|
serverPoolStrategy: &ip.PoolStrategy{
|
||||||
Checker: serverChecker,
|
Checker: serverChecker,
|
||||||
},
|
},
|
||||||
@@ -153,8 +155,12 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
},
|
},
|
||||||
cacheClient: &cache.Client{},
|
cacheClient: &cache.Client{},
|
||||||
}
|
}
|
||||||
|
if config.CrowdsecMode == configuration.AppsecMode {
|
||||||
|
return bouncer, nil
|
||||||
|
}
|
||||||
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
|
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
|
||||||
bouncer.cacheClient.New(
|
bouncer.cacheClient.Init(
|
||||||
|
log,
|
||||||
config.RedisCacheEnabled,
|
config.RedisCacheEnabled,
|
||||||
config.RedisCacheHost,
|
config.RedisCacheHost,
|
||||||
config.RedisCachePassword,
|
config.RedisCachePassword,
|
||||||
@@ -164,17 +170,17 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
||||||
if config.CrowdsecMode == configuration.AloneMode {
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
if err := getToken(bouncer); err != nil {
|
if err := getToken(bouncer); err != nil {
|
||||||
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleStreamTicker(bouncer)
|
handleStreamTicker(bouncer)
|
||||||
isStartup = false
|
isStartup = false
|
||||||
ticker = startTicker(config, func() {
|
ticker = startTicker(config, log, func() {
|
||||||
handleStreamTicker(bouncer)
|
handleStreamTicker(bouncer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
|
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
|
||||||
|
|
||||||
return bouncer, nil
|
return bouncer, nil
|
||||||
}
|
}
|
||||||
@@ -191,36 +197,41 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
// Here we check for the trusted IPs in the customHeader
|
// Here we check for the trusted IPs in the customHeader
|
||||||
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
|
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if our IP is in the trusted list we bypass the next checks
|
// if our IP is in the trusted list we bypass the next checks
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
|
||||||
if isTrusted {
|
if isTrusted {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bouncer.crowdsecMode == configuration.AppsecMode {
|
||||||
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// TODO This should be simplified
|
// TODO This should be simplified
|
||||||
if bouncer.crowdsecMode != configuration.NoneMode {
|
if bouncer.crowdsecMode != configuration.NoneMode {
|
||||||
isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP)
|
isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP)
|
||||||
if cacheErr != nil {
|
if cacheErr != nil {
|
||||||
errString := cacheErr.Error()
|
errString := cacheErr.Error()
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
|
||||||
if errString != cache.CacheMiss {
|
if errString != cache.CacheMiss {
|
||||||
logger.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
|
||||||
if isBanned {
|
if isBanned {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
} else {
|
} else {
|
||||||
@@ -235,13 +246,13 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
if isCrowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
} else {
|
} else {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = handleNoStreamCache(bouncer, remoteIP)
|
err = handleNoStreamCache(bouncer, remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
} else {
|
} else {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
@@ -281,7 +292,7 @@ func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWrit
|
|||||||
if bouncer.appsecEnabled {
|
if bouncer.appsecEnabled {
|
||||||
err := appsecQuery(bouncer, remoteIP, req)
|
err := appsecQuery(bouncer, remoteIP, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -292,17 +303,17 @@ func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWrit
|
|||||||
func handleStreamTicker(bouncer *Bouncer) {
|
func handleStreamTicker(bouncer *Bouncer) {
|
||||||
if err := handleStreamCache(bouncer); err != nil {
|
if err := handleStreamCache(bouncer); err != nil {
|
||||||
isCrowdsecStreamHealthy = false
|
isCrowdsecStreamHealthy = false
|
||||||
logger.Error(err.Error())
|
bouncer.log.Error(err.Error())
|
||||||
} else {
|
} else {
|
||||||
isCrowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTicker(config *configuration.Config, work func()) chan bool {
|
func startTicker(config *configuration.Config, log *logger.Log, work func()) chan bool {
|
||||||
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
||||||
stop := make(chan bool, 1)
|
stop := make(chan bool, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer logger.Debug("ticker:stopped")
|
defer log.Debug("ticker:stopped")
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -379,7 +390,7 @@ func getToken(bouncer *Bouncer) error {
|
|||||||
}
|
}
|
||||||
if login.Code == 200 && len(login.Token) > 0 {
|
if login.Code == 200 && len(login.Token) > 0 {
|
||||||
bouncer.crowdsecKey = login.Token
|
bouncer.crowdsecKey = login.Token
|
||||||
logger.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
|
bouncer.log.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("getToken statusCode:%d", login.Code)
|
return fmt.Errorf("getToken statusCode:%d", login.Code)
|
||||||
@@ -392,7 +403,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
// because updated routine information is in the cache
|
// because updated routine information is in the cache
|
||||||
_, err := bouncer.cacheClient.GetDecision(cacheTimeoutKey)
|
_, err := bouncer.cacheClient.GetDecision(cacheTimeoutKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Debug("handleStreamCache:alreadyUpdated")
|
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err.Error() != cache.CacheMiss {
|
if err.Error() != cache.CacheMiss {
|
||||||
@@ -423,7 +434,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
bouncer.cacheClient.DeleteDecision(decision.Value)
|
bouncer.cacheClient.DeleteDecision(decision.Value)
|
||||||
}
|
}
|
||||||
logger.Debug("handleStreamCache:updated")
|
bouncer.log.Debug("handleStreamCache:updated")
|
||||||
isCrowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -448,7 +459,7 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err = res.Body.Close(); err != nil {
|
if err = res.Body.Close(); err != nil {
|
||||||
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
|
bouncer.log.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
@@ -503,11 +514,11 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err = res.Body.Close(); err != nil {
|
if err = res.Body.Close(); err != nil {
|
||||||
logger.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
|
bouncer.log.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if res.StatusCode == http.StatusInternalServerError {
|
if res.StatusCode == http.StatusInternalServerError {
|
||||||
logger.Debug("crowdsecQuery statusCode:500")
|
bouncer.log.Debug("crowdsecQuery statusCode:500")
|
||||||
if bouncer.appsecFailureBlock {
|
if bouncer.appsecFailureBlock {
|
||||||
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
|
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+23
-19
@@ -44,7 +44,9 @@ func (localCache) deleteDecision(clientIP string) {
|
|||||||
cache.Del(clientIP)
|
cache.Del(clientIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
type redisCache struct{}
|
type redisCache struct {
|
||||||
|
log *logger.Log
|
||||||
|
}
|
||||||
|
|
||||||
func (redisCache) getDecision(clientIP string) (bool, error) {
|
func (redisCache) getDecision(clientIP string) (bool, error) {
|
||||||
banned, err := redis.Get(clientIP)
|
banned, err := redis.Get(clientIP)
|
||||||
@@ -58,15 +60,15 @@ func (redisCache) getDecision(clientIP string) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (redisCache) setDecision(clientIP string, value string, duration int64) {
|
func (rc redisCache) setDecision(clientIP string, value string, duration int64) {
|
||||||
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
||||||
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (redisCache) deleteDecision(clientIP string) {
|
func (rc redisCache) deleteDecision(clientIP string) {
|
||||||
if err := redis.Del(clientIP); err != nil {
|
if err := redis.Del(clientIP); err != nil {
|
||||||
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,40 +81,42 @@ type cacheInterface interface {
|
|||||||
// Client Cache client.
|
// Client Cache client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
cache cacheInterface
|
cache cacheInterface
|
||||||
|
log *logger.Log
|
||||||
}
|
}
|
||||||
|
|
||||||
// New Initialize cache client.
|
// Init Initialize cache client.
|
||||||
func (client *Client) New(isRedis bool, host, pass, database string) {
|
func (c *Client) Init(log *logger.Log, isRedis bool, host, pass, database string) {
|
||||||
|
c.log = log
|
||||||
if isRedis {
|
if isRedis {
|
||||||
redis.Init(host, pass, database)
|
redis.Init(host, pass, database)
|
||||||
client.cache = &redisCache{}
|
c.cache = &redisCache{log: log}
|
||||||
} else {
|
} else {
|
||||||
client.cache = &localCache{}
|
c.cache = &localCache{}
|
||||||
}
|
}
|
||||||
logger.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
|
c.log.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDecision delete decision in cache.
|
// DeleteDecision delete decision in cache.
|
||||||
func (client *Client) DeleteDecision(clientIP string) {
|
func (c *Client) DeleteDecision(clientIP string) {
|
||||||
logger.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
|
c.log.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
|
||||||
client.cache.deleteDecision(clientIP)
|
c.cache.deleteDecision(clientIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDecision check in the cache if the IP has the banned / not banned value.
|
// GetDecision check in the cache if the IP has the banned / not banned value.
|
||||||
// Otherwise return with an error to add the IP in cache if we are on.
|
// Otherwise return with an error to add the IP in cache if we are on.
|
||||||
func (client *Client) GetDecision(clientIP string) (bool, error) {
|
func (c *Client) GetDecision(clientIP string) (bool, error) {
|
||||||
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
|
c.log.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
|
||||||
return client.cache.getDecision(clientIP)
|
return c.cache.getDecision(clientIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
||||||
func (client *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
|
func (c *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
|
||||||
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
|
c.log.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
|
||||||
var value string
|
var value string
|
||||||
if isBanned {
|
if isBanned {
|
||||||
value = cacheBannedValue
|
value = cacheBannedValue
|
||||||
} else {
|
} else {
|
||||||
value = cacheNoBannedValue
|
value = cacheNoBannedValue
|
||||||
}
|
}
|
||||||
client.cache.setDecision(clientIP, value, duration)
|
c.cache.setDecision(clientIP, value, duration)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
-3
@@ -4,12 +4,14 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_GetDecision(t *testing.T) {
|
func Test_GetDecision(t *testing.T) {
|
||||||
IPInCache := "10.0.0.10"
|
IPInCache := "10.0.0.10"
|
||||||
IPNotInCache := "10.0.0.20"
|
IPNotInCache := "10.0.0.20"
|
||||||
client := &Client{cache: &localCache{}}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
||||||
client.SetDecision(IPInCache, true, 10)
|
client.SetDecision(IPInCache, true, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
@@ -45,7 +47,7 @@ func Test_GetDecision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_SetDecision(t *testing.T) {
|
func Test_SetDecision(t *testing.T) {
|
||||||
client := &Client{cache: &localCache{}}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
||||||
IPInCache := "10.0.0.11"
|
IPInCache := "10.0.0.11"
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
@@ -76,7 +78,7 @@ func Test_SetDecision(t *testing.T) {
|
|||||||
func Test_DeleteDecision(t *testing.T) {
|
func Test_DeleteDecision(t *testing.T) {
|
||||||
IPInCache := "10.0.0.12"
|
IPInCache := "10.0.0.12"
|
||||||
IPNotInCache := "10.0.0.22"
|
IPNotInCache := "10.0.0.22"
|
||||||
client := &Client{cache: &localCache{}}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
||||||
client.SetDecision(IPInCache, true, 10)
|
client.SetDecision(IPInCache, true, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const (
|
|||||||
StreamMode = "stream"
|
StreamMode = "stream"
|
||||||
LiveMode = "live"
|
LiveMode = "live"
|
||||||
NoneMode = "none"
|
NoneMode = "none"
|
||||||
|
AppsecMode = "appsec"
|
||||||
HTTPS = "https"
|
HTTPS = "https"
|
||||||
HTTP = "http"
|
HTTP = "http"
|
||||||
)
|
)
|
||||||
@@ -54,7 +55,7 @@ type Config struct {
|
|||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
||||||
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomHeader,omitempty"`
|
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
||||||
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||||
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||||
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
||||||
@@ -234,11 +235,9 @@ func validateParamsTLS(config *Config) error {
|
|||||||
|
|
||||||
func validateParamsIPs(listIP []string, key string) error {
|
func validateParamsIPs(listIP []string, key string) error {
|
||||||
if len(listIP) > 0 {
|
if len(listIP) > 0 {
|
||||||
if _, err := ip.NewChecker(listIP); err != nil {
|
if _, err := ip.NewChecker(logger.New("INFO"), listIP); err != nil {
|
||||||
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.Debug(fmt.Sprintf("No IP provided for %s", key))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -264,8 +263,8 @@ func validateParamsRequired(config *Config) error {
|
|||||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) {
|
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
|
||||||
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'")
|
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
|
||||||
}
|
}
|
||||||
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
||||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||||
@@ -274,16 +273,18 @@ func validateParamsRequired(config *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTLSConfigCrowdsec get TLS config from Config.
|
// GetTLSConfigCrowdsec get TLS config from Config.
|
||||||
func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
|
//
|
||||||
|
//nolint:nestif
|
||||||
|
func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error) {
|
||||||
tlsConfig := new(tls.Config)
|
tlsConfig := new(tls.Config)
|
||||||
tlsConfig.RootCAs = x509.NewCertPool()
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
//nolint:gocritic
|
//nolint:gocritic
|
||||||
if config.CrowdsecLapiScheme != HTTPS {
|
if config.CrowdsecLapiScheme != HTTPS {
|
||||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
|
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
|
||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
} else if config.CrowdsecLapiTLSInsecureVerify {
|
} else if config.CrowdsecLapiTLSInsecureVerify {
|
||||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
|
|
||||||
tlsConfig.InsecureSkipVerify = true
|
tlsConfig.InsecureSkipVerify = true
|
||||||
|
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
|
||||||
// If we return here and still want to use client auth this won't work
|
// If we return here and still want to use client auth this won't work
|
||||||
// return tlsConfig, nil
|
// return tlsConfig, nil
|
||||||
} else {
|
} else {
|
||||||
@@ -291,13 +292,14 @@ func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cert := []byte(certAuthority)
|
if len(certAuthority) > 0 {
|
||||||
if !tlsConfig.RootCAs.AppendCertsFromPEM(cert) {
|
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
|
||||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority read cert failed")
|
|
||||||
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
||||||
// and CA not load, we can't communicate with https
|
// and CA not load, we can't communicate with https
|
||||||
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
||||||
}
|
}
|
||||||
|
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getMinimalConfig() *Config {
|
func getMinimalConfig() *Config {
|
||||||
@@ -147,7 +149,7 @@ func Test_validateParamsIPs(t *testing.T) {
|
|||||||
{name: "Not validate localhost", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
|
{name: "Not validate localhost", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
|
||||||
{name: "Not validate a weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
|
{name: "Not validate a weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
|
||||||
{name: "Not validate a weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
|
{name: "Not validate a weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
|
||||||
{name: "Validate an ip not trimed", args: args{listIP: []string{0: " 0.0.0.0/0"}}, wantErr: false},
|
{name: "Validate an ip not trimmed", args: args{listIP: []string{0: " 0.0.0.0/0"}}, wantErr: false},
|
||||||
{name: "Validate an ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
|
{name: "Validate an ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
|
||||||
{name: "Validate an ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
|
{name: "Validate an ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
|
||||||
}
|
}
|
||||||
@@ -231,7 +233,7 @@ func Test_GetTLSConfigCrowdsec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := GetTLSConfigCrowdsec(tt.args.config)
|
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO"))
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|||||||
+4
-4
@@ -8,7 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CHECKER
|
// CHECKER
|
||||||
@@ -20,14 +20,14 @@ type Checker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
|
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
|
||||||
func NewChecker(trustedIPs []string) (*Checker, error) {
|
func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
|
||||||
checker := &Checker{}
|
checker := &Checker{}
|
||||||
|
|
||||||
for _, ipMaskRaw := range trustedIPs {
|
for _, ipMaskRaw := range trustedIPs {
|
||||||
ipMask := strings.TrimSpace(ipMaskRaw)
|
ipMask := strings.TrimSpace(ipMaskRaw)
|
||||||
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
||||||
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
||||||
logger.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
|
log.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func NewChecker(trustedIPs []string) (*Checker, error) {
|
|||||||
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
||||||
}
|
}
|
||||||
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
||||||
logger.Debug(fmt.Sprintf("IP network %v is trusted", ipAddr))
|
log.Debug(fmt.Sprintf("IP network %v is trusted", ipAddr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return checker, nil
|
return checker, nil
|
||||||
|
|||||||
+25
-16
@@ -8,32 +8,41 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// Log Logger struct.
|
||||||
loggerInfo = log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
|
type Log struct {
|
||||||
loggerDebug = log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
|
logError *log.Logger
|
||||||
loggerError = log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
|
logInfo *log.Logger
|
||||||
)
|
logDebug *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
// Init Set Default log level to info in case log level to defined.
|
// New Set Default log level to info in case log level to defined.
|
||||||
func Init(logLevel string) {
|
func New(logLevel string) *Log {
|
||||||
loggerError.SetOutput(os.Stderr)
|
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
loggerInfo.SetOutput(os.Stdout)
|
logInfo := log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
|
logDebug := log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
|
logError.SetOutput(os.Stderr)
|
||||||
|
logInfo.SetOutput(os.Stdout)
|
||||||
if logLevel == "DEBUG" {
|
if logLevel == "DEBUG" {
|
||||||
loggerDebug.SetOutput(os.Stdout)
|
logDebug.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
return &Log{
|
||||||
|
logError: logError,
|
||||||
|
logInfo: logInfo,
|
||||||
|
logDebug: logDebug,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info log to Stdout.
|
// Info log to Stdout.
|
||||||
func Info(str string) {
|
func (l *Log) Info(str string) {
|
||||||
loggerInfo.Printf(str)
|
l.logInfo.Printf(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug log to Stdout.
|
// Debug log to Stdout.
|
||||||
func Debug(str string) {
|
func (l *Log) Debug(str string) {
|
||||||
loggerDebug.Printf(str)
|
l.logDebug.Printf(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error log to Stderr.
|
// Error log to Stderr.
|
||||||
func Error(str string) {
|
func (l *Log) Error(str string) {
|
||||||
loggerError.Printf(str)
|
l.logError.Printf(str)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user