mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
30
Commits
v0.1.5-alpha
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2569f26805 | ||
|
|
773f001239 | ||
|
|
6b46c63287 | ||
|
|
c8f59c2233 | ||
|
|
2110c70bbd | ||
|
|
00bbcfdb94 | ||
|
|
a6012015e6 | ||
|
|
feb40d9b5f | ||
|
|
6ab8ccb9db | ||
|
|
dd36df5147 | ||
|
|
ae959e4e0d | ||
|
|
7f5f69d134 | ||
|
|
1d7c5948f2 | ||
|
|
2559c71930 | ||
|
|
593681fd53 | ||
|
|
c7714cb793 | ||
|
|
75267a1746 | ||
|
|
45eca85f3a | ||
|
|
1d9bd3fab0 | ||
|
|
0aff6e1789 | ||
|
|
0abf1b7390 | ||
|
|
cb65e48a37 | ||
|
|
cba7c1231f | ||
|
|
da47ef320d | ||
|
|
bbbb5acac5 | ||
|
|
6cf89bfc15 | ||
|
|
f39317263f | ||
|
|
6dbd498212 | ||
|
|
7c2ac9830d | ||
|
|
b985f2d748 |
@@ -3,7 +3,7 @@ name: Main
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ iconPath: .assets/icon.png
|
|||||||
|
|
||||||
import: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
import: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
|
||||||
summary: 'Crowdsec Bouncer Traefik Plugin'
|
summary: Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request
|
||||||
|
|
||||||
testData:
|
testData:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
[](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/actions)
|

|
||||||
|

|
||||||
|

|
||||||
|
[](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/actions)
|
||||||
|
[](https://goreportcard.com/badge/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin)
|
||||||
|
|
||||||
# Crowdsec Bouncer Traefik plugin
|
# Crowdsec Bouncer Traefik plugin
|
||||||
|
|
||||||
@@ -6,7 +10,7 @@ This plugins aims to implement a Crowdsec Bouncer into a traefik plugin.
|
|||||||
> [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
|
> [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
|
||||||
> We leverage local behavior analysis and crowd power to build the largest CTI network in the world.
|
> We leverage local behavior analysis and crowd power to build the largest CTI network in the world.
|
||||||
|
|
||||||
The purpose is to enable treafik to authorize and block requests from IP based and their reputation and behavior.
|
The purpose is to enable treafik to authorize or block requests from IP based and their reputation and behavior.
|
||||||
|
|
||||||
The crowdsec utility will provide the community blocklist which contains highly reported and validated IP banned from the crowdsec network.
|
The crowdsec utility will provide the community blocklist which contains highly reported and validated IP banned from the crowdsec network.
|
||||||
|
|
||||||
@@ -16,7 +20,7 @@ There are 3 operating modes (CrowdsecMode) for this plugin:
|
|||||||
- none -> If the client IP is on ban list, it will get a http code 403 response.
|
- 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
|
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.
|
- live -> If the client IP is on ban list, it will get a http code 403 response.
|
||||||
Otherwise, request will continue as usual.
|
Otherwise, request will continue as usual.
|
||||||
The bouncer can leverage use of a local cache in order to reduce the number
|
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
|
of requests made to the Crowdsec LAPI. It will keep in cache the status for
|
||||||
@@ -137,6 +141,33 @@ You can then run all the containers:
|
|||||||
docker-compose -f docker-compose-local.yml up -d
|
docker-compose -f docker-compose-local.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Generate LAPI-KEY
|
||||||
|
You need to generate a crowdsec API key for the LAPI.
|
||||||
|
You can follow the documentation here: https://docs.crowdsec.net/docs/user_guides/lapi_mgmt/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose-local.yml up -d crowdsec
|
||||||
|
docker exec crowdsec cscli bouncers add crowdsecBouncer
|
||||||
|
```
|
||||||
|
|
||||||
|
This LApi key must be set where is noted FIXME-LAPI-KEY in the docker-compose-test.yml
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
whoami:
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
|
||||||
|
...
|
||||||
|
crowdsec:
|
||||||
|
environment:
|
||||||
|
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run all the containers:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose-local.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
#### Add manually an IP to the blocklist
|
#### Add manually an IP to the blocklist
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+107
-108
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -24,8 +25,7 @@ const (
|
|||||||
cacheNoBannedValue = "f"
|
cacheNoBannedValue = "f"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cache = ttl_map.New()
|
// 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"`
|
||||||
@@ -36,6 +36,7 @@ type Config struct {
|
|||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateConfig creates the default plugin configuration.
|
||||||
func CreateConfig() *Config {
|
func CreateConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -48,6 +49,7 @@ func CreateConfig() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bouncer a Bouncer plugin.
|
||||||
type Bouncer struct {
|
type Bouncer struct {
|
||||||
next http.Handler
|
next http.Handler
|
||||||
name string
|
name string
|
||||||
@@ -62,6 +64,7 @@ type Bouncer struct {
|
|||||||
updateInterval int64
|
updateInterval int64
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
cache *ttl_map.Heap
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
@@ -102,12 +105,12 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
|
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
|
||||||
return nil, fmt.Errorf("CrowdsecLapiScheme must be one of: http, https")
|
return nil, fmt.Errorf("CrowdsecLapiScheme must be one of: http, https")
|
||||||
}
|
}
|
||||||
testUrl := url.URL{
|
testURL := url.URL{
|
||||||
Scheme: config.CrowdsecLapiScheme,
|
Scheme: config.CrowdsecLapiScheme,
|
||||||
Host: config.CrowdsecLapiHost,
|
Host: config.CrowdsecLapiHost,
|
||||||
Path: crowdsecRoute,
|
Path: crowdsecRoute,
|
||||||
}
|
}
|
||||||
_, err := http.NewRequest(http.MethodGet, testUrl.String(), nil)
|
_, err := http.NewRequest(http.MethodGet, testURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
return nil, fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||||
}
|
}
|
||||||
@@ -132,23 +135,23 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
},
|
},
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
},
|
},
|
||||||
|
cache: ttl_map.New(),
|
||||||
}
|
}
|
||||||
// if we are on a stream mode, we fetch in a go routine every minute the new decisions
|
// if we are on a stream mode, we fetch in a go routine every minute the new decisions.
|
||||||
if config.CrowdsecMode == "stream" {
|
if config.CrowdsecMode == "stream" {
|
||||||
go handleStreamCache(bouncer, true)
|
go handleStreamCache(bouncer, true)
|
||||||
}
|
}
|
||||||
return bouncer, nil
|
return bouncer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the serve HTTP should be split as it's too long
|
// ServeHTTP principal function of plugin.
|
||||||
func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
if !a.enabled {
|
if !a.enabled {
|
||||||
log.Printf("Crowdsec Bouncer not enabled")
|
|
||||||
a.next.ServeHTTP(rw, req)
|
a.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Make sur remote address does not include the port
|
// TODO Make sur remote address does not include the port.
|
||||||
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to extract ip from remote address: %v", err)
|
log.Printf("failed to extract ip from remote address: %v", err)
|
||||||
@@ -157,7 +160,7 @@ func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if a.crowdsecMode == "stream" || a.crowdsecMode == "live" {
|
if a.crowdsecMode == "stream" || a.crowdsecMode == "live" {
|
||||||
isBanned, err := getDecision(remoteHost)
|
isBanned, err := getDecision(a.cache, remoteHost)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isBanned {
|
if isBanned {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
@@ -168,78 +171,24 @@ func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right here if we cannot join the stream we forbid the request to go on
|
// Right here if we cannot join the stream we forbid the request to go on.
|
||||||
if a.crowdsecMode == "stream" {
|
if a.crowdsecMode == "stream" {
|
||||||
if a.crowdsecStreamHealthy {
|
if a.crowdsecStreamHealthy {
|
||||||
a.next.ServeHTTP(rw, req)
|
a.next.ServeHTTP(rw, req)
|
||||||
} else {
|
} else {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
return
|
} else {
|
||||||
|
handleNoStreamCache(a, rw, req, remoteHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are now in none or live mode
|
|
||||||
noneUrl := url.URL{
|
|
||||||
Scheme: a.crowdsecScheme,
|
|
||||||
Host: a.crowdsecHost,
|
|
||||||
Path: crowdsecRoute,
|
|
||||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost),
|
|
||||||
}
|
|
||||||
request, _ := http.NewRequest(http.MethodGet, noneUrl.String(), nil)
|
|
||||||
request.Header.Add(crowdsecAuthHeader, a.crowdsecKey)
|
|
||||||
res, err := a.client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to get decision: %s", err)
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
log.Printf("failed to get decision, status code: %d", res.StatusCode)
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to read body: %s", err)
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !bytes.Equal(body, []byte("null")) {
|
|
||||||
var decisions []Decision
|
|
||||||
err = json.Unmarshal(body, &decisions)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to parse body: %s", err)
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(decisions) == 0 {
|
|
||||||
if a.crowdsecMode == "live" {
|
|
||||||
setDecision(remoteHost, false, a.defaultDecisionTimeout)
|
|
||||||
}
|
|
||||||
a.next.ServeHTTP(rw, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
duration, err := time.ParseDuration(decisions[0].Duration)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to parse duration: %s", err)
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
setDecision(remoteHost, true, int64(duration.Seconds()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if a.crowdsecMode == "live" {
|
|
||||||
setDecision(remoteHost, false, a.defaultDecisionTimeout)
|
|
||||||
}
|
|
||||||
a.next.ServeHTTP(rw, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CUSTOM CODE
|
// CUSTOM CODE.
|
||||||
// TODO place in another file
|
// TODO place in another file.
|
||||||
|
|
||||||
|
// Decision Body returned from Crowdsec LAPI.
|
||||||
type Decision struct {
|
type Decision struct {
|
||||||
Id int `json:"id"`
|
ID int `json:"id"`
|
||||||
Origin string `json:"origin"`
|
Origin string `json:"origin"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
@@ -249,6 +198,7 @@ type Decision struct {
|
|||||||
Simulated bool `json:"simulated"`
|
Simulated bool `json:"simulated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stream Body returned from Crowdsec Stream LAPI.
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
Deleted []Decision `json:"deleted"`
|
Deleted []Decision `json:"deleted"`
|
||||||
New []Decision `json:"new"`
|
New []Decision `json:"new"`
|
||||||
@@ -263,21 +213,18 @@ func contains(source []string, target string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(clientIP string) (bool, error) {
|
func getDecision(cache *ttl_map.Heap, clientIP string) (bool, error) {
|
||||||
isBanned, ok := cache.Get(clientIP)
|
banned, isCached := cache.Get(clientIP)
|
||||||
if ok && len(isBanned.(string)) > 0 {
|
bannedString, isValid := banned.(string)
|
||||||
if isBanned == cacheNoBannedValue {
|
if isCached && isValid && len(bannedString) > 0 {
|
||||||
return false, nil
|
return bannedString == cacheBannedValue, nil
|
||||||
} else {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("no data")
|
return false, fmt.Errorf("no cache data")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDecision(clientIP string, isBanned bool, duration int64) {
|
func setDecision(cache *ttl_map.Heap, clientIP string, isBanned bool, duration int64) {
|
||||||
if isBanned {
|
if isBanned {
|
||||||
cache.Set(clientIP, cacheBannedValue, duration)
|
cache.Set(clientIP, cacheBannedValue, duration)
|
||||||
} else {
|
} else {
|
||||||
@@ -285,39 +232,61 @@ func setDecision(clientIP string, isBanned bool, duration int64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleNoStreamCache(a *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
|
||||||
|
// We are now in none or live mode.
|
||||||
|
routeURL := url.URL{
|
||||||
|
Scheme: a.crowdsecScheme,
|
||||||
|
Host: a.crowdsecHost,
|
||||||
|
Path: crowdsecRoute,
|
||||||
|
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost),
|
||||||
|
}
|
||||||
|
body := crowdsecQuery(a, routeURL.String())
|
||||||
|
|
||||||
|
if bytes.Equal(body, []byte("null")) {
|
||||||
|
if a.crowdsecMode == "live" {
|
||||||
|
setDecision(a.cache, remoteHost, false, a.defaultDecisionTimeout)
|
||||||
|
}
|
||||||
|
a.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var decisions []Decision
|
||||||
|
err := json.Unmarshal(body, &decisions)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to parse body: %s", err)
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(decisions) == 0 {
|
||||||
|
if a.crowdsecMode == "live" {
|
||||||
|
setDecision(a.cache, remoteHost, false, a.defaultDecisionTimeout)
|
||||||
|
}
|
||||||
|
a.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
duration, err := time.ParseDuration(decisions[0].Duration)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to parse duration: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDecision(a.cache, remoteHost, true, int64(duration.Seconds()))
|
||||||
|
}
|
||||||
|
|
||||||
func handleStreamCache(a *Bouncer, initialized bool) {
|
func handleStreamCache(a *Bouncer, initialized bool) {
|
||||||
// TODO clean properly on exit
|
// TODO clean properly on exit.
|
||||||
time.AfterFunc(time.Duration(a.updateInterval)*time.Second, func() {
|
time.AfterFunc(time.Duration(a.updateInterval)*time.Second, func() {
|
||||||
handleStreamCache(a, false)
|
handleStreamCache(a, false)
|
||||||
})
|
})
|
||||||
streamUrl := url.URL{
|
streamRouteURL := url.URL{
|
||||||
Scheme: a.crowdsecScheme,
|
Scheme: a.crowdsecScheme,
|
||||||
Host: a.crowdsecHost,
|
Host: a.crowdsecHost,
|
||||||
Path: crowdsecStreamRoute,
|
Path: crowdsecStreamRoute,
|
||||||
RawQuery: fmt.Sprintf("startup=%t", initialized),
|
RawQuery: fmt.Sprintf("startup=%t", initialized),
|
||||||
}
|
}
|
||||||
req, _ := http.NewRequest(http.MethodGet, streamUrl.String(), nil)
|
body := crowdsecQuery(a, streamRouteURL.String())
|
||||||
req.Header.Add(crowdsecAuthHeader, a.crowdsecKey)
|
|
||||||
res, err := a.client.Do(req)
|
|
||||||
if err != nil || res.StatusCode == http.StatusForbidden {
|
|
||||||
log.Printf("error while fetching decisions: %s", err)
|
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if res.StatusCode == http.StatusForbidden {
|
|
||||||
log.Printf("error while fetching decisions, status code: %d", res.StatusCode)
|
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error while reading body: %s", err)
|
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var stream Stream
|
var stream Stream
|
||||||
err = json.Unmarshal(body, &stream)
|
err := json.Unmarshal(body, &stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error while parsing body: %s", err)
|
log.Printf("error while parsing body: %s", err)
|
||||||
a.crowdsecStreamHealthy = false
|
a.crowdsecStreamHealthy = false
|
||||||
@@ -326,11 +295,41 @@ func handleStreamCache(a *Bouncer, initialized bool) {
|
|||||||
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(decision.Value, true, int64(duration.Seconds()))
|
setDecision(a.cache, decision.Value, true, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
cache.Del(decision.Value)
|
a.cache.Del(decision.Value)
|
||||||
}
|
}
|
||||||
a.crowdsecStreamHealthy = true
|
a.crowdsecStreamHealthy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func crowdsecQuery(a *Bouncer, stringURL string) ([]byte) {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, stringURL, nil)
|
||||||
|
req.Header.Add(crowdsecAuthHeader, a.crowdsecKey)
|
||||||
|
res, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while fetching %v: %s", stringURL, err)
|
||||||
|
a.crowdsecStreamHealthy = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if res.StatusCode == http.StatusForbidden {
|
||||||
|
log.Printf("error while fetching %v, status code: %d", stringURL, res.StatusCode)
|
||||||
|
a.crowdsecStreamHealthy = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func (body io.ReadCloser) {
|
||||||
|
err = body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to close body reader: %s", err)
|
||||||
|
}
|
||||||
|
}(res.Body)
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while reading body: %s", err)
|
||||||
|
a.crowdsecStreamHealthy = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v2.8.7"
|
||||||
|
container_name: "traefik"
|
||||||
|
command:
|
||||||
|
# - "--log.level=DEBUG"
|
||||||
|
- "--accesslog"
|
||||||
|
- "--accesslog.filepath=/var/log/traefik/traefik.log"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
|
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
|
- "logs:/var/log/traefik"
|
||||||
|
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- 'crowdsec'
|
||||||
|
|
||||||
|
whoami:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service"
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
|
||||||
|
- "traefik.http.routers.whoami.entrypoints=web"
|
||||||
|
- "traefik.http.routers.whoami.middlewares=crowdsec@docker"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
|
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
|
container_name: "crowdsec"
|
||||||
|
command: rm -rf /etc/crowdsec/acquis.yaml
|
||||||
|
environment:
|
||||||
|
COLLECTIONS: crowdsecurity/traefik
|
||||||
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
|
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5
|
||||||
|
volumes:
|
||||||
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
|
- logs:/var/log/traefik:ro
|
||||||
|
- crowdsec-db:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config:/etc/crowdsec/
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs:
|
||||||
|
crowdsec-db:
|
||||||
|
crowdsec-config:
|
||||||
+2
-2
@@ -20,6 +20,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8000:80
|
- 8000:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- 'crowdsec'
|
||||||
|
|
||||||
whoami:
|
whoami:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
@@ -40,8 +42,6 @@ services:
|
|||||||
COLLECTIONS: crowdsecurity/traefik
|
COLLECTIONS: crowdsecurity/traefik
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
||||||
depends_on:
|
|
||||||
- 'traefik'
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
- logs:/var/log/traefik:ro
|
- logs:/var/log/traefik:ro
|
||||||
|
|||||||
Reference in New Issue
Block a user