Compare commits

...
14 Commits
Author SHA1 Message Date
maxlerebourg 123cf15434 Add CrowdsecAppsecUnreachableBlock (#175)
*  Add CrowdsecAppsecUnreachableBlock

* 🍱 update readme

* 🍱 fix lint

* 🍱 fix lint
2024-06-09 10:59:45 +02:00
mathieuHa f89c5e25a9 ✏️ fix(validation) Replace typo on CaptchaProvider config validation (#176) 2024-06-09 10:47:17 +02:00
maxlerebourg 58946d9fa2 📄 Update README.md for lang doc (#170) 2024-05-22 11:24:11 +02:00
mathieuHaandMax Lerebourg 6187a722ca 167 feature update to go 122 (#168)
* ⬆️ Upgrade golang version

* 🚨 Optimize Lint for strings

* 🔒️ Add allow list of packages

* 🚨 Fix final lint

* 👷 Update ci

* 🍱 upgrade dependencies

* 🍱 fix comment

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2024-05-18 13:20:14 +02:00
maxlerebourgandmax.lerebourg 70ad0365f0 🐛 fix content-type header for ban and captcha page (#166)
* 🍱 fix content-type header for ban and captcha page

* 🍱 Add comment to warn future developer

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-05-16 18:40:28 +02:00
mathieuHa 8975216af8 📝 Update readme on loglevel written to stdout/stderr (#164) 2024-05-16 09:32:14 +02:00
mathieuHa e6b10b8ac4 ⬆️ 📝 Update exemples and version traefik 3, crowdsec 1.6.1-2, plugin … (#160)
* ⬆️ 📝 Update exemples and version traefik 3, crowdsec 1.6.1-2, plugin 1.3.0

* 📝 🐛 Fix race in exemple and doc for tls
2024-05-01 20:03:19 +02:00
mathieuHa 1f11531509 📝 Update doc for appsec with required collection and upgrade images (#158) 2024-05-01 19:50:00 +02:00
mathieuHaandmax.lerebourg ee97250acf Add grace period to reach LAPI without blocking further queries (#153)
*  Add grace period to reach LAPI without blocking further queries

* 🐛 Fix config validation for maxFailedStreamUpdate

* 🚨 Fix some lint issue

* 🚨 Bypass lint complexity on ServeHTTP

* 🍱 fix and improve

* 🚨 Fix lint

* 🚨 Fix lint

* 🐛 Fix logic for update max failure

* 📝 Update doc and docker compose local reset

* 🍱 fix log nightmare

* 🍱 fix

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-05-01 18:07:44 +02:00
mathieuHaandmaxlerebourg b6a0404efd Add user-agent header from crowdsec (#149)
*  Add user-agent header from crowdsec

* 🐛 : https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/issues/151

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2024-04-16 13:26:46 +02:00
mathieuHa a0e701f985 📝 Update typo on doc for ban custom page (#147) 2024-04-05 09:30:49 +02:00
mathieuHaandmax.lerebourg b8dd883bb6 130 feature custom html error page integration on 403 like captcha but for ban (#145)
*  Add ban html template

* 📝 Add doc for custom ban page

* ⚰️ Remove old code due to merge

* ✏️ Fix merge remaining in html

*  render banTemplate from html/template to string

* 🚨 : fix lint

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-04-04 21:32:31 +02:00
cb4f95d5d0 📝 Add first diagrams (#144)
* 📝 Add first diagrams

* 📝 Add diagram for none mode

* 📝 Add diagrams for each mode

* 📝 Fix rendering

* 📝 Fix chart rendering

* 🍱 fix readme

* 🍱 fix readme

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-04-04 07:26:30 +02:00
mathieuHaandMax Lerebourg 615e7ccf69 Add ban html template (#142)
*  Add ban html template

* 📝 Add doc for custom ban page

* 🍱 fix Mathieu work

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2024-04-03 17:58:18 +02:00
40 changed files with 1352 additions and 270 deletions
-47
View File
@@ -1,47 +0,0 @@
name: Go Matrix
on: [push, pull_request]
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ 1.19, 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v2
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test
run: go test -v -cover ./...
+3 -3
View File
@@ -12,9 +12,9 @@ jobs:
name: Main Process
runs-on: ubuntu-latest
env:
GO_VERSION: 1.19
GOLANGCI_LINT_VERSION: v1.50.0
YAEGI_VERSION: v0.14.2
GO_VERSION: 1.22
GOLANGCI_LINT_VERSION: v1.57.2
YAEGI_VERSION: v0.16.1
CGO_ENABLED: 0
defaults:
run:
+25 -5
View File
@@ -1,15 +1,11 @@
run:
timeout: 3m
skip-files: []
skip-dirs: []
linters-settings:
govet:
enable-all: true
disable:
- fieldalignment
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
goconst:
@@ -25,6 +21,31 @@ linters-settings:
- FIXME
gofumpt:
extra-rules: true
depguard:
rules:
Main:
files:
- $all
- "!$test"
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
- github.com/leprosus/golang-ttl-map
- github.com/maxlerebourg/simpleredis
Test:
files:
- $test
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
linters:
enable-all: true
@@ -67,7 +88,6 @@ linters:
issues:
exclude-use-default: false
max-per-linter: 0
max-same-issues: 0
exclude:
- "G402: TLS InsecureSkipVerify may be true."
+30 -16
View File
@@ -26,25 +26,28 @@ run_local:
docker compose -f docker-compose.local.yml up -d --remove-orphans
run_behindproxy:
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
docker compose -f examples/behind-proxy/docker-compose.yml up -d --remove-orphans
run_cacheredis:
docker compose -f examples/redis-cache/docker-compose.redis.yml up -d --remove-orphans
docker compose -f examples/redis-cache/docker-compose.yml up -d --remove-orphans
run_trustedips:
docker compose -f examples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans
docker compose -f examples/trusted-ips/docker-compose.yml up -d --remove-orphans
run_binaryvm:
cd examples/binary-vm/ && sudo vagrant up
run_tlsauth:
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml up -d && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml restart && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml logs -f
docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans
run_appsec:
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml up -d
docker compose -f examples/appsec-enabled/docker-compose.yml up -d
run_captcha:
docker compose -f examples/captcha/docker-compose.captcha.yml up -d
docker compose -f examples/captcha/docker-compose.yml up -d
run_custom_ban_page:
docker compose -f examples/custom-ban-page/docker-compose.yml up -d
run:
docker compose -f docker-compose.yml up -d --remove-orphans
@@ -59,19 +62,25 @@ restart:
docker compose -f docker-compose.yml restart
restart_behindproxy:
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml restart
docker compose -f examples/behind-proxy/docker-compose.yml restart
restart_cacheredis:
docker compose -f examples/redis-cache/docker-compose.redis.yml restart
docker compose -f examples/redis-cache/docker-compose.yml restart
restart_trustedips:
docker compose -f examples/trusted-ips/docker-compose.trusted.yml restart
docker compose -f examples/trusted-ips/docker-compose.yml restart
restart_tlsauth:
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml
docker compose -f examples/tls-auth/docker-compose.yml
restart_appsec:
docker compose -f examples/tls-auth/docker-compose.appsec-enabled.yml
docker compose -f examples/tls-auth/docker-compose.yml
restart_captcha:
docker compose -f examples/captcha/docker-compose.yml
restart_custombanpage:
docker compose -f examples/custom-ban-page/docker-compose.yml
show_logs:
docker compose -f docker-compose.yml restart
@@ -83,11 +92,13 @@ show_dev_logs:
docker compose -f docker-compose.dev.yml logs -f
clean_all_docker:
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
docker compose -f examples/redis-cache/docker-compose.redis.yml down --remove-orphans
docker compose -f examples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down --remove-orphans
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml down --remove-orphans
docker compose -f examples/behind-proxy/docker-compose.yml down --remove-orphans
docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans
docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans
docker compose -f examples/tls-auth/docker-compose.yml down --remove-orphans
docker compose -f examples/appsec-enabled/docker-compose.yml down --remove-orphans
docker compose -f examples/captcha/docker-compose.yml down --remove-orphans
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
docker compose -f docker-compose.local.yml down --remove-orphans
docker compose -f docker-compose.yml down --remove-orphans
@@ -98,3 +109,6 @@ clean_vagrant:
show_metrics:
docker exec crowdsec cscli metrics
show_decisions:
docker exec crowdsec cscli decisions list
+267 -10
View File
@@ -36,15 +36,15 @@ On successfull completion, he will be cleaned for a specified period of time bef
The following captcha providers are supported now:
- [hcaptcha](https://www.hcaptcha.com/)
- [recaptcha](https://www.google.com/recaptcha/about/)
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
- [turnstile](https://www.cloudflare.com/products/turnstile/)
There are 4 operating modes (CrowdsecMode) for this plugin:
There are 5 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. |
| 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 locally banned IP, but can work without a crowdsec service. |
| appsec | Disable Crowdsec IP checking but apply Crowdsec Appsec checking. This mode is intended to be used when Crowdsec IP checking is applied at the Firewall Level. |
@@ -53,6 +53,247 @@ The `streaming mode` is recommended for performance, decisions are updated every
The cache can be local to Traefik using the filesystem, or a separate Redis instance.
Below are Mermaid diagrams detailling how each mode work:
<details><summary>Mode none workflow</summary>
> A Ban decision exists in CrowdsecLAPI
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant CrowdsecLAPI
TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a Crowdsec Decision ?
Destroy CrowdsecLAPI
CrowdsecLAPI-->>TraefikPlugin: Yes a ban Decision
TraefikPlugin->>User: No, HTTP 403
```
> No decision in CrowdsecLAPI
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant CrowdsecLAPI
TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a crowdsec decision ?
Destroy CrowdsecLAPI
CrowdsecLAPI-->>TraefikPlugin: Nothing, all good!
Destroy TraefikPlugin
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
Webserver->>User: HTTP Response
```
</details>
<details><summary>Mode live workflow</summary>
> A Ban decision exists in CrowdsecLAPI but not in cache
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
PluginCache-->>TraefikPlugin: Nothing, all good!
create participant CrowdsecLAPI
TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a crowdsec decision ?
Destroy CrowdsecLAPI
CrowdsecLAPI-->>TraefikPlugin: Yes a ban Decision
TraefikPlugin-->>PluginCache: Store the information for this IP for DefaultDecisionSeconds
Destroy PluginCache
PluginCache-->>TraefikPlugin: Done
TraefikPlugin->>User: No, HTTP 403
```
> No decision in cache
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
PluginCache-->>TraefikPlugin: Nothing, all good!
create participant CrowdsecLAPI
TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a crowdsec decision ?
Destroy CrowdsecLAPI
CrowdsecLAPI-->>TraefikPlugin: Nothing, all good!
TraefikPlugin-->>PluginCache: Store the information for this IP for DefaultDecisionSeconds
Destroy PluginCache
PluginCache-->>TraefikPlugin: Done
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
Webserver->>User: HTTP Response
```
</details>
<details><summary>Mode stream workflow</summary>
> Cache Synchronization every UpdateIntervalSeconds
```mermaid
sequenceDiagram
participant TraefikPlugin
participant CrowdsecLAPI
TraefikPlugin->>CrowdsecLAPI: What are the current decisions
Destroy CrowdsecLAPI
CrowdsecLAPI->>TraefikPlugin: Here is the list
create participant PluginCache
TraefikPlugin-->>PluginCache: Store this list
Destroy PluginCache
PluginCache-->>TraefikPlugin: Done
```
> A Ban decision exists in cache
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
Destroy PluginCache
PluginCache-->>TraefikPlugin: Yes a ban decision
Destroy TraefikPlugin
TraefikPlugin->>User: No, HTTP 403
```
> No decision in cache
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
Destroy PluginCache
PluginCache-->>TraefikPlugin: Nothing, all good!
Destroy TraefikPlugin
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
Webserver->>User: HTTP Response
```
</details>
<details><summary>Mode alone Workflow</summary>
> Cache Synchronization every 2 hours to the Crowdsec Central API
```mermaid
sequenceDiagram
participant TraefikPlugin
participant CrowdsecCAPI
TraefikPlugin->>CrowdsecCAPI: What are the current decisions from CAPI
Destroy CrowdsecCAPI
CrowdsecCAPI->>TraefikPlugin: Here is the list
create participant PluginCache
TraefikPlugin-->>PluginCache: Store this list
Destroy PluginCache
PluginCache-->>TraefikPlugin: Done
```
> A Ban decision exists in cache
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
Destroy PluginCache
PluginCache-->>TraefikPlugin: Yes a ban decision
Destroy TraefikPlugin
TraefikPlugin->>User: No, HTTP 403
```
> No decision in cache
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
Destroy PluginCache
PluginCache-->>TraefikPlugin: Nothing, all good!
Destroy TraefikPlugin
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
Webserver->>User: HTTP Response
```
</details>
<details><summary>Mode appsec workflow</summary>
> The request is detected as malicious
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant CrowdsecAppSec
TraefikPlugin-->>CrowdsecAppSec: Is this request malicious ?
Destroy CrowdsecAppSec
CrowdsecAppSec-->>TraefikPlugin: Yes I think so
Destroy TraefikPlugin
TraefikPlugin->>User: No, HTTP 403
```
> The request is not detected as malicious
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant CrowdsecAppSec
TraefikPlugin-->>CrowdsecAppSec: Is this request malicious ?
Destroy CrowdsecAppSec
CrowdsecAppSec-->>TraefikPlugin: No I don't think so
Destroy TraefikPlugin
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
Webserver->>User: HTTP Response
```
</details>
<details><summary>Captcha decision workflow</summary>
```mermaid
sequenceDiagram
participant User
participant TraefikPlugin
User->>TraefikPlugin: Can I access that webpage
create participant PluginCache
TraefikPlugin-->>PluginCache: Does the User IP has a Crowdsec Decision ?
PluginCache-->>TraefikPlugin: Yes a Catpcha Decision
TraefikPlugin->>User: Please complete this captcha
User->>TraefikPlugin: Fine, done!
create participant ProviderCaptcha
TraefikPlugin-->>ProviderCaptcha: Is the validation OK ?
Destroy ProviderCaptcha
ProviderCaptcha-->>TraefikPlugin: Yes
TraefikPlugin-->>PluginCache: Set the User IP Clean for captchaGracePeriodSeconds
Destroy PluginCache
PluginCache-->>TraefikPlugin: Done
Destroy TraefikPlugin
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
Webserver->>User: HTTP Response
```
</details>
## Usage
@@ -76,7 +317,7 @@ Only one instance of the plugin is *possible*.
- Enable the plugin
- LogLevel
- string
- default: `INFO`, expected values are: `INFO`, `DEBUG`
- default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`, log are written to `stdout` / `stderr`
- CrowdsecMode
- string
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
@@ -92,6 +333,10 @@ Only one instance of the plugin is *possible*.
- bool
- default: true
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
- CrowdsecAppsecUnreachableBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server is unreachable.
- CrowdsecLapiScheme
- string
- default: `http`, expected values are: `http`, `https`
@@ -155,6 +400,10 @@ Only one instance of the plugin is *possible*.
- int64
- default: 60
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
- UpdateMaxFailure
- int64
- default: 0
- Used only in `stream` and `alone` mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
- DefaultDecisionSeconds
- int64
- default: 60
@@ -177,15 +426,18 @@ Only one instance of the plugin is *possible*.
- CaptchaSecretKey
- string
- Site secret key for the captcha provider
- CaptchaHTMLFilePath
- string
- default: /captcha.html
- Path where the captcha template is stored
- CaptchaGracePeriodSeconds
- int64
- default: 1800 (= 30 minutes)
- Period after validation of a captcha before a new validation is required if Crowdsec decision is still valid
- CaptchaHTMLFilePath
- string
- default: /captcha.html
- Path where the captcha template is stored
- BanHTMLFilePath
- string
- default: ""
- Path where the ban html file is stored (default empty ""=disabled)
### Configuration
@@ -231,12 +483,14 @@ http:
enabled: false
logLevel: DEBUG
updateIntervalSeconds: 60
updateMaxFailure: 0
defaultDecisionSeconds: 60
httpTimeoutSeconds: 10
crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
@@ -285,11 +539,12 @@ http:
captchaSecretKey: FIXME
captchaGracePeriodSeconds: 1800
captchaHTMLFilePath: /captcha.html
banHTMLFilePath: /ban.html
```
#### Fill variable with value of file
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword` and `CrowdsecLapiKey` can be provided with the content as raw or through a file path that Traefik can read.
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey` and `CaptchaSecretKey` 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:
@@ -378,6 +633,8 @@ docker exec crowdsec cscli decisions remove --ip 10.0.0.10 -t captcha
#### 9. Using Traefik with Captcha remediation feature enabled [examples/captcha/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/captcha/README.md)
#### 10. Using Traefik with Custom Ban HTML Page [examples/custom-ban-page/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/custom-ban-page/README.md)
### Local Mode
Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
+329
View File
@@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CrowdSec Access Forbidden</title>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box
}
:after,
:before {
--tw-content: ""
}
html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
body {
line-height: inherit;
margin: 0
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit
}
a {
color: inherit;
text-decoration: inherit
}
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
pre {
margin: 0
}
*,
::backdrop,
:after,
:before {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #3b82f680;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia:
}
.flex {
display: flex
}
.flex-wrap {
flex-wrap: wrap
}
.inline-flex {
display: inline-flex
}
.h-24 {
height: 6rem
}
.h-6 {
height: 1.5rem
}
.h-full {
height: 100%
}
.h-screen {
height: 100vh
}
.text-center {
text-align: center
}
.w-24 {
width: 6rem
}
.w-6 {
width: 1.5rem
}
.w-full {
width: 100%
}
.w-screen {
width: 100vw
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem
}
.flex-col {
flex-direction: column
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.space-y-1>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
}
.space-y-4>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
}
.rounded-xl {
border-radius: .75rem
}
.border-2 {
border-width: 2px
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0/var(--tw-border-opacity))
}
.p-4 {
padding: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-bottom: .5rem;
padding-top: .5rem
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
@media (min-width:640px) {
.sm\:w-2\/3 {
width: 66.666667%
}
}
@media (min-width:768px) {
.md\:flex-row {
flex-direction: row
}
}
@media (min-width:1024px) {
.lg\:w-1\/2 {
width: 50%
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem
}
.lg\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
}
@media (min-width:1280px) {
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
}
</style>
</head>
<body class="h-screen w-screen p-4">
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
class="warning">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
</path>
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Access Forbidden</h1>
</div>
<div class="flex justify-center flex-wrap">
<p class="my-3">This security check has been powered by</p>
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
<defs>
<clipPath id="a">
<path d="M0 52h84v201.2H0zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M170 52h84.4v201.2H170zm0 0" />
</clipPath>
</defs>
<path
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
<path
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
<g clip-path="url(#a)">
<path
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
</g>
<g clip-path="url(#b)">
<path
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
</g>
<path
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
</svg>
<span>CrowdSec</span>
</a>
</div>
</div>
</div>
</body>
</html>
+71 -31
View File
@@ -7,6 +7,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -28,6 +29,7 @@ const (
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
crowdsecAppsecUserAgent = "X-Crowdsec-Appsec-User-Agent"
crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
@@ -42,6 +44,7 @@ const (
var (
isStartup = true
isCrowdsecStreamHealthy = true
updateFailure = 0
ticker chan bool
)
@@ -60,6 +63,7 @@ type Bouncer struct {
appsecEnabled bool
appsecHost string
appsecFailureBlock bool
appsecUnreachableBlock bool
crowdsecScheme string
crowdsecHost string
crowdsecKey string
@@ -68,10 +72,12 @@ type Bouncer struct {
crowdsecPassword string
crowdsecScenarios []string
updateInterval int64
updateMaxFailure int
defaultDecisionTimeout int64
customHeader string
crowdsecStreamRoute string
crowdsecHeader string
banTemplateString string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
@@ -81,11 +87,11 @@ type Bouncer struct {
}
// New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
log := logger.New(config.LogLevel)
err := configuration.ValidateParams(config)
if err != nil {
log.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
log.Error("New:validateParams " + err.Error())
return nil, err
}
@@ -108,17 +114,29 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
if err != nil {
log.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
return nil, err
}
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, err
}
config.CrowdsecLapiKey = apiKey
}
var banTemplateString string
if config.BanHTMLFilePath != "" {
var buf bytes.Buffer
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
err = banTemplate.Execute(&buf, nil)
if err != nil {
log.Error("New:banTemplate is bad formatted " + err.Error())
return nil, err
}
banTemplateString = buf.String()
}
bouncer := &Bouncer{
next: next,
name: name,
@@ -129,6 +147,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey,
@@ -136,8 +155,10 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure,
customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
log: log,
@@ -191,7 +212,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode {
if err := getToken(bouncer); err != nil {
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error()))
bouncer.log.Error("New:getToken " + err.Error())
return nil, err
}
}
@@ -201,7 +222,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
handleStreamTicker(bouncer)
})
}
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
bouncer.log.Debug("New initialized mode:" + config.CrowdsecMode)
return bouncer, nil
}
@@ -219,13 +240,13 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
handleBanServeHTTP(bouncer, rw)
return
}
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
handleBanServeHTTP(bouncer, rw)
return
}
// if our IP is in the trusted list we bypass the next checks
@@ -248,7 +269,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
if cacheErrString != cache.CacheMiss {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
rw.WriteHeader(http.StatusForbidden)
handleBanServeHTTP(bouncer, rw)
return
}
} else {
@@ -256,7 +277,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if value == cache.NoBannedValue {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else {
handleErrorServeHTTP(bouncer, remoteIP, value, rw, req)
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
}
return
}
@@ -267,8 +288,8 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isCrowdsecStreamHealthy {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else {
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden)
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
handleBanServeHTTP(bouncer, rw)
}
} else {
value, err := handleNoStreamCache(bouncer, remoteIP)
@@ -276,7 +297,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else {
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
handleErrorServeHTTP(bouncer, remoteIP, value, rw, req)
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
}
}
}
@@ -309,8 +330,19 @@ type Login struct {
Expire string `json:"expire"`
}
func handleErrorServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
bouncer.log.Debug(fmt.Sprintf("handleErrorServeHTTP ip:%s remediation:%s", remoteIP, remediation))
// To append Headers we need to call rw.WriteHeader after set any header.
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
if bouncer.banTemplateString == "" {
rw.WriteHeader(http.StatusForbidden)
return
}
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, bouncer.banTemplateString)
}
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue {
if bouncer.captchaClient.Check(remoteIP) {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
@@ -319,14 +351,14 @@ func handleErrorServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw htt
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
return
}
rw.WriteHeader(http.StatusForbidden)
handleBanServeHTTP(bouncer, rw)
}
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
if bouncer.appsecEnabled {
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
handleBanServeHTTP(bouncer, rw)
return
}
}
@@ -335,10 +367,15 @@ func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWrit
func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false
bouncer.log.Error(err.Error())
bouncer.log.Debug(fmt.Sprintf("handleStreamTicker updateFailure:%d isCrowdsecStreamHealthy:%t %s", updateFailure, isCrowdsecStreamHealthy, err.Error()))
if bouncer.updateMaxFailure != -1 && updateFailure >= bouncer.updateMaxFailure && isCrowdsecStreamHealthy {
isCrowdsecStreamHealthy = false
bouncer.log.Error(fmt.Sprintf("handleStreamTicker:error updateFailure:%d %s", updateFailure, err.Error()))
}
updateFailure++
} else {
isCrowdsecStreamHealthy = true
updateFailure = 0
}
}
@@ -409,7 +446,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
case "captcha":
value = cache.CaptchaValue
default:
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
}
if isLiveMode {
durationSecond := int64(duration.Seconds())
@@ -418,7 +455,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
}
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
}
return value, fmt.Errorf("handleNoStreamCache:banned")
return value, errors.New("handleNoStreamCache:banned")
}
func getToken(bouncer *Bouncer) error {
@@ -434,7 +471,6 @@ func getToken(bouncer *Bouncer) error {
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 {
@@ -484,7 +520,7 @@ func handleStreamCache(bouncer *Bouncer) error {
case "captcha":
value = cache.CaptchaValue
default:
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
}
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
}
@@ -493,7 +529,6 @@ func handleStreamCache(bouncer *Bouncer) error {
bouncer.cacheClient.Delete(decision.Value)
}
bouncer.log.Debug("handleStreamCache:updated")
isCrowdsecStreamHealthy = true
return nil
}
@@ -513,11 +548,11 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
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)
return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
bouncer.log.Error("crowdsecQuery:closeBody " + err.Error())
}
}()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
@@ -564,21 +599,26 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
req.Header.Set(crowdsecAppsecIPHeader, ip)
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.Path)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
res, err := bouncer.httpClient.Do(req)
if err != nil {
return fmt.Errorf("appsecQuery %w", err)
bouncer.log.Error("appsecQuery:unreachable")
if bouncer.appsecUnreachableBlock {
return fmt.Errorf("appsecQuery:unreachable %w", err)
}
return nil
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
bouncer.log.Error("appsecQuery:closeBody " + err.Error())
}
}()
if res.StatusCode == http.StatusInternalServerError {
bouncer.log.Debug("crowdsecQuery statusCode:500")
bouncer.log.Info("appsecQuery:failure")
if bouncer.appsecFailureBlock {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
return errors.New("appsecQuery statusCode:500")
}
return nil
}
+2 -2
View File
@@ -18,7 +18,7 @@ func TestServeHTTP(t *testing.T) {
cfg.CrowdsecLapiKey = "test"
ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})
handler, err := New(ctx, next, cfg, "demo-plugin")
if err != nil {
@@ -93,7 +93,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run(tt.name, func(_ *testing.T) {
bouncer := &Bouncer{
next: tt.fields.next,
name: tt.fields.name,
+8 -5
View File
@@ -1,6 +1,6 @@
services:
traefik:
image: "traefik:v2.11.0"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -16,6 +16,8 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-local:/var/log/traefik
- './ban.html:/ban.html:ro'
- './captcha.html:/captcha.html:ro'
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
@@ -31,9 +33,9 @@ services:
- "traefik.enable=true"
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
whoami2:
image: traefik/whoami
container_name: "simple-service-bar"
@@ -47,14 +49,15 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=stream"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
crowdsec:
image: crowdsecurity/crowdsec:1.6.0
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
volumes:
+7 -5
View File
@@ -1,6 +1,6 @@
services:
traefik:
image: "traefik:v2.11.0"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -12,9 +12,11 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.15"
- "--experimental.plugins.bouncer.version=v1.3.0"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
# - './ban.html:/ban.html:ro'
# - './captcha.html:/captcha.html:ro'
- "logs:/var/log/traefik"
ports:
- 8000:80
@@ -34,7 +36,7 @@ services:
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
whoami2:
image: traefik/whoami
container_name: "simple-service-bar"
@@ -57,11 +59,11 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec
# We need to register one api key per service we will use
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.10.7"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,12 +15,12 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.2.0"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-appsec-enabled:/var/log/traefik
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
@@ -51,11 +51,11 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
@@ -1,8 +1,6 @@
version: "3.8"
services:
cloudflare:
image: "traefik:v2.10.4"
image: "traefik:v3.0.0"
container_name: "cloudflare"
restart: unless-stopped
command:
@@ -21,7 +19,7 @@ services:
- 8080:8080
traefik:
image: "traefik:v2.10.4"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -35,7 +33,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.13"
- "--experimental.plugins.bouncer.version=v1.3.0"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-traefik:/var/log/traefik
@@ -47,7 +45,7 @@ services:
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo"
container_name: "simple-service-foo-cloudflare"
restart: unless-stopped
labels:
- "traefik.enable=true"
@@ -66,7 +64,7 @@ services:
whoami-bar:
image: traefik/whoami
container_name: "simple-service-bar"
container_name: "simple-service-bar-cloudflare"
restart: unless-stopped
labels:
- "traefik.enable=true"
@@ -81,7 +79,7 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.5.2
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
@@ -2,7 +2,7 @@
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.10.4/traefik_v2.10.4_linux_amd64.tar.gz"
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v3.0.0/traefik_v3.0.0_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,7 +1,7 @@
#!/bin/bash
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.9.0/whoami_v1.9.0_linux_amd64.tar.gz"
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.10.1/whoami_v1.10.1_linux_amd64.tar.gz"
tar -zxvf whoami.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./whoami /usr/local/bin/
+2 -2
View File
@@ -32,7 +32,7 @@ It is not directly accessible from Traefik even when importing the plugin, so [d
```yaml
...
traefik:
image: "traefik:v2.11.0"
image: "traefik:v3.0.0"
volumes:
- './captcha.html:/captcha.html'
...
@@ -52,7 +52,7 @@ More information is available on configuring Crowdsec in the [official documenta
```yaml
...
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
image: crowdsecurity/crowdsec:v1.6.1-2
volumes:
# For captcha and ban mixed decision
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
@@ -1,6 +1,6 @@
services:
traefik:
image: "traefik:v2.11.0"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -12,14 +12,14 @@ 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.2.0"
- "--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.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-captcha-enabled:/var/log/traefik
- './captcha.html:/captcha.html'
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
@@ -55,7 +55,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
+47
View File
@@ -0,0 +1,47 @@
# Example
## Adding a custom ban page
Traefik can return a custom HTML ban page along with the 403 HTTP response code.
This can be usefull as some browser (Firefox for instance) return a 403 blank webpage and we can mistake a server/reverse-proxy error with a ban from Crowdsec.
### Traefik configuration
```yaml
labels:
# Define ban HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
```
The ban HTML file must be present in the Traefik container (bind mounted or added during a custom build).
It is not directly accessible from Traefik even when importing the plugin, so [download](https://raw.githubusercontent.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/master/ban.html) it locally to expose it to Traefik.
```yaml
...
traefik:
image: "traefik:v2.11.0"
volumes:
- './ban.html:/ban.html'
...
```
## Exemple navigation
We can try to query normally the whoami server:
```bash
curl http://localhost:8000/foo
```
We can try to ban ourself
```bash
docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 4h --type ban
```
![image decision ban](image_decision_ban.png)
We will see in the browser the ban custom page:
To play the demo environment run:
```bash
make run_custom_ban_page
```
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
+330
View File
@@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CrowdSec Access Forbidden</title>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box
}
:after,
:before {
--tw-content: ""
}
html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
body {
line-height: inherit;
margin: 0
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit
}
a {
color: inherit;
text-decoration: inherit
}
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
pre {
margin: 0
}
*,
::backdrop,
:after,
:before {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #3b82f680;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia:
}
.flex {
display: flex
}
.flex-wrap {
flex-wrap: wrap
}
.inline-flex {
display: inline-flex
}
.h-24 {
height: 6rem
}
.h-6 {
height: 1.5rem
}
.h-full {
height: 100%
}
.h-screen {
height: 100vh
}
.text-center {
text-align: center
}
.w-24 {
width: 6rem
}
.w-6 {
width: 1.5rem
}
.w-full {
width: 100%
}
.w-screen {
width: 100vw
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem
}
.flex-col {
flex-direction: column
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.space-y-1>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
}
.space-y-4>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
}
.rounded-xl {
border-radius: .75rem
}
.border-2 {
border-width: 2px
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0/var(--tw-border-opacity))
}
.p-4 {
padding: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-bottom: .5rem;
padding-top: .5rem
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
@media (min-width:640px) {
.sm\:w-2\/3 {
width: 66.666667%
}
}
@media (min-width:768px) {
.md\:flex-row {
flex-direction: row
}
}
@media (min-width:1024px) {
.lg\:w-1\/2 {
width: 50%
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem
}
.lg\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
}
@media (min-width:1280px) {
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
}
</style>
</head>
<body class="h-screen w-screen p-4">
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
class="warning">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
</path>
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Access Forbidden</h1>
</div>
<div class="flex justify-center flex-wrap">
<p class="my-3">This security check has been powered by</p>
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
<defs>
<clipPath id="a">
<path d="M0 52h84v201.2H0zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M170 52h84.4v201.2H170zm0 0" />
</clipPath>
</defs>
<path
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
<path
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
<g clip-path="url(#a)">
<path
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
</g>
<g clip-path="url(#b)">
<path
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
</g>
<path
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
</svg>
<span>CrowdSec</span>
</a>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,67 @@
services:
traefik:
image: "traefik:v3.0.0"
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.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-custom-ban-page-enabled:/var/log/traefik
- './ban.html:/ban.html'
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# Define ban HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- './acquis.yaml:/etc/crowdsec/acquis.yaml:ro'
- logs-custom-ban-page-enabled:/var/log/traefik:ro
- crowdsec-db-custom-ban-page-enabled:/var/lib/crowdsec/data/
- crowdsec-config-custom-ban-page-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-custom-ban-page-enabled:
crowdsec-db-custom-ban-page-enabled:
crowdsec-config-custom-ban-page-enabled:
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+3 -1
View File
@@ -32,13 +32,15 @@ minikube start
helm repo add traefik https://traefik.github.io/charts
helm repo update
kubectl create ns traefik
helm upgrade --version v26.1.0 --install --namespace=traefik \
helm upgrade --version v28.0.0 --install --namespace=traefik \
--values=./traefik/values.yml \
traefik traefik/traefik
```
_A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_
_v28.0.0 of the Traefik helm chart is only compatible with v3 of Traefik_
#### View the Traefik dashboard
> Port forward the dashboard:
+1 -1
View File
@@ -1,5 +1,5 @@
image:
tag: v1.6.0
tag: v1.6.1-2
agent:
acquisition:
+1 -1
View File
@@ -1,5 +1,5 @@
# dashboard.yaml
apiVersion: traefik.containo.us/v1alpha1
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dashboard
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: traefik.containo.us/v1alpha1
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: bouncer
+2 -2
View File
@@ -1,5 +1,5 @@
image:
tag: v2.11
tag: v3.0.0
logs:
general:
@@ -14,4 +14,4 @@ experimental:
plugins:
bouncer:
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.2.0"
version: "v1.3.0"
@@ -1,8 +1,6 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.4"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,7 +13,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -52,7 +50,7 @@ services:
whoami-redis-secure:
image: traefik/whoami
container_name: "simple-service-bar"
container_name: "simple-service-bar-redis"
restart: unless-stopped
labels:
- "traefik.enable=true"
@@ -73,7 +71,7 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.5.2
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
@@ -1,8 +1,6 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.4"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,16 +13,18 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo"
container_name: "simple-service-foo-alone"
restart: unless-stopped
labels:
- "traefik.enable=true"
@@ -37,9 +37,7 @@ services:
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=Password2"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
volumes:
logs-local:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ubuntu:22.04
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl wget
RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && VNUMBER=${VERSION#"v"} && wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl && chmod +x cfssl && mv cfssl /usr/local/bin
+3 -4
View File
@@ -72,11 +72,11 @@ The service `whoami-bar` will authenticate with a **client certificate** signed
Access to a route that communicate via https and authenticate with API-key:
```
curl http://localhost:80/foo
curl http://localhost:8000/foo
```
Access to a route that communicate via https and authenticate with a client certificate:
```
curl http://localhost:80/bar
curl http://localhost:8000/bar
```
Access to the traefik dashboard
```
@@ -89,5 +89,4 @@ 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"
> Traefik need to be restart if certificates are regenerated after his launch
@@ -1,8 +1,6 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.4"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,7 +13,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -25,10 +23,11 @@ services:
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8000:80
- 8080:8080
depends_on:
- crowdsec
- gencert
# Use HTTPS scheme but with lapikey authentication
# whoami-foo:
@@ -63,10 +62,10 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
crowdsec:
image: crowdsecurity/crowdsec:v1.5.2
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
@@ -80,11 +79,13 @@ services:
CERT_FILE: "/etc/crowdsec/certs/server.pem"
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
CACERT_FILE: "/etc/crowdsec/certs/inter.pem"
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
AGENTS_ALLOWED_OU: "agent-ou"
BOUNCERS_ALLOWED_OU: "bouncer-ou"
LEVEL_DEBUG: "true"
# Disabled because it restart in loop otherwise
DISABLE_AGENT: "true"
# DISABLE_AGENT: "true"
# Disabled for the examples
DISABLE_ONLINE_API: "true"
volumes:
@@ -97,7 +98,9 @@ services:
- crowdsec-config-tls-auth:/etc/crowdsec/
labels:
- "traefik.enable=false"
depends_on:
- gencert
gencert:
build: .
volumes:
@@ -1,8 +1,6 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.4"
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,7 +13,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
@@ -29,7 +27,7 @@ services:
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
container_name: "simple-service-foo-trustedips"
restart: unless-stopped
labels:
- "traefik.enable=true"
@@ -48,7 +46,7 @@ services:
whoami2:
image: traefik/whoami
container_name: "simple-service-bar"
container_name: "simple-service-bar-trustedips"
restart: unless-stopped
labels:
- "traefik.enable=true"
@@ -67,7 +65,7 @@ services:
crowdsec:
image: crowdsecurity/crowdsec:v1.5.2
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
+2 -2
View File
@@ -1,8 +1,8 @@
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
go 1.19
go 1.22
require (
github.com/leprosus/golang-ttl-map v1.1.7
github.com/maxlerebourg/simpleredis v1.0.9
github.com/maxlerebourg/simpleredis v1.0.11
)
+2 -2
View File
@@ -1,4 +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.9 h1:aj1hKaYPeOVE4Ksu3TV/zsreUDDWOpKXBAvoFysiqII=
github.com/maxlerebourg/simpleredis v1.0.9/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
github.com/maxlerebourg/simpleredis v1.0.11 h1:B33TUeIrHtJH2/Qj2bRdU+UZ1BvZwFyP55JWMxHirWg=
github.com/maxlerebourg/simpleredis v1.0.11/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
+5 -4
View File
@@ -3,6 +3,7 @@
package cache
import (
"errors"
"fmt"
ttl_map "github.com/leprosus/golang-ttl-map"
@@ -38,7 +39,7 @@ func (localCache) get(key string) (string, error) {
if isCached && isValid && len(valueString) > 0 {
return valueString, nil
}
return "", fmt.Errorf(CacheMiss)
return "", errors.New(CacheMiss)
}
func (localCache) set(key, value string, duration int64) {
@@ -60,20 +61,20 @@ func (redisCache) get(key string) (string, error) {
return valueString, nil
}
if err.Error() == simpleredis.RedisMiss {
return "", fmt.Errorf(CacheMiss)
return "", errors.New(CacheMiss)
}
return "", err
}
func (rc redisCache) set(key, value string, duration int64) {
if err := redis.Set(key, []byte(value), duration); err != nil {
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
rc.log.Error("cache:setDecisionRedisCache" + err.Error())
}
}
func (rc redisCache) delete(key string) {
if err := redis.Del(key); err != nil {
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
}
}
+14 -34
View File
@@ -7,7 +7,6 @@ import (
"html/template"
"net/http"
"net/url"
"os"
"strings"
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
@@ -22,7 +21,7 @@ type Client struct {
siteKey string
secretKey string
gracePeriodSeconds int64
htmlPage *template.Template
captchaTemplate *template.Template
cacheClient *cache.Client
httpClient *http.Client
log *logger.Log
@@ -55,26 +54,8 @@ var (
}
)
func compileTemplate(path string) (*template.Template, error) {
var err error
if path == "" {
return nil, fmt.Errorf("no captcha template provided")
}
//nolint:gosec
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
html := string(b)
compiledTemplate, err := template.New("captcha").Parse(html)
if err != nil {
return nil, fmt.Errorf("impossible to compile captcha template: %w", err)
}
return compiledTemplate, nil
}
// New Initialize captcha client.
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, htmlPagePath string, gracePeriodSeconds int64) error {
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, captchaTemplatePath string, gracePeriodSeconds int64) error {
c.Valid = provider != ""
if !c.Valid {
return nil
@@ -82,11 +63,8 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
c.siteKey = siteKey
c.secretKey = secretKey
c.provider = provider
html, err := compileTemplate(htmlPagePath)
if err != nil {
return err
}
c.htmlPage = html
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
c.captchaTemplate = html
c.gracePeriodSeconds = gracePeriodSeconds
c.log = log
c.httpClient = httpClient
@@ -98,29 +76,31 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
valid, err := c.Validate(r)
if err != nil {
c.log.Debug(fmt.Sprintf("captcha:ServeHTTP:validate %s", err.Error()))
c.log.Info("captcha:ServeHTTP:validate " + err.Error())
rw.WriteHeader(http.StatusBadRequest)
return
}
if valid {
c.log.Debug("captcha:ServeHTTP captcha:valid")
c.cacheClient.Set(fmt.Sprintf("%s_captcha", remoteIP), cache.CaptchaDoneValue, c.gracePeriodSeconds)
c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
return
}
err = c.htmlPage.Execute(rw, map[string]string{
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey,
"FrontendJS": captcha[c.provider].js,
"FrontendKey": captcha[c.provider].key,
})
if err != nil {
c.log.Info("captcha:ServeHTTP Can't serve HTML")
c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error())
}
}
// Check Verify if the captcha is already done.
func (c *Client) Check(remoteIP string) bool {
value, _ := c.cacheClient.Get(fmt.Sprintf("%s_captcha", remoteIP))
value, _ := c.cacheClient.Get(remoteIP + "_captcha")
passed := value == cache.CaptchaDoneValue
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
return passed
@@ -133,10 +113,10 @@ type responseProvider struct {
// Validate Verify the captcha from provider API.
func (c *Client) Validate(r *http.Request) (bool, error) {
if r.Method != http.MethodPost {
c.log.Debug(fmt.Sprintf("captcha:Validate invalid method: %s", r.Method))
c.log.Debug("captcha:Validate invalid method: " + r.Method)
return false, nil
}
var response = r.FormValue(fmt.Sprintf("%s-response", captcha[c.provider].key))
var response = r.FormValue(captcha[c.provider].key + "-response")
if response == "" {
c.log.Debug("captcha:Validate no captcha response found in request")
return false, nil
@@ -150,7 +130,7 @@ func (c *Client) Validate(r *http.Request) (bool, error) {
}
defer func() {
if err = res.Body.Close(); err != nil {
c.log.Error(fmt.Sprintf("captcha:Validate %s", err.Error()))
c.log.Error("captcha:Validate " + err.Error())
}
}()
if !strings.Contains(res.Header.Get("content-type"), "application/json") {
+73 -34
View File
@@ -4,7 +4,9 @@ package configuration
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"html/template"
"net/http"
"net/url"
"os"
@@ -39,6 +41,7 @@ type Config struct {
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
@@ -56,6 +59,7 @@ type Config struct {
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
@@ -66,6 +70,7 @@ type Config struct {
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
CaptchaProvider string `json:"captchaProvider,omitempty"`
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
@@ -87,31 +92,34 @@ func contains(source []string, target string) bool {
// New creates the default plugin configuration.
func New() *Config {
return &Config{
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60,
HTTPTimeoutSeconds: 10,
CaptchaProvider: "",
CaptchaSiteKey: "",
CaptchaSecretKey: "",
CaptchaHTMLFilePath: "/captcha.html",
CaptchaGracePeriodSeconds: 1800,
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true,
CrowdsecAppsecUnreachableBlock: true,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
UpdateMaxFailure: 0,
DefaultDecisionSeconds: 60,
HTTPTimeoutSeconds: 10,
CaptchaProvider: "",
CaptchaSiteKey: "",
CaptchaSecretKey: "",
CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "",
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
}
}
@@ -119,7 +127,7 @@ func New() *Config {
func GetVariable(config *Config, key string) (string, error) {
value := ""
object := reflect.Indirect(reflect.ValueOf(config))
field := object.FieldByName(fmt.Sprintf("%sFile", key))
field := object.FieldByName(key + "File")
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
fp := field.String()
if fp != "" {
@@ -142,6 +150,25 @@ func GetVariable(config *Config, key string) (string, error) {
return strings.TrimSpace(value), nil
}
// GetHTMLTemplate get compiled HTML template.
func GetHTMLTemplate(path string) (*template.Template, error) {
var err error
if path == "" {
return nil, errors.New("no html template provided")
}
//nolint:gosec
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
html := string(b)
compiledTemplate, err := template.New("html").Parse(html)
if err != nil {
return nil, fmt.Errorf("impossible to compile html template: %w", err)
}
return compiledTemplate, nil
}
// ValidateParams validate all the param gave by user.
//
//nolint:gocyclo,gocognit
@@ -178,6 +205,14 @@ func ValidateParams(config *Config) error {
if _, err := GetVariable(config, "CaptchaSecretKey"); err != nil {
return err
}
if _, err := GetHTMLTemplate(config.CaptchaHTMLFilePath); err != nil {
return err
}
}
if config.BanHTMLFilePath != "" {
if _, err := GetHTMLTemplate(config.BanHTMLFilePath); err != nil {
return err
}
}
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
@@ -202,7 +237,7 @@ 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 all empty")
return errors.New("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
lapiKey = strings.TrimSpace(lapiKey)
if err = validateParamsAPIKey(lapiKey); err != nil {
@@ -235,7 +270,7 @@ func validateURL(variable, scheme, host string) error {
// 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)) {
if !reg.MatchString(lapiKey) {
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
}
return nil
@@ -247,12 +282,12 @@ func validateParamsTLS(config *Config) error {
return err
}
if certAuth == "" {
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
return errors.New("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
}
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
return fmt.Errorf("failed parsing pem file")
return errors.New("failed parsing pem file")
}
return nil
}
@@ -288,14 +323,18 @@ func validateParamsRequired(config *Config) error {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if config.UpdateMaxFailure < -1 {
return errors.New("UpdateMaxFailure: cannot be less than -1")
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
}
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
return errors.New("CaptchaProvider: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
}
return nil
}
@@ -324,7 +363,7 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
// here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
}
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
}
+2 -1
View File
@@ -3,6 +3,7 @@
package ip
import (
"errors"
"fmt"
"net"
"net/http"
@@ -45,7 +46,7 @@ func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
// Contains checks if provided address is in the trusted IPs.
func (ip *Checker) Contains(addr string) (bool, error) {
if len(addr) == 0 {
return false, fmt.Errorf("Contains:noAddress")
return false, errors.New("Contains:noAddress")
}
ipAddr, err := parseIP(addr)
+1 -2
View File
@@ -18,7 +18,7 @@ const (
RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout"
RedisNoAuth = "redis:noauth"
RedisIssue = "redis:issue?"
RedisIssue = "redis:issue?"
)
// A redisCmd is used to communicate with redis at low level using commands.
@@ -122,7 +122,6 @@ func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
}
read, _ = reader.ReadLineBytes()
return redisCmd{Data: read}
}
}
}
+2 -2
View File
@@ -1,6 +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.9
## explicit; go 1.19
# github.com/maxlerebourg/simpleredis v1.0.11
## explicit; go 1.22
github.com/maxlerebourg/simpleredis