diff --git a/Makefile b/Makefile index 1906caf..949ec86 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ run_cacheredis: docker-compose -f exemples/redis-cache/docker-compose.redis.yml up -d --remove-orphans run_trustedips: - docker-compose -f exemples/trusted-ips/docker-compose.redis.yml up -d --remove-orphans + docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans run: docker-compose -f docker-compose.yml up -d --remove-orphans @@ -46,6 +46,15 @@ restart_local: restart: docker-compose -f docker-compose.yml restart +restart_behindproxy: + docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml restart + +restart_cacheredis: + docker-compose -f exemples/redis-cache/docker-compose.redis.yml restart + +restart_trustedips: + docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml restart + show_logs: docker-compose -f docker-compose.yml restart diff --git a/README.md b/README.md index 26bbdcf..d0b83c4 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,10 @@ make run - string - default: "redis:6379" - hostname and port for the redis service -- TrustedIPs +- ClientTrustedIPs - string - default: [] - - List of IPs we trust, they will bypass any more check to the bouncer or cache (usefull for LAN or VPN IP) + - List of client IPs we trust, they will bypass any more check to the bouncer or cache (usefull for LAN or VPN IP) ### Configuration @@ -142,7 +142,7 @@ http: forwardedHeadersTrustedIPs: - 10.0.10.23/32 - 10.0.20.0/24 - trustedIPs: + clientTrustedIPs: - 192.168.1.0/24 forwardedHeadersCustomName: X-Custom-Header redisCacheEnabled: false @@ -229,6 +229,7 @@ You need to configure your Traefik to trust Forwarded headers by your front prox In the exemple we use another instance of traefik with the container named cloudflare to simulate a front proxy The "internal" Traefik instance is configured to trust the cloudflare forward headers +This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers ```yaml - "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5" ``` @@ -261,12 +262,14 @@ make run_cacheredis You need to configure your Traefik to trust Forwarded headers by your front proxy In the exemple we use a whoami container protected by crowdsec, and we ban or IP before allowing using TrustedIPs +If you are using another proxy in front, you need to add it's IP in the trusted IP for the forwarded headers. +This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers The "internal" Traefik instance is configured to trust the forward headers ```yaml - "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5" ``` -We configure the middleware to trust as well the IP: +We configure the middleware to trust as well the IP of the intermediate proxy if needed: ```yaml - "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5" ``` @@ -279,14 +282,14 @@ You should get a 403 on http://localhost/foo > Replace *10.0.10.30* by your IP -And the IPS that will not be filtered by the plugin +Add the IPs that will not be filtered by the plugin ```yaml - - "traefik.http.middlewares.crowdsec.plugin.bouncer.trustedips=10.0.10.30/32" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.clientTrustedips=10.0.10.30/32" ``` > Replace *10.0.10.30/32* by your IP or IP range, so it's not getting checked against ban cache of crowdsec -You should get a 200 on http://localhost/foo +You should get a 200 on http://localhost/foo even if you are on the ban cache To play the demo environnement run: ```bash diff --git a/bouncer.go b/bouncer.go index 8dd985d..a2187c1 100644 --- a/bouncer.go +++ b/bouncer.go @@ -45,7 +45,7 @@ type Config struct { DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"` ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"` - TrustedIPs []string `json:"TrustedIps,omitempty"` + ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"` RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"` RedisCacheHost string `json:"redisCacheHost,omitempty"` } @@ -54,7 +54,7 @@ type Config struct { func CreateConfig() *Config { return &Config{ Enabled: false, - LogLevel: "INFO", + LogLevel: "DEBUG", CrowdsecMode: liveMode, CrowdsecLapiScheme: "http", CrowdsecLapiHost: "crowdsec:8080", @@ -62,7 +62,7 @@ func CreateConfig() *Config { UpdateIntervalSeconds: 60, DefaultDecisionSeconds: 60, ForwardedHeadersTrustedIPs: []string{}, - TrustedIPs: []string{}, + ClientTrustedIPs: []string{}, ForwardedHeadersCustomName: "X-Forwarded-For", RedisCacheEnabled: false, RedisCacheHost: "redis:6379", @@ -83,9 +83,8 @@ type Bouncer struct { updateInterval int64 defaultDecisionTimeout int64 customHeader string - poolStrategy *ip.PoolStrategy - poolStrategyTrusted *ip.PoolStrategy - CheckerTrusted *ip.Checker + serverPoolStrategy *ip.PoolStrategy + clientPoolStrategy *ip.PoolStrategy client *http.Client } @@ -98,16 +97,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h return nil, err } - checker, err := ip.NewChecker(config.ForwardedHeadersTrustedIPs) - if err != nil { - logger.Debug("Checker == nil") - logger.Debug(err.Error()) - } - checkerTrusted, err := ip.NewChecker(config.TrustedIPs) - if err != nil { - logger.Debug("CheckerTrusted == nil") - logger.Debug(err.Error()) - } + serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs) + clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs) bouncer := &Bouncer{ next: next, @@ -122,13 +113,12 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h updateInterval: config.UpdateIntervalSeconds, customHeader: config.ForwardedHeadersCustomName, defaultDecisionTimeout: config.DefaultDecisionSeconds, - poolStrategy: &ip.PoolStrategy{ - Checker: checker, + serverPoolStrategy: &ip.PoolStrategy{ + Checker: serverChecker, }, - poolStrategyTrusted: &ip.PoolStrategy{ - Checker: checkerTrusted, + clientPoolStrategy: &ip.PoolStrategy{ + Checker: clientChecker, }, - CheckerTrusted: checkerTrusted, client: &http.Client{ Transport: &http.Transport{ MaxIdleConns: 10, @@ -157,36 +147,24 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } // Here we check for the trusted IPs in the customHeader - remoteHost, err := ip.GetRemoteIP(req, bouncer.poolStrategyTrusted, bouncer.customHeader) + remoteHost, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader) if err != nil { logger.Info(err.Error()) bouncer.next.ServeHTTP(rw, req) return } - if bouncer.CheckerTrusted == nil { - logger.Debug("CheckerTrusted == nil") - } else { - trusted, err := bouncer.CheckerTrusted.Contains(remoteHost) - if err != nil { - logger.Info(err.Error()) - return - } - // if our IP is in the trusted list we bypass the next checks - if trusted { - logger.Debug(fmt.Sprintf("IP %v is trusted", remoteHost)) - bouncer.next.ServeHTTP(rw, req) - return - } - } - // Here we get the IP from the trusted header customHeader - remoteHost, err = ip.GetRemoteIP(req, bouncer.poolStrategy, bouncer.customHeader) + trusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteHost) if err != nil { logger.Info(err.Error()) + return + } + // if our IP is in the trusted list we bypass the next checks + logger.Debug(fmt.Sprintf("ServeHTTP ip:%v isTrusted:%v", remoteHost, trusted)) + if trusted { bouncer.next.ServeHTTP(rw, req) return } - logger.Debug(fmt.Sprintf("ServeHTTP ip:%v", remoteHost)) healthy := crowdsecStreamHealthy if bouncer.crowdsecMode != noneMode { @@ -196,7 +174,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if err.Error() == simpleredis.RedisUnreachable { healthy = false } - } else { + } else { logger.Debug(fmt.Sprintf("ServeHTTP cache:hit isBanned:%v", isBanned)) if isBanned { rw.WriteHeader(http.StatusForbidden) @@ -423,8 +401,8 @@ func validateParams(config *Config) error { } else { logger.Debug("No IP provided for ForwardedHeadersTrustedIPs") } - if len(config.TrustedIPs) > 0 { - _, err = ip.NewChecker(config.TrustedIPs) + if len(config.ClientTrustedIPs) > 0 { + _, err = ip.NewChecker(config.ClientTrustedIPs) if err != nil { return fmt.Errorf("TrustedIPs must be a list of IP/CIDR :%w", err) } diff --git a/exemples/behind-proxy/docker-compose.cloudflare.yml b/exemples/behind-proxy/docker-compose.cloudflare.yml index 03c81c0..a69fb25 100644 --- a/exemples/behind-proxy/docker-compose.cloudflare.yml +++ b/exemples/behind-proxy/docker-compose.cloudflare.yml @@ -2,7 +2,7 @@ version: "3.8" services: cloudflare: - image: "traefik:v2.9.1" + image: "traefik:v2.9.4" container_name: "cloudflare" command: # - "--log.level=DEBUG" @@ -21,7 +21,7 @@ services: - 8080:8080 traefik: - image: "traefik:v2.9.1" + image: "traefik:v2.9.4" container_name: "traefik" command: # - "--log.level=DEBUG" diff --git a/exemples/trusted-ips/docker-compose.redis.yml b/exemples/trusted-ips/docker-compose.trusted.yml similarity index 96% rename from exemples/trusted-ips/docker-compose.redis.yml rename to exemples/trusted-ips/docker-compose.trusted.yml index ffc99ed..21baad5 100644 --- a/exemples/trusted-ips/docker-compose.redis.yml +++ b/exemples/trusted-ips/docker-compose.trusted.yml @@ -42,7 +42,7 @@ services: # crowdseclapikey must be uniq to the middleware attached to the service - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" # Replace 10.0.10.30/32 by your IP range which is "trusted" - - "traefik.http.middlewares.crowdsec.plugin.bouncer.trustedips=10.0.10.30/32" + - "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.50.30/32" crowdsec: diff --git a/pkg/ip/ip.go b/pkg/ip/ip.go index 69914c7..c5631b7 100644 --- a/pkg/ip/ip.go +++ b/pkg/ip/ip.go @@ -20,10 +20,6 @@ type Checker struct { // NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs. func NewChecker(trustedIPs []string) (*Checker, error) { - if len(trustedIPs) == 0 { - return nil, errors.New("no trusted IPs provided") - } - checker := &Checker{} for _, ipMask := range trustedIPs {