mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-21 11:38:59 +02:00
16 read ip address from x forwarded for or x real ip instead of remoteaddr if it exists (#17)
* Handle X forwarded for in the plugin * Update docker-compose.cloudflare.yml Change to version v1.0.9 * 🍱 fix + isolate cache * 🍱 missing files * 🍱 lint Co-authored-by: MathieuHa <mathieu@hanotaux.fr> Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
This commit is contained in:
co-authored by
MathieuHa
Max Lerebourg
parent
6dde683a0a
commit
552b30a9ef
@@ -25,6 +25,9 @@ run_dev:
|
|||||||
run_local:
|
run_local:
|
||||||
docker-compose -f docker-compose.local.yml up -d --remove-orphans
|
docker-compose -f docker-compose.local.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_behindproxy:
|
||||||
|
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
|
||||||
|
|
||||||
run:
|
run:
|
||||||
docker-compose -f docker-compose.yml up -d --remove-orphans
|
docker-compose -f docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
@@ -37,6 +40,11 @@ restart_docker_local:
|
|||||||
restart_docker:
|
restart_docker:
|
||||||
docker-compose -f docker-compose.yml restart
|
docker-compose -f docker-compose.yml restart
|
||||||
|
|
||||||
|
clean_all_docker:
|
||||||
|
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
|
||||||
|
docker-compose -f docker-compose.local.yml down --remove-orphans
|
||||||
|
docker-compose -f docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
show_metrics:
|
show_metrics:
|
||||||
docker exec crowdsec cscli metrics
|
docker exec crowdsec cscli metrics
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ At each start of synchronisation, the middleware will wait a random number of se
|
|||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
- Used only in `live` mode, decision duration of accepted IPs
|
- Used only in `live` mode, decision duration of accepted IPs
|
||||||
|
- ForwardedHeadersTrustedIPs
|
||||||
|
- []string
|
||||||
|
- default: []
|
||||||
|
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -116,6 +120,9 @@ http:
|
|||||||
crowdsecLapiKey: privateKey
|
crowdsecLapiKey: privateKey
|
||||||
crowdsecLapiHost: crowdsec:8080
|
crowdsecLapiHost: crowdsec:8080
|
||||||
crowdsecLapiScheme: http
|
crowdsecLapiScheme: http
|
||||||
|
forwardedHeadersTrustedIPs:
|
||||||
|
- 10.0.10.23/32
|
||||||
|
- 10.0.20.0/24
|
||||||
```
|
```
|
||||||
These are the default values of the plugin except for LapiKey.
|
These are the default values of the plugin except for LapiKey.
|
||||||
|
|
||||||
@@ -185,6 +192,33 @@ For local developpement a docker-compose.local.yml is provided and reproduce the
|
|||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.local.yml up -d
|
docker-compose -f docker-compose.local.yml up -d
|
||||||
```
|
```
|
||||||
|
Equivalent to
|
||||||
|
```bash
|
||||||
|
make run_local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemples
|
||||||
|
|
||||||
|
1. Behind another proxy service (ex: clouflare)
|
||||||
|
|
||||||
|
You need to configure your Traefik to trust Forwarded headers by your front proxy
|
||||||
|
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
|
||||||
|
```yaml
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
We configure the middleware to trust as well the IP:
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the environnement run:
|
||||||
|
```bash
|
||||||
|
make run_behind_proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### About
|
### About
|
||||||
|
|
||||||
|
|||||||
+70
-41
@@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
ttl_map "github.com/leprosus/golang-ttl-map"
|
ttl_map "github.com/leprosus/golang-ttl-map"
|
||||||
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/ip"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,7 +24,6 @@ const (
|
|||||||
liveMode = "live"
|
liveMode = "live"
|
||||||
noneMode = "none"
|
noneMode = "none"
|
||||||
crowdsecLapiHeader = "X-Api-Key"
|
crowdsecLapiHeader = "X-Api-Key"
|
||||||
crowdsecCapiHeader = "Authorization"
|
|
||||||
crowdsecLapiRoute = "v1/decisions"
|
crowdsecLapiRoute = "v1/decisions"
|
||||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||||
cacheBannedValue = "t"
|
cacheBannedValue = "t"
|
||||||
@@ -32,25 +32,27 @@ const (
|
|||||||
|
|
||||||
// Config the plugin configuration.
|
// Config the plugin configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
|
ForwardedHeadersTrustedIPs []string `json:"forwardedheaderstrustedips,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateConfig creates the default plugin configuration.
|
// CreateConfig creates the default plugin configuration.
|
||||||
func CreateConfig() *Config {
|
func CreateConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
CrowdsecMode: liveMode,
|
CrowdsecMode: liveMode,
|
||||||
CrowdsecLapiScheme: "http",
|
CrowdsecLapiScheme: "http",
|
||||||
CrowdsecLapiHost: "crowdsec:8080",
|
CrowdsecLapiHost: "crowdsec:8080",
|
||||||
CrowdsecLapiKey: "",
|
CrowdsecLapiKey: "",
|
||||||
UpdateIntervalSeconds: 60,
|
UpdateIntervalSeconds: 60,
|
||||||
DefaultDecisionSeconds: 60,
|
DefaultDecisionSeconds: 60,
|
||||||
|
ForwardedHeadersTrustedIPs: []string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ type Bouncer struct {
|
|||||||
crowdsecMode string
|
crowdsecMode string
|
||||||
updateInterval int64
|
updateInterval int64
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
|
poolStrategy *ip.PoolStrategy
|
||||||
client *http.Client
|
client *http.Client
|
||||||
cache *ttl_map.Heap
|
cache *ttl_map.Heap
|
||||||
}
|
}
|
||||||
@@ -79,6 +82,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||||
|
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
next: next,
|
next: next,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -92,6 +97,9 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
crowdsecKey: config.CrowdsecLapiKey,
|
crowdsecKey: config.CrowdsecLapiKey,
|
||||||
updateInterval: config.UpdateIntervalSeconds,
|
updateInterval: config.UpdateIntervalSeconds,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
|
poolStrategy: &ip.PoolStrategy{
|
||||||
|
Checker: checker,
|
||||||
|
},
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
@@ -124,16 +132,14 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Make sur remote address does not include the port.
|
remoteHost, err := getRemoteIP(bouncer, req)
|
||||||
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("failed to extract ip from remote address: %v", err))
|
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if bouncer.crowdsecMode == streamMode || bouncer.crowdsecMode == liveMode {
|
if bouncer.crowdsecMode != noneMode {
|
||||||
isBanned, err := getDecision(bouncer, remoteHost)
|
isBanned, err := getDecision(bouncer.cache, remoteHost)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isBanned {
|
if isBanned {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
@@ -177,13 +183,6 @@ type Stream struct {
|
|||||||
New []Decision `json:"new"`
|
New []Decision `json:"new"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login Body returned from Crowdsec Login CAPI.
|
|
||||||
type Login struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Expire string `json:"expire"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func logger(str string) {
|
func logger(str string) {
|
||||||
log.Printf("Crowdsec Bouncer Traefik Plugin - %s", str)
|
log.Printf("Crowdsec Bouncer Traefik Plugin - %s", str)
|
||||||
}
|
}
|
||||||
@@ -197,10 +196,24 @@ func contains(source []string, target string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It returns the first IP that is not in the pool, or the empty string otherwise.
|
||||||
|
func getRemoteIP(bouncer *Bouncer, req *http.Request) (string, error) {
|
||||||
|
remoteIP := bouncer.poolStrategy.GetIP(req)
|
||||||
|
if len(remoteIP) != 0 {
|
||||||
|
return remoteIP, nil
|
||||||
|
}
|
||||||
|
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger(fmt.Sprintf("failed to extract ip from remote address: %v", err))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return remoteIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get Decision check in the cache if the IP has the banned / not banned value.
|
// Get Decision 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 getDecision(bouncer *Bouncer, clientIP string) (bool, error) {
|
func getDecision(cache *ttl_map.Heap, clientIP string) (bool, error) {
|
||||||
banned, isCached := bouncer.cache.Get(clientIP)
|
banned, isCached := cache.Get(clientIP)
|
||||||
bannedString, isValid := banned.(string)
|
bannedString, isValid := banned.(string)
|
||||||
if isCached && isValid && len(bannedString) > 0 {
|
if isCached && isValid && len(bannedString) > 0 {
|
||||||
return bannedString == cacheBannedValue, nil
|
return bannedString == cacheBannedValue, nil
|
||||||
@@ -208,20 +221,21 @@ func getDecision(bouncer *Bouncer, clientIP string) (bool, error) {
|
|||||||
return false, fmt.Errorf("no cache data")
|
return false, fmt.Errorf("no cache data")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDecision(bouncer *Bouncer, clientIP string, isBanned bool, duration int64) {
|
func setDecision(cache *ttl_map.Heap, clientIP string, isBanned bool, duration int64) {
|
||||||
if bouncer.crowdsecMode == noneMode {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isBanned {
|
if isBanned {
|
||||||
logger(fmt.Sprintf("%v banned", clientIP))
|
logger(fmt.Sprintf("%v banned", clientIP))
|
||||||
bouncer.cache.Set(clientIP, cacheBannedValue, duration)
|
cache.Set(clientIP, cacheBannedValue, duration)
|
||||||
} else {
|
} else {
|
||||||
bouncer.cache.Set(clientIP, cacheNoBannedValue, duration)
|
cache.Set(clientIP, cacheNoBannedValue, duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteDecision(cache *ttl_map.Heap, clientIP string) {
|
||||||
|
cache.Del(clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are now in none or live mode.
|
||||||
func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
|
func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
|
||||||
// We are now in none or live mode.
|
|
||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
@@ -231,7 +245,9 @@ func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Req
|
|||||||
body := crowdsecQuery(bouncer, routeURL.String())
|
body := crowdsecQuery(bouncer, routeURL.String())
|
||||||
|
|
||||||
if bytes.Equal(body, []byte("null")) {
|
if bytes.Equal(body, []byte("null")) {
|
||||||
setDecision(bouncer, remoteHost, false, bouncer.defaultDecisionTimeout)
|
if bouncer.crowdsecMode == liveMode {
|
||||||
|
setDecision(bouncer.cache, remoteHost, false, bouncer.defaultDecisionTimeout)
|
||||||
|
}
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -244,7 +260,9 @@ func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(decisions) == 0 {
|
if len(decisions) == 0 {
|
||||||
setDecision(bouncer, remoteHost, false, bouncer.defaultDecisionTimeout)
|
if bouncer.crowdsecMode == liveMode {
|
||||||
|
setDecision(bouncer.cache, remoteHost, false, bouncer.defaultDecisionTimeout)
|
||||||
|
}
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -254,7 +272,9 @@ func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Req
|
|||||||
logger(fmt.Sprintf("failed to parse duration: %s", err))
|
logger(fmt.Sprintf("failed to parse duration: %s", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setDecision(bouncer, remoteHost, true, int64(duration.Seconds()))
|
if bouncer.crowdsecMode == liveMode {
|
||||||
|
setDecision(bouncer.cache, remoteHost, true, int64(duration.Seconds()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStreamCache(bouncer *Bouncer) {
|
func handleStreamCache(bouncer *Bouncer) {
|
||||||
@@ -276,11 +296,11 @@ func handleStreamCache(bouncer *Bouncer) {
|
|||||||
for _, decision := range stream.New {
|
for _, decision := range stream.New {
|
||||||
duration, err := time.ParseDuration(decision.Duration)
|
duration, err := time.ParseDuration(decision.Duration)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
setDecision(bouncer, decision.Value, true, int64(duration.Seconds()))
|
setDecision(bouncer.cache, decision.Value, true, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
bouncer.cache.Del(decision.Value)
|
deleteDecision(bouncer.cache, decision.Value)
|
||||||
}
|
}
|
||||||
bouncer.crowdsecStreamHealthy = true
|
bouncer.crowdsecStreamHealthy = true
|
||||||
}
|
}
|
||||||
@@ -350,5 +370,14 @@ func validateParams(config *Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||||
}
|
}
|
||||||
|
if len(config.ForwardedHeadersTrustedIPs) > 0 {
|
||||||
|
_, err = ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ForwardedHeadersTrustedIPs must be a list of IP/CIDR :%v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger("No IP provided for ForwardedHeadersTrustedIPs")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,6 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- logs-local:/var/log/traefik
|
- logs-local:/var/log/traefik
|
||||||
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapilogin=3829a6c9870e4726a377d8951ebb64a1m8psGvMaq1ykJ3zX"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapipwd=Q5pgN8bRNInHGdx6QCksdPOJVLeLQ7ipJntSeuP3r8088zXzRVs4G8liXAKfI1k6"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/http-backdoors-attempts,baudneo/zoneminder-bf"
|
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
@@ -36,30 +29,24 @@ services:
|
|||||||
container_name: "simple-service1"
|
container_name: "simple-service1"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# Definition of the router
|
|
||||||
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)"
|
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)"
|
||||||
- "traefik.http.routers.router1.entrypoints=web"
|
- "traefik.http.routers.router1.entrypoints=web"
|
||||||
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
|
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
|
||||||
# Definition of the service
|
|
||||||
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.26.0.1"
|
||||||
|
|
||||||
whoami2:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service2"
|
container_name: "simple-service2"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# Definition of the router
|
|
||||||
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)"
|
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)"
|
||||||
- "traefik.http.routers.router2.entrypoints=web"
|
- "traefik.http.routers.router2.entrypoints=web"
|
||||||
- "traefik.http.routers.router2.middlewares=crowdsec2@docker"
|
- "traefik.http.routers.router2.middlewares=crowdsec2@docker"
|
||||||
# Definition of the service
|
|
||||||
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
|
|||||||
+8
-5
@@ -5,7 +5,6 @@ services:
|
|||||||
image: "traefik:v2.8.8"
|
image: "traefik:v2.8.8"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
command:
|
command:
|
||||||
# - "--log.level=DEBUG"
|
|
||||||
- "--accesslog"
|
- "--accesslog"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
@@ -14,7 +13,7 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.0.6"
|
- "--experimental.plugins.bouncer.version=v1.0.9"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
- "logs:/var/log/traefik"
|
- "logs:/var/log/traefik"
|
||||||
@@ -35,10 +34,12 @@ services:
|
|||||||
- "traefik.http.routers.router1.middlewares=crowdsec2@docker"
|
- "traefik.http.routers.router1.middlewares=crowdsec2@docker"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definition of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
# crowdseclapikey must be unique to the middleware attached to the service
|
||||||
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
|
||||||
|
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
whoami2:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
@@ -53,8 +54,10 @@ services:
|
|||||||
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
- "traefik.http.services.service2.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definitin of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
# crowdseclapikey must be unique to the middleware attached to the service
|
||||||
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
|
||||||
|
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
http:
|
||||||
|
# Add the router
|
||||||
|
routers:
|
||||||
|
router0:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: service-foo
|
||||||
|
rule: Path(`/foo`)
|
||||||
|
|
||||||
|
# Add the service
|
||||||
|
services:
|
||||||
|
service-foo:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://traefik/foo:80
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
cloudflare:
|
||||||
|
image: "traefik:v2.8.8"
|
||||||
|
container_name: "cloudflare"
|
||||||
|
command:
|
||||||
|
# - "--log.level=DEBUG"
|
||||||
|
- "--accesslog"
|
||||||
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--providers.file.filename=/cloud.yaml"
|
||||||
|
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./cloudflare-exemple.yaml:/cloud.yaml:ro
|
||||||
|
- logs-cloudflare:/var/log/traefik
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 8080:8080
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v2.8.8"
|
||||||
|
container_name: "traefik"
|
||||||
|
command:
|
||||||
|
# - "--log.level=DEBUG"
|
||||||
|
- "--accesslog"
|
||||||
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
|
||||||
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
- "--experimental.plugins.bouncer.version=v1.0.9"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-dev:/var/log/traefik
|
||||||
|
ports:
|
||||||
|
- 90:80
|
||||||
|
- 9080:8080
|
||||||
|
depends_on:
|
||||||
|
- crowdsec
|
||||||
|
|
||||||
|
whoami1:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service1"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Definition of the router
|
||||||
|
- "traefik.http.routers.router1.rule=Path(`/foo`)"
|
||||||
|
- "traefik.http.routers.router1.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
|
||||||
|
# Definition of the service
|
||||||
|
- "traefik.http.services.service1.loadbalancer.server.port=80"
|
||||||
|
# Definitin of the middleware
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
|
||||||
|
# crowdseclapikey must be uniq to the middleware attached to the service
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdsecmode=live"
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
|
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
|
container_name: "crowdsec"
|
||||||
|
environment:
|
||||||
|
COLLECTIONS: crowdsecurity/traefik
|
||||||
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
|
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
|
||||||
|
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
|
||||||
|
volumes:
|
||||||
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
|
- logs-dev:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-dev:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-dev:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-dev:
|
||||||
|
logs-cloudflare:
|
||||||
|
crowdsec-db-dev:
|
||||||
|
crowdsec-config-dev:
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//CHECKER
|
||||||
|
|
||||||
|
// Checker allows to check that addresses are in a trusted IPs.
|
||||||
|
type Checker struct {
|
||||||
|
authorizedIPs []*net.IP
|
||||||
|
authorizedIPsNet []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
||||||
|
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ipAddr, err := net.ParseCIDR(ipMask)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
||||||
|
}
|
||||||
|
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks if provided address is in the trusted IPs.
|
||||||
|
func (ip *Checker) Contains(addr string) (bool, error) {
|
||||||
|
if len(addr) == 0 {
|
||||||
|
return false, errors.New("empty IP address")
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddr, err := parseIP(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to parse address: %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.ContainsIP(ipAddr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsIP checks if provided address is in the trusted IPs.
|
||||||
|
func (ip *Checker) ContainsIP(addr net.IP) bool {
|
||||||
|
for _, authorizedIP := range ip.authorizedIPs {
|
||||||
|
if authorizedIP.Equal(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, authorizedNet := range ip.authorizedIPsNet {
|
||||||
|
if authorizedNet.Contains(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIP(addr string) (net.IP, error) {
|
||||||
|
userIP := net.ParseIP(addr)
|
||||||
|
if userIP == nil {
|
||||||
|
return nil, fmt.Errorf("can't parse IP from address %s", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// STRATEGY
|
||||||
|
|
||||||
|
const (
|
||||||
|
xForwardedFor = "X-Forwarded-For"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PoolStrategy is a strategy based on an IP Checker.
|
||||||
|
// It allows to check whether addresses are in a given pool of IPs.
|
||||||
|
type PoolStrategy struct {
|
||||||
|
Checker *Checker
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIP checks the list of Forwarded IPs (most recent first) against the
|
||||||
|
// Checker pool of IPs. It returns the first IP that is not in the pool, or the
|
||||||
|
// empty string otherwise.
|
||||||
|
func (s *PoolStrategy) GetIP(req *http.Request) string {
|
||||||
|
if s.Checker == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
xff := req.Header.Get(xForwardedFor)
|
||||||
|
xffs := strings.Split(xff, ",")
|
||||||
|
|
||||||
|
for i := len(xffs) - 1; i >= 0; i-- {
|
||||||
|
xffTrimmed := strings.TrimSpace(xffs[i])
|
||||||
|
if len(xffTrimmed) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if contain, _ := s.Checker.Contains(xffTrimmed); !contain {
|
||||||
|
return xffTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user