mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4132445a79 | ||
|
|
5b15d6fdc4 | ||
|
|
1b310b2c15 | ||
|
|
87a839f22d | ||
|
|
f14a4e3a68 | ||
|
|
11b615b9bd | ||
|
|
8f937d22a2 |
@@ -1,15 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
title: "[BUG] My short bug title"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
**Describe the bug** 🐛
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior** 👀
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Context** 🔎
|
||||
Before posting, make sure to use the `logLevel: DEBUG` of the plugin and see if you can fix the error yourself.
|
||||
If not, post your plugin configuration file (anonimize IP and secret key) and a partial logs file where the error is encountered.
|
||||
|
||||
**Version (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu, Debian, Docker]
|
||||
- Traefik version: [e.g. 2.9.*, 3.0.*]
|
||||
- Plugin version: [e.g. 1.1.*]
|
||||
- Redis ? : [e.g. 7.0.*]
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
@@ -17,22 +30,4 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
title: "[FEATURE] My short feature title"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
**Is your feature request related to a problem? Please describe.** 🐛
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
**Describe the solution you'd like** ✨
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
|
||||
|
||||
@@ -6,29 +6,30 @@
|
||||
|
||||
# Crowdsec Bouncer Traefik plugin
|
||||
|
||||
This plugin aims to implement a Crowdsec Bouncer in a traefik plugin.
|
||||
This plugin aims to implement a Crowdsec Bouncer in a Traefik plugin.
|
||||
|
||||
> [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.
|
||||
|
||||
The purpose is to enable treafik to authorize or block requests from IPs based on their reputation and behavior.
|
||||
The purpose is to enable Traefik to authorize or block requests from IPs based on their reputation and behavior.
|
||||
|
||||
The crowdsec utility will provide the community blocklist which contains highly reported and validated IPs banned from the crowdsec network.
|
||||
The Crowdsec utility will provide the community blocklist which contains highly reported and validated IPs banned from the Crowdsec network.
|
||||
|
||||
When used with crowdsec it will leverage the local API which will analyze traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns against your website.
|
||||
When used with Crowdsec it will leverage the local API which will analyze Traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns used against your website.
|
||||
|
||||
There are 3 operating modes (CrowdsecMode) for this plugin:
|
||||
There are 4 operating modes (CrowdsecMode) for this plugin:
|
||||
|
||||
| Mode | Description |
|
||||
|------|------|
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
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.
|
||||
|
||||
The cache can be local to Traefik using the filesystem, or a separate redis instance.
|
||||
Support for Redis is currently in beta (requires version 7.0.X).
|
||||
The cache can be local to Traefik using the filesystem, or a separate Redis instance.
|
||||
Support for Redis is currently in beta (requires version 7.0.X of Redis).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -41,9 +42,8 @@ make run
|
||||
|
||||
### Note
|
||||
|
||||
**/!\ Since Release 1.1.0, the cache is no longer duplicated but shared by all services**
|
||||
*This lowers the overhead of the cache in memory and the numbers of cache to fetch it from crowdsec in situations with many services*
|
||||
|
||||
**/!\ Cache is shared by all services**
|
||||
*This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP*
|
||||
|
||||
### Variables
|
||||
- Enabled
|
||||
@@ -55,7 +55,7 @@ make run
|
||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
||||
- CrowdsecMode
|
||||
- string
|
||||
- default: `live`, expected values are: `none`, `live`, `stream`
|
||||
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`
|
||||
- CrowdsecLapiScheme
|
||||
- string
|
||||
- default: `http`, expected values are: `http`, `https`
|
||||
@@ -83,14 +83,6 @@ make run
|
||||
- string
|
||||
- default: ""
|
||||
- PEM-encoded client private key of the Bouncer
|
||||
- UpdateIntervalSeconds
|
||||
- int64
|
||||
- default: 60
|
||||
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
||||
- DefaultDecisionSeconds
|
||||
- int64
|
||||
- default: 60
|
||||
- Used only in `live` mode, decision duration of accepted IPs
|
||||
- ClientTrustedIPs
|
||||
- string
|
||||
- default: []
|
||||
@@ -106,11 +98,28 @@ make run
|
||||
- RedisCacheEnabled
|
||||
- bool
|
||||
- default: false
|
||||
- enable redis cache instead of filesystem cache
|
||||
- enable Redis cache instead of filesystem cache
|
||||
- RedisCacheHost
|
||||
- string
|
||||
- default: "redis:6379"
|
||||
- hostname and port for the redis service
|
||||
- hostname and port for the Redis service
|
||||
- UpdateIntervalSeconds
|
||||
- int64
|
||||
- default: 60
|
||||
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
||||
- DefaultDecisionSeconds
|
||||
- int64
|
||||
- default: 60
|
||||
- Used only in `live` mode, maximum decision duration
|
||||
- CrowdsecCapiMachineId
|
||||
- string
|
||||
- Used only in `alone` mode, login for Crowdsec CAPI
|
||||
- CrowdsecCapiPassword
|
||||
- string
|
||||
- Used only in `alone` mode, password for Crowdsec CAPI
|
||||
- CrowdsecCapiScenarios
|
||||
- []string
|
||||
- Used only in `alone` mode, scenarios for Crowdsec CAPI
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -133,7 +142,7 @@ experimental:
|
||||
http:
|
||||
routers:
|
||||
my-router:
|
||||
rule: host(`woami.localhost`)
|
||||
rule: host(`whoami.localhost`)
|
||||
service: service-foo
|
||||
entryPoints:
|
||||
- web
|
||||
@@ -160,6 +169,12 @@ http:
|
||||
crowdsecLapiHost: crowdsec:8080
|
||||
crowdsecLapiScheme: http
|
||||
crowdsecLapiTLSInsecureVerify: false
|
||||
crowdsecCapiMachineId: login
|
||||
crowdsecCapiPassword: password
|
||||
crowdsecCapiScenarios:
|
||||
- crowdsecurity/http-path-traversal-probing
|
||||
- crowdsecurity/http-xss-probing
|
||||
- crowdsecurity/http-generic-bf
|
||||
forwardedHeadersTrustedIPs:
|
||||
- 10.0.10.23/32
|
||||
- 10.0.20.0/24
|
||||
@@ -190,12 +205,11 @@ http:
|
||||
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
||||
|
||||
```
|
||||
|
||||
#### Fill variable with value of file
|
||||
|
||||
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority` and `CrowdsecLapiKey` can be provided with the content as raw or through a file path that Traefik can read.
|
||||
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword` and `CrowdsecLapiKey` can be provided with the content as raw or through a file path that Traefik can read.
|
||||
The file variable will be used as preference if both content and file are provided for the same variable.
|
||||
|
||||
Format is:
|
||||
@@ -209,7 +223,7 @@ Please see below for more details on each option.
|
||||
|
||||
#### Generate LAPI KEY
|
||||
You can generate a crowdsec API key for the LAPI.
|
||||
You can follow the documentation here: https://docs.crowdsec.net/docs/user_guides/lapi_mgmt/
|
||||
You can follow the documentation here: [docs.crowdsec.net/docs/user_guides/lapi_mgmt](https://docs.crowdsec.net/docs/user_guides/lapi_mgmt)
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose-local.yml up -d crowdsec
|
||||
@@ -242,7 +256,7 @@ docker-compose up -d
|
||||
#### Use certificates to authenticate with CrowdSec
|
||||
|
||||
You can follow the example in `exemples/tls-auth` to view how to authenticate with client certificates with the LAPI.
|
||||
In that case communications with the LAPI must go through HTTPS.
|
||||
In that case, communications with the LAPI must go through HTTPS.
|
||||
|
||||
A script is available to generate certificates in `exemples/tls-auth/gencerts.sh` and must be in the same directory as the inputs for the PKI creation.
|
||||
|
||||
@@ -252,7 +266,7 @@ To communicate with the LAPI in HTTPS you need to either accept any certificates
|
||||
Set the `crowdsecLapiScheme` to https.
|
||||
|
||||
Crowdsec must be listening in HTTPS for this to work.
|
||||
Please see the tls-auth exemple or the official documentation: [https://docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
|
||||
Please see the [tls-auth exemple](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md) or the official documentation: [docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
|
||||
|
||||
#### Manually add an IP to the blocklist (for testing purposes)
|
||||
|
||||
@@ -262,6 +276,22 @@ docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m # this will be ef
|
||||
docker exec crowdsec cscli decisions remove --ip 10.0.0.10
|
||||
```
|
||||
|
||||
### Exemples
|
||||
|
||||
#### 1. Behind another proxy service (ex: clouflare) [exemples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/behind-proxy/README.md)
|
||||
|
||||
#### 2. With Redis as an external shared cache [exemples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/redis-cache/README.md)
|
||||
|
||||
#### 3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec [exemples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/trusted-ips/README.md)
|
||||
|
||||
#### 4. Using Crowdsec and Traefik installed as binary in a single VM [exemples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/binary-vm/README.md)
|
||||
|
||||
#### 5. Using https communication and tls authentication with Crowdsec [exemples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md)
|
||||
|
||||
#### 6. Using Crowdsec and Traefik in Kubernetes [exemples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/kubernetes/README.md)
|
||||
|
||||
#### 7. Using Traefik in standalone mode without Crowdsec [exemples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/standalone-mode/README.md)
|
||||
|
||||
### Local Mode
|
||||
|
||||
Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
|
||||
@@ -286,7 +316,8 @@ The source code of the plugin should be organized as follows:
|
||||
└── vendor/*
|
||||
```
|
||||
|
||||
For local development, a docker-compose.local.yml is provided which reproduces the directory layout needed by traefik. This works once you have generated and filled your LAPI-KEY (crowdsecLapiKey), if not look below for information
|
||||
For local development, a `docker-compose.local.yml` is provided which reproduces the directory layout needed by Traefik.
|
||||
This works once you have generated and filled your *LAPI-KEY* (crowdsecLapiKey), if not read above for informations.
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
@@ -296,39 +327,13 @@ Equivalent to
|
||||
make run_local
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
1. Behind another proxy service (ex: clouflare)
|
||||
|
||||
A complete exemple is available in [exemples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/behind-proxy/README.md)
|
||||
|
||||
2. With Redis as an external shared cache
|
||||
|
||||
A complete exemple is available in [exemples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/redis-cache/README.md)
|
||||
|
||||
3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
|
||||
|
||||
A complete exemple is available in [exemples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/trusted-ips/README.md)
|
||||
|
||||
4. Using Crowdsec and Traefik installed as binary in a single VM
|
||||
|
||||
A complete exemple is available in [exemples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/binary-vm/README.md)
|
||||
|
||||
5. Using https communication and tls authentication with Crowdsec
|
||||
|
||||
A complete exemple is available in [exemples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md)
|
||||
|
||||
6. Using Crowdsec and Traefik in Kubernetes
|
||||
|
||||
A complete exemple is available in [exemples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/kubernetes/README.md)
|
||||
|
||||
### About
|
||||
|
||||
Me and [mathieuHa](https://github.com/mathieuHa) have been using traefik since 2020 at [Primadviz](https://primadviz.com).
|
||||
[mathieuHa](https://github.com/mathieuHa) and [I](https://github.com/maxlerebourg) have been using Traefik since 2020 at [Primadviz](https://primadviz.com).
|
||||
We come from a web development and security engineer background and wanted to add the power of a very promising technology (Crowdsec) to the edge router we love.
|
||||
|
||||
We initially ran into this project: https://github.com/fbonalair/traefik-crowdsec-bouncer
|
||||
We initially ran into this project: [github.com/fbonalair/traefik-crowdsec-bouncer](https://github.com/fbonalair/traefik-crowdsec-bouncer)
|
||||
It was using traefik and forward auth middleware to verify every request.
|
||||
They had to go through a webserver which then contacts another webservice (the crowdsec LAPI) to make a decision based on the source IP.
|
||||
We initially proposed some improvements by implementing a streaming mode and a local cache.
|
||||
With the Traefik hackathon we decided to implement our solution directly as a traefik plugin which could be found by everyone on plugins.traefik.io and be more performant.
|
||||
With the Traefik hackathon we decided to implement our solution directly as a Traefik plugin which could be found by everyone on [plugins.traefik.io](https://plugins.traefik.io) and be more performant.
|
||||
|
||||
+131
-39
@@ -5,6 +5,7 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -14,23 +15,27 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
simpleredis "github.com/maxlerebourg/simpleredis"
|
||||
|
||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
|
||||
)
|
||||
|
||||
const (
|
||||
crowdsecLapiHeader = "X-Api-Key"
|
||||
crowdsecCapiHeader = "Authorization"
|
||||
crowdsecLapiRoute = "v1/decisions"
|
||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||
crowdsecCapiLogin = "v2/watchers/login"
|
||||
crowdsecCapiStreamRoute = "v2/decisions/stream"
|
||||
cacheTimeoutKey = "updated"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
isCrowdsecStreamHealthy = false
|
||||
isCrowdsecStreamHealthy = true
|
||||
ticker chan bool
|
||||
)
|
||||
|
||||
@@ -50,12 +55,18 @@ type Bouncer struct {
|
||||
crowdsecHost string
|
||||
crowdsecKey string
|
||||
crowdsecMode string
|
||||
crowdsecMachineID string
|
||||
crowdsecPassword string
|
||||
crowdsecScenarios []string
|
||||
updateInterval int64
|
||||
defaultDecisionTimeout int64
|
||||
customHeader string
|
||||
crowdsecStreamRoute string
|
||||
crowdsecHeader string
|
||||
clientPoolStrategy *ip.PoolStrategy
|
||||
serverPoolStrategy *ip.PoolStrategy
|
||||
client *http.Client
|
||||
httpClient *http.Client
|
||||
cacheClient *cache.Client
|
||||
}
|
||||
|
||||
// New creates the crowdsec bouncer plugin.
|
||||
@@ -63,24 +74,39 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
logger.Init(config.LogLevel)
|
||||
err := configuration.ValidateParams(config)
|
||||
if err != nil {
|
||||
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||
logger.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
||||
|
||||
tlsConfig, err := configuration.GetTLSConfigCrowdsec(config)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||
return nil, err
|
||||
var tlsConfig *tls.Config
|
||||
crowdsecStreamRoute := ""
|
||||
crowdsecHeader := ""
|
||||
if config.CrowdsecMode == configuration.AloneMode {
|
||||
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineId")
|
||||
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
|
||||
config.CrowdsecLapiHost = "api.crowdsec.net"
|
||||
config.CrowdsecLapiScheme = "https"
|
||||
config.UpdateIntervalSeconds = 7200
|
||||
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
||||
crowdsecHeader = crowdsecCapiHeader
|
||||
} else {
|
||||
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
||||
crowdsecHeader = crowdsecLapiHeader
|
||||
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||
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()))
|
||||
return nil, err
|
||||
}
|
||||
config.CrowdsecLapiKey = apiKey
|
||||
}
|
||||
apiKey, err := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||
if err != nil && len(tlsConfig.Certificates) == 0 {
|
||||
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
apiKey = strings.TrimSuffix(apiKey, "\n")
|
||||
|
||||
bouncer := &Bouncer{
|
||||
next: next,
|
||||
@@ -91,17 +117,22 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
crowdsecMode: config.CrowdsecMode,
|
||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||
crowdsecHost: config.CrowdsecLapiHost,
|
||||
crowdsecKey: apiKey,
|
||||
crowdsecKey: config.CrowdsecLapiKey,
|
||||
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
||||
crowdsecPassword: config.CrowdsecCapiPassword,
|
||||
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
||||
updateInterval: config.UpdateIntervalSeconds,
|
||||
customHeader: config.ForwardedHeadersCustomName,
|
||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||
crowdsecHeader: crowdsecHeader,
|
||||
serverPoolStrategy: &ip.PoolStrategy{
|
||||
Checker: serverChecker,
|
||||
},
|
||||
clientPoolStrategy: &ip.PoolStrategy{
|
||||
Checker: clientChecker,
|
||||
},
|
||||
client: &http.Client{
|
||||
httpClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
@@ -109,16 +140,24 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
cacheClient: &cache.Client{},
|
||||
}
|
||||
if config.RedisCacheEnabled {
|
||||
cache.InitRedisClient(config.RedisCacheHost)
|
||||
}
|
||||
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
|
||||
bouncer.cacheClient.New(config.RedisCacheEnabled, config.RedisCacheHost)
|
||||
|
||||
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
||||
if config.CrowdsecMode == configuration.AloneMode {
|
||||
err = getToken(bouncer)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ticker = startTicker(config, func() {
|
||||
handleStreamCache(bouncer)
|
||||
})
|
||||
go handleStreamCache(bouncer)
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
|
||||
|
||||
return bouncer, nil
|
||||
}
|
||||
@@ -154,9 +193,9 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// TODO This should be simplified
|
||||
if bouncer.crowdsecMode != configuration.NoneMode {
|
||||
isBanned, erro := cache.GetDecision(remoteIP)
|
||||
isBanned, erro := bouncer.cacheClient.GetDecision(remoteIP)
|
||||
if erro != nil {
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:true %s", remoteIP, erro.Error()))
|
||||
if erro.Error() == simpleredis.RedisUnreachable {
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
@@ -173,19 +212,20 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// Right here if we cannot join the stream we forbid the request to go on.
|
||||
if bouncer.crowdsecMode == configuration.StreamMode {
|
||||
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
||||
if isCrowdsecStreamHealthy {
|
||||
bouncer.next.ServeHTTP(rw, req)
|
||||
} else {
|
||||
logger.Error(fmt.Sprintf("ServeHTTP:isCrowdsecStreamHealthy ip:%s", remoteIP))
|
||||
logger.Error(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
} else {
|
||||
err = handleNoStreamCache(bouncer, remoteIP)
|
||||
if err != nil {
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s %s", remoteIP, err.Error()))
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
|
||||
bouncer.next.ServeHTTP(rw, req)
|
||||
}
|
||||
}
|
||||
@@ -212,6 +252,13 @@ type Stream struct {
|
||||
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 startTicker(config *configuration.Config, work func()) chan bool {
|
||||
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
||||
stop := make(chan bool, 1)
|
||||
@@ -238,14 +285,14 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
||||
Path: crowdsecLapiRoute,
|
||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
||||
}
|
||||
body, err := crowdsecQuery(bouncer, routeURL.String())
|
||||
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bytes.Equal(body, []byte("null")) {
|
||||
if isLiveMode {
|
||||
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -257,7 +304,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
||||
}
|
||||
if len(decisions) == 0 {
|
||||
if isLiveMode {
|
||||
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -266,29 +313,57 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
||||
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
||||
}
|
||||
if isLiveMode {
|
||||
cache.SetDecision(remoteIP, true, int64(duration.Seconds()))
|
||||
durationSecond := int64(duration.Seconds())
|
||||
if bouncer.defaultDecisionTimeout < durationSecond {
|
||||
durationSecond = bouncer.defaultDecisionTimeout
|
||||
}
|
||||
bouncer.cacheClient.SetDecision(remoteIP, true, durationSecond)
|
||||
}
|
||||
return fmt.Errorf("handleNoStreamCache:banned")
|
||||
}
|
||||
|
||||
func getToken(bouncer *Bouncer) error {
|
||||
loginURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
Host: bouncer.crowdsecHost,
|
||||
Path: crowdsecCapiLogin,
|
||||
}
|
||||
body, err := crowdsecQuery(bouncer, loginURL.String(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var login Login
|
||||
err = json.Unmarshal(body, &login)
|
||||
if err != nil {
|
||||
isCrowdsecStreamHealthy = false
|
||||
return fmt.Errorf("getToken:parsingBody %w", err)
|
||||
}
|
||||
if login.Code == 200 && len(login.Token) > 0 {
|
||||
bouncer.crowdsecKey = login.Token
|
||||
logger.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("getToken statusCode:%d", login.Code)
|
||||
}
|
||||
|
||||
func handleStreamCache(bouncer *Bouncer) {
|
||||
// TODO clean properly on exit.
|
||||
// Instead of blocking the goroutine interval for all the secondary node,
|
||||
// if the master service is shut down, other goroutine can take the lead
|
||||
// because updated routine information is in the cache
|
||||
_, err := cache.GetDecision(cacheTimeoutKey)
|
||||
_, err := bouncer.cacheClient.GetDecision(cacheTimeoutKey)
|
||||
if err == nil {
|
||||
logger.Debug("handleStreamCache:alreadyUpdated")
|
||||
return
|
||||
}
|
||||
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
||||
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
||||
streamRouteURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
Host: bouncer.crowdsecHost,
|
||||
Path: crowdsecLapiStreamRoute,
|
||||
Path: bouncer.crowdsecStreamRoute,
|
||||
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
|
||||
}
|
||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
|
||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
isCrowdsecStreamHealthy = false
|
||||
@@ -304,23 +379,40 @@ func handleStreamCache(bouncer *Bouncer) {
|
||||
for _, decision := range stream.New {
|
||||
duration, err := time.ParseDuration(decision.Duration)
|
||||
if err == nil {
|
||||
cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
||||
bouncer.cacheClient.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
||||
}
|
||||
}
|
||||
for _, decision := range stream.Deleted {
|
||||
cache.DeleteDecision(decision.Value)
|
||||
bouncer.cacheClient.DeleteDecision(decision.Value)
|
||||
}
|
||||
logger.Debug("handleStreamCache:updated")
|
||||
isCrowdsecStreamHealthy = true
|
||||
}
|
||||
|
||||
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
|
||||
func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, error) {
|
||||
var req *http.Request
|
||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||
req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey)
|
||||
res, err := bouncer.client.Do(req)
|
||||
if isPost {
|
||||
data := []byte(fmt.Sprintf(
|
||||
`{"machine_id": "%v","password": "%v","scenarios": ["%v"]}`,
|
||||
bouncer.crowdsecMachineID,
|
||||
bouncer.crowdsecPassword,
|
||||
strings.Join(bouncer.crowdsecScenarios, `","`),
|
||||
))
|
||||
req, _ = http.NewRequest(http.MethodPost, stringURL, bytes.NewBuffer(data))
|
||||
} else {
|
||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||
}
|
||||
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
|
||||
res, err := bouncer.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
|
||||
}
|
||||
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
||||
if errToken := getToken(bouncer); errToken != nil {
|
||||
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
|
||||
}
|
||||
return crowdsecQuery(bouncer, stringURL, false)
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
|
||||
}
|
||||
|
||||
+7
-3
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||
)
|
||||
@@ -77,7 +78,8 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
||||
customHeader string
|
||||
clientPoolStrategy *ip.PoolStrategy
|
||||
serverPoolStrategy *ip.PoolStrategy
|
||||
client *http.Client
|
||||
httpClient *http.Client
|
||||
cacheClient *cache.Client
|
||||
}
|
||||
type args struct {
|
||||
rw http.ResponseWriter
|
||||
@@ -106,7 +108,8 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
||||
customHeader: tt.fields.customHeader,
|
||||
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
||||
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
||||
client: tt.fields.client,
|
||||
httpClient: tt.fields.httpClient,
|
||||
cacheClient: tt.fields.cacheClient,
|
||||
}
|
||||
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
|
||||
})
|
||||
@@ -155,6 +158,7 @@ func Test_crowdsecQuery(t *testing.T) {
|
||||
type args struct {
|
||||
bouncer *Bouncer
|
||||
stringURL string
|
||||
isPost bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -166,7 +170,7 @@ func Test_crowdsecQuery(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL)
|
||||
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL, tt.args.isPost)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.4"
|
||||
image: "traefik:v2.9.6"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.4"
|
||||
image: "traefik:v2.9.6"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.3"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.8"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "logs:/var/log/traefik"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#### Generate CAPI credentials (only for `alone` mode)
|
||||
You need to create a crowdsec API credentials for the CAPI.
|
||||
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}"
|
||||
```
|
||||
|
||||
These CAPI credentials must be set in your docker-compose.yml or in your config files
|
||||
```yaml
|
||||
...
|
||||
whoami:
|
||||
labels:
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiMachineId=LOGIN"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiPassword=PASSWORD"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/http-generic-bf,crowdsecurity/http-xss-probing,..."
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||
```
|
||||
|
||||
You can then run all the containers:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
@@ -2,4 +2,7 @@ module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/leprosus/golang-ttl-map v1.1.7
|
||||
require (
|
||||
github.com/leprosus/golang-ttl-map v1.1.7
|
||||
github.com/maxlerebourg/simpleredis v1.0.3
|
||||
)
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
|
||||
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
|
||||
github.com/maxlerebourg/simpleredis v1.0.3 h1:VhXq9bVytWDqD/TS/GjHKayvQb/VUeEql5F+yUbdOiI=
|
||||
github.com/maxlerebourg/simpleredis v1.0.3/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
|
||||
|
||||
Vendored
+41
-36
@@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
ttl_map "github.com/leprosus/golang-ttl-map"
|
||||
simpleredis "github.com/maxlerebourg/simpleredis"
|
||||
|
||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,14 +18,13 @@ const (
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
cache = ttl_map.New()
|
||||
redis simpleredis.SimpleRedis
|
||||
redisEnabled = false
|
||||
redis simpleredis.SimpleRedis
|
||||
cache = ttl_map.New()
|
||||
)
|
||||
|
||||
// FileSystem Cache
|
||||
type localCache struct{}
|
||||
|
||||
func getDecisionLocalCache(clientIP string) (bool, error) {
|
||||
func (localCache) getDecision(clientIP string) (bool, error) {
|
||||
banned, isCached := cache.Get(clientIP)
|
||||
bannedString, isValid := banned.(string)
|
||||
if isCached && isValid && len(bannedString) > 0 {
|
||||
@@ -34,17 +33,17 @@ func getDecisionLocalCache(clientIP string) (bool, error) {
|
||||
return false, fmt.Errorf("cache:miss")
|
||||
}
|
||||
|
||||
func setDecisionLocalCache(clientIP string, value string, duration int64) {
|
||||
func (localCache) setDecision(clientIP string, value string, duration int64) {
|
||||
cache.Set(clientIP, value, duration)
|
||||
}
|
||||
|
||||
func deleteDecisionLocalCache(clientIP string) {
|
||||
func (localCache) deleteDecision(clientIP string) {
|
||||
cache.Del(clientIP)
|
||||
}
|
||||
|
||||
// Redis Cache
|
||||
type redisCache struct{}
|
||||
|
||||
func getDecisionRedisCache(clientIP string) (bool, error) {
|
||||
func (redisCache) getDecision(clientIP string) (bool, error) {
|
||||
banned, err := redis.Get(clientIP)
|
||||
bannedString := string(banned)
|
||||
if err == nil && len(bannedString) > 0 {
|
||||
@@ -53,55 +52,61 @@ func getDecisionRedisCache(clientIP string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
func setDecisionRedisCache(clientIP string, value string, duration int64) {
|
||||
func (redisCache) setDecision(clientIP string, value string, duration int64) {
|
||||
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
||||
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDecisionRedisCache(clientIP string) {
|
||||
func (redisCache) deleteDecision(clientIP string) {
|
||||
if err := redis.Del(clientIP); err != nil {
|
||||
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteDecision delete decision in cache.
|
||||
func DeleteDecision(clientIP string) {
|
||||
if redisEnabled {
|
||||
deleteDecisionRedisCache(clientIP)
|
||||
type cacheInterface interface {
|
||||
setDecision(clientIP string, value string, duration int64)
|
||||
getDecision(clientIP string) (bool, error)
|
||||
deleteDecision(clientIP string)
|
||||
}
|
||||
|
||||
// Client Cache client.
|
||||
type Client struct {
|
||||
cache cacheInterface
|
||||
}
|
||||
|
||||
// New Initialize cache client.
|
||||
func (client *Client) New(isRedis bool, host string) {
|
||||
if isRedis {
|
||||
redis.Init(host)
|
||||
client.cache = &redisCache{}
|
||||
} else {
|
||||
deleteDecisionLocalCache(clientIP)
|
||||
client.cache = &localCache{}
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
|
||||
}
|
||||
|
||||
// DeleteDecision delete decision in cache.
|
||||
func (client *Client) DeleteDecision(clientIP string) {
|
||||
logger.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
|
||||
client.cache.deleteDecision(clientIP)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func GetDecision(clientIP string) (bool, error) {
|
||||
if redisEnabled {
|
||||
return getDecisionRedisCache(clientIP)
|
||||
}
|
||||
return getDecisionLocalCache(clientIP)
|
||||
func (client *Client) GetDecision(clientIP string) (bool, error) {
|
||||
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
|
||||
return client.cache.getDecision(clientIP)
|
||||
}
|
||||
|
||||
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
||||
func SetDecision(clientIP string, isBanned bool, duration int64) {
|
||||
func (client *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
|
||||
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
|
||||
var value string
|
||||
if isBanned {
|
||||
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v banned", clientIP))
|
||||
value = cacheBannedValue
|
||||
} else {
|
||||
value = cacheNoBannedValue
|
||||
}
|
||||
if redisEnabled {
|
||||
setDecisionRedisCache(clientIP, value, duration)
|
||||
} else {
|
||||
setDecisionLocalCache(clientIP, value, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// InitRedisClient loads variables.
|
||||
func InitRedisClient(host string) {
|
||||
redisEnabled = true
|
||||
redis.Init(host)
|
||||
logger.Debug("cache:InitRedisClient redis:initialized")
|
||||
client.cache.setDecision(clientIP, value, duration)
|
||||
}
|
||||
|
||||
Vendored
+38
-162
@@ -6,10 +6,11 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_getDecisionLocalCache(t *testing.T) {
|
||||
func Test_GetDecision(t *testing.T) {
|
||||
IPInCache := "10.0.0.10"
|
||||
IPNotInCache := "10.0.0.20"
|
||||
setDecisionLocalCache(IPInCache, "t", 10)
|
||||
client := &Client{cache: &localCache{}}
|
||||
client.SetDecision(IPInCache, true, 10)
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
@@ -22,205 +23,80 @@ func Test_getDecisionLocalCache(t *testing.T) {
|
||||
}{
|
||||
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
|
||||
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
{name: "Fetch invalid value", args: args{clientIP: "zaeaea"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getDecisionLocalCache(tt.args.clientIP)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getDecisionLocalCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getDecisionLocalCache() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||
t.Errorf("getDecisionLocalCache() err = %v, want %v", err.Error(), tt.valueErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setDecisionLocalCache(t *testing.T) {
|
||||
IPInCache := "10.0.0.10"
|
||||
type args struct {
|
||||
clientIP string
|
||||
value string
|
||||
duration int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{name: "Set valid IP in local cache as t", args: args{clientIP: IPInCache, value: "t", duration: 0}},
|
||||
{name: "Set valid IP in local cache as f", args: args{clientIP: IPInCache, value: "f", duration: 0}},
|
||||
{name: "Set valid IP in local cache as empty str", args: args{clientIP: IPInCache, value: "", duration: 0}},
|
||||
{name: "Set valid IP in local cache as f for -1 sec", args: args{clientIP: IPInCache, value: "f", duration: -1}},
|
||||
{name: "Set valid IP in local cache as f for 10 sec", args: args{clientIP: IPInCache, value: "f", duration: 10}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setDecisionLocalCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deleteDecisionLocalCache(t *testing.T) {
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
deleteDecisionLocalCache(tt.args.clientIP)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getDecisionRedisCache(t *testing.T) {
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getDecisionRedisCache(tt.args.clientIP)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getDecisionRedisCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getDecisionRedisCache() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setDecisionRedisCache(t *testing.T) {
|
||||
type args struct {
|
||||
clientIP string
|
||||
value string
|
||||
duration int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setDecisionRedisCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deleteDecisionRedisCache(t *testing.T) {
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
deleteDecisionRedisCache(tt.args.clientIP)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDecision(t *testing.T) {
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
DeleteDecision(tt.args.clientIP)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDecision(t *testing.T) {
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetDecision(tt.args.clientIP)
|
||||
got, err := client.GetDecision(tt.args.clientIP)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||
t.Errorf("GetDecision() err = %v, want %v", err.Error(), tt.valueErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDecision(t *testing.T) {
|
||||
func Test_SetDecision(t *testing.T) {
|
||||
client := &Client{cache: &localCache{}}
|
||||
IPInCache := "10.0.0.11"
|
||||
type args struct {
|
||||
clientIP string
|
||||
isBanned bool
|
||||
value bool
|
||||
duration int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: true, duration: 0}, want: false},
|
||||
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: true, duration: 10}, want: true},
|
||||
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: false, duration: 10}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
SetDecision(tt.args.clientIP, tt.args.isBanned, tt.args.duration)
|
||||
client.SetDecision(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||
got, _ := client.GetDecision(tt.args.clientIP)
|
||||
if got != tt.want {
|
||||
t.Errorf("SetDecision() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitRedisClient(t *testing.T) {
|
||||
func Test_DeleteDecision(t *testing.T) {
|
||||
IPInCache := "10.0.0.12"
|
||||
IPNotInCache := "10.0.0.22"
|
||||
client := &Client{cache: &localCache{}}
|
||||
client.SetDecision(IPInCache, true, 10)
|
||||
type args struct {
|
||||
host string
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: false},
|
||||
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
InitRedisClient(tt.args.host)
|
||||
client.DeleteDecision(tt.args.clientIP)
|
||||
got, _ := client.GetDecision(tt.args.clientIP)
|
||||
if got != tt.want {
|
||||
t.Errorf("DeleteDecision() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
|
||||
// Enums for crowdsec mode.
|
||||
const (
|
||||
AloneMode = "alone"
|
||||
StreamMode = "stream"
|
||||
LiveMode = "live"
|
||||
NoneMode = "none"
|
||||
@@ -41,6 +43,11 @@ type Config struct {
|
||||
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
||||
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
||||
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
||||
CrowdsecCapiMachineID string `json:"crowdsecCapiMachineId,omitempty"`
|
||||
CrowdsecCapiMachineIDFile string `json:"crowdsecCapiMachineIdFile,omitempty"`
|
||||
CrowdsecCapiPassword string `json:"crowdsecCapiPassword,omitempty"`
|
||||
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
||||
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
||||
@@ -99,11 +106,11 @@ func GetVariable(config *Config, key string) (string, error) {
|
||||
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
|
||||
}
|
||||
value = string(fileValue)
|
||||
return value, nil
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
field = object.FieldByName(key)
|
||||
value = field.String()
|
||||
return value, nil
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
|
||||
// ValidateParams validate all the param gave by user.
|
||||
@@ -114,6 +121,23 @@ func ValidateParams(config *Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.CrowdsecMode == AloneMode {
|
||||
if _, err := GetVariable(config, "CrowdsecCapiMachineId"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := GetVariable(config, "CrowdsecCapiPassword"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This only check that the format of the URL scheme:// is correct and do not make requests
|
||||
testURL := url.URL{
|
||||
Scheme: config.CrowdsecLapiScheme,
|
||||
@@ -123,13 +147,6 @@ func ValidateParams(config *Config) error {
|
||||
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||
}
|
||||
|
||||
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -144,20 +161,17 @@ func ValidateParams(config *Config) error {
|
||||
}
|
||||
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
|
||||
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
|
||||
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be both empty")
|
||||
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
|
||||
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
|
||||
// check LAPIKey
|
||||
lapiKey = strings.TrimSuffix(lapiKey, "\n")
|
||||
err = validateParamsAPIKey(lapiKey)
|
||||
if err != nil {
|
||||
lapiKey = strings.TrimSpace(lapiKey)
|
||||
if err = validateParamsAPIKey(lapiKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Case https to contact Crowdsec LAPI and certificate must be provided
|
||||
if config.CrowdsecLapiScheme == HTTPS && !config.CrowdsecLapiTLSInsecureVerify {
|
||||
err = validateParamsTLS(config)
|
||||
if err != nil {
|
||||
if err = validateParamsTLS(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -165,107 +179,16 @@ func ValidateParams(config *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParamsAPIKey(lapiKey string) error {
|
||||
for i := 0; i < len(lapiKey); i++ {
|
||||
c := lapiKey[i]
|
||||
if !validHeaderFieldByte(c) {
|
||||
return fmt.Errorf("CrowdsecLapiKey contains the following forbidden caracter %q", c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validHeaderFieldByte reports whether b is a valid byte in a header
|
||||
// field name. RFC 7230 says:
|
||||
//
|
||||
// header-field = field-name ":" OWS field-value OWS
|
||||
// field-name = token
|
||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
||||
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
||||
// token = 1*tchar
|
||||
func validHeaderFieldByte(b byte) bool {
|
||||
// isTokenTable is a copy of net/http/lex.go's isTokenTable.
|
||||
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
|
||||
var isTokenTable = [127]bool{
|
||||
'!': true,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'W': true,
|
||||
'V': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'|': true,
|
||||
'~': true,
|
||||
// valid ! # $ % & ' * + - . ^ _ ` | ~ DIGIT ALPHA
|
||||
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
|
||||
func validateParamsAPIKey(lapiKey string) error {
|
||||
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~]*$")
|
||||
if !reg.Match([]byte(lapiKey)) {
|
||||
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
|
||||
}
|
||||
return int(b) < len(isTokenTable) && isTokenTable[b]
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParamsTLS(config *Config) error {
|
||||
@@ -315,8 +238,8 @@ func validateParamsRequired(config *Config) error {
|
||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||
}
|
||||
}
|
||||
if !contains([]string{NoneMode, LiveMode, StreamMode}, config.CrowdsecMode) {
|
||||
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
|
||||
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) {
|
||||
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'")
|
||||
}
|
||||
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||
@@ -330,10 +253,10 @@ func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
|
||||
tlsConfig.RootCAs = x509.NewCertPool()
|
||||
//nolint:gocritic
|
||||
if config.CrowdsecLapiScheme != HTTPS {
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme not https")
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
|
||||
return tlsConfig, nil
|
||||
} else if config.CrowdsecLapiTLSInsecureVerify {
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify is true")
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
// If we return here and still want to use client auth this won't work
|
||||
// return tlsConfig, nil
|
||||
|
||||
@@ -70,6 +70,10 @@ func Test_GetVariable(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_ValidateParams(t *testing.T) {
|
||||
cfg1 := New()
|
||||
cfg1.CrowdsecLapiKey = "test\n\n"
|
||||
cfg2 := New()
|
||||
cfg2.CrowdsecLapiKey = "test@"
|
||||
cfg3 := getMinimalConfig()
|
||||
cfg3.CrowdsecMode = "bad"
|
||||
cfg4 := getMinimalConfig()
|
||||
@@ -90,6 +94,8 @@ func Test_ValidateParams(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
|
||||
{name: "Validate a non trimed crowdsec lapi key", args: args{config: cfg1}, wantErr: false},
|
||||
{name: "Not validate unauthorized character in crowdsec lapi key", args: args{config: cfg2}, wantErr: true},
|
||||
{name: "Not validate an absent crowdsec lapi key", args: args{config: New()}, wantErr: true},
|
||||
{name: "Not validate a not listed item", args: args{config: cfg3}, wantErr: true},
|
||||
{name: "Not validate a bad number", args: args{config: cfg4}, wantErr: true},
|
||||
@@ -141,8 +147,9 @@ func Test_validateParamsIPs(t *testing.T) {
|
||||
{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 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", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
|
||||
{name: "Validate a 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},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -185,6 +192,31 @@ func Test_validateParamsRequired(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateParamsAPIKey(t *testing.T) {
|
||||
type args struct {
|
||||
lapiKey string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Validate all the valid characters", args: args{lapiKey: "test!#$%&'*+-.^_`|~"}, wantErr: false},
|
||||
{name: "Not validate a @", args: args{lapiKey: "test@"}, wantErr: true},
|
||||
{name: "Not validate a (", args: args{lapiKey: "test("}, wantErr: true},
|
||||
{name: "Not validate a [", args: args{lapiKey: "test["}, wantErr: true},
|
||||
{name: "Not validate a ?", args: args{lapiKey: "test?"}, wantErr: true},
|
||||
{name: "Not validate a \\n, (must be trimed before)", args: args{lapiKey: "test\n"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := validateParamsAPIKey(tt.args.lapiKey); (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateParamsAPIKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GetTLSConfigCrowdsec(t *testing.T) {
|
||||
type args struct {
|
||||
config *Config
|
||||
|
||||
+2
-3
@@ -23,9 +23,8 @@ type Checker struct {
|
||||
func NewChecker(trustedIPs []string) (*Checker, error) {
|
||||
checker := &Checker{}
|
||||
|
||||
for _, ipMask := range trustedIPs {
|
||||
// remove leading and trailing spaces
|
||||
ipMask = strings.TrimSpace(ipMask)
|
||||
for _, ipMaskRaw := range trustedIPs {
|
||||
ipMask := strings.TrimSpace(ipMaskRaw)
|
||||
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
||||
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
||||
logger.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# simpleredis
|
||||
Minimal go redis with only get, set and delete operation
|
||||
Generated
Vendored
+2
-6
@@ -10,8 +10,6 @@ import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||
)
|
||||
|
||||
// Error strings for redis.
|
||||
@@ -47,9 +45,7 @@ func genRedisArray(params ...[]byte) []byte {
|
||||
|
||||
func send(wr *textproto.Writer, method string, data []byte) {
|
||||
if err := wr.PrintfLine(string(data)); err != nil {
|
||||
logger.Error(fmt.Sprintf("redis:%s %s", method, err.Error()))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("redis:%s", method))
|
||||
fmt.Printf("redis:%s %s", method, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +58,7 @@ func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
|
||||
}
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
logger.Error(fmt.Sprintf("redis:connClose %s", err.Error()))
|
||||
fmt.Printf("redis:connClose %s", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
Vendored
+3
@@ -1,3 +1,6 @@
|
||||
# github.com/leprosus/golang-ttl-map v1.1.7
|
||||
## explicit; go 1.15
|
||||
github.com/leprosus/golang-ttl-map
|
||||
# github.com/maxlerebourg/simpleredis v1.0.3
|
||||
## explicit; go 1.19
|
||||
github.com/maxlerebourg/simpleredis
|
||||
|
||||
Reference in New Issue
Block a user