Compare commits

...
Author SHA1 Message Date
Max Lerebourg dd322a966a 🍱 fix lint 2023-03-04 12:11:22 +01:00
Max Lerebourg f0bb140596 🍱 fix lint 2023-03-04 12:08:03 +01:00
Max Lerebourg 46e581eca2 🍱 fix readme to add redis pass 2023-03-04 12:05:09 +01:00
maxlerebourg 50690d1ac7 handle redis password (#87)
*  handle redis password

* 🍱 fix version
2023-03-04 11:51:54 +01:00
maxlerebourg b079073ff6 handle isHealthy in the main function and log error became… (#84)
*  handle isHealthy in the main function and log error became debug

* fix: lint

* fix: lint
2023-03-01 14:18:19 +01:00
maxlerebourg 976cbb7d1f 81 bug stream mode stops blocking (#82)
*  fix isHealthy issue at startup

* 🍱 not added in first commit ?

* 🍱 remove unused import

* 🍱 fix lint

* fix: lint
2023-01-30 14:03:10 +01:00
Max Lerebourg 80726df450 🐛 fix alone mode 2023-01-25 20:43:07 +01:00
maxlerebourg 4132445a79 cache decision in live mode for maximum defaultDecisionTimeout params (#79) 2023-01-18 21:03:09 +01:00
mathieuHaandmaxlerebourg 5b15d6fdc4 📝 Update Readme spelling and clarify (#76)
* 📝 Update Readme spelling and clarify

* 📝 Update Readme exemple

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* Update README.md

* Update README.md

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2023-01-02 13:18:45 +01:00
maxlerebourgandMathieu HANOTAUX 1b310b2c15 Standalone mode come back (#74)
*  Standalone mode come back

* 🍱 fix lint

* 🐛 fix tests cache

* 🐛 fix tests

* 🐛 fix tests

* 🚨 fix lint

* 🚨 fix lint

* 🍱 add logging

* :rotating-light: fix lint

* 🍱 fix comments

* 🐛 fix tests

* 🚨 Fix lint

Co-authored-by: Mathieu HANOTAUX <mathieu@hanotaux.fr>
2023-01-02 11:53:57 +01:00
maxlerebourg 87a839f22d Update issue templates 2022-12-31 16:25:52 +01:00
maxlerebourg f14a4e3a68 add external simpleredis and add log for cache (#73)
*  add external simpleredis and add log for cache

* 🐛 fix comment
2022-12-31 16:18:08 +01:00
maxlerebourg 11b615b9bd 📃 Update issue templates 2022-12-31 11:31:49 +01:00
maxlerebourg 8f937d22a2 🐛 fix all incoherence (#71)
* 🐛 fix all incoherence

* 🐛 fix comment + lint
2022-12-31 11:15:36 +01:00
mathieuHa 041b0f67b7 📝 55 feature clean readme doc (#70)
* 📝 Clean documentation in Readme

* 📝 Update readme

* 📝 Place exemples in seperated README

* ⬆️  Update traefik to 2.9.6 and crowdsec to 1.4.3 in exemples

* 📝 Add exemple at the right place
2022-12-30 23:35:33 +01:00
mathieuHa 5458db93cf 📝 40 Add working exemple on kubernetes (#56)
* 📝 Add initial doc and file structure

* 🍱 Add struct for crowdsec

* 📝 Update doc install cs

* 🚧 Update exemple, add traefik settings

* 📝 Update doc for kubernetes

* 📝 update doc on kubernetes usage

* 📝 Add read logs from traefik

* 📝 Add read logs from traefik

* 📝 Update doc lisibility

* 📝 Update doc with new namespace

* 📝 Update doc with enable plugin in traefik

* 📝 Update doc on kube

* 📝 Finish exemple of Kubernetes

* 📝 Update after review
2022-12-30 22:20:45 +01:00
mathieuHa 29878e837c 🐛 65 confused about configuring tls (#69)
* 🐛 Fix bug reading LAPIKEY, update exemples

* 🚨 Fix lint
2022-12-30 11:24:14 +01:00
mathieuHa aff7c496c2 🐛Remove trailing and leading spaces between each IP or range (#68) 2022-12-29 10:30:19 +01:00
45 changed files with 1243 additions and 581 deletions
+16 -21
View File
@@ -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 ✨
+5 -6
View File
@@ -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 ✨
+73 -217
View File
@@ -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,32 @@ 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
- RedisCachePassword
- string
- default: ""
- Password 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 +146,7 @@ experimental:
http:
routers:
my-router:
rule: host(`woami.localhost`)
rule: host(`whoami.localhost`)
service: service-foo
entryPoints:
- web
@@ -160,6 +173,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
@@ -168,6 +187,7 @@ http:
forwardedHeadersCustomName: X-Custom-Header
redisCacheEnabled: false
redisCacheHost: "redis:6379"
redisCachePassword: password
crowdsecLapiTLSCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
@@ -190,17 +210,16 @@ 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:
- Content: VariableName: XXX
- File : VariableNameFILE: /path
- File : VariableNameFile: /path
#### Authenticate with LAPI
@@ -209,7 +228,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
@@ -241,18 +260,18 @@ 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.
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.
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.
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.
#### Use HTTPS to communicate with the LAPI
To communicate with the LAPI in HTTPS you need to either accept any certificates by setting the crowdsecLapiTLSInsecureVerify to true or add the CA used by the server certificate of Crowdsec using crowdsecLapiTLSCertificateAuthority or crowdsecLapiTLSCertificateAuthorityFile.
Set the crowdsecLapiScheme to https.
To communicate with the LAPI in HTTPS you need to either accept any certificates by setting the `crowdsecLapiTLSInsecureVerify` to true or add the CA used by the server certificate of Crowdsec using `crowdsecLapiTLSCertificateAuthority` or `crowdsecLapiTLSCertificateAuthorityFile`.
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 +281,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 +321,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,193 +332,13 @@ Equivalent to
make run_local
```
### Examples
1. Behind another proxy service (ex: clouflare)
You need to configure your Traefik to trust Forwarded headers by your front proxy
In the example we use another instance of traefik with the container named cloudflare to simulate a front proxy
The "internal" Traefik instance is configured to trust the cloudflare forward headers
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
```yaml
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
```
We configure the middleware to trust as well the IP:
```yaml
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
```
To play the demo environment run:
```bash
make run_behindproxy
```
2. With Redis as an external shared cache
The plugin must be configured to connect to a redis instance
```yaml
redisCacheHost: "redis:6379"
```
Here **redis** is the hostname of a container located in the same network as Traefik and **6379** is the default port of redis
To play the demo environment run:
```bash
make run_cacheredis
```
3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
You need to configure your Traefik to trust Forwarded headers by your front proxy
In the example we use a whoami container protected by crowdsec, and we ban our IP before allowing using TrustedIPs
If you are using another proxy in front, you need to add its IP in the trusted IP for the forwarded headers.
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
The "internal" Traefik instance is configured to trust the forward headers
```yaml
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
```
We configure the middleware to trust as well as the IP of the intermediate proxy if needed:
```yaml
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
```
Add your IP to the ban list
```bash
docker exec crowdsec cscli decisions add --ip 10.0.10.30 -d 10m
```
You should get a 403 on http://localhost/foo
> Replace *10.0.10.30* by your IP
Add the IPs that will not be filtered by the plugin
```yaml
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clientTrustedips=10.0.10.30/32"
```
> Replace *10.0.10.30/32* by your IP or IP range, so it's not getting checked against ban cache of crowdsec
You should get a 200 on http://localhost/foo even if you are on the ban cache
To play the demo environment run:
```bash
make run_trustedips
```
4. Using Crowdsec and Traefik installed as binary in a single VM
Please see details in `exemples/binary-vm/README.md`
To play the demo environment run:
```bash
make run_binaryvm
```
5. Using https communication and tls authentication with Crowdsec
##### Summary
This example demonstrates the use of https between the Traefik plugin and the Crowdsec LAPI.
It is possible to communicate with the LAPI in https and still authenticate with API key.
You can add the client TLS certificate generated to authenticate without any Token in the plugin.
However, note that it is not possible to authenticate with TLS client certificate without https setup for the LAPI.
The example is detailed below and will be placed in the `examples/tls-auth/README.md` file.
##### Details
Simple HTTPS communication: It is possible to talk to Crowdsec LAPI which is configured with a self-signed certificate
In that case the setting **crowdsecLapiTLSInsecureVerify** must be set to true.
It is recommended to validate the certificate presented by Crowdsec LAPI using the Certificate Authority which created it.
You can provide the Certificate Authority using:
* A file path readable by Traefik
```yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
crowdsecLapiTlsCertificateAuthorityFile: /etc/traefik/certs/crowdsecCA.pem
```
* The PEM encoded certificate as a text variable
In the static file configuration of Traefik
```yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
crowdsecLapiTlsCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
...
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
In a dynamic configuration of a provider (ex docker) as a Label
```yaml
services:
whoami-foo:
image: traefik/whoami
labels:
- |
traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTlsCertificateAuthority=
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
...
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
The example tls-auth presents 2 services, foo and bar which comes with the bouncer.
At startup, certificates are created in a shared docker volume by a sidecar container which exits after.
Traefik will use client and CA certificates.
The Bouncer will use server and CA certificates.
The service `whoami-foo` will authenticate with an **API key** over HTTPS after verifying the server certificate with CA.
The service `whoami-bar` will authenticate with a **client certificate** signed by the CA.
Access to a route that communicate via https and authenticate with API-key:
```
curl http://localhost:80/foo
```
Access to a route that communicate via https and authenticate with a client certificate:
```
curl http://localhost:80/bar
```
Access to the traefik dashboard
```
curl http://localhost:8080/dashboard/#/
```
To play the demo environnement run:
```bash
make run_tlsauth
```
Note:
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
### 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.
+149 -45
View File
@@ -5,11 +5,13 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"text/template"
"time"
@@ -17,19 +19,22 @@ import (
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
isStartup = true
isCrowdsecStreamHealthy = true
ticker chan bool
)
@@ -49,12 +54,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.
@@ -62,23 +73,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)
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, 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()))
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
}
bouncer := &Bouncer{
next: next,
@@ -89,17 +116,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,
@@ -107,16 +139,33 @@ 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)
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
bouncer.cacheClient.New(config.RedisCacheEnabled, config.RedisCacheHost, config.RedisCachePassword)
//nolint:nestif
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode {
if err := getToken(bouncer); err != nil {
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
return nil, err
}
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
}
if err := handleStreamCache(bouncer); err != nil {
return nil, err
}
isStartup = false
ticker = startTicker(config, func() {
handleStreamCache(bouncer)
})
go handleStreamCache(bouncer)
if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false
logger.Error(err.Error())
} else {
isCrowdsecStreamHealthy = true
}
})
}
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
return bouncer, nil
}
@@ -152,10 +201,12 @@ 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)
if erro != nil {
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
if erro.Error() == simpleredis.RedisUnreachable {
isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP)
if cacheErr != nil {
errString := cacheErr.Error()
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
if errString != cache.CacheMiss {
logger.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
rw.WriteHeader(http.StatusForbidden)
return
}
@@ -171,19 +222,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.Debug(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)
}
}
@@ -210,6 +262,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)
@@ -236,14 +295,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
}
@@ -255,7 +314,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
}
@@ -264,61 +323,106 @@ 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 handleStreamCache(bouncer *Bouncer) {
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) error {
// 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
return nil
}
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
if err.Error() != cache.CacheMiss {
return err
}
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
Path: bouncer.crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
}
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
if err != nil {
logger.Error(err.Error())
isCrowdsecStreamHealthy = false
return
return err
}
var stream Stream
err = json.Unmarshal(body, &stream)
if err != nil {
logger.Error(fmt.Sprintf("handleStreamCache:parsingBody %s", err.Error()))
isCrowdsecStreamHealthy = false
return
return fmt.Errorf("handleStreamCache:parsingBody %w", err)
}
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
return nil
}
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, error) {
var req *http.Request
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(crowdsecLapiHeader, bouncer.crowdsecKey)
res, err := bouncer.client.Do(req)
}
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)
}
+13 -4
View File
@@ -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)
})
@@ -141,12 +144,17 @@ func Test_handleStreamCache(t *testing.T) {
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handleStreamCache(tt.args.bouncer)
err := handleStreamCache(tt.args.bouncer)
if (err != nil) != tt.wantErr {
t.Errorf("handleStreamCache() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
@@ -155,6 +163,7 @@ func Test_crowdsecQuery(t *testing.T) {
type args struct {
bouncer *Bouncer
stringURL string
isPost bool
}
tests := []struct {
name string
@@ -166,7 +175,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
+1 -1
View File
@@ -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
View File
@@ -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"
+21
View File
@@ -0,0 +1,21 @@
# Exemple
## Behind another proxy service (ex: clouflare)
You need to configure your Traefik to trust Forwarded headers by your front proxy
In the example we use another instance of traefik with the container named cloudflare to simulate a front proxy
The "internal" Traefik instance is configured to trust the cloudflare forward headers
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
```yaml
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
```
We configure the middleware to trust as well the IP:
```yaml
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
```
To play the demo environment run:
```bash
make run_behindproxy
```
@@ -2,7 +2,7 @@ version: "3.8"
services:
cloudflare:
image: "traefik:v2.9.4"
image: "traefik:v2.9.6"
container_name: "cloudflare"
restart: unless-stopped
command:
@@ -22,7 +22,7 @@ services:
- 8080:8080
traefik:
image: "traefik:v2.9.4"
image: "traefik:v2.9.6"
container_name: "traefik"
restart: unless-stopped
command:
@@ -36,7 +36,7 @@ services:
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.3"
- "--experimental.plugins.bouncer.version=v1.1.7"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-traefik:/var/log/traefik
@@ -88,7 +88,7 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:v1.4.3
container_name: "crowdsec"
restart: unless-stopped
environment:
+2
View File
@@ -1,3 +1,5 @@
# Exemple
### Install vagrant
##### On linux
+2
View File
@@ -67,6 +67,8 @@ Vagrant.configure("2") do |config|
# vb.memory = "2048"
# end
config.vm.provision "file", source: "./files", destination: "/home/vagrant/vagrant_data"
config.vm.provision "file", source: "./scripts", destination: "/home/vagrant/scripts"
# config.vm.provision "file", source: "../../", destination: "/home/vagrant/plugin"
config.vm.provision "shell", path: "scripts/install_traefik.sh"
config.vm.provision "shell", path: "scripts/configure_traefik.sh"
@@ -19,7 +19,7 @@ http:
plugin:
bouncer:
enabled: true
crowdseclapikey: whoami-demo
crowdseclapikey: "whoami-demo"
updateintervalseconds: 60
crowdsecmode: live
loglevel: "DEBUG"
+1 -1
View File
@@ -31,7 +31,7 @@ experimental:
plugins:
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.1.5
version: v1.1.6
################################################################
# Certificate Resolver
+1 -1
View File
@@ -28,4 +28,4 @@ ProtectSystem=full
NoNewPrivileges=true
[Install]
WantedBy=multi-user.
WantedBy=multi-user.target
@@ -1,5 +1,5 @@
#!/bin/bash
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
sudo apt install crowdsec -y
DEBIAN_FRONTEND=noninteractive curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
DEBIAN_FRONTEND=noninteractive sudo apt install crowdsec -y
sudo cp /home/vagrant/vagrant_data/crowdsec/acquis.yaml /etc/crowdsec/acquis.yaml
@@ -1,7 +1,8 @@
#!/bin/bash
sudo apt-get update && apt-get install wget -y && apt-get upgrade -y
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.9.5/traefik_v2.9.5_linux_amd64.tar.gz"
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
# DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.9.6/traefik_v2.9.6_linux_amd64.tar.gz"
tar -zxvf traefik.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./traefik /usr/local/bin/
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
sudo apt-get update && apt-get install wget -y
DEBIAN_FRONTEND=noninteractive sudo apt-get update && apt-get install wget -y
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.8.7/whoami_v1.8.7_linux_amd64.tar.gz"
tar -zxvf whoami.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
+91
View File
@@ -0,0 +1,91 @@
### Kubernetes Exemple
#### Official docs
##### Install Kubernetes on Docker Desktop
Install Docker Desktop
[https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/)
In settings, click on `Kubernetes` menu, and click `Enable Kubernetes`, then `Apply and Restart`.
In case of any issue, you can reset the cluster from this menu and the button `Reset Kubernetes Cluster`.
##### Install Traefik
[getting-started/install-traefik/#use-the-helm-chart](https://doc.traefik.io/traefik/getting-started/install-traefik/#use-the-helm-chart)
```bash
helm repo add traefik https://traefik.github.io/charts
helm repo update
kubectl create ns traefik
helm upgrade --install --namespace=traefik \
--values=./traefik/values.yml \
traefik traefik/traefik
```
#### View the Traefik dashboard
> Port forward the dashboard:
```bash
kubectl --namespace=traefik port-forward $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) 9000:9000
```
Access the dashboard with: [localhost:9000/dashboard/#/](http://localhost:9000/dashboard/#/)
#### Install the plugin
```bash
kubectl apply -f traefik/plugin.yml
```
#### Install Whoami
```bash
kubectl apply -f whoami/whoami.yml
kubectl apply -f whoami/whoami-services.yml
kubectl apply -f whoami/whoami-ingress.yml
```
#### Access Whoami
> Port forward web port of Traefik
```bash
kubectl --namespace=traefik port-forward $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) 8000:8000
```
Access the whoami with: [localhost:8000/](http://localhost:8000/)
#### Install Crowdsec
[helm/crowdsec/crowdsec](https://artifacthub.io/packages/helm/crowdsec/crowdsec)
```bash
helm repo add crowdsec https://crowdsecurity.github.io/helm-charts
helm repo update
kubectl create ns crowdsec
helm upgrade --install --namespace=crowdsec \
--values=./crowdsec/values.yml \
crowdsec crowdsec/crowdsec
```
#### Read Traefik Logs
```bash
kubectl get pod --namespace traefik
kubectl logs $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) --namespace traefik -f
```
#### Use CSCLI in Crowdsec container
```bash
kubectl -n crowdsec exec -it $(kubectl get pods -n crowdsec --selector "k8s-app=crowdsec,type=lapi" --output=name) bash
```
#### Shell in Traefik container
```bash
kubectl -n traefik exec -it $(kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name) sh
```
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: crowdsec
namespace: crowdsec
spec:
ports:
- name: web
port: 8080
targetPort: web
selector:
app: crowdsec
+25
View File
@@ -0,0 +1,25 @@
image:
tag: v1.4.4-rc1
agent:
acquisition:
- namespace: traefik
podName: traefik-*
program: traefik
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik"
lapi:
dashboard:
enabled: false
env:
- name: ENROLL_KEY
value: 40796d93c2958f9e58345514e67740e5
- name: ENROLL_INSTANCE_NAME
value: k8s_cluster
- name: ENROLL_TAGS
value: "k8s linux test"
# If it's a test, we don't want to share signals with CrowdSec so disable the Online API.
- name: DISABLE_ONLINE_API
value: true
+14
View File
@@ -0,0 +1,14 @@
# dashboard.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: dashboard
spec:
entryPoints:
- web
routes:
- match: Host(`traefik.localhost`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
kind: Rule
services:
- name: api@internal
kind: TraefikService
+14
View File
@@ -0,0 +1,14 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: bouncer
namespace: traefik
spec:
plugin:
bouncer:
CrowdsecLapiKey: 40796d93c2958f9e58345514e67740e5
Enabled: "true"
logLevel: DEBUG
crowdsecMode: live
crowdsecLapiHost: crowdsec-service.crowdsec.svc.cluster.local:8080
crowdsecLapiScheme: http
+19
View File
@@ -0,0 +1,19 @@
image:
tag: v2.9.6
logs:
general:
level: DEBUG
access:
enabled: true
fields:
headers:
defaultmode: keep
experimental:
plugins:
enabled: true
additionalArguments:
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7-beta1"
@@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
namespace: traefik
annotations:
traefik.ingress.kubernetes.io/router.middlewares: traefik-bouncer@kubernetescrd
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
name: web
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: traefik
spec:
ports:
- name: web
port: 80
targetPort: web
selector:
app: whoami
+24
View File
@@ -0,0 +1,24 @@
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoami
namespace: traefik
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- name: web
containerPort: 80
+13
View File
@@ -0,0 +1,13 @@
# Exemple
## With Redis as an external shared cache
The plugin must be configured to connect to a redis instance
```yaml
redisCacheHost: "redis:6379"
```
Here **redis** is the hostname of a container located in the same network as Traefik and **6379** is the default port of redis
To play the demo environment run:
```bash
make run_cacheredis
```
@@ -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:
@@ -15,7 +15,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.4"
- "--experimental.plugins.bouncer.version=v1.1.7"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -68,7 +68,7 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:v1.4.3
container_name: "crowdsec"
restart: unless-stopped
environment:
+24
View File
@@ -0,0 +1,24 @@
#### 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.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
- "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,..."
```
You can then run all the containers:
```bash
docker-compose up -d
```
@@ -0,0 +1,45 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 80:80
- 8080:8080
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.CrowdsecCapiPassword=Password2"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
volumes:
logs-local:
+93
View File
@@ -0,0 +1,93 @@
# Exemple
## Using https communication and tls authentication with Crowdsec
##### Summary
This example demonstrates the use of https between the Traefik plugin and the Crowdsec LAPI.
It is possible to communicate with the LAPI in https and still authenticate with API key.
You can add the client TLS certificate generated to authenticate without any Token in the plugin.
However, note that it is not possible to authenticate with TLS client certificate without https setup for the LAPI.
##### Details
Simple HTTPS communication: It is possible to talk to Crowdsec LAPI which is configured with a self-signed certificate
In that case the setting **crowdsecLapiTLSInsecureVerify** must be set to true.
It is recommended to validate the certificate presented by Crowdsec LAPI using the Certificate Authority which created it.
You can provide the Certificate Authority using:
* A file path readable by Traefik
```yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
crowdsecLapiTlsCertificateAuthorityFile: /etc/traefik/certs/crowdsecCA.pem
```
* The PEM encoded certificate as a text variable
In the static file configuration of Traefik
```yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
crowdsecLapiTlsCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
...
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
In a dynamic configuration of a provider (ex docker) as a Label
```yaml
services:
whoami-foo:
image: traefik/whoami
labels:
- |
traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTlsCertificateAuthority=
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
...
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
The example tls-auth presents 2 services, foo and bar which comes with the bouncer.
At startup, certificates are created in a shared docker volume by a sidecar container which exits after.
Traefik will use client and CA certificates.
The Bouncer will use server and CA certificates.
The service `whoami-foo` will authenticate with an **API key** over HTTPS after verifying the server certificate with CA.
The service `whoami-bar` will authenticate with a **client certificate** signed by the CA.
Access to a route that communicate via https and authenticate with API-key:
```
curl http://localhost:80/foo
```
Access to a route that communicate via https and authenticate with a client certificate:
```
curl http://localhost:80/bar
```
Access to the traefik dashboard
```
curl http://localhost:8080/dashboard/#/
```
To play the demo environnement run:
```bash
make run_tlsauth
```
Note:
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
@@ -2,11 +2,11 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.4"
image: "traefik:v2.9.6"
container_name: "traefik"
restart: unless-stopped
command:
- "--log.level=DEBUG"
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
@@ -14,11 +14,12 @@ services:
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
# - "--experimental.plugins.bouncer.version=v1.1.5"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./LAPIKEY:/etc/traefik/LAPIKEY:ro
- logs-tls-auth:/var/log/traefik
- crowdsec-certs-tls-auth:/etc/traefik/crowdsec-certs
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
+39
View File
@@ -0,0 +1,39 @@
# Exemple
## Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
You need to configure your Traefik to trust Forwarded headers by your front proxy
In the example we use a whoami container protected by crowdsec, and we ban our IP before allowing using TrustedIPs
If you are using another proxy in front, you need to add its IP in the trusted IP for the forwarded headers.
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
The "internal" Traefik instance is configured to trust the forward headers
```yaml
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
```
We configure the middleware to trust as well as the IP of the intermediate proxy if needed:
```yaml
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
```
Add your IP to the ban list
```bash
docker exec crowdsec cscli decisions add --ip 10.0.10.30 -d 10m
```
You should get a 403 on http://localhost/foo
> Replace *10.0.10.30* by your IP
Add the IPs that will not be filtered by the plugin
```yaml
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clientTrustedips=10.0.10.30/32"
```
> Replace *10.0.10.30/32* by your IP or IP range, so it's not getting checked against ban cache of crowdsec
You should get a 200 on http://localhost/foo even if you are on the ban cache
To play the demo environment run:
```bash
make run_trustedips
```
@@ -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:
@@ -15,7 +15,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.7"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -69,7 +69,7 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:v1.4.3
container_name: "crowdsec"
restart: unless-stopped
environment:
+4 -1
View File
@@ -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.5
)
+2
View File
@@ -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.5 h1:1ubyIpTgIb+dadpILivAxuhZTHOOapEHRhcMakveuYY=
github.com/maxlerebourg/simpleredis v1.0.5/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
+48 -37
View File
@@ -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 (
@@ -16,92 +16,103 @@ const (
cacheNoBannedValue = "f"
)
// CacheMiss error string when cache is miss.
const CacheMiss = "cache:miss"
//nolint:gochecknoglobals
var (
cache = ttl_map.New()
redis simpleredis.SimpleRedis
redisEnabled = false
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 {
return bannedString == cacheBannedValue, nil
}
return false, fmt.Errorf("cache:miss")
return false, fmt.Errorf(CacheMiss)
}
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 {
return bannedString == cacheBannedValue, nil
}
if err.Error() == simpleredis.RedisMiss {
return false, fmt.Errorf(CacheMiss)
}
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)
} else {
deleteDecisionLocalCache(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, pass string) {
if isRedis {
redis.Init(host, pass)
client.cache = &redisCache{}
} else {
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)
}
+38 -162
View File
@@ -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
}
})
}
}
+63 -20
View File
@@ -10,6 +10,8 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
@@ -17,6 +19,7 @@ import (
// Enums for crowdsec mode.
const (
AloneMode = "alone"
StreamMode = "stream"
LiveMode = "live"
NoneMode = "none"
@@ -40,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"`
@@ -47,6 +55,8 @@ type Config struct {
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"`
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
}
func contains(source []string, target string) bool {
@@ -75,6 +85,7 @@ func New() *Config {
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
}
}
@@ -98,19 +109,42 @@ 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.
//
//nolint:gocyclo,gocognit
func ValidateParams(config *Config) error {
if err := validateParamsRequired(config); err != nil {
return err
}
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
return err
}
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
return err
}
if _, err := GetVariable(config, "RedisCachePassword"); 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,
@@ -120,13 +154,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
@@ -141,17 +168,33 @@ 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")
}
// Case https to contact Crowdsec LAPI and certificate must be provided
if config.CrowdsecLapiScheme == HTTPS && !config.CrowdsecLapiTLSInsecureVerify {
err = validateParamsTLS(config)
if err != nil {
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
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 {
if err = validateParamsTLS(config); err != nil {
return err
}
}
return nil
}
// validHeaderFieldByte reports whether b is a valid byte in a header
// field name. RFC 7230 says:
// 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 nil
}
@@ -202,8 +245,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'")
@@ -217,10 +260,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
+33 -1
View File
@@ -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 -1
View File
@@ -23,7 +23,8 @@ type Checker struct {
func NewChecker(trustedIPs []string) (*Checker, error) {
checker := &Checker{}
for _, ipMask := range trustedIPs {
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
View File
@@ -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.
+29
View File
@@ -0,0 +1,29 @@
# simpleredis
Minimal go redis with only `get`, `set` and `delete` operation.
With **NO** extern dependencies.
## Example
```go
import simpleredis "github.com/maxlerebourg/simpleredis"
var redis simpleredis.SimpleRedis
redis.Init("redis:6379", "") // redisHost, redisPass
err := redis.Set("test", []bytes("whatever"), 60), // Set key "test" with "whatever" for 60 seconds
if err != nil {
...
}
val, err := redis.Get("test") // get key test
if err != nil {
// err could be only redis:unreachable, redis:miss or redis:timeout available in simpleredis.RedisUnreachable
...
}
err = redis.Del("test")
if err != nil {
...
}
```
## Author
Max Lerebourg @ [Primadviz.com](https://primadviz.com)
@@ -10,8 +10,6 @@ import (
"net/textproto"
"strings"
"time"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
// Error strings for redis.
@@ -19,10 +17,11 @@ const (
RedisUnreachable = "redis:unreachable"
RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout"
RedisNoAuth = "redis:noauth"
)
// A RedisCmd is used to communicate with redis at low level using commands.
type RedisCmd struct {
// A redisCmd is used to communicate with redis at low level using commands.
type redisCmd struct {
Command string
Name string
Data []byte
@@ -32,7 +31,8 @@ type RedisCmd struct {
// A SimpleRedis is used to communicate with redis.
type SimpleRedis struct {
redisHost string
host string
pass string
}
func genRedisArray(params ...[]byte) []byte {
@@ -47,28 +47,45 @@ 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())
}
}
func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
func askRedis(sr *SimpleRedis, cmd redisCmd, channel chan redisCmd) {
dialer := net.Dialer{Timeout: 2 * time.Second}
conn, err := dialer.Dial("tcp", hostnamePort)
conn, err := dialer.Dial("tcp", sr.host)
if err != nil {
channel <- RedisCmd{Error: fmt.Errorf(RedisUnreachable)}
channel <- redisCmd{Error: fmt.Errorf(RedisUnreachable)}
return
}
defer func() {
if err := conn.Close(); err != nil {
logger.Error(fmt.Sprintf("redis:connClose %s", err.Error()))
fmt.Printf("redis:connClose %s", err.Error())
}
}()
writer := textproto.NewWriter(bufio.NewWriter(conn))
reader := textproto.NewReader(bufio.NewReader(conn))
if sr.pass != "" {
data := genRedisArray([]byte("AUTH"), []byte(sr.pass))
send(writer, "auth", data)
for {
select {
case <-time.After(time.Second * 1):
channel <- redisCmd{Error: fmt.Errorf(RedisTimeout)}
return
default:
read, _ := reader.ReadLineBytes()
if string(read) != "+OK" {
channel <- redisCmd{Error: fmt.Errorf(RedisNoAuth)}
return
}
break
}
}
}
switch cmd.Command {
case "SET":
data := genRedisArray([]byte("SET"), []byte(cmd.Name), cmd.Data, []byte("EX"), []byte(fmt.Sprintf("%d", cmd.Duration)))
@@ -82,16 +99,20 @@ func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
for {
select {
case <-time.After(time.Second * 1):
channel <- RedisCmd{Error: fmt.Errorf(RedisTimeout)}
channel <- redisCmd{Error: fmt.Errorf(RedisTimeout)}
return
default:
read, _ := reader.ReadLineBytes()
if string(read) != "$1" {
channel <- RedisCmd{Error: fmt.Errorf(RedisMiss)}
str := string(read)
if strings.Contains(str, "-NOAUTH") {
channel <- redisCmd{Error: fmt.Errorf(RedisNoAuth)}
return
} else if str != "$1" {
channel <- redisCmd{Error: fmt.Errorf(RedisMiss)}
return
}
read, _ = reader.ReadLineBytes()
channel <- RedisCmd{Data: read}
channel <- redisCmd{Data: read}
return
}
}
@@ -99,18 +120,19 @@ func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
}
// Init sets the redisHost used to connect to redis.
func (sr *SimpleRedis) Init(redisHost string) {
sr.redisHost = redisHost
func (sr *SimpleRedis) Init(host string, pass string) {
sr.host = host
sr.pass = pass
}
// Get fetches the value for key name in redis.
func (sr *SimpleRedis) Get(name string) ([]byte, error) {
redisCmd := RedisCmd{
cmd := redisCmd{
Command: "GET",
Name: name,
}
channel := make(chan RedisCmd)
go askRedis(sr.redisHost, redisCmd, channel)
channel := make(chan redisCmd)
go askRedis(sr, cmd, channel)
resp := <-channel
if resp.Error != nil {
return nil, resp.Error
@@ -120,22 +142,22 @@ func (sr *SimpleRedis) Get(name string) ([]byte, error) {
// Set updates the value for key name in redis with value data for duration.
func (sr *SimpleRedis) Set(name string, data []byte, duration int64) error {
redisCmd := RedisCmd{
cmd := redisCmd{
Command: "SET",
Name: name,
Data: data,
Duration: duration,
}
go askRedis(sr.redisHost, redisCmd, nil)
go askRedis(sr, cmd, nil)
return nil
}
// Del removes the key name in redis.
func (sr *SimpleRedis) Del(name string) error {
redisCmd := RedisCmd{
cmd := redisCmd{
Command: "DEL",
Name: name,
}
go askRedis(sr.redisHost, redisCmd, nil)
go askRedis(sr, cmd, nil)
return nil
}
+3
View File
@@ -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.5
## explicit; go 1.19
github.com/maxlerebourg/simpleredis