mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
28
Commits
v1.3.0-beta1
...
v1.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5418d35feb | ||
|
|
a184ae6db9 | ||
|
|
e4c84409e7 | ||
|
|
4708d76854 | ||
|
|
c34d7f4617 | ||
|
|
0e9620bfe9 | ||
|
|
92f05b0ba5 | ||
|
|
980a7dd05e | ||
|
|
5c8a60118f | ||
|
|
8fb0a016b6 | ||
|
|
45d5f38c4d | ||
|
|
f1de1c924e | ||
|
|
36e6043c32 | ||
|
|
123cf15434 | ||
|
|
f89c5e25a9 | ||
|
|
58946d9fa2 | ||
|
|
6187a722ca | ||
|
|
70ad0365f0 | ||
|
|
8975216af8 | ||
|
|
e6b10b8ac4 | ||
|
|
1f11531509 | ||
|
|
ee97250acf | ||
|
|
b6a0404efd | ||
|
|
a0e701f985 | ||
|
|
b8dd883bb6 | ||
|
|
cb4f95d5d0 | ||
|
|
615e7ccf69 | ||
|
|
6059f23dc3 |
@@ -0,0 +1,24 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for Go
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
# Maintain dependencies for build tools
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/tools"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -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 ./...
|
|
||||||
@@ -12,9 +12,9 @@ jobs:
|
|||||||
name: Main Process
|
name: Main Process
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.19
|
GO_VERSION: 1.23
|
||||||
GOLANGCI_LINT_VERSION: v1.50.0
|
GOLANGCI_LINT_VERSION: v1.63.4
|
||||||
YAEGI_VERSION: v0.14.2
|
YAEGI_VERSION: v0.16.1
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -24,20 +24,20 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/marketplace/actions/setup-go-environment
|
# https://github.com/marketplace/actions/setup-go-environment
|
||||||
- name: Set up Go ${{ env.GO_VERSION }}
|
- name: Set up Go ${{ env.GO_VERSION }}
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/checkout
|
# https://github.com/marketplace/actions/checkout
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: go/src/github.com/${{ github.repository }}
|
path: go/src/github.com/${{ github.repository }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/cache
|
# https://github.com/marketplace/actions/cache
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/go/pkg/mod
|
path: ${{ github.workspace }}/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
|||||||
+28
-21
@@ -1,15 +1,11 @@
|
|||||||
run:
|
run:
|
||||||
timeout: 3m
|
timeout: 3m
|
||||||
skip-files: []
|
|
||||||
skip-dirs: []
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
govet:
|
govet:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
- fieldalignment
|
- fieldalignment
|
||||||
golint:
|
|
||||||
min-confidence: 0
|
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 15
|
min-complexity: 15
|
||||||
goconst:
|
goconst:
|
||||||
@@ -25,24 +21,37 @@ linters-settings:
|
|||||||
- FIXME
|
- FIXME
|
||||||
gofumpt:
|
gofumpt:
|
||||||
extra-rules: true
|
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:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
- deadcode # deprecated
|
|
||||||
- exhaustivestruct # deprecated
|
|
||||||
- golint # deprecated
|
|
||||||
- ifshort # deprecated
|
|
||||||
- interfacer # deprecated
|
|
||||||
- maligned # deprecated
|
|
||||||
- nosnakecase # deprecated
|
|
||||||
- scopelint # deprecated
|
|
||||||
- scopelint # deprecated
|
|
||||||
- structcheck # deprecated
|
|
||||||
- varcheck # deprecated
|
|
||||||
- sqlclosecheck # not relevant (SQL)
|
- sqlclosecheck # not relevant (SQL)
|
||||||
- rowserrcheck # not relevant (SQL)
|
- rowserrcheck # not relevant (SQL)
|
||||||
- execinquery # not relevant (SQL)
|
|
||||||
- cyclop # duplicate of gocyclo
|
- cyclop # duplicate of gocyclo
|
||||||
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
|
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
|
||||||
- dupl
|
- dupl
|
||||||
@@ -53,21 +62,19 @@ linters:
|
|||||||
- wsl
|
- wsl
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exhaustruct
|
- exhaustruct
|
||||||
- goerr113
|
- err113
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
- ifshort
|
|
||||||
- noctx
|
- noctx
|
||||||
- lll
|
- lll
|
||||||
- gomnd
|
|
||||||
- forbidigo
|
- forbidigo
|
||||||
- varnamelen
|
- varnamelen
|
||||||
- wastedassign # is disabled because of generics
|
- wastedassign # is disabled because of generics
|
||||||
- gofumpt
|
- gofumpt
|
||||||
- gci
|
- gci
|
||||||
|
- mnd
|
||||||
|
- exportloopref
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
max-per-linter: 0
|
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
exclude:
|
exclude:
|
||||||
- "G402: TLS InsecureSkipVerify may be true."
|
- "G402: TLS InsecureSkipVerify may be true."
|
||||||
|
|||||||
@@ -26,25 +26,28 @@ run_local:
|
|||||||
docker compose -f docker-compose.local.yml up -d --remove-orphans
|
docker compose -f docker-compose.local.yml up -d --remove-orphans
|
||||||
|
|
||||||
run_behindproxy:
|
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:
|
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:
|
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:
|
run_binaryvm:
|
||||||
cd examples/binary-vm/ && sudo vagrant up
|
cd examples/binary-vm/ && sudo vagrant up
|
||||||
|
|
||||||
run_tlsauth:
|
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:
|
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:
|
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:
|
run:
|
||||||
docker compose -f docker-compose.yml up -d --remove-orphans
|
docker compose -f docker-compose.yml up -d --remove-orphans
|
||||||
@@ -59,19 +62,25 @@ restart:
|
|||||||
docker compose -f docker-compose.yml restart
|
docker compose -f docker-compose.yml restart
|
||||||
|
|
||||||
restart_behindproxy:
|
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:
|
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:
|
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:
|
restart_tlsauth:
|
||||||
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml
|
docker compose -f examples/tls-auth/docker-compose.yml
|
||||||
|
|
||||||
restart_appsec:
|
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:
|
show_logs:
|
||||||
docker compose -f docker-compose.yml restart
|
docker compose -f docker-compose.yml restart
|
||||||
@@ -83,11 +92,13 @@ show_dev_logs:
|
|||||||
docker compose -f docker-compose.dev.yml logs -f
|
docker compose -f docker-compose.dev.yml logs -f
|
||||||
|
|
||||||
clean_all_docker:
|
clean_all_docker:
|
||||||
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
|
docker compose -f examples/behind-proxy/docker-compose.yml down --remove-orphans
|
||||||
docker compose -f examples/redis-cache/docker-compose.redis.yml down --remove-orphans
|
docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans
|
||||||
docker compose -f examples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
|
docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans
|
||||||
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down --remove-orphans
|
docker compose -f examples/tls-auth/docker-compose.yml down --remove-orphans
|
||||||
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.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.local.yml down --remove-orphans
|
||||||
docker compose -f docker-compose.yml down --remove-orphans
|
docker compose -f docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
@@ -98,3 +109,6 @@ clean_vagrant:
|
|||||||
show_metrics:
|
show_metrics:
|
||||||
docker exec crowdsec cscli metrics
|
docker exec crowdsec cscli metrics
|
||||||
|
|
||||||
|
show_decisions:
|
||||||
|
docker exec crowdsec cscli decisions list
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ The AppSec Component offers:
|
|||||||
- Low-effort virtual patching capabilities.
|
- Low-effort virtual patching capabilities.
|
||||||
- Support for your legacy ModSecurity rules.
|
- Support for your legacy ModSecurity rules.
|
||||||
- Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection.
|
- Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection.
|
||||||
|
|
||||||
More information on appsec in the [Crowdsec Documentation](https://doc.crowdsec.net/docs/next/appsec/intro/).
|
More information on appsec in the [Crowdsec Documentation](https://doc.crowdsec.net/docs/next/appsec/intro/).
|
||||||
|
|
||||||
Remediation offered by [Crowdsec](https://docs.crowdsec.net/u/bouncers/intro) and supported by the plugin can be either `ban` or `captcha`.
|
Remediation offered by [Crowdsec](https://docs.crowdsec.net/u/bouncers/intro) and supported by the plugin can be either `ban` or `captcha`.
|
||||||
@@ -34,15 +35,15 @@ For the `captcha` remediation, the user will be redirected to a page to complete
|
|||||||
|
|
||||||
On successfull completion, he will be cleaned for a specified period of time before a new resolution challenge is expected if Crowdsec still has a decision to verify the user behavior. See the example captcha for more informations and configuration intructions.
|
On successfull completion, he will be cleaned for a specified period of time before a new resolution challenge is expected if Crowdsec still has a decision to verify the user behavior. See the example captcha for more informations and configuration intructions.
|
||||||
The following captcha providers are supported now:
|
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/)
|
|
||||||
|
|
||||||
|
- [hcaptcha](https://www.hcaptcha.com/)
|
||||||
|
- [recaptcha](https://www.google.com/recaptcha/about/)
|
||||||
|
- [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 |
|
| 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 |
|
| 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. |
|
| 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. |
|
||||||
@@ -51,14 +52,256 @@ There are 4 operating modes (CrowdsecMode) for this plugin:
|
|||||||
|
|
||||||
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.
|
The cache can be local to Traefik in memory or using 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
|
## Usage
|
||||||
|
|
||||||
To get started, use the `docker-compose.yml` file.
|
To get started, use the `docker-compose.yml` file.
|
||||||
|
|
||||||
You can run it with:
|
You can run it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
@@ -66,17 +309,26 @@ make run
|
|||||||
### Note
|
### Note
|
||||||
|
|
||||||
**/!\ Cache is shared by all 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*
|
_This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP_
|
||||||
Only one instance of the plugin is *possible*.
|
Only one instance of the plugin is _possible_.
|
||||||
|
|
||||||
|
**/!\ Appsec maximum body limit is defaulted to 10MB**
|
||||||
|
_By careful when you upgrade to >1.4.x_
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
|
|
||||||
- Enabled
|
- Enabled
|
||||||
- bool
|
- bool
|
||||||
- default: false
|
- default: false
|
||||||
- Enable the plugin
|
- Enable the plugin
|
||||||
- LogLevel
|
- LogLevel
|
||||||
- string
|
- string
|
||||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
- default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`
|
||||||
|
- Log are written to `stdout` / `stderr` of file if LogFilePath is provided
|
||||||
|
- LogFilePath
|
||||||
|
- string
|
||||||
|
- default: ""
|
||||||
|
- File Path to write logs, must be writable by Traefik, Log rotation may require a restart of traefik
|
||||||
- CrowdsecMode
|
- CrowdsecMode
|
||||||
- string
|
- string
|
||||||
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
|
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
|
||||||
@@ -88,10 +340,22 @@ Only one instance of the plugin is *possible*.
|
|||||||
- string
|
- string
|
||||||
- default: "crowdsec:7422"
|
- default: "crowdsec:7422"
|
||||||
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
|
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
|
||||||
|
- CrowdsecAppsecPath
|
||||||
|
- string
|
||||||
|
- default: "/"
|
||||||
|
- Crowdsec Appsec Server available on this path. Will be appended to CrowdsecAppsecHost. Need to finish with "/".
|
||||||
- CrowdsecAppsecFailureBlock
|
- CrowdsecAppsecFailureBlock
|
||||||
- bool
|
- bool
|
||||||
- default: true
|
- default: true
|
||||||
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
|
- 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.
|
||||||
|
- CrowdsecAppsecBodyLimit
|
||||||
|
- int64
|
||||||
|
- default: 10485760 (= 10MB)
|
||||||
|
- Transmit only the first number of bytes to Crowdsec Appsec Server.
|
||||||
- CrowdsecLapiScheme
|
- CrowdsecLapiScheme
|
||||||
- string
|
- string
|
||||||
- default: `http`, expected values are: `http`, `https`
|
- default: `http`, expected values are: `http`, `https`
|
||||||
@@ -99,6 +363,10 @@ Only one instance of the plugin is *possible*.
|
|||||||
- string
|
- string
|
||||||
- default: "crowdsec:8080"
|
- default: "crowdsec:8080"
|
||||||
- Crowdsec LAPI available on which host and port.
|
- Crowdsec LAPI available on which host and port.
|
||||||
|
- CrowdsecLapiPath
|
||||||
|
- string
|
||||||
|
- default: "/"
|
||||||
|
- Crowdsec LAPI Server available on this path. Will be appended to CrowdsecLapiHost. Need to finish with "/".
|
||||||
- CrowdsecLapiKey
|
- CrowdsecLapiKey
|
||||||
- string
|
- string
|
||||||
- default: ""
|
- default: ""
|
||||||
@@ -123,18 +391,22 @@ Only one instance of the plugin is *possible*.
|
|||||||
- string
|
- string
|
||||||
- default: []
|
- default: []
|
||||||
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
|
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
|
||||||
- ForwardedHeadersTrustedIPs
|
- RemediationHeadersCustomName
|
||||||
- []string
|
- string
|
||||||
- default: []
|
- default: ""
|
||||||
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
|
- Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
|
||||||
- ForwardedHeadersCustomName
|
- ForwardedHeadersCustomName
|
||||||
- string
|
- string
|
||||||
- default: "X-Forwarded-For"
|
- default: "X-Forwarded-For"
|
||||||
- Name of the header where the real IP of the client should be retrieved
|
- Name of the header where the real IP of the client should be retrieved
|
||||||
|
- ForwardedHeadersTrustedIPs
|
||||||
|
- []string
|
||||||
|
- default: []
|
||||||
|
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
|
||||||
- RedisCacheEnabled
|
- RedisCacheEnabled
|
||||||
- bool
|
- bool
|
||||||
- default: false
|
- default: false
|
||||||
- enable Redis cache instead of filesystem cache
|
- enable Redis cache instead of in-memory cache
|
||||||
- RedisCacheHost
|
- RedisCacheHost
|
||||||
- string
|
- string
|
||||||
- default: "redis:6379"
|
- default: "redis:6379"
|
||||||
@@ -147,6 +419,10 @@ Only one instance of the plugin is *possible*.
|
|||||||
- string
|
- string
|
||||||
- default: ""
|
- default: ""
|
||||||
- Database selection for the Redis service
|
- Database selection for the Redis service
|
||||||
|
- RedisUnreachableBlock
|
||||||
|
- bool
|
||||||
|
- default: true
|
||||||
|
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
|
||||||
- HTTPTimeoutSeconds
|
- HTTPTimeoutSeconds
|
||||||
- int64
|
- int64
|
||||||
- default: 10
|
- default: 10
|
||||||
@@ -155,6 +431,10 @@ Only one instance of the plugin is *possible*.
|
|||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
- 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
|
- DefaultDecisionSeconds
|
||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
@@ -177,21 +457,25 @@ Only one instance of the plugin is *possible*.
|
|||||||
- CaptchaSecretKey
|
- CaptchaSecretKey
|
||||||
- string
|
- string
|
||||||
- Site secret key for the captcha provider
|
- Site secret key for the captcha provider
|
||||||
- CaptchaHTMLFilePath
|
|
||||||
- string
|
|
||||||
- default: /captcha.html
|
|
||||||
- Path where the captcha template is stored
|
|
||||||
- CaptchaGracePeriodSeconds
|
- CaptchaGracePeriodSeconds
|
||||||
- int64
|
- int64
|
||||||
- default: 1800 (= 30 minutes)
|
- default: 1800 (= 30 minutes)
|
||||||
- Period after validation of a captcha before a new validation is required if Crowdsec decision is still valid
|
- 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
|
### Configuration
|
||||||
|
|
||||||
For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).
|
For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).
|
||||||
|
|
||||||
The following declaration (given here in YAML) defines a plugin:
|
The following declaration (given here in YAML) defines a plugin:
|
||||||
|
|
||||||
> Note that you don't need to copy all thoses settings but only the ones you want to use.
|
> Note that you don't need to copy all thoses settings but only the ones you want to use.
|
||||||
> See the examples for advanced usage.
|
> See the examples for advanced usage.
|
||||||
|
|
||||||
@@ -230,17 +514,23 @@ http:
|
|||||||
bouncer:
|
bouncer:
|
||||||
enabled: false
|
enabled: false
|
||||||
logLevel: DEBUG
|
logLevel: DEBUG
|
||||||
|
LogFilePath: ""
|
||||||
updateIntervalSeconds: 60
|
updateIntervalSeconds: 60
|
||||||
|
updateMaxFailure: 0
|
||||||
defaultDecisionSeconds: 60
|
defaultDecisionSeconds: 60
|
||||||
httpTimeoutSeconds: 10
|
httpTimeoutSeconds: 10
|
||||||
crowdsecMode: live
|
crowdsecMode: live
|
||||||
crowdsecAppsecEnabled: false
|
crowdsecAppsecEnabled: false
|
||||||
crowdsecAppsecHost: crowdsec:7422
|
crowdsecAppsecHost: crowdsec:7422
|
||||||
|
crowdsecAppsecPath: "/"
|
||||||
crowdsecAppsecFailureBlock: true
|
crowdsecAppsecFailureBlock: true
|
||||||
|
crowdsecAppsecUnreachableBlock: true
|
||||||
|
crowdsecAppsecBodyLimit: 10485760
|
||||||
crowdsecLapiKey: privateKey-foo
|
crowdsecLapiKey: privateKey-foo
|
||||||
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
|
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
|
||||||
crowdsecLapiHost: crowdsec:8080
|
|
||||||
crowdsecLapiScheme: http
|
crowdsecLapiScheme: http
|
||||||
|
crowdsecLapiHost: crowdsec:8080
|
||||||
|
crowdsecLapiPath: "/"
|
||||||
crowdsecLapiTLSInsecureVerify: false
|
crowdsecLapiTLSInsecureVerify: false
|
||||||
crowdsecCapiMachineId: login
|
crowdsecCapiMachineId: login
|
||||||
crowdsecCapiPassword: password
|
crowdsecCapiPassword: password
|
||||||
@@ -254,10 +544,12 @@ http:
|
|||||||
clientTrustedIPs:
|
clientTrustedIPs:
|
||||||
- 192.168.1.0/24
|
- 192.168.1.0/24
|
||||||
forwardedHeadersCustomName: X-Custom-Header
|
forwardedHeadersCustomName: X-Custom-Header
|
||||||
|
remediationHeadersCustomName: cs-remediation
|
||||||
redisCacheEnabled: false
|
redisCacheEnabled: false
|
||||||
redisCacheHost: "redis:6379"
|
redisCacheHost: "redis:6379"
|
||||||
redisCachePassword: password
|
redisCachePassword: password
|
||||||
redisCacheDatabase: "5"
|
redisCacheDatabase: "5"
|
||||||
|
redisCacheUnreachableBlock: true
|
||||||
crowdsecLapiTLSCertificateAuthority: |-
|
crowdsecLapiTLSCertificateAuthority: |-
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||||
@@ -285,14 +577,16 @@ http:
|
|||||||
captchaSecretKey: FIXME
|
captchaSecretKey: FIXME
|
||||||
captchaGracePeriodSeconds: 1800
|
captchaGracePeriodSeconds: 1800
|
||||||
captchaHTMLFilePath: /captcha.html
|
captchaHTMLFilePath: /captcha.html
|
||||||
|
banHTMLFilePath: /ban.html
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Fill variable with value of file
|
#### 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.
|
The file variable will be used as preference if both content and file are provided for the same variable.
|
||||||
|
|
||||||
Format is:
|
Format is:
|
||||||
|
|
||||||
- Content: VariableName: XXX
|
- Content: VariableName: XXX
|
||||||
- File : VariableNameFile: /path
|
- File : VariableNameFile: /path
|
||||||
|
|
||||||
@@ -302,6 +596,7 @@ You can authenticate to the LAPI either with LAPIKEY or by using client certific
|
|||||||
Please see below for more details on each option.
|
Please see below for more details on each option.
|
||||||
|
|
||||||
#### Generate LAPI KEY
|
#### Generate LAPI KEY
|
||||||
|
|
||||||
You can generate a crowdsec API key for the LAPI.
|
You can generate a crowdsec API key for the LAPI.
|
||||||
You can follow the documentation here: [docs.crowdsec.net/docs/user_guides/lapi_mgmt](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)
|
||||||
|
|
||||||
@@ -311,24 +606,26 @@ docker exec crowdsec cscli bouncers add crowdsecBouncer
|
|||||||
```
|
```
|
||||||
|
|
||||||
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml
|
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
..
|
||||||
whoami:
|
whoami:
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=http"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=http"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapihost=crowdsec:8080"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapihost=crowdsec:8080"
|
||||||
...
|
..
|
||||||
crowdsec:
|
crowdsec:
|
||||||
environment:
|
environment:
|
||||||
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
||||||
> Crowdsec does not require a specific format for la LAPI-key, you may use something like FIXME-LAPI-KEY but that is not recommanded for obvious reasons
|
> Crowdsec does not require a specific format for la LAPI-key, you may use something like FIXME-LAPI-KEY but that is not recommanded for obvious reasons
|
||||||
|
|
||||||
You can then run all the containers:
|
You can then run all the containers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -378,6 +675,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)
|
#### 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
|
### Local Mode
|
||||||
|
|
||||||
Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
|
Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
|
||||||
@@ -403,12 +702,14 @@ The source code of the plugin should be organized as follows:
|
|||||||
```
|
```
|
||||||
|
|
||||||
For local development, a `docker-compose.local.yml` is provided which reproduces the directory layout needed by Traefik.
|
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.
|
This works once you have generated and filled your _LAPI-KEY_ (crowdsecLapiKey), if not read above for informations.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.local.yml up -d
|
docker compose -f docker-compose.local.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Equivalent to
|
Equivalent to
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make run_local
|
make run_local
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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>
|
||||||
+115
-45
@@ -1,5 +1,5 @@
|
|||||||
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
|
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
|
||||||
// It can cache results to filesystem or redis, or even ask crowdsec for every requests.
|
// It can cache results in memory or using redis, or even ask crowdsec for every requests.
|
||||||
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -28,6 +29,7 @@ const (
|
|||||||
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
|
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
|
||||||
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
|
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
|
||||||
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
|
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
|
||||||
|
crowdsecAppsecUserAgent = "X-Crowdsec-Appsec-User-Agent"
|
||||||
crowdsecLapiHeader = "X-Api-Key"
|
crowdsecLapiHeader = "X-Api-Key"
|
||||||
crowdsecLapiRoute = "v1/decisions"
|
crowdsecLapiRoute = "v1/decisions"
|
||||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||||
@@ -42,6 +44,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
isStartup = true
|
isStartup = true
|
||||||
isCrowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
|
updateFailure = 0
|
||||||
ticker chan bool
|
ticker chan bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,19 +62,27 @@ type Bouncer struct {
|
|||||||
enabled bool
|
enabled bool
|
||||||
appsecEnabled bool
|
appsecEnabled bool
|
||||||
appsecHost string
|
appsecHost string
|
||||||
|
appsecPath string
|
||||||
appsecFailureBlock bool
|
appsecFailureBlock bool
|
||||||
|
appsecUnreachableBlock bool
|
||||||
|
appsecBodyLimit int64
|
||||||
crowdsecScheme string
|
crowdsecScheme string
|
||||||
crowdsecHost string
|
crowdsecHost string
|
||||||
|
crowdsecPath string
|
||||||
crowdsecKey string
|
crowdsecKey string
|
||||||
crowdsecMode string
|
crowdsecMode string
|
||||||
crowdsecMachineID string
|
crowdsecMachineID string
|
||||||
crowdsecPassword string
|
crowdsecPassword string
|
||||||
crowdsecScenarios []string
|
crowdsecScenarios []string
|
||||||
updateInterval int64
|
updateInterval int64
|
||||||
|
updateMaxFailure int
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
customHeader string
|
remediationCustomHeader string
|
||||||
|
forwardedCustomHeader string
|
||||||
crowdsecStreamRoute string
|
crowdsecStreamRoute string
|
||||||
crowdsecHeader string
|
crowdsecHeader string
|
||||||
|
redisUnreachableBlock bool
|
||||||
|
banTemplateString string
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
@@ -81,11 +92,11 @@ type Bouncer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// 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)
|
log := logger.New(config.LogLevel, config.LogFilePath)
|
||||||
err := configuration.ValidateParams(config)
|
err := configuration.ValidateParams(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
|
log.Error("New:validateParams " + err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +109,10 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
if config.CrowdsecMode == configuration.AloneMode {
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
|
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
|
||||||
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
|
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
|
||||||
|
config.CrowdsecLapiScheme = configuration.HTTPS
|
||||||
config.CrowdsecLapiHost = crowdsecCapiHost
|
config.CrowdsecLapiHost = crowdsecCapiHost
|
||||||
config.CrowdsecLapiScheme = "https"
|
config.CrowdsecLapiPath = "/"
|
||||||
|
config.CrowdsecAppsecEnabled = false
|
||||||
config.UpdateIntervalSeconds = 7200 // 2 hours
|
config.UpdateIntervalSeconds = 7200 // 2 hours
|
||||||
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
||||||
crowdsecHeader = crowdsecCapiHeader
|
crowdsecHeader = crowdsecCapiHeader
|
||||||
@@ -108,17 +121,29 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
crowdsecHeader = crowdsecLapiHeader
|
crowdsecHeader = crowdsecLapiHeader
|
||||||
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
|
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||||
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
|
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
|
return nil, errAPIKey
|
||||||
}
|
}
|
||||||
config.CrowdsecLapiKey = apiKey
|
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{
|
bouncer := &Bouncer{
|
||||||
next: next,
|
next: next,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -128,16 +153,24 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
crowdsecMode: config.CrowdsecMode,
|
crowdsecMode: config.CrowdsecMode,
|
||||||
appsecEnabled: config.CrowdsecAppsecEnabled,
|
appsecEnabled: config.CrowdsecAppsecEnabled,
|
||||||
appsecHost: config.CrowdsecAppsecHost,
|
appsecHost: config.CrowdsecAppsecHost,
|
||||||
|
appsecPath: config.CrowdsecAppsecPath,
|
||||||
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
||||||
|
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
||||||
|
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
||||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||||
crowdsecHost: config.CrowdsecLapiHost,
|
crowdsecHost: config.CrowdsecLapiHost,
|
||||||
|
crowdsecPath: config.CrowdsecLapiPath,
|
||||||
crowdsecKey: config.CrowdsecLapiKey,
|
crowdsecKey: config.CrowdsecLapiKey,
|
||||||
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
||||||
crowdsecPassword: config.CrowdsecCapiPassword,
|
crowdsecPassword: config.CrowdsecCapiPassword,
|
||||||
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
||||||
updateInterval: config.UpdateIntervalSeconds,
|
updateInterval: config.UpdateIntervalSeconds,
|
||||||
customHeader: config.ForwardedHeadersCustomName,
|
updateMaxFailure: config.UpdateMaxFailure,
|
||||||
|
remediationCustomHeader: config.RemediationHeadersCustomName,
|
||||||
|
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
|
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||||
|
banTemplateString: banTemplateString,
|
||||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
crowdsecHeader: crowdsecHeader,
|
crowdsecHeader: crowdsecHeader,
|
||||||
log: log,
|
log: log,
|
||||||
@@ -181,6 +214,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
config.CaptchaProvider,
|
config.CaptchaProvider,
|
||||||
config.CaptchaSiteKey,
|
config.CaptchaSiteKey,
|
||||||
config.CaptchaSecretKey,
|
config.CaptchaSecretKey,
|
||||||
|
config.RemediationHeadersCustomName,
|
||||||
config.CaptchaHTMLFilePath,
|
config.CaptchaHTMLFilePath,
|
||||||
config.CaptchaGracePeriodSeconds,
|
config.CaptchaGracePeriodSeconds,
|
||||||
)
|
)
|
||||||
@@ -191,7 +225,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.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
||||||
if config.CrowdsecMode == configuration.AloneMode {
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
if err := getToken(bouncer); err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,31 +235,31 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
|||||||
handleStreamTicker(bouncer)
|
handleStreamTicker(bouncer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
|
bouncer.log.Debug("New initialized mode:" + config.CrowdsecMode)
|
||||||
|
|
||||||
return bouncer, nil
|
return bouncer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP principal function of plugin.
|
// ServeHTTP principal function of plugin.
|
||||||
//
|
//
|
||||||
//nolint:nestif
|
//nolint:nestif,gocyclo
|
||||||
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
if !bouncer.enabled {
|
if !bouncer.enabled {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here we check for the trusted IPs in the customHeader
|
// Here we check for the trusted IPs in the forwardedCustomHeader
|
||||||
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
|
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
handleBanServeHTTP(bouncer, rw)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
handleBanServeHTTP(bouncer, rw)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if our IP is in the trusted list we bypass the next checks
|
// if our IP is in the trusted list we bypass the next checks
|
||||||
@@ -246,9 +280,14 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
if cacheErr != nil {
|
if cacheErr != nil {
|
||||||
cacheErrString := cacheErr.Error()
|
cacheErrString := cacheErr.Error()
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
||||||
|
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
||||||
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
||||||
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
if cacheErrString != cache.CacheMiss {
|
if cacheErrString != cache.CacheMiss {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
handleBanServeHTTP(bouncer, rw)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -256,7 +295,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
if value == cache.NoBannedValue {
|
if value == cache.NoBannedValue {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
} else {
|
} else {
|
||||||
handleErrorServeHTTP(bouncer, remoteIP, value, rw, req)
|
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -267,8 +306,8 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
if isCrowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
handleBanServeHTTP(bouncer, rw)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value, err := handleNoStreamCache(bouncer, remoteIP)
|
value, err := handleNoStreamCache(bouncer, remoteIP)
|
||||||
@@ -276,7 +315,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
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 +348,25 @@ type Login struct {
|
|||||||
Expire string `json:"expire"`
|
Expire string `json:"expire"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleErrorServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
|
// To append Headers we need to call rw.WriteHeader after set any header.
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleErrorServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
|
||||||
|
if bouncer.remediationCustomHeader != "" {
|
||||||
|
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
||||||
|
}
|
||||||
|
if bouncer.banTemplateString == "" {
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
|
||||||
|
if err != nil {
|
||||||
|
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Valid && remediation == cache.CaptchaValue {
|
||||||
if bouncer.captchaClient.Check(remoteIP) {
|
if bouncer.captchaClient.Check(remoteIP) {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||||
@@ -319,14 +375,14 @@ func handleErrorServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw htt
|
|||||||
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
handleBanServeHTTP(bouncer, rw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
|
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
|
||||||
if bouncer.appsecEnabled {
|
if bouncer.appsecEnabled {
|
||||||
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
handleBanServeHTTP(bouncer, rw)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,10 +391,15 @@ func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWrit
|
|||||||
|
|
||||||
func handleStreamTicker(bouncer *Bouncer) {
|
func handleStreamTicker(bouncer *Bouncer) {
|
||||||
if err := handleStreamCache(bouncer); err != nil {
|
if err := handleStreamCache(bouncer); err != nil {
|
||||||
|
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
|
isCrowdsecStreamHealthy = false
|
||||||
bouncer.log.Error(err.Error())
|
bouncer.log.Error(fmt.Sprintf("handleStreamTicker:error updateFailure:%d %s", updateFailure, err.Error()))
|
||||||
|
}
|
||||||
|
updateFailure++
|
||||||
} else {
|
} else {
|
||||||
isCrowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
|
updateFailure = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +426,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
|||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: crowdsecLapiRoute,
|
Path: bouncer.crowdsecPath + crowdsecLapiRoute,
|
||||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
||||||
@@ -409,7 +470,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
|||||||
case "captcha":
|
case "captcha":
|
||||||
value = cache.CaptchaValue
|
value = cache.CaptchaValue
|
||||||
default:
|
default:
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
|
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
|
||||||
}
|
}
|
||||||
if isLiveMode {
|
if isLiveMode {
|
||||||
durationSecond := int64(duration.Seconds())
|
durationSecond := int64(duration.Seconds())
|
||||||
@@ -418,7 +479,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
|||||||
}
|
}
|
||||||
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
|
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
|
||||||
}
|
}
|
||||||
return value, fmt.Errorf("handleNoStreamCache:banned")
|
return value, errors.New("handleNoStreamCache:banned")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getToken(bouncer *Bouncer) error {
|
func getToken(bouncer *Bouncer) error {
|
||||||
@@ -434,7 +495,6 @@ func getToken(bouncer *Bouncer) error {
|
|||||||
var login Login
|
var login Login
|
||||||
err = json.Unmarshal(body, &login)
|
err = json.Unmarshal(body, &login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
isCrowdsecStreamHealthy = false
|
|
||||||
return fmt.Errorf("getToken:parsingBody %w", err)
|
return fmt.Errorf("getToken:parsingBody %w", err)
|
||||||
}
|
}
|
||||||
if login.Code == 200 && len(login.Token) > 0 {
|
if login.Code == 200 && len(login.Token) > 0 {
|
||||||
@@ -462,7 +522,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
streamRouteURL := url.URL{
|
streamRouteURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: bouncer.crowdsecStreamRoute,
|
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
|
||||||
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
|
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
|
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
|
||||||
@@ -484,7 +544,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
case "captcha":
|
case "captcha":
|
||||||
value = cache.CaptchaValue
|
value = cache.CaptchaValue
|
||||||
default:
|
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()))
|
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
@@ -493,7 +553,6 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
bouncer.cacheClient.Delete(decision.Value)
|
bouncer.cacheClient.Delete(decision.Value)
|
||||||
}
|
}
|
||||||
bouncer.log.Debug("handleStreamCache:updated")
|
bouncer.log.Debug("handleStreamCache:updated")
|
||||||
isCrowdsecStreamHealthy = true
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,13 +570,15 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
|
|||||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||||
}
|
}
|
||||||
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
|
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
|
||||||
|
req.Header.Add("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/1.X.X")
|
||||||
|
|
||||||
res, err := bouncer.httpClient.Do(req)
|
res, err := bouncer.httpClient.Do(req)
|
||||||
if err != nil {
|
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() {
|
defer func() {
|
||||||
if err = res.Body.Close(); err != nil {
|
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 {
|
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
@@ -541,15 +602,19 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
|||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.appsecHost,
|
Host: bouncer.appsecHost,
|
||||||
Path: "/",
|
Path: bouncer.appsecPath,
|
||||||
}
|
}
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
if httpReq.Body != nil && httpReq.ContentLength > 0 {
|
if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil && httpReq.ContentLength > 0 {
|
||||||
bodyBytes, err := io.ReadAll(httpReq.Body)
|
var bodyBuffer bytes.Buffer
|
||||||
|
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
|
||||||
|
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
|
||||||
|
bodyBytes, err := io.ReadAll(teeReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("appsecQuery:GetBody %w", err)
|
return fmt.Errorf("appsecQuery:GetBody %w", err)
|
||||||
}
|
}
|
||||||
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
// Conserve body intact after reading it for other middlewares and service
|
||||||
|
httpReq.Body = io.NopCloser(io.MultiReader(&bodyBuffer, httpReq.Body))
|
||||||
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
|
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
|
||||||
} else {
|
} else {
|
||||||
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
||||||
@@ -564,21 +629,26 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
|||||||
req.Header.Set(crowdsecAppsecIPHeader, ip)
|
req.Header.Set(crowdsecAppsecIPHeader, ip)
|
||||||
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
|
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
|
||||||
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
|
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)
|
res, err := bouncer.httpClient.Do(req)
|
||||||
if err != nil {
|
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() {
|
defer func() {
|
||||||
if err = res.Body.Close(); err != nil {
|
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 {
|
if res.StatusCode == http.StatusInternalServerError {
|
||||||
bouncer.log.Debug("crowdsecQuery statusCode:500")
|
bouncer.log.Info("appsecQuery:failure")
|
||||||
if bouncer.appsecFailureBlock {
|
if bouncer.appsecFailureBlock {
|
||||||
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
|
return errors.New("appsecQuery statusCode:500")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -18,7 +18,7 @@ func TestServeHTTP(t *testing.T) {
|
|||||||
cfg.CrowdsecLapiKey = "test"
|
cfg.CrowdsecLapiKey = "test"
|
||||||
|
|
||||||
ctx := context.Background()
|
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")
|
handler, err := New(ctx, next, cfg, "demo-plugin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
|||||||
crowdsecMode string
|
crowdsecMode string
|
||||||
updateInterval int64
|
updateInterval int64
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
customHeader string
|
forwardedCustomHeader string
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
@@ -93,7 +93,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
|||||||
// TODO: Add test cases.
|
// TODO: Add test cases.
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(_ *testing.T) {
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
next: tt.fields.next,
|
next: tt.fields.next,
|
||||||
name: tt.fields.name,
|
name: tt.fields.name,
|
||||||
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
|||||||
crowdsecMode: tt.fields.crowdsecMode,
|
crowdsecMode: tt.fields.crowdsecMode,
|
||||||
updateInterval: tt.fields.updateInterval,
|
updateInterval: tt.fields.updateInterval,
|
||||||
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
|
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
|
||||||
customHeader: tt.fields.customHeader,
|
forwardedCustomHeader: tt.fields.forwardedCustomHeader,
|
||||||
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
||||||
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
||||||
httpClient: tt.fields.httpClient,
|
httpClient: tt.fields.httpClient,
|
||||||
|
|||||||
+1
-1
@@ -293,7 +293,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
|
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
|
||||||
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.11.0"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -16,6 +16,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- logs-local:/var/log/traefik
|
- logs-local:/var/log/traefik
|
||||||
|
- './ban.html:/ban.html:ro'
|
||||||
|
- './captcha.html:/captcha.html:ro'
|
||||||
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
ports:
|
ports:
|
||||||
- 8000:80
|
- 8000:80
|
||||||
@@ -47,14 +49,15 @@ services:
|
|||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=stream"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:1.6.0
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
|
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+6
-4
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.11.0"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -12,9 +12,11 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.15"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
|
# - './ban.html:/ban.html:ro'
|
||||||
|
# - './captcha.html:/captcha.html:ro'
|
||||||
- "logs:/var/log/traefik"
|
- "logs:/var/log/traefik"
|
||||||
ports:
|
ports:
|
||||||
- 8000:80
|
- 8000:80
|
||||||
@@ -57,11 +59,11 @@ services:
|
|||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.6.0
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
COLLECTIONS: crowdsecurity/traefik
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
# We need to register one api key per service we will use
|
# We need to register one api key per service we will use
|
||||||
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
|
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.10.7"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,12 +15,12 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.2.0"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- logs-appsec-enabled:/var/log/traefik
|
- 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:
|
ports:
|
||||||
- 8000:80
|
- 8000:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
@@ -51,11 +51,11 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.6.0
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
COLLECTIONS: crowdsecurity/traefik
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+6
-8
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cloudflare:
|
cloudflare:
|
||||||
image: "traefik:v2.10.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "cloudflare"
|
container_name: "cloudflare"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -21,7 +19,7 @@ services:
|
|||||||
- 8080:8080
|
- 8080:8080
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.10.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -35,7 +33,7 @@ services:
|
|||||||
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- logs-traefik:/var/log/traefik
|
- logs-traefik:/var/log/traefik
|
||||||
@@ -47,7 +45,7 @@ services:
|
|||||||
|
|
||||||
whoami-foo:
|
whoami-foo:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-foo"
|
container_name: "simple-service-foo-cloudflare"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -66,7 +64,7 @@ services:
|
|||||||
|
|
||||||
whoami-bar:
|
whoami-bar:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-bar"
|
container_name: "simple-service-bar-cloudflare"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -81,7 +79,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.5.2
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
|
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
|
||||||
# DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes
|
# 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
|
tar -zxvf traefik.tar.gz
|
||||||
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
||||||
sudo mv ./traefik /usr/local/bin/
|
sudo mv ./traefik /usr/local/bin/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
DEBIAN_FRONTEND=noninteractive 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.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
|
tar -zxvf whoami.tar.gz
|
||||||
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
||||||
sudo mv ./whoami /usr/local/bin/
|
sudo mv ./whoami /usr/local/bin/
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ It is not directly accessible from Traefik even when importing the plugin, so [d
|
|||||||
```yaml
|
```yaml
|
||||||
...
|
...
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.11.0"
|
image: "traefik:v3.0.0"
|
||||||
volumes:
|
volumes:
|
||||||
- './captcha.html:/captcha.html'
|
- './captcha.html:/captcha.html'
|
||||||
...
|
...
|
||||||
@@ -52,7 +52,7 @@ More information is available on configuring Crowdsec in the [official documenta
|
|||||||
```yaml
|
```yaml
|
||||||
...
|
...
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.6.0
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
volumes:
|
volumes:
|
||||||
# For captcha and ban mixed decision
|
# For captcha and ban mixed decision
|
||||||
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||||
@@ -144,3 +144,18 @@ sequenceDiagram
|
|||||||
PluginCache-->>TraefikPlugin: Yes a ban Decision
|
PluginCache-->>TraefikPlugin: Yes a ban Decision
|
||||||
TraefikPlugin->>User: No, HTTP 403
|
TraefikPlugin->>User: No, HTTP 403
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Captcha Configuration:
|
||||||
|
|
||||||
|
- Recaptcha
|
||||||
|
|
||||||
|
Choose v2 (challenge) and configure the domain to protect:
|
||||||
|

|
||||||
|
|
||||||
|
- Turnstile
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
- Hcatpcha
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.11.0"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -12,14 +12,14 @@ services:
|
|||||||
- "--providers.docker.exposedbydefault=false"
|
- "--providers.docker.exposedbydefault=false"
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
# - "--experimental.plugins.bouncer.version=v1.2.0"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- logs-captcha-enabled:/var/log/traefik
|
- logs-captcha-enabled:/var/log/traefik
|
||||||
- './captcha.html:/captcha.html'
|
- './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:
|
ports:
|
||||||
- 8000:80
|
- 8000:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
@@ -55,7 +55,7 @@ services:
|
|||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.6.0
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -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
|
||||||
|
```
|
||||||
|

|
||||||
|
|
||||||
|
We will see in the browser the ban custom page:
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_custom_ban_page
|
||||||
|
```
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -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 |
@@ -32,13 +32,15 @@ minikube start
|
|||||||
helm repo add traefik https://traefik.github.io/charts
|
helm repo add traefik https://traefik.github.io/charts
|
||||||
helm repo update
|
helm repo update
|
||||||
kubectl create ns traefik
|
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 \
|
--values=./traefik/values.yml \
|
||||||
traefik traefik/traefik
|
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_
|
_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
|
#### View the Traefik dashboard
|
||||||
|
|
||||||
> Port forward the dashboard:
|
> Port forward the dashboard:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
image:
|
image:
|
||||||
tag: v1.6.0
|
tag: v1.6.1-2
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
acquisition:
|
acquisition:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# dashboard.yaml
|
# dashboard.yaml
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
metadata:
|
metadata:
|
||||||
name: dashboard
|
name: dashboard
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
apiVersion: traefik.containo.us/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: Middleware
|
kind: Middleware
|
||||||
metadata:
|
metadata:
|
||||||
name: bouncer
|
name: bouncer
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
image:
|
image:
|
||||||
tag: v2.11
|
tag: v3.0.0
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
general:
|
general:
|
||||||
@@ -14,4 +14,4 @@ experimental:
|
|||||||
plugins:
|
plugins:
|
||||||
bouncer:
|
bouncer:
|
||||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
version: "v1.2.0"
|
version: "v1.3.0"
|
||||||
|
|||||||
+4
-6
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.10.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,7 +13,7 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
@@ -52,7 +50,7 @@ services:
|
|||||||
|
|
||||||
whoami-redis-secure:
|
whoami-redis-secure:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-bar"
|
container_name: "simple-service-bar-redis"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -73,7 +71,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.5.2
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -3,7 +3,9 @@ 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
|
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
|
||||||
|
|
||||||
```bash
|
```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\"}"
|
LOGIN=...
|
||||||
|
PASSWORD=...
|
||||||
|
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
|
These CAPI credentials must be set in your docker-compose.yml or in your config files
|
||||||
|
|||||||
+8
-11
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.10.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,16 +13,18 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
|
|
||||||
whoami-foo:
|
whoami-foo:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-foo"
|
container_name: "simple-service-foo-alone"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -35,11 +35,8 @@ services:
|
|||||||
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=Password2"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
|
||||||
|
|
||||||
volumes:
|
|
||||||
logs-local:
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ubuntu:22.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl wget
|
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
|
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
|
||||||
|
|||||||
@@ -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:
|
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:
|
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
|
Access to the traefik dashboard
|
||||||
```
|
```
|
||||||
@@ -89,5 +89,4 @@ make run_tlsauth
|
|||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing
|
> Traefik need to be restart if certificates are regenerated after his launch
|
||||||
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
|
|
||||||
+10
-7
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.10.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,7 +13,7 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
@@ -25,10 +23,11 @@ services:
|
|||||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 8000:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- crowdsec
|
- crowdsec
|
||||||
|
- gencert
|
||||||
|
|
||||||
# Use HTTPS scheme but with lapikey authentication
|
# Use HTTPS scheme but with lapikey authentication
|
||||||
# whoami-foo:
|
# whoami-foo:
|
||||||
@@ -66,7 +65,7 @@ services:
|
|||||||
- "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:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.5.2
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -80,11 +79,13 @@ services:
|
|||||||
CERT_FILE: "/etc/crowdsec/certs/server.pem"
|
CERT_FILE: "/etc/crowdsec/certs/server.pem"
|
||||||
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
|
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
|
||||||
CACERT_FILE: "/etc/crowdsec/certs/inter.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"
|
AGENTS_ALLOWED_OU: "agent-ou"
|
||||||
BOUNCERS_ALLOWED_OU: "bouncer-ou"
|
BOUNCERS_ALLOWED_OU: "bouncer-ou"
|
||||||
LEVEL_DEBUG: "true"
|
LEVEL_DEBUG: "true"
|
||||||
# Disabled because it restart in loop otherwise
|
# Disabled because it restart in loop otherwise
|
||||||
DISABLE_AGENT: "true"
|
# DISABLE_AGENT: "true"
|
||||||
# Disabled for the examples
|
# Disabled for the examples
|
||||||
DISABLE_ONLINE_API: "true"
|
DISABLE_ONLINE_API: "true"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -97,6 +98,8 @@ services:
|
|||||||
- crowdsec-config-tls-auth:/etc/crowdsec/
|
- crowdsec-config-tls-auth:/etc/crowdsec/
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=false"
|
- "traefik.enable=false"
|
||||||
|
depends_on:
|
||||||
|
- gencert
|
||||||
|
|
||||||
gencert:
|
gencert:
|
||||||
build: .
|
build: .
|
||||||
+5
-7
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.10.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,7 +13,7 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
@@ -29,7 +27,7 @@ services:
|
|||||||
|
|
||||||
whoami1:
|
whoami1:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-foo"
|
container_name: "simple-service-foo-trustedips"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -48,7 +46,7 @@ services:
|
|||||||
|
|
||||||
whoami2:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-bar"
|
container_name: "simple-service-bar-trustedips"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -67,7 +65,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.5.2
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
|
||||||
go 1.19
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/leprosus/golang-ttl-map v1.1.7
|
github.com/leprosus/golang-ttl-map v1.1.7
|
||||||
github.com/maxlerebourg/simpleredis v1.0.9
|
github.com/maxlerebourg/simpleredis v1.0.11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
|
||||||
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
|
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.11 h1:B33TUeIrHtJH2/Qj2bRdU+UZ1BvZwFyP55JWMxHirWg=
|
||||||
github.com/maxlerebourg/simpleredis v1.0.9/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
|
github.com/maxlerebourg/simpleredis v1.0.11/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
|
||||||
|
|||||||
Vendored
+12
-5
@@ -3,6 +3,7 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
ttl_map "github.com/leprosus/golang-ttl-map"
|
ttl_map "github.com/leprosus/golang-ttl-map"
|
||||||
@@ -22,6 +23,8 @@ const (
|
|||||||
CaptchaDoneValue = "d"
|
CaptchaDoneValue = "d"
|
||||||
// CacheMiss error string when cache is miss.
|
// CacheMiss error string when cache is miss.
|
||||||
CacheMiss = "cache:miss"
|
CacheMiss = "cache:miss"
|
||||||
|
// CacheUnreachable error string when cache is unreachable.
|
||||||
|
CacheUnreachable = "cache:unreachable"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
@@ -38,7 +41,7 @@ func (localCache) get(key string) (string, error) {
|
|||||||
if isCached && isValid && len(valueString) > 0 {
|
if isCached && isValid && len(valueString) > 0 {
|
||||||
return valueString, nil
|
return valueString, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf(CacheMiss)
|
return "", errors.New(CacheMiss)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (localCache) set(key, value string, duration int64) {
|
func (localCache) set(key, value string, duration int64) {
|
||||||
@@ -59,21 +62,25 @@ func (redisCache) get(key string) (string, error) {
|
|||||||
if err == nil && len(valueString) > 0 {
|
if err == nil && len(valueString) > 0 {
|
||||||
return valueString, nil
|
return valueString, nil
|
||||||
}
|
}
|
||||||
if err.Error() == simpleredis.RedisMiss {
|
errRedisMessage := err.Error()
|
||||||
return "", fmt.Errorf(CacheMiss)
|
if errRedisMessage == simpleredis.RedisMiss {
|
||||||
|
return "", errors.New(CacheMiss)
|
||||||
|
}
|
||||||
|
if errRedisMessage == simpleredis.RedisUnreachable {
|
||||||
|
return "", errors.New(CacheUnreachable)
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc redisCache) set(key, value string, duration int64) {
|
func (rc redisCache) set(key, value string, duration int64) {
|
||||||
if err := redis.Set(key, []byte(value), duration); err != nil {
|
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) {
|
func (rc redisCache) delete(key string) {
|
||||||
if err := redis.Del(key); err != nil {
|
if err := redis.Del(key); err != nil {
|
||||||
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
-3
@@ -11,7 +11,7 @@ import (
|
|||||||
func Test_Get(t *testing.T) {
|
func Test_Get(t *testing.T) {
|
||||||
IPInCache := "10.0.0.10"
|
IPInCache := "10.0.0.10"
|
||||||
IPNotInCache := "10.0.0.20"
|
IPNotInCache := "10.0.0.20"
|
||||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
client.Set(IPInCache, BannedValue, 10)
|
client.Set(IPInCache, BannedValue, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
@@ -47,7 +47,7 @@ func Test_Get(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_Set(t *testing.T) {
|
func Test_Set(t *testing.T) {
|
||||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
IPInCache := "10.0.0.11"
|
IPInCache := "10.0.0.11"
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
@@ -88,7 +88,7 @@ func Test_Set(t *testing.T) {
|
|||||||
func Test_Delete(t *testing.T) {
|
func Test_Delete(t *testing.T) {
|
||||||
IPInCache := "10.0.0.12"
|
IPInCache := "10.0.0.12"
|
||||||
IPNotInCache := "10.0.0.22"
|
IPNotInCache := "10.0.0.22"
|
||||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
client.Set(IPInCache, BannedValue, 10)
|
client.Set(IPInCache, BannedValue, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
|
|||||||
+23
-36
@@ -7,7 +7,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
@@ -20,8 +20,9 @@ type Client struct {
|
|||||||
provider string
|
provider string
|
||||||
siteKey string
|
siteKey string
|
||||||
secretKey string
|
secretKey string
|
||||||
|
remediationCustomHeader string
|
||||||
gracePeriodSeconds int64
|
gracePeriodSeconds int64
|
||||||
htmlPage *template.Template
|
captchaTemplate *template.Template
|
||||||
cacheClient *cache.Client
|
cacheClient *cache.Client
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
log *logger.Log
|
log *logger.Log
|
||||||
@@ -48,32 +49,14 @@ var (
|
|||||||
},
|
},
|
||||||
configuration.TurnstileProvider: {
|
configuration.TurnstileProvider: {
|
||||||
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||||
key: "cf-captcha",
|
key: "cf-turnstile",
|
||||||
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
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.
|
// 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, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
|
||||||
c.Valid = provider != ""
|
c.Valid = provider != ""
|
||||||
if !c.Valid {
|
if !c.Valid {
|
||||||
return nil
|
return nil
|
||||||
@@ -81,11 +64,9 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
|
|||||||
c.siteKey = siteKey
|
c.siteKey = siteKey
|
||||||
c.secretKey = secretKey
|
c.secretKey = secretKey
|
||||||
c.provider = provider
|
c.provider = provider
|
||||||
html, err := compileTemplate(htmlPagePath)
|
c.remediationCustomHeader = remediationCustomHeader
|
||||||
if err != nil {
|
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
|
||||||
return err
|
c.captchaTemplate = html
|
||||||
}
|
|
||||||
c.htmlPage = html
|
|
||||||
c.gracePeriodSeconds = gracePeriodSeconds
|
c.gracePeriodSeconds = gracePeriodSeconds
|
||||||
c.log = log
|
c.log = log
|
||||||
c.httpClient = httpClient
|
c.httpClient = httpClient
|
||||||
@@ -97,29 +78,34 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
|
|||||||
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
|
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
|
||||||
valid, err := c.Validate(r)
|
valid, err := c.Validate(r)
|
||||||
if err != nil {
|
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)
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if valid {
|
if valid {
|
||||||
c.log.Debug("captcha:ServeHTTP captcha: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)
|
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = c.htmlPage.Execute(rw, map[string]string{
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if c.remediationCustomHeader != "" {
|
||||||
|
rw.Header().Set(c.remediationCustomHeader, "captcha")
|
||||||
|
}
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
err = c.captchaTemplate.Execute(rw, map[string]string{
|
||||||
"SiteKey": c.siteKey,
|
"SiteKey": c.siteKey,
|
||||||
"FrontendJS": captcha[c.provider].js,
|
"FrontendJS": captcha[c.provider].js,
|
||||||
"FrontendKey": captcha[c.provider].key,
|
"FrontendKey": captcha[c.provider].key,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// Check Verify if the captcha is already done.
|
||||||
func (c *Client) Check(remoteIP string) bool {
|
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
|
passed := value == cache.CaptchaDoneValue
|
||||||
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
|
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
|
||||||
return passed
|
return passed
|
||||||
@@ -132,10 +118,10 @@ type responseProvider struct {
|
|||||||
// Validate Verify the captcha from provider API.
|
// Validate Verify the captcha from provider API.
|
||||||
func (c *Client) Validate(r *http.Request) (bool, error) {
|
func (c *Client) Validate(r *http.Request) (bool, error) {
|
||||||
if r.Method != http.MethodPost {
|
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
|
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 == "" {
|
if response == "" {
|
||||||
c.log.Debug("captcha:Validate no captcha response found in request")
|
c.log.Debug("captcha:Validate no captcha response found in request")
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -149,10 +135,11 @@ func (c *Client) Validate(r *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err = res.Body.Close(); err != nil {
|
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 res.Header.Get("content-type") != "application/json" {
|
if !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
|
||||||
|
c.log.Debug("captcha:Validate responseType:noJson")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
var captchaResponse responseProvider
|
var captchaResponse responseProvider
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ package configuration
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,6 +28,9 @@ const (
|
|||||||
AppsecMode = "appsec"
|
AppsecMode = "appsec"
|
||||||
HTTPS = "https"
|
HTTPS = "https"
|
||||||
HTTP = "http"
|
HTTP = "http"
|
||||||
|
LogDEBUG = "DEBUG"
|
||||||
|
LogINFO = "INFO"
|
||||||
|
LogERROR = "ERROR"
|
||||||
HcaptchaProvider = "hcaptcha"
|
HcaptchaProvider = "hcaptcha"
|
||||||
RecaptchaProvider = "recaptcha"
|
RecaptchaProvider = "recaptcha"
|
||||||
TurnstileProvider = "turnstile"
|
TurnstileProvider = "turnstile"
|
||||||
@@ -35,12 +40,17 @@ const (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
LogLevel string `json:"logLevel,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
|
LogFilePath string `json:"logFilePath,omitempty"`
|
||||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||||
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
|
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
|
||||||
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
|
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
|
||||||
|
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
|
||||||
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
||||||
|
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
|
||||||
|
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
|
||||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||||
|
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
|
||||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||||
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
||||||
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
||||||
@@ -56,8 +66,10 @@ type Config struct {
|
|||||||
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
||||||
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
|
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
||||||
|
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
|
||||||
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
||||||
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||||
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||||
@@ -66,6 +78,8 @@ type Config struct {
|
|||||||
RedisCachePassword string `json:"redisCachePassword,omitempty"`
|
RedisCachePassword string `json:"redisCachePassword,omitempty"`
|
||||||
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
|
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
|
||||||
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
|
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
|
||||||
|
RedisCacheUnreachableBlock bool `json:"redisCacheUnreachableBlock,omitempty"`
|
||||||
|
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
|
||||||
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
|
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
|
||||||
CaptchaProvider string `json:"captchaProvider,omitempty"`
|
CaptchaProvider string `json:"captchaProvider,omitempty"`
|
||||||
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
|
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
|
||||||
@@ -88,23 +102,31 @@ func contains(source []string, target string) bool {
|
|||||||
func New() *Config {
|
func New() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
LogLevel: "INFO",
|
LogLevel: LogINFO,
|
||||||
|
LogFilePath: "",
|
||||||
CrowdsecMode: LiveMode,
|
CrowdsecMode: LiveMode,
|
||||||
CrowdsecAppsecEnabled: false,
|
CrowdsecAppsecEnabled: false,
|
||||||
CrowdsecAppsecHost: "crowdsec:7422",
|
CrowdsecAppsecHost: "crowdsec:7422",
|
||||||
|
CrowdsecAppsecPath: "/",
|
||||||
CrowdsecAppsecFailureBlock: true,
|
CrowdsecAppsecFailureBlock: true,
|
||||||
|
CrowdsecAppsecUnreachableBlock: true,
|
||||||
|
CrowdsecAppsecBodyLimit: 10485760,
|
||||||
CrowdsecLapiScheme: HTTP,
|
CrowdsecLapiScheme: HTTP,
|
||||||
CrowdsecLapiHost: "crowdsec:8080",
|
CrowdsecLapiHost: "crowdsec:8080",
|
||||||
|
CrowdsecLapiPath: "/",
|
||||||
CrowdsecLapiKey: "",
|
CrowdsecLapiKey: "",
|
||||||
CrowdsecLapiTLSInsecureVerify: false,
|
CrowdsecLapiTLSInsecureVerify: false,
|
||||||
UpdateIntervalSeconds: 60,
|
UpdateIntervalSeconds: 60,
|
||||||
|
UpdateMaxFailure: 0,
|
||||||
DefaultDecisionSeconds: 60,
|
DefaultDecisionSeconds: 60,
|
||||||
HTTPTimeoutSeconds: 10,
|
HTTPTimeoutSeconds: 10,
|
||||||
CaptchaProvider: "",
|
CaptchaProvider: "",
|
||||||
CaptchaSiteKey: "",
|
CaptchaSiteKey: "",
|
||||||
CaptchaSecretKey: "",
|
CaptchaSecretKey: "",
|
||||||
CaptchaHTMLFilePath: "/captcha.html",
|
|
||||||
CaptchaGracePeriodSeconds: 1800,
|
CaptchaGracePeriodSeconds: 1800,
|
||||||
|
CaptchaHTMLFilePath: "/captcha.html",
|
||||||
|
BanHTMLFilePath: "",
|
||||||
|
RemediationHeadersCustomName: "",
|
||||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||||
ForwardedHeadersTrustedIPs: []string{},
|
ForwardedHeadersTrustedIPs: []string{},
|
||||||
ClientTrustedIPs: []string{},
|
ClientTrustedIPs: []string{},
|
||||||
@@ -112,6 +134,7 @@ func New() *Config {
|
|||||||
RedisCacheHost: "redis:6379",
|
RedisCacheHost: "redis:6379",
|
||||||
RedisCachePassword: "",
|
RedisCachePassword: "",
|
||||||
RedisCacheDatabase: "",
|
RedisCacheDatabase: "",
|
||||||
|
RedisCacheUnreachableBlock: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +142,7 @@ func New() *Config {
|
|||||||
func GetVariable(config *Config, key string) (string, error) {
|
func GetVariable(config *Config, key string) (string, error) {
|
||||||
value := ""
|
value := ""
|
||||||
object := reflect.Indirect(reflect.ValueOf(config))
|
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
|
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
|
||||||
fp := field.String()
|
fp := field.String()
|
||||||
if fp != "" {
|
if fp != "" {
|
||||||
@@ -142,6 +165,25 @@ func GetVariable(config *Config, key string) (string, error) {
|
|||||||
return strings.TrimSpace(value), nil
|
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.
|
// ValidateParams validate all the param gave by user.
|
||||||
//
|
//
|
||||||
//nolint:gocyclo,gocognit
|
//nolint:gocyclo,gocognit
|
||||||
@@ -178,13 +220,21 @@ func ValidateParams(config *Config) error {
|
|||||||
if _, err := GetVariable(config, "CaptchaSecretKey"); err != nil {
|
if _, err := GetVariable(config, "CaptchaSecretKey"); err != nil {
|
||||||
return err
|
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 {
|
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost, config.CrowdsecLapiPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
|
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost, config.CrowdsecAppsecPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +252,7 @@ func ValidateParams(config *Config) error {
|
|||||||
}
|
}
|
||||||
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
|
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
|
||||||
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
|
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 == "") {
|
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
|
||||||
lapiKey = strings.TrimSpace(lapiKey)
|
lapiKey = strings.TrimSpace(lapiKey)
|
||||||
if err = validateParamsAPIKey(lapiKey); err != nil {
|
if err = validateParamsAPIKey(lapiKey); err != nil {
|
||||||
@@ -217,14 +267,25 @@ func ValidateParams(config *Config) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check logging configuration
|
||||||
|
|
||||||
|
if !contains([]string{LogERROR, LogDEBUG, LogINFO}, config.LogLevel) {
|
||||||
|
return fmt.Errorf("LogLevel should be one of (%s,%s,%s)", LogDEBUG, LogINFO, LogERROR)
|
||||||
|
}
|
||||||
|
if config.LogFilePath != "" {
|
||||||
|
_, err = os.OpenFile(filepath.Clean(config.LogFilePath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LogFilePath is not writable %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateURL(variable, scheme, host string) error {
|
func validateURL(variable, scheme, host, path string) error {
|
||||||
// This only check that the format of the URL scheme://host is correct and do not make requests
|
// This only check that the format of the URL scheme://host/path is correct and do not make requests
|
||||||
testURL := url.URL{Scheme: scheme, Host: host}
|
testURL := url.URL{Scheme: scheme, Host: host, Path: path}
|
||||||
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
|
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
|
||||||
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v' must be an URL", variable, scheme, host)
|
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v%v' must be a valid URL", variable, scheme, host, path)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -235,7 +296,7 @@ func validateURL(variable, scheme, host string) error {
|
|||||||
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
|
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
|
||||||
func validateParamsAPIKey(lapiKey string) error {
|
func validateParamsAPIKey(lapiKey string) error {
|
||||||
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
|
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 fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -247,19 +308,19 @@ func validateParamsTLS(config *Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if certAuth == "" {
|
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 := new(tls.Config)
|
||||||
tlsConfig.RootCAs = x509.NewCertPool()
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
|
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
|
||||||
return fmt.Errorf("failed parsing pem file")
|
return errors.New("failed parsing pem file")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateParamsIPs(listIP []string, key string) error {
|
func validateParamsIPs(listIP []string, key string) error {
|
||||||
if len(listIP) > 0 {
|
if len(listIP) > 0 {
|
||||||
if _, err := ip.NewChecker(logger.New("INFO"), listIP); err != nil {
|
if _, err := ip.NewChecker(logger.New(LogINFO, ""), listIP); err != nil {
|
||||||
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,14 +349,21 @@ func validateParamsRequired(config *Config) error {
|
|||||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if config.UpdateMaxFailure < -1 {
|
||||||
|
return errors.New("UpdateMaxFailure: cannot be less than -1")
|
||||||
|
}
|
||||||
|
if config.CrowdsecAppsecBodyLimit < 0 {
|
||||||
|
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
|
||||||
|
}
|
||||||
|
|
||||||
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
|
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) {
|
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) {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -324,7 +392,7 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
|
|||||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
|
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
|
||||||
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
||||||
// and CA not load, we can't communicate with https
|
// 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")
|
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func Test_GetTLSConfigCrowdsec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO"))
|
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO", ""))
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@
|
|||||||
package ip
|
package ip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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.
|
// Contains checks if provided address is in the trusted IPs.
|
||||||
func (ip *Checker) Contains(addr string) (bool, error) {
|
func (ip *Checker) Contains(addr string) (bool, error) {
|
||||||
if len(addr) == 0 {
|
if len(addr) == 0 {
|
||||||
return false, fmt.Errorf("Contains:noAddress")
|
return false, errors.New("Contains:noAddress")
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAddr, err := parseIP(addr)
|
ipAddr, err := parseIP(addr)
|
||||||
|
|||||||
+21
-4
@@ -3,9 +3,11 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Log Logger struct.
|
// Log Logger struct.
|
||||||
@@ -16,15 +18,30 @@ type Log struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Set Default log level to info in case log level to defined.
|
// New Set Default log level to info in case log level to defined.
|
||||||
func New(logLevel string) *Log {
|
func New(logLevel string, logFilePath string) *Log {
|
||||||
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
logInfo := log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
logInfo := log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
logDebug := log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
logDebug := log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
logError.SetOutput(os.Stderr)
|
logError.SetOutput(os.Stderr)
|
||||||
logInfo.SetOutput(os.Stdout)
|
logInfo.SetOutput(os.Stdout)
|
||||||
|
// we initialize logger to STDOUT/STDERR first so if the file logger cannot be initialized we can inform the user
|
||||||
if logLevel == "DEBUG" {
|
if logLevel == "DEBUG" {
|
||||||
logDebug.SetOutput(os.Stdout)
|
logDebug.SetOutput(os.Stdout)
|
||||||
}
|
}
|
||||||
|
if logFilePath != "" {
|
||||||
|
logFile, err := os.OpenFile(filepath.Clean(logFilePath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
_ = fmt.Errorf("LogFilePath is not writable %w", err)
|
||||||
|
} else {
|
||||||
|
logInfo.SetOutput(logFile)
|
||||||
|
logError.SetOutput(logFile)
|
||||||
|
if logLevel == "DEBUG" {
|
||||||
|
logDebug.SetOutput(logFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &Log{
|
return &Log{
|
||||||
logError: logError,
|
logError: logError,
|
||||||
logInfo: logInfo,
|
logInfo: logInfo,
|
||||||
@@ -34,15 +51,15 @@ func New(logLevel string) *Log {
|
|||||||
|
|
||||||
// Info log to Stdout.
|
// Info log to Stdout.
|
||||||
func (l *Log) Info(str string) {
|
func (l *Log) Info(str string) {
|
||||||
l.logInfo.Printf(str)
|
l.logInfo.Printf("%s", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug log to Stdout.
|
// Debug log to Stdout.
|
||||||
func (l *Log) Debug(str string) {
|
func (l *Log) Debug(str string) {
|
||||||
l.logDebug.Printf(str)
|
l.logDebug.Printf("%s", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error log to Stderr.
|
// Error log to Stderr.
|
||||||
func (l *Log) Error(str string) {
|
func (l *Log) Error(str string) {
|
||||||
l.logError.Printf(str)
|
l.logError.Printf("%s", str)
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -122,7 +122,6 @@ func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
|
|||||||
}
|
}
|
||||||
read, _ = reader.ReadLineBytes()
|
read, _ = reader.ReadLineBytes()
|
||||||
return redisCmd{Data: read}
|
return redisCmd{Data: read}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# github.com/leprosus/golang-ttl-map v1.1.7
|
# github.com/leprosus/golang-ttl-map v1.1.7
|
||||||
## explicit; go 1.15
|
## explicit; go 1.15
|
||||||
github.com/leprosus/golang-ttl-map
|
github.com/leprosus/golang-ttl-map
|
||||||
# github.com/maxlerebourg/simpleredis v1.0.9
|
# github.com/maxlerebourg/simpleredis v1.0.11
|
||||||
## explicit; go 1.19
|
## explicit; go 1.22
|
||||||
github.com/maxlerebourg/simpleredis
|
github.com/maxlerebourg/simpleredis
|
||||||
|
|||||||
Reference in New Issue
Block a user