mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b57496f8 | ||
|
|
71d845faae | ||
|
|
0d8fd2a7a9 | ||
|
|
1f6a8991c8 | ||
|
|
7f776fe0fe | ||
|
|
e54c1d5c4f | ||
|
|
c2bbc4dac5 | ||
|
|
889c5b55fe | ||
|
|
efb3a67019 | ||
|
|
0780027252 | ||
|
|
892909b9b8 | ||
|
|
c26923dee5 | ||
|
|
a9d83f2097 | ||
|
|
e20ccc5d0c | ||
|
|
50beb4294f | ||
|
|
4ab4f3f183 | ||
|
|
2aac531ea7 | ||
|
|
e8e60c958f | ||
|
|
a2d3708bc3 | ||
|
|
65a2f79fb3 | ||
|
|
a2ecc95dc9 | ||
|
|
7c4f5163e9 | ||
|
|
734975c206 | ||
|
|
7397834e58 | ||
|
|
84a5674b14 | ||
|
|
de7e382fde | ||
|
|
abae855d9e | ||
|
|
4bb6e830dc | ||
|
|
78869ecf77 | ||
|
|
7f99266f99 | ||
|
|
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 | ||
|
|
497d1a2928 | ||
|
|
c3e0c2d4c3 | ||
|
|
575d3a02e5 | ||
|
|
6c183d9231 | ||
|
|
bd71b58f19 | ||
|
|
b68c692ed1 | ||
|
|
fc3da2fc2d | ||
|
|
1a9bdc578f | ||
|
|
f2aea695fc | ||
|
|
2827fef273 | ||
|
|
07c8fae927 | ||
|
|
13c209be3f | ||
|
|
93340ffa55 | ||
|
|
0c2668d578 | ||
|
|
abae7ee028 | ||
|
|
1fcd4f4e2f | ||
|
|
39fcc38980 | ||
|
|
dd322a966a | ||
|
|
f0bb140596 | ||
|
|
46e581eca2 | ||
|
|
50690d1ac7 | ||
|
|
b079073ff6 | ||
|
|
976cbb7d1f | ||
|
|
80726df450 | ||
|
|
4132445a79 | ||
|
|
5b15d6fdc4 | ||
|
|
1b310b2c15 | ||
|
|
87a839f22d | ||
|
|
f14a4e3a68 | ||
|
|
11b615b9bd | ||
|
|
8f937d22a2 | ||
|
|
041b0f67b7 | ||
|
|
5458db93cf |
@@ -1,15 +1,28 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: "[BUG] My short bug title"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug** 🐛
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Expected behavior** 👀
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Context** 🔎
|
||||||
|
Before posting, make sure to use the `logLevel: DEBUG` of the plugin and see if you can fix the error yourself.
|
||||||
|
If not, post your plugin configuration file (anonimize IP and secret key) and a partial logs file where the error is encountered.
|
||||||
|
|
||||||
|
**Version (please complete the following information):**
|
||||||
|
- OS: [e.g. Ubuntu, Debian, Docker]
|
||||||
|
- Traefik version: [e.g. 2.9.*, 3.0.*]
|
||||||
|
- Plugin version: [e.g. 1.1.*]
|
||||||
|
- Redis ? : [e.g. 7.0.*]
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
@@ -17,22 +30,6 @@ Steps to reproduce the behavior:
|
|||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
<!---
|
||||||
A clear and concise description of what you expected to happen.
|
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
|
||||||
|
-->
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ''
|
title: "[FEATURE] My short feature title"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.** 🐛
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like** ✨
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
Add any other context or screenshots about the feature request here.
|
||||||
|
|
||||||
|
<!---
|
||||||
|
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
|
||||||
|
-->
|
||||||
|
|||||||
@@ -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 ./...
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
name: Main
|
name: Main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -12,9 +15,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 +27,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@v6
|
||||||
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@v6
|
||||||
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@v5
|
||||||
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') }}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Release Version Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-version:
|
||||||
|
name: Update version in source
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
TAG="${{ github.event.release.tag_name }}"
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Update version in version.go
|
||||||
|
run: |
|
||||||
|
sed -i 's/pluginVersion = "[^"]*"/pluginVersion = "'"${{ steps.get_version.outputs.version }}"'"/' version.go
|
||||||
|
cat version.go
|
||||||
|
|
||||||
|
- name: Commit, push, and retag
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add version.go
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "Version already up to date, nothing to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "⬆️ chore: bump version to ${{ steps.get_version.outputs.version }}"
|
||||||
|
git push origin main
|
||||||
|
# Move the release tag to include the version update
|
||||||
|
git tag -f "${{ steps.get_version.outputs.tag }}"
|
||||||
|
git push -f origin "${{ steps.get_version.outputs.tag }}"
|
||||||
+30
-22
@@ -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:
|
||||||
@@ -19,30 +15,43 @@ linters-settings:
|
|||||||
locale: US
|
locale: US
|
||||||
funlen:
|
funlen:
|
||||||
lines: -1
|
lines: -1
|
||||||
statements: 50
|
statements: 60
|
||||||
godox:
|
godox:
|
||||||
keywords:
|
keywords:
|
||||||
- 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,20 @@ 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
|
||||||
|
- contextcheck
|
||||||
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."
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
.PHONY: lint test vendor clean
|
.PHONY: lint test vendor clean e2e
|
||||||
|
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
|
|
||||||
|
E2E_SCENARIOS := stream-mode live-mode none-mode trusted-ips custom-ban-page captcha appsec
|
||||||
|
|
||||||
default: lint test
|
default: lint test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@@ -13,6 +15,11 @@ test:
|
|||||||
yaegi_test:
|
yaegi_test:
|
||||||
yaegi test -v .
|
yaegi test -v .
|
||||||
|
|
||||||
|
e2e: $(addprefix e2e_,$(E2E_SCENARIOS))
|
||||||
|
|
||||||
|
e2e_%:
|
||||||
|
./tests/e2e/scenarios/$*/run.sh
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
||||||
@@ -20,71 +27,99 @@ clean:
|
|||||||
rm -rf ./vendor
|
rm -rf ./vendor
|
||||||
|
|
||||||
run_dev:
|
run_dev:
|
||||||
docker-compose -f docker-compose.dev.yml up -d --remove-orphans
|
docker compose -f docker-compose.dev.yml up -d --remove-orphans
|
||||||
|
|
||||||
run_local:
|
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 exemples/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 exemples/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 exemples/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 exemples/binary-vm/ && sudo vagrant up
|
cd examples/binary-vm/ && sudo vagrant up
|
||||||
|
|
||||||
run_tlsauth:
|
run_tlsauth:
|
||||||
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml down && docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml up -d && docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml restart && docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml logs -f
|
docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_appsec:
|
||||||
|
docker compose -f examples/appsec-enabled/docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_custom_captcha:
|
||||||
|
docker compose -f examples/custom-captcha/docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_captcha:
|
||||||
|
docker compose -f examples/captcha/docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_custom_ban_page:
|
||||||
|
docker compose -f examples/custom-ban-page/docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
run:
|
run:
|
||||||
docker-compose -f docker-compose.yml up -d --remove-orphans
|
docker compose -f docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
restart_dev:
|
restart_dev:
|
||||||
docker-compose -f docker-compose.dev.yml restart
|
docker compose -f docker-compose.dev.yml restart
|
||||||
|
|
||||||
restart_local:
|
restart_local:
|
||||||
docker-compose -f docker-compose.local.yml restart
|
docker compose -f docker-compose.local.yml restart
|
||||||
|
|
||||||
restart:
|
restart:
|
||||||
docker-compose -f docker-compose.yml restart
|
docker compose -f docker-compose.yml restart
|
||||||
|
|
||||||
restart_behindproxy:
|
restart_behindproxy:
|
||||||
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml restart
|
docker compose -f examples/behind-proxy/docker-compose.yml restart
|
||||||
|
|
||||||
restart_cacheredis:
|
restart_cacheredis:
|
||||||
docker-compose -f exemples/redis-cache/docker-compose.redis.yml restart
|
docker compose -f examples/redis-cache/docker-compose.yml restart
|
||||||
|
|
||||||
restart_trustedips:
|
restart_trustedips:
|
||||||
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml restart
|
docker compose -f examples/trusted-ips/docker-compose.yml restart
|
||||||
|
|
||||||
restart_tlsauth:
|
restart_tlsauth:
|
||||||
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml
|
docker compose -f examples/tls-auth/docker-compose.yml
|
||||||
|
|
||||||
|
restart_appsec:
|
||||||
|
docker compose -f examples/tls-auth/docker-compose.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
|
||||||
|
|
||||||
show_local_logs:
|
show_local_logs:
|
||||||
docker-compose -f docker-compose.local.yml logs -f
|
docker compose -f docker-compose.local.yml logs -f
|
||||||
|
|
||||||
show_dev_logs:
|
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 exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
|
docker compose -f examples/behind-proxy/docker-compose.yml down --remove-orphans
|
||||||
docker-compose -f exemples/redis-cache/docker-compose.redis.yml down --remove-orphans
|
docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans
|
||||||
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
|
docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans
|
||||||
docker-compose -f exemples/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 docker-compose.local.yml down --remove-orphans
|
docker compose -f examples/appsec-enabled/docker-compose.yml down --remove-orphans
|
||||||
docker-compose -f docker-compose.yml down --remove-orphans
|
docker compose -f examples/captcha/docker-compose.yml down --remove-orphans
|
||||||
|
docker compose -f examples/custom-captcha/docker-compose.yml down --remove-orphans
|
||||||
|
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
|
||||||
|
docker compose -f docker-compose.local.yml down --remove-orphans
|
||||||
|
docker compose -f docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
clean_vagrant:
|
clean_vagrant:
|
||||||
cd exemples/binary-vm/ && sudo vagrant destroy -f
|
cd examples/binary-vm/ && sudo vagrant destroy -f
|
||||||
|
|
||||||
|
|
||||||
show_metrics:
|
show_metrics:
|
||||||
docker exec crowdsec cscli metrics
|
docker exec crowdsec cscli metrics
|
||||||
|
|
||||||
|
show_decisions:
|
||||||
|
docker exec crowdsec cscli decisions list
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
|
---
|
||||||
filenames:
|
filenames:
|
||||||
- /var/log/traefik/access.log
|
- /var/log/traefik/access.log
|
||||||
labels:
|
labels:
|
||||||
type: traefik
|
type: traefik
|
||||||
|
|
||||||
|
---
|
||||||
|
listen_addr: 0.0.0.0:7422
|
||||||
|
appsec_config: crowdsecurity/virtual-patching
|
||||||
|
name: myAppSecComponent
|
||||||
|
source: appsec
|
||||||
|
labels:
|
||||||
|
type: appsec
|
||||||
|
|||||||
@@ -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>
|
||||||
+621
-120
@@ -1,37 +1,76 @@
|
|||||||
// 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 (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
htmltemplate "html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||||
|
captcha "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha"
|
||||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
crowdsecLapiHeader = "X-Api-Key"
|
crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip"
|
||||||
crowdsecLapiRoute = "v1/decisions"
|
crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri"
|
||||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
|
||||||
cacheTimeoutKey = "updated"
|
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
|
||||||
|
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
|
||||||
|
crowdsecAppsecUserAgent = "X-Crowdsec-Appsec-User-Agent"
|
||||||
|
crowdsecLapiHeader = "X-Api-Key"
|
||||||
|
crowdsecLapiRoute = "v1/decisions"
|
||||||
|
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||||
|
crowdsecLapiMetricsRoute = "v1/usage-metrics"
|
||||||
|
crowdsecCapiHost = "api.crowdsec.net"
|
||||||
|
crowdsecCapiHeader = "Authorization"
|
||||||
|
crowdsecCapiLoginRoute = "v2/watchers/login"
|
||||||
|
crowdsecCapiStreamRoute = "v2/decisions/stream"
|
||||||
|
cacheTimeoutKey = "updated"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ##############################################################
|
||||||
|
// Important: traefik creates an instance of the bouncer per route.
|
||||||
|
// We rely on globals (both here and in the memory cache) to share info between
|
||||||
|
// routes. This means that some of the plugins parameters will only work "once"
|
||||||
|
// and will take the values of the first middleware that was instantiated even
|
||||||
|
// if you have different middlewares with different parameters. This design
|
||||||
|
// makes it impossible to have multiple crowdsec implementations per cluster (unless you have multiple traefik deployments in it)
|
||||||
|
// - updateInterval
|
||||||
|
// - updateMaxFailure
|
||||||
|
// - defaultDecisionTimeout
|
||||||
|
// - redisUnreachableBlock
|
||||||
|
// - appsecEnabled
|
||||||
|
// - appsecHost
|
||||||
|
// - metricsUpdateIntervalSeconds
|
||||||
|
// - others...
|
||||||
|
// ###################################
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
isCrowdsecStreamHealthy = false
|
isCrowdsecStreamStartup = true
|
||||||
ticker chan bool
|
isCrowdsecStreamHealthy = true
|
||||||
|
updateFailure int64
|
||||||
|
streamTicker chan bool
|
||||||
|
metricsTicker chan bool
|
||||||
|
lastMetricsPush time.Time
|
||||||
|
blockedRequests int64
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateConfig creates the default plugin configuration.
|
// CreateConfig creates the default plugin configuration.
|
||||||
@@ -45,148 +84,316 @@ type Bouncer struct {
|
|||||||
name string
|
name string
|
||||||
template *template.Template
|
template *template.Template
|
||||||
|
|
||||||
enabled bool
|
enabled bool
|
||||||
crowdsecScheme string
|
appsecEnabled bool
|
||||||
crowdsecHost string
|
appsecScheme string
|
||||||
crowdsecKey string
|
appsecHost string
|
||||||
crowdsecMode string
|
appsecPath string
|
||||||
updateInterval int64
|
appsecKey string
|
||||||
defaultDecisionTimeout int64
|
appsecFailureBlock bool
|
||||||
customHeader string
|
appsecUnreachableBlock bool
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
appsecBodyLimit int64
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
crowdsecScheme string
|
||||||
client *http.Client
|
crowdsecHost string
|
||||||
|
crowdsecPath string
|
||||||
|
crowdsecKey string
|
||||||
|
crowdsecMode string
|
||||||
|
crowdsecMachineID string
|
||||||
|
crowdsecPassword string
|
||||||
|
crowdsecScenarios []string
|
||||||
|
updateInterval int64
|
||||||
|
updateMaxFailure int64
|
||||||
|
defaultDecisionTimeout int64
|
||||||
|
remediationStatusCode int
|
||||||
|
remediationCustomHeader string
|
||||||
|
forwardedCustomHeader string
|
||||||
|
crowdsecStreamRoute string
|
||||||
|
crowdsecHeader string
|
||||||
|
redisUnreachableBlock bool
|
||||||
|
banTemplate *htmltemplate.Template
|
||||||
|
traceCustomHeader string
|
||||||
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
|
httpClient *http.Client
|
||||||
|
httpAppsecClient *http.Client
|
||||||
|
cacheClient *cache.Client
|
||||||
|
captchaClient *captcha.Client
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
//
|
||||||
logger.Init(config.LogLevel)
|
//nolint:nestif,gocyclo,gocognit
|
||||||
err := configuration.ValidateParams(config)
|
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
||||||
|
config.LogLevel = strings.ToUpper(config.LogLevel)
|
||||||
|
log := logger.NewWithFormat(config.LogLevel, config.LogFilePath, config.LogFormat)
|
||||||
|
err := configuration.ValidateParams(config, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
|
log.Error("New:validateParams " + err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
|
||||||
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
|
||||||
|
|
||||||
tlsConfig, err := configuration.GetTLSConfigCrowdsec(config)
|
var tlsAppsecConfig *tls.Config
|
||||||
if err != nil {
|
if config.CrowdsecAppsecEnabled {
|
||||||
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
tlsAppsecConfig, err = configuration.GetTLSConfigCrowdsec(config, log, true)
|
||||||
return nil, err
|
if config.CrowdsecAppsecScheme == "" {
|
||||||
|
config.CrowdsecAppsecScheme = config.CrowdsecLapiScheme
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error("New:getTLSConfigCrowdsec fail to get tlsAppsecConfig " + err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
apiAppsecKey, errAppsecKey := configuration.GetVariable(config, "CrowdsecAppsecKey")
|
||||||
|
if errAppsecKey != nil && len(tlsAppsecConfig.Certificates) == 0 {
|
||||||
|
log.Info("New:crowdsecLapiKey fail to get CrowdsecAppsecKey and no client certificate setup " + errAppsecKey.Error())
|
||||||
|
}
|
||||||
|
config.CrowdsecAppsecKey = apiAppsecKey
|
||||||
}
|
}
|
||||||
apiKey, err := configuration.GetVariable(config, "CrowdsecLapiKey")
|
|
||||||
if err != nil && len(tlsConfig.Certificates) == 0 {
|
var tlsConfig *tls.Config
|
||||||
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
|
crowdsecStreamRoute := ""
|
||||||
return nil, err
|
crowdsecHeader := ""
|
||||||
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
|
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
|
||||||
|
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
|
||||||
|
config.CrowdsecLapiScheme = configuration.HTTPS
|
||||||
|
config.CrowdsecLapiHost = crowdsecCapiHost
|
||||||
|
config.CrowdsecLapiPath = "/"
|
||||||
|
config.UpdateIntervalSeconds = 7200 // 2 hours
|
||||||
|
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
||||||
|
crowdsecHeader = crowdsecCapiHeader
|
||||||
|
} else {
|
||||||
|
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
||||||
|
crowdsecHeader = crowdsecLapiHeader
|
||||||
|
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
apiKey, errKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||||
|
if errKey != nil && len(tlsConfig.Certificates) == 0 {
|
||||||
|
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errKey.Error())
|
||||||
|
return nil, errKey
|
||||||
|
}
|
||||||
|
config.CrowdsecLapiKey = apiKey
|
||||||
|
if config.CrowdsecAppsecKey == "" {
|
||||||
|
config.CrowdsecAppsecKey = apiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var banTemplate *htmltemplate.Template
|
||||||
|
if config.BanHTMLFilePath != "" {
|
||||||
|
banTemplate, _ = configuration.GetHTMLTemplate(config.BanHTMLFilePath)
|
||||||
}
|
}
|
||||||
apiKey = strings.TrimSuffix(apiKey, "\n")
|
|
||||||
|
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
next: next,
|
next: next,
|
||||||
name: name,
|
name: name,
|
||||||
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
|
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
|
||||||
|
|
||||||
enabled: config.Enabled,
|
enabled: config.Enabled,
|
||||||
crowdsecMode: config.CrowdsecMode,
|
crowdsecMode: config.CrowdsecMode,
|
||||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
appsecEnabled: config.CrowdsecAppsecEnabled,
|
||||||
crowdsecHost: config.CrowdsecLapiHost,
|
appsecScheme: config.CrowdsecAppsecScheme,
|
||||||
crowdsecKey: apiKey,
|
appsecHost: config.CrowdsecAppsecHost,
|
||||||
updateInterval: config.UpdateIntervalSeconds,
|
appsecPath: config.CrowdsecAppsecPath,
|
||||||
customHeader: config.ForwardedHeadersCustomName,
|
appsecKey: config.CrowdsecAppsecKey,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
||||||
|
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
||||||
|
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
||||||
|
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||||
|
crowdsecHost: config.CrowdsecLapiHost,
|
||||||
|
crowdsecPath: config.CrowdsecLapiPath,
|
||||||
|
crowdsecKey: config.CrowdsecLapiKey,
|
||||||
|
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
||||||
|
crowdsecPassword: config.CrowdsecCapiPassword,
|
||||||
|
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
||||||
|
updateInterval: config.UpdateIntervalSeconds,
|
||||||
|
updateMaxFailure: config.UpdateMaxFailure,
|
||||||
|
remediationCustomHeader: config.RemediationHeadersCustomName,
|
||||||
|
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
||||||
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
|
remediationStatusCode: config.RemediationStatusCode,
|
||||||
|
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
traceCustomHeader: config.TraceHeadersCustomName,
|
||||||
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
|
crowdsecHeader: crowdsecHeader,
|
||||||
|
log: log,
|
||||||
serverPoolStrategy: &ip.PoolStrategy{
|
serverPoolStrategy: &ip.PoolStrategy{
|
||||||
Checker: serverChecker,
|
Checker: serverChecker,
|
||||||
},
|
},
|
||||||
clientPoolStrategy: &ip.PoolStrategy{
|
clientPoolStrategy: &ip.PoolStrategy{
|
||||||
Checker: clientChecker,
|
Checker: clientChecker,
|
||||||
},
|
},
|
||||||
client: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: tlsConfig,
|
||||||
},
|
},
|
||||||
Timeout: 10 * time.Second,
|
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
||||||
},
|
},
|
||||||
|
httpAppsecClient: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
TLSClientConfig: tlsAppsecConfig,
|
||||||
|
},
|
||||||
|
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
||||||
|
},
|
||||||
|
cacheClient: &cache.Client{},
|
||||||
|
captchaClient: &captcha.Client{},
|
||||||
}
|
}
|
||||||
if config.RedisCacheEnabled {
|
if config.CrowdsecMode == configuration.AppsecMode {
|
||||||
cache.InitRedisClient(config.RedisCacheHost)
|
return bouncer, nil
|
||||||
}
|
}
|
||||||
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
|
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
|
||||||
ticker = startTicker(config, func() {
|
bouncer.cacheClient.New(
|
||||||
handleStreamCache(bouncer)
|
log,
|
||||||
|
config.RedisCacheEnabled,
|
||||||
|
config.RedisCacheHost,
|
||||||
|
config.RedisCachePassword,
|
||||||
|
config.RedisCacheDatabase,
|
||||||
|
)
|
||||||
|
config.CaptchaSiteKey, _ = configuration.GetVariable(config, "CaptchaSiteKey")
|
||||||
|
config.CaptchaSecretKey, _ = configuration.GetVariable(config, "CaptchaSecretKey")
|
||||||
|
err = bouncer.captchaClient.New(
|
||||||
|
log,
|
||||||
|
bouncer.cacheClient,
|
||||||
|
&http.Client{
|
||||||
|
Transport: &http.Transport{MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second},
|
||||||
|
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
||||||
|
},
|
||||||
|
config.CaptchaProvider,
|
||||||
|
config.CaptchaCustomJsURL,
|
||||||
|
config.CaptchaCustomKey,
|
||||||
|
config.CaptchaCustomResponse,
|
||||||
|
config.CaptchaCustomValidateURL,
|
||||||
|
config.CaptchaSiteKey,
|
||||||
|
config.CaptchaSecretKey,
|
||||||
|
config.RemediationHeadersCustomName,
|
||||||
|
config.CaptchaHTMLFilePath,
|
||||||
|
config.CaptchaGracePeriodSeconds,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("CaptchaClient not valid " + err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && streamTicker == nil {
|
||||||
|
if config.CrowdsecMode == configuration.AloneMode {
|
||||||
|
if err := getToken(bouncer); err != nil {
|
||||||
|
bouncer.log.Error("New:getToken " + err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.StreamStartupBlock {
|
||||||
|
handleStreamTicker(bouncer)
|
||||||
|
} else {
|
||||||
|
go handleStreamTicker(bouncer)
|
||||||
|
}
|
||||||
|
streamTicker = startTicker("stream", config.UpdateIntervalSeconds, log, func() {
|
||||||
|
handleStreamTicker(bouncer)
|
||||||
})
|
})
|
||||||
go handleStreamCache(bouncer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start metrics ticker if not already running
|
||||||
|
if metricsTicker == nil && config.MetricsUpdateIntervalSeconds > 0 {
|
||||||
|
lastMetricsPush = time.Now() // Initialize lastMetricsPush when starting the metrics ticker
|
||||||
|
go handleMetricsTicker(bouncer)
|
||||||
|
metricsTicker = startTicker("metrics", config.MetricsUpdateIntervalSeconds, log, func() {
|
||||||
|
handleMetricsTicker(bouncer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
logger.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)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.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)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
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
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
|
||||||
if isTrusted {
|
if isTrusted {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bouncer.crowdsecMode == configuration.AppsecMode {
|
||||||
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// TODO This should be simplified
|
// TODO This should be simplified
|
||||||
if bouncer.crowdsecMode != configuration.NoneMode {
|
if bouncer.crowdsecMode != configuration.NoneMode {
|
||||||
isBanned, erro := cache.GetDecision(remoteIP)
|
value, cacheErr := bouncer.cacheClient.Get(remoteIP)
|
||||||
if erro != nil {
|
if cacheErr != nil {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
|
cacheErrString := cacheErr.Error()
|
||||||
if erro.Error() == simpleredis.RedisUnreachable {
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
||||||
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
||||||
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cacheErrString != cache.CacheMiss {
|
||||||
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
||||||
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
|
||||||
if isBanned {
|
if value == cache.NoBannedValue {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.handleRemediationServeHTTP(rw, req, remoteIP, value)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right here if we cannot join the stream we forbid the request to go on.
|
// Right here if we cannot join the stream we forbid the request to go on.
|
||||||
if bouncer.crowdsecMode == configuration.StreamMode {
|
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
if isCrowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
logger.Error(fmt.Sprintf("ServeHTTP:isCrowdsecStreamHealthy ip:%s", remoteIP))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = handleNoStreamCache(bouncer, remoteIP)
|
value, err := handleNoStreamCache(bouncer, remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Debug("handleNoStreamCache:crowdsecQuery " + err.Error())
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
}
|
||||||
|
if value == cache.NoBannedValue {
|
||||||
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
bouncer.next.ServeHTTP(rw, req)
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
||||||
|
bouncer.handleRemediationServeHTTP(rw, req, remoteIP, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,11 +419,100 @@ type Stream struct {
|
|||||||
New []Decision `json:"new"`
|
New []Decision `json:"new"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTicker(config *configuration.Config, work func()) chan bool {
|
// Login Body returned from Crowdsec Login CAPI.
|
||||||
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
type Login struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Expire string `json:"expire"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// To append Headers we need to call rw.WriteHeader after set any header.
|
||||||
|
func (bouncer *Bouncer) handleBanServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP, reason string) {
|
||||||
|
atomic.AddInt64(&blockedRequests, 1)
|
||||||
|
|
||||||
|
if bouncer.remediationCustomHeader != "" {
|
||||||
|
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
||||||
|
}
|
||||||
|
if bouncer.banTemplate == nil {
|
||||||
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
|
|
||||||
|
if req.Method == http.MethodHead {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateData := map[string]string{
|
||||||
|
"RemediationReason": reason,
|
||||||
|
"ClientIP": remoteIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
if bouncer.traceCustomHeader != "" {
|
||||||
|
headerVal := req.Header.Get(bouncer.traceCustomHeader)
|
||||||
|
|
||||||
|
if headerVal != "" {
|
||||||
|
templateData["TraceID"] = headerVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bouncer.banTemplate.Execute(rw, templateData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
bouncer.log.Warn("handleBanServeHTTP could not write template to ResponseWriter: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bouncer *Bouncer) handleRemediationServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP, remediation string) {
|
||||||
|
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
||||||
|
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue && req.Method != http.MethodHead {
|
||||||
|
if bouncer.captchaClient.Check(remoteIP) {
|
||||||
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&blockedRequests, 1) // If we serve a captcha that should count as a dropped request.
|
||||||
|
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonLAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bouncer *Bouncer) handleNextServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP string) {
|
||||||
|
if bouncer.appsecEnabled {
|
||||||
|
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
||||||
|
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
||||||
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonAPPSEC)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStreamTicker(bouncer *Bouncer) {
|
||||||
|
if err := handleStreamCache(bouncer); err != nil {
|
||||||
|
bouncer.log.Warn(fmt.Sprintf("handleStreamTicker updateFailure:%d isCrowdsecStreamHealthy:%t %s", updateFailure, isCrowdsecStreamHealthy, err.Error()))
|
||||||
|
if bouncer.updateMaxFailure != -1 && updateFailure >= bouncer.updateMaxFailure && isCrowdsecStreamHealthy {
|
||||||
|
isCrowdsecStreamHealthy = false
|
||||||
|
bouncer.log.Error(fmt.Sprintf("handleStreamTicker:error updateFailure:%d %s", updateFailure, err.Error()))
|
||||||
|
}
|
||||||
|
updateFailure++
|
||||||
|
} else {
|
||||||
|
isCrowdsecStreamHealthy = true
|
||||||
|
updateFailure = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMetricsTicker(bouncer *Bouncer) {
|
||||||
|
if err := reportMetrics(bouncer); err != nil {
|
||||||
|
bouncer.log.Error("handleMetricsTicker:reportMetrics " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTicker(name string, updateInterval int64, log *slog.Logger, work func()) chan bool {
|
||||||
|
ticker := time.NewTicker(time.Duration(updateInterval) * time.Second)
|
||||||
stop := make(chan bool, 1)
|
stop := make(chan bool, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer logger.Debug("ticker:stopped")
|
defer log.Debug(name + "_ticker:stopped")
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -230,109 +526,314 @@ func startTicker(config *configuration.Config, work func()) chan bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We are now in none or live mode.
|
// We are now in none or live mode.
|
||||||
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
||||||
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
|
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
|
||||||
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", remoteIP),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, routeURL.String())
|
body, err := crowdsecQuery(bouncer, routeURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return cache.BannedValue, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(body, []byte("null")) {
|
if bytes.Equal(body, []byte("null")) {
|
||||||
if isLiveMode {
|
if isLiveMode {
|
||||||
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
bouncer.cacheClient.Set(remoteIP, cache.NoBannedValue, bouncer.defaultDecisionTimeout)
|
||||||
}
|
}
|
||||||
return nil
|
return cache.NoBannedValue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var decisions []Decision
|
var decisions []Decision
|
||||||
err = json.Unmarshal(body, &decisions)
|
err = json.Unmarshal(body, &decisions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("handleNoStreamCache:parseBody %w", err)
|
return cache.BannedValue, fmt.Errorf("handleNoStreamCache:parseBody %w", err)
|
||||||
}
|
}
|
||||||
if len(decisions) == 0 {
|
if len(decisions) == 0 {
|
||||||
if isLiveMode {
|
if isLiveMode {
|
||||||
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
bouncer.cacheClient.Set(remoteIP, cache.NoBannedValue, bouncer.defaultDecisionTimeout)
|
||||||
}
|
}
|
||||||
return nil
|
return cache.NoBannedValue, nil
|
||||||
}
|
}
|
||||||
duration, err := time.ParseDuration(decisions[0].Duration)
|
var decision Decision
|
||||||
|
for _, d := range decisions {
|
||||||
|
decision = d
|
||||||
|
if decision.Type == "ban" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
duration, err := time.ParseDuration(decision.Duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
return cache.BannedValue, fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
||||||
}
|
}
|
||||||
if isLiveMode {
|
var value string
|
||||||
cache.SetDecision(remoteIP, true, int64(duration.Seconds()))
|
switch decision.Type {
|
||||||
|
case "ban":
|
||||||
|
value = cache.BannedValue
|
||||||
|
case "captcha":
|
||||||
|
value = cache.CaptchaValue
|
||||||
|
default:
|
||||||
|
bouncer.log.Info("handleStreamCache:unknownType " + decision.Type)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("handleNoStreamCache:banned")
|
if isLiveMode && bouncer.defaultDecisionTimeout > 0 {
|
||||||
|
durationSecond := int64(duration.Seconds())
|
||||||
|
if bouncer.defaultDecisionTimeout < durationSecond {
|
||||||
|
durationSecond = bouncer.defaultDecisionTimeout
|
||||||
|
}
|
||||||
|
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
|
||||||
|
}
|
||||||
|
return value, errors.New("handleNoStreamCache:banned")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStreamCache(bouncer *Bouncer) {
|
func getToken(bouncer *Bouncer) error {
|
||||||
|
loginURL := url.URL{
|
||||||
|
Scheme: bouncer.crowdsecScheme,
|
||||||
|
Host: bouncer.crowdsecHost,
|
||||||
|
Path: crowdsecCapiLoginRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the login-specific payload here
|
||||||
|
loginData := []byte(fmt.Sprintf(
|
||||||
|
`{"machine_id": "%v","password": "%v","scenarios": ["%v"]}`,
|
||||||
|
bouncer.crowdsecMachineID,
|
||||||
|
bouncer.crowdsecPassword,
|
||||||
|
strings.Join(bouncer.crowdsecScenarios, `","`),
|
||||||
|
))
|
||||||
|
|
||||||
|
body, err := crowdsecQuery(bouncer, loginURL.String(), loginData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var login Login
|
||||||
|
err = json.Unmarshal(body, &login)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getToken:parsingBody %w", err)
|
||||||
|
}
|
||||||
|
if login.Code == http.StatusOK && len(login.Token) > 0 {
|
||||||
|
bouncer.crowdsecKey = login.Token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bouncer.log.Warn(fmt.Sprintf("getToken statusCode:%d", login.Code))
|
||||||
|
return fmt.Errorf("getToken statusCode:%d", login.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStreamCache(bouncer *Bouncer) error {
|
||||||
// TODO clean properly on exit.
|
// TODO clean properly on exit.
|
||||||
// Instead of blocking the goroutine interval for all the secondary node,
|
// Instead of blocking the goroutine interval for all the secondary node,
|
||||||
// if the master service is shut down, other goroutine can take the lead
|
// if the master service is shut down, other goroutine can take the lead
|
||||||
// because updated routine information is in the cache
|
// because updated routine information is in the cache
|
||||||
_, err := cache.GetDecision(cacheTimeoutKey)
|
_, err := bouncer.cacheClient.Get(cacheTimeoutKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Debug("handleStreamCache:alreadyUpdated")
|
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
|
||||||
return
|
isCrowdsecStreamStartup = false
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
if err.Error() != cache.CacheMiss {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bouncer.cacheClient.Set(cacheTimeoutKey, cache.NoBannedValue, bouncer.updateInterval-1)
|
||||||
streamRouteURL := url.URL{
|
streamRouteURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: crowdsecLapiStreamRoute,
|
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
|
||||||
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
|
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isCrowdsecStreamStartup),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
|
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err.Error())
|
return err
|
||||||
isCrowdsecStreamHealthy = false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var stream Stream
|
var stream Stream
|
||||||
err = json.Unmarshal(body, &stream)
|
err = json.Unmarshal(body, &stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("handleStreamCache:parsingBody %s", err.Error()))
|
return fmt.Errorf("handleStreamCache:parsingBody %w", err)
|
||||||
isCrowdsecStreamHealthy = false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
for _, decision := range stream.New {
|
for _, decision := range stream.New {
|
||||||
duration, err := time.ParseDuration(decision.Duration)
|
duration, err := time.ParseDuration(decision.Duration)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
var value string
|
||||||
|
switch decision.Type {
|
||||||
|
case "ban":
|
||||||
|
value = cache.BannedValue
|
||||||
|
case "captcha":
|
||||||
|
value = cache.CaptchaValue
|
||||||
|
default:
|
||||||
|
bouncer.log.Info("handleStreamCache:unknownType " + decision.Type)
|
||||||
|
}
|
||||||
|
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
cache.DeleteDecision(decision.Value)
|
bouncer.cacheClient.Delete(decision.Value)
|
||||||
}
|
}
|
||||||
isCrowdsecStreamHealthy = true
|
bouncer.log.Debug("handleStreamCache:updated")
|
||||||
|
isCrowdsecStreamStartup = false
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
|
func crowdsecQuery(bouncer *Bouncer, stringURL string, data []byte) ([]byte, error) {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
if len(data) > 0 {
|
||||||
req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey)
|
req, _ = http.NewRequest(http.MethodPost, stringURL, bytes.NewBuffer(data))
|
||||||
res, err := bouncer.client.Do(req)
|
} else {
|
||||||
if err != nil {
|
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||||
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
|
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
req.Header.Set(bouncer.crowdsecHeader, bouncer.crowdsecKey)
|
||||||
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
|
req.Header.Set("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/"+pluginVersion)
|
||||||
|
|
||||||
|
res, err := bouncer.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
|
bouncer.log.Error("crowdsecQuery:closeBody " + err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
body, err := io.ReadAll(res.Body)
|
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
|
if errToken := getToken(bouncer); errToken != nil {
|
||||||
|
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
|
||||||
|
}
|
||||||
|
return crowdsecQuery(bouncer, stringURL, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the status code starts with 2
|
||||||
|
statusStr := strconv.Itoa(res.StatusCode)
|
||||||
|
if len(statusStr) < 1 || statusStr[0] != '2' {
|
||||||
|
return nil, fmt.Errorf("crowdsecQuery method:%s url:%s, statusCode:%d (expected: 2xx)", req.Method, stringURL, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("crowdsecQuery:readBody %w", err)
|
return nil, fmt.Errorf("crowdsecQuery:readBody %w", err)
|
||||||
}
|
}
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
||||||
|
routeURL := url.URL{
|
||||||
|
Scheme: bouncer.appsecScheme,
|
||||||
|
Host: bouncer.appsecHost,
|
||||||
|
Path: bouncer.appsecPath,
|
||||||
|
}
|
||||||
|
var req *http.Request
|
||||||
|
if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil {
|
||||||
|
var bodyBuffer bytes.Buffer
|
||||||
|
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
|
||||||
|
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
|
||||||
|
bodyBytes, err := io.ReadAll(teeReader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("appsecQuery:GetBody %w", err)
|
||||||
|
}
|
||||||
|
// 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))
|
||||||
|
} else {
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, headers := range httpReq.Header {
|
||||||
|
for _, value := range headers {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.Header.Set(crowdsecAppsecHeader, bouncer.appsecKey)
|
||||||
|
req.Header.Set(crowdsecAppsecIPHeader, ip)
|
||||||
|
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
|
||||||
|
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
|
||||||
|
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
|
||||||
|
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
|
||||||
|
req.Header.Set("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/"+pluginVersion)
|
||||||
|
|
||||||
|
res, err := bouncer.httpAppsecClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
bouncer.log.Error("appsecQuery:unreachable")
|
||||||
|
if bouncer.appsecUnreachableBlock {
|
||||||
|
return fmt.Errorf("appsecQuery:unreachable %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = res.Body.Close(); err != nil {
|
||||||
|
bouncer.log.Error("appsecQuery:closeBody " + err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if res.StatusCode == http.StatusInternalServerError {
|
||||||
|
bouncer.log.Info("appsecQuery:failure")
|
||||||
|
if bouncer.appsecFailureBlock {
|
||||||
|
return errors.New("appsecQuery statusCode:500")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("appsecQuery:readBody %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportMetrics(bouncer *Bouncer) error {
|
||||||
|
now := time.Now()
|
||||||
|
currentCount := atomic.LoadInt64(&blockedRequests)
|
||||||
|
windowSizeSeconds := int(now.Sub(lastMetricsPush).Seconds())
|
||||||
|
|
||||||
|
bouncer.log.Debug(fmt.Sprintf("reportMetrics: blocked_requests=%d window_size=%ds", currentCount, windowSizeSeconds))
|
||||||
|
|
||||||
|
metrics := map[string]interface{}{
|
||||||
|
"remediation_components": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"version": pluginVersion,
|
||||||
|
"type": "bouncer",
|
||||||
|
"name": "traefik_plugin",
|
||||||
|
"metrics": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"items": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"name": "dropped",
|
||||||
|
"value": currentCount,
|
||||||
|
"unit": "request",
|
||||||
|
"labels": map[string]string{
|
||||||
|
"type": "traefik_plugin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"meta": map[string]interface{}{
|
||||||
|
"window_size_seconds": windowSizeSeconds,
|
||||||
|
"utc_now_timestamp": now.Unix(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"utc_startup_timestamp": time.Now().Unix(),
|
||||||
|
"feature_flags": []string{},
|
||||||
|
"os": map[string]string{
|
||||||
|
"name": "unknown",
|
||||||
|
"version": "unknown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(metrics)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reportMetrics:marshal %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsURL := url.URL{
|
||||||
|
Scheme: bouncer.crowdsecScheme,
|
||||||
|
Host: bouncer.crowdsecHost,
|
||||||
|
Path: bouncer.crowdsecPath + crowdsecLapiMetricsRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = crowdsecQuery(bouncer, metricsURL.String(), data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reportMetrics:query %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.StoreInt64(&blockedRequests, 0)
|
||||||
|
lastMetricsPush = now
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getTestConfig returns a minimal valid configuration for testing.
|
||||||
|
// Override specific fields by modifying the returned config.
|
||||||
|
func getTestConfig() *configuration.Config {
|
||||||
|
return &configuration.Config{
|
||||||
|
Enabled: true,
|
||||||
|
LogLevel: "INFO",
|
||||||
|
LogFormat: "common",
|
||||||
|
LogFilePath: "",
|
||||||
|
CrowdsecMode: "none",
|
||||||
|
CrowdsecLapiKey: "test-key",
|
||||||
|
CrowdsecLapiHost: "localhost",
|
||||||
|
CrowdsecLapiScheme: "http",
|
||||||
|
UpdateIntervalSeconds: 60,
|
||||||
|
DefaultDecisionSeconds: 60,
|
||||||
|
HTTPTimeoutSeconds: 10,
|
||||||
|
ForwardedHeadersTrustedIPs: []string{"127.0.0.1"},
|
||||||
|
ForwardedHeadersCustomName: "",
|
||||||
|
RemediationStatusCode: 403,
|
||||||
|
BanHTMLFilePath: "",
|
||||||
|
RemediationHeadersCustomName: "",
|
||||||
|
CaptchaProvider: "",
|
||||||
|
CaptchaSiteKey: "",
|
||||||
|
CaptchaSecretKey: "",
|
||||||
|
CaptchaGracePeriodSeconds: 1,
|
||||||
|
CaptchaHTMLFilePath: "",
|
||||||
|
RedisCacheEnabled: false,
|
||||||
|
RedisCacheHost: "",
|
||||||
|
RedisCachePassword: "",
|
||||||
|
RedisCacheDatabase: "",
|
||||||
|
RedisCacheUnreachableBlock: false,
|
||||||
|
CrowdsecAppsecEnabled: false,
|
||||||
|
CrowdsecAppsecHost: "",
|
||||||
|
CrowdsecAppsecPath: "",
|
||||||
|
CrowdsecAppsecFailureBlock: false,
|
||||||
|
CrowdsecAppsecUnreachableBlock: false,
|
||||||
|
CrowdsecLapiTLSInsecureVerify: true,
|
||||||
|
CrowdsecLapiTLSCertificateBouncer: "",
|
||||||
|
CrowdsecLapiTLSCertificateBouncerKey: "",
|
||||||
|
CrowdsecCapiMachineID: "",
|
||||||
|
CrowdsecCapiPassword: "",
|
||||||
|
CrowdsecCapiScenarios: []string{},
|
||||||
|
UpdateMaxFailure: 0,
|
||||||
|
MetricsUpdateIntervalSeconds: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create and execute a bouncer request for testing
|
||||||
|
func createAndExecuteBouncerRequest(t *testing.T, config *configuration.Config) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create a mock next handler
|
||||||
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create the bouncer plugin (this will initialize the logger with file output)
|
||||||
|
bouncerHandler, err := New(context.Background(), nextHandler, config, "test-bouncer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create bouncer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test request to trigger logging
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil)
|
||||||
|
req.RemoteAddr = "192.168.1.100:12345" // Use a non-trusted IP to trigger logging
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Process the request (this should generate log entries)
|
||||||
|
bouncerHandler.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
// Give a moment for log writes to complete
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse log file and extract found levels
|
||||||
|
func parseLogFileAndExtractLevels(t *testing.T, logFile string) map[string]bool {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Verify the log file was created and contains entries
|
||||||
|
if _, statErr := os.Stat(logFile); os.IsNotExist(statErr) {
|
||||||
|
t.Fatalf("Log file was not created: %s", logFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the log file content
|
||||||
|
// #nosec G304 - logFile is a test-generated temporary file path
|
||||||
|
logContent, err := os.ReadFile(logFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read log file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logString := string(logContent)
|
||||||
|
if len(logString) == 0 {
|
||||||
|
return make(map[string]bool) // Return empty map for empty log files
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and verify JSON log entries
|
||||||
|
lines := strings.Split(strings.TrimSpace(logString), "\n")
|
||||||
|
foundLevels := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var logEntry map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &logEntry); err != nil {
|
||||||
|
t.Errorf("Invalid JSON log entry: %s, error: %v", line, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify required fields
|
||||||
|
validateLogEntry(t, logEntry)
|
||||||
|
|
||||||
|
// Track log levels we've seen
|
||||||
|
if level, ok := logEntry["level"].(string); ok {
|
||||||
|
foundLevels[level] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundLevels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to validate log entry structure
|
||||||
|
func validateLogEntry(t *testing.T, logEntry map[string]interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if logEntry["time"] == nil {
|
||||||
|
t.Error("Log entry missing 'time' field")
|
||||||
|
}
|
||||||
|
if logEntry["level"] == nil {
|
||||||
|
t.Error("Log entry missing 'level' field")
|
||||||
|
}
|
||||||
|
if logEntry["msg"] == nil {
|
||||||
|
t.Error("Log entry missing 'msg' field")
|
||||||
|
}
|
||||||
|
if logEntry["component"] != "CrowdsecBouncerTraefikPlugin" {
|
||||||
|
t.Errorf("Expected component 'CrowdsecBouncerTraefikPlugin', got %v", logEntry["component"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to verify expected and forbidden log levels
|
||||||
|
func verifyLogLevels(t *testing.T, foundLevels map[string]bool, expectedLevels, forbiddenLevels []string, logLevel string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Handle case where no logs are expected
|
||||||
|
if len(expectedLevels) == 0 {
|
||||||
|
if len(foundLevels) > 0 {
|
||||||
|
t.Errorf("Expected no logs at %s level, but found: %v", logLevel, foundLevels)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Verify we got some log entries
|
||||||
|
if len(foundLevels) == 0 {
|
||||||
|
t.Fatal("No valid log entries found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify expected levels are present
|
||||||
|
for _, expectedLevel := range expectedLevels {
|
||||||
|
if !foundLevels[expectedLevel] {
|
||||||
|
t.Errorf("Expected to find %s level logs, but didn't. Found levels: %v", expectedLevel, foundLevels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify forbidden levels are NOT present
|
||||||
|
for _, forbiddenLevel := range forbiddenLevels {
|
||||||
|
if foundLevels[forbiddenLevel] {
|
||||||
|
t.Errorf("Found forbidden %s level logs at %s level. Found levels: %v", forbiddenLevel, logLevel, foundLevels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBouncerFileLoggingLevels(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
logLevel string
|
||||||
|
expectedLevels []string // Levels that should appear
|
||||||
|
forbiddenLevels []string // Levels that should NOT appear
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "DEBUG level should show DEBUG only",
|
||||||
|
logLevel: "DEBUG",
|
||||||
|
expectedLevels: []string{"DEBUG"},
|
||||||
|
forbiddenLevels: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "INFO level should show no logs (bouncer doesn't generate INFO during normal operation)",
|
||||||
|
logLevel: "INFO",
|
||||||
|
expectedLevels: []string{}, // No logs expected for normal operation
|
||||||
|
forbiddenLevels: []string{"DEBUG"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create temporary directory for log file
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
logFile := filepath.Join(tmpDir, "bouncer.log")
|
||||||
|
|
||||||
|
// Get test config and override specific fields
|
||||||
|
config := getTestConfig()
|
||||||
|
config.LogLevel = tt.logLevel
|
||||||
|
config.LogFormat = "json" // Use JSON format for easier parsing
|
||||||
|
config.LogFilePath = logFile
|
||||||
|
|
||||||
|
// Create and execute bouncer request
|
||||||
|
createAndExecuteBouncerRequest(t, config)
|
||||||
|
|
||||||
|
// Parse log file and extract found levels
|
||||||
|
foundLevels := parseLogFileAndExtractLevels(t, logFile)
|
||||||
|
|
||||||
|
// Handle empty log files for higher log levels (expected behavior)
|
||||||
|
if len(foundLevels) == 0 && len(tt.expectedLevels) > 0 {
|
||||||
|
t.Fatalf("Expected log entries but log file is empty for level %s", tt.logLevel)
|
||||||
|
}
|
||||||
|
if len(foundLevels) == 0 {
|
||||||
|
// Empty file is expected for this log level
|
||||||
|
t.Logf("LogLevel %s: No logs generated (expected behavior)", tt.logLevel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify expected and forbidden log levels
|
||||||
|
verifyLogLevels(t, foundLevels, tt.expectedLevels, tt.forbiddenLevels, tt.logLevel)
|
||||||
|
|
||||||
|
t.Logf("LogLevel %s: Successfully logged to file with levels: %v", tt.logLevel, foundLevels)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBouncerFileLoggingCommonFormat(t *testing.T) {
|
||||||
|
// Create temporary directory for log file
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
logFile := filepath.Join(tmpDir, "bouncer-common.log")
|
||||||
|
|
||||||
|
// Get test config and override specific fields
|
||||||
|
config := getTestConfig()
|
||||||
|
config.LogLevel = "DEBUG"
|
||||||
|
config.LogFormat = "common" // Use common format
|
||||||
|
config.LogFilePath = logFile
|
||||||
|
|
||||||
|
// Create a mock next handler
|
||||||
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create the bouncer plugin
|
||||||
|
bouncerHandler, err := New(context.Background(), nextHandler, config, "test-bouncer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create bouncer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test request to trigger logging
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil)
|
||||||
|
req.RemoteAddr = "192.168.1.100:12345"
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Process the request
|
||||||
|
bouncerHandler.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
// Give a moment for log writes to complete
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify the log file was created and contains entries
|
||||||
|
if _, statErr := os.Stat(logFile); os.IsNotExist(statErr) {
|
||||||
|
t.Fatalf("Log file was not created: %s", logFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the log file content
|
||||||
|
// #nosec G304 - logFile is a test-generated temporary file path
|
||||||
|
logContent, err := os.ReadFile(logFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read log file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logString := string(logContent)
|
||||||
|
if len(logString) == 0 {
|
||||||
|
t.Fatal("Log file is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify common format structure
|
||||||
|
lines := strings.Split(strings.TrimSpace(logString), "\n")
|
||||||
|
foundDebug := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common format should contain time, level, msg, and component
|
||||||
|
if strings.Contains(line, "level=DEBUG") {
|
||||||
|
foundDebug = true
|
||||||
|
}
|
||||||
|
if !strings.Contains(line, "component=CrowdsecBouncerTraefikPlugin") {
|
||||||
|
t.Errorf("Log line missing component field: %s", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should see DEBUG level logs since we set LogLevel to DEBUG
|
||||||
|
if !foundDebug {
|
||||||
|
t.Errorf("Expected to find DEBUG level logs in common format. Log content:\n%s", logString)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully logged to file %s in common format with %d lines", logFile, len(lines))
|
||||||
|
}
|
||||||
+166
-11
@@ -2,12 +2,14 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
htmltemplate "html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
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"
|
||||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
)
|
)
|
||||||
@@ -17,7 +19,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 {
|
||||||
@@ -74,10 +76,11 @@ 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
|
||||||
client *http.Client
|
httpClient *http.Client
|
||||||
|
cacheClient *cache.Client
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
rw http.ResponseWriter
|
rw http.ResponseWriter
|
||||||
@@ -91,7 +94,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,
|
||||||
@@ -103,10 +106,11 @@ 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,
|
||||||
client: tt.fields.client,
|
httpClient: tt.fields.httpClient,
|
||||||
|
cacheClient: tt.fields.cacheClient,
|
||||||
}
|
}
|
||||||
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
|
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
|
||||||
})
|
})
|
||||||
@@ -127,7 +131,7 @@ func Test_handleNoStreamCache(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) {
|
||||||
if err := handleNoStreamCache(tt.args.bouncer, tt.args.remoteIP); (err != nil) != tt.wantErr {
|
if _, err := handleNoStreamCache(tt.args.bouncer, tt.args.remoteIP); (err != nil) != tt.wantErr {
|
||||||
t.Errorf("handleNoStreamCache() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("handleNoStreamCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -139,14 +143,19 @@ func Test_handleStreamCache(t *testing.T) {
|
|||||||
bouncer *Bouncer
|
bouncer *Bouncer
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
// 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(t *testing.T) {
|
||||||
handleStreamCache(tt.args.bouncer)
|
err := handleStreamCache(tt.args.bouncer)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("handleStreamCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +164,7 @@ func Test_crowdsecQuery(t *testing.T) {
|
|||||||
type args struct {
|
type args struct {
|
||||||
bouncer *Bouncer
|
bouncer *Bouncer
|
||||||
stringURL string
|
stringURL string
|
||||||
|
data []byte
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -166,7 +176,7 @@ func Test_crowdsecQuery(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 := crowdsecQuery(tt.args.bouncer, tt.args.stringURL)
|
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL, tt.args.data)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@@ -177,3 +187,148 @@ func Test_crowdsecQuery(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleBanServeHTTPWithDifferentMethods(t *testing.T) {
|
||||||
|
html := "<html>You are banned</html>"
|
||||||
|
banTemplate, _ := htmltemplate.New("html").Parse(html)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
banTemplate *htmltemplate.Template
|
||||||
|
expectBodyContent bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GET request should have body with template",
|
||||||
|
method: http.MethodGet,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HEAD request should NOT have body even with template",
|
||||||
|
method: http.MethodHead,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST request should have body with template",
|
||||||
|
method: http.MethodPost,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT request should have body with template",
|
||||||
|
method: http.MethodPut,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DELETE request should have body with template",
|
||||||
|
method: http.MethodDelete,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
bouncer := &Bouncer{
|
||||||
|
remediationStatusCode: http.StatusForbidden,
|
||||||
|
remediationCustomHeader: "X-Test-Remediation",
|
||||||
|
banTemplate: tt.banTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
req := &http.Request{Method: tt.method}
|
||||||
|
bouncer.handleBanServeHTTP(rw, req, "0.0.0.0", "TEST")
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if rw.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("Expected status code 403, got %d", rw.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom header
|
||||||
|
headerValue := rw.Header().Get("X-Test-Remediation")
|
||||||
|
if headerValue != "ban" {
|
||||||
|
t.Errorf("Expected header X-Test-Remediation to be 'ban', got %s", headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check body content
|
||||||
|
body := rw.Body.String()
|
||||||
|
hasBodyContent := len(body) > 0
|
||||||
|
|
||||||
|
if hasBodyContent != tt.expectBodyContent {
|
||||||
|
t.Errorf("Method %s: expected body content: %v, got body content: %v (body: %q)",
|
||||||
|
tt.method, tt.expectBodyContent, hasBodyContent, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we expect body content, verify it matches template
|
||||||
|
if tt.expectBodyContent && body != html {
|
||||||
|
t.Errorf("Expected body %q, got %q", html, body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaptchaMethodBasedLogic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
remediation string
|
||||||
|
expectBanFallback bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GET with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodGet,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HEAD with captcha remediation should fallback to ban",
|
||||||
|
method: http.MethodHead,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodPost,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodPut,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DELETE with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodDelete,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PATCH with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodPatch,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodOptions,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Test the core logic: captcha is served for all methods except HEAD
|
||||||
|
shouldUseCaptcha := tt.remediation == cache.CaptchaValue && tt.method != http.MethodHead
|
||||||
|
|
||||||
|
if shouldUseCaptcha == tt.expectBanFallback {
|
||||||
|
t.Errorf("Method %s with %s remediation: expected ban fallback %v, but logic would use captcha %v",
|
||||||
|
tt.method, tt.remediation, tt.expectBanFallback, shouldUseCaptcha)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+338
@@ -0,0 +1,338 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CrowdSec Captcha</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>
|
||||||
|
<script src="{{ .FrontendJS }}" async defer></script>
|
||||||
|
</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 Captcha</h1>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
<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>
|
||||||
|
<script>
|
||||||
|
function captchaCallback() {
|
||||||
|
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+44
-17
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v3.5.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -18,9 +16,11 @@ 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:
|
||||||
- 80:80
|
- 8000:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- crowdsec
|
- crowdsec
|
||||||
@@ -31,35 +31,62 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
|
||||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
|
|
||||||
whoami2:
|
bar:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-bar"
|
container_name: "simple-service-bar"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
- "traefik.http.routers.router-bar.rule=PathPrefix(`/bar`)"
|
||||||
- "traefik.http.routers.router-bar.entrypoints=web"
|
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||||
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.metricsupdateintervalseconds=15"
|
||||||
|
- "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.ForwardedHeadersTrustedIPs=172.21.0.1/8"
|
||||||
|
|
||||||
|
bar2:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service-bar2"
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.router-bar2.rule=PathPrefix(`/bar2`)"
|
||||||
|
- "traefik.http.routers.router-bar2.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router-bar2.middlewares=crowdsec2@docker"
|
||||||
|
- "traefik.http.services.service-bar2.loadbalancer.server.port=80"
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdsecmode=stream"
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.updateintervalseconds=10"
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.updatemaxfailure=-1"
|
||||||
|
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
|
||||||
|
bar3:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service-bar3"
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.router-bar3.rule=PathPrefix(`/bar3`)"
|
||||||
|
- "traefik.http.routers.router-bar3.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router-bar3.middlewares=crowdsec2@docker"
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
image: crowdsecurity/crowdsec:v1.6.8
|
||||||
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_1: 40796d93c2958f9e58345514e67740e5
|
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
|
||||||
BOUNCER_KEY_TRAEFIK_2: 44c36dac5c4140af9f06f397508e82c7
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
- logs-local:/var/log/traefik:ro
|
- logs-local:/var/log/traefik:ro
|
||||||
|
|||||||
+16
-21
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -14,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.3"
|
- "--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
|
||||||
@@ -33,16 +33,10 @@ services:
|
|||||||
# Definition of the router
|
# Definition of the router
|
||||||
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
||||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||||
# Definition of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
|
||||||
# crowdseclapikey must be unique to the middleware attached to the service
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
|
|
||||||
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
|
||||||
|
|
||||||
whoami2:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service-bar"
|
container_name: "simple-service-bar"
|
||||||
@@ -52,26 +46,27 @@ services:
|
|||||||
# Definition of the router
|
# Definition of the router
|
||||||
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
||||||
- "traefik.http.routers.router-bar.entrypoints=web"
|
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||||
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definitin of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be unique to the middleware attached to the service
|
# crowdseclapikey is the key to authenticate to crowdsec
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1="
|
||||||
|
# enable AppSec real time check
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||||
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
image: crowdsecurity/crowdsec:v1.6.8
|
||||||
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_1: FIXME-LAPI-KEY-1
|
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
|
||||||
BOUNCER_KEY_TRAEFIK_2: FIXME-LAPI-KEY-2
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
- logs:/var/log/traefik:ro
|
- logs:/var/log/traefik:ro
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Example
|
||||||
|
## Enabling AppSec WAF feature from crowdsec
|
||||||
|
|
||||||
|
You mostly need to configure Crowdsec for this to work by enabling virtual patching and configuring some custom rules.
|
||||||
|
In the example we use a whoami container protected by crowdsec with virtual patching enabled.
|
||||||
|
|
||||||
|
The Traefik instance just needs to know where appsec engine is located
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
|
||||||
|
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
|
||||||
|
```
|
||||||
|
We can try to query normally the whoami server:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/foo
|
||||||
|
```
|
||||||
|
|
||||||
|
And then we verify that a malicious request will be blocked:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/foo/rpc2
|
||||||
|
```
|
||||||
|
You should get a 403 on http://localhost:8000/foo/rpc2
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_appsec
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
|
|
||||||
|
---
|
||||||
|
listen_addr: 0.0.0.0:7422
|
||||||
|
appsec_config: crowdsecurity/virtual-patching
|
||||||
|
name: myAppSecComponent
|
||||||
|
source: appsec
|
||||||
|
labels:
|
||||||
|
type: appsec
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v3.5.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.5.0"
|
||||||
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-appsec-enabled:/var/log/traefik
|
||||||
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
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"
|
||||||
|
# Enable AppSec
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||||
|
# Define AppSec host and port informations
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
|
||||||
|
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
|
container_name: "crowdsec"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
|
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
||||||
|
volumes:
|
||||||
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
|
- logs-appsec-enabled:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-appsec-enabled:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-appsec-enabled:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-appsec-enabled:
|
||||||
|
crowdsec-db-appsec-enabled:
|
||||||
|
crowdsec-config-appsec-enabled:
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Example
|
||||||
|
## Behind another proxy service (ex: clouflare)
|
||||||
|
|
||||||
|
You need to configure your Traefik to trust Forwarded headers by your front proxy
|
||||||
|
In the example we use another instance of traefik with the container named cloudflare to simulate a front proxy
|
||||||
|
|
||||||
|
The "internal" Traefik instance is configured to trust the cloudflare forward headers
|
||||||
|
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
|
||||||
|
```yaml
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
We configure the middleware to trust as well the IP:
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_behindproxy
|
||||||
|
```
|
||||||
+20
-30
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cloudflare:
|
cloudflare:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "cloudflare"
|
container_name: "cloudflare"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -12,17 +10,16 @@ services:
|
|||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
- "--providers.file.filename=/cloud.yaml"
|
- "--providers.file.filename=/cloud.yaml"
|
||||||
- "--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
|
||||||
- ./cloudflare-exemple.yaml:/cloud.yaml:ro
|
- ./cloudflare-example.yaml:/cloud.yaml:ro
|
||||||
- logs-cloudflare:/var/log/traefik
|
- logs-cloudflare:/var/log/traefik
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -36,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.3"
|
- "--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
|
||||||
@@ -46,56 +43,49 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- crowdsec
|
- crowdsec
|
||||||
|
|
||||||
whoami1:
|
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"
|
||||||
# Definition of the router
|
# Definition of the router
|
||||||
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
||||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definition of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=live"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecmode=live"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
|
||||||
|
|
||||||
whoami2:
|
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"
|
||||||
# Definition of the router
|
# Definition of the router
|
||||||
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
||||||
- "traefik.http.routers.router-bar.entrypoints=web"
|
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||||
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||||
# Definitin of the middleware
|
# Definition of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecmode=live"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
|
|
||||||
|
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
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
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
|
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
||||||
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
- logs-cloudflare:/var/log/traefik:ro
|
- logs-cloudflare:/var/log/traefik:ro
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Example
|
||||||
|
|
||||||
### Install vagrant
|
### Install vagrant
|
||||||
|
|
||||||
##### On linux
|
##### On linux
|
||||||
+1
-1
@@ -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.9.5/traefik_v2.9.5_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
-1
@@ -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.8.7/whoami_v1.8.7_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/
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# Example
|
||||||
|
## Enabling catpcha response from crowdsec
|
||||||
|
|
||||||
|
Crowdsec support 3 remediations solutions `ban`, `captcha`, and `throttle`.
|
||||||
|
This plugins support the `ban` and `captcha` remediation.
|
||||||
|
|
||||||
|
### Traefik configuration
|
||||||
|
|
||||||
|
The minimal configuration is defined below.
|
||||||
|
For now 3 captcha providers are supported:
|
||||||
|
- [hcaptcha](https://www.hcaptcha.com/)
|
||||||
|
- [recaptcha](https://www.google.com/recaptcha/about/)
|
||||||
|
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
# Choose captcha provider
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=hcaptcha"
|
||||||
|
# Define captcha site key
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
|
||||||
|
# Define captcha secret key
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
|
||||||
|
# Define captcha grace period seconds
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800"
|
||||||
|
# Define captcha HTML file path
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
The captcha 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/captcha.html) it locally to expose it to Traefik.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v3.0.0"
|
||||||
|
volumes:
|
||||||
|
- './captcha.html:/captcha.html'
|
||||||
|
...
|
||||||
|
```
|
||||||
|
### Crowdsec configuration
|
||||||
|
|
||||||
|
Crowdsec by default will take the ban action on suspicious activity detected in logs.
|
||||||
|
To instruct Crowdsec to use captcha remediation, change the `/etc/crowdsec/profiles.yaml`.
|
||||||
|
|
||||||
|
2 modes are supported:
|
||||||
|
- Always return a captcha decision
|
||||||
|
- Return a captcha decision the first X times and then a ban decision.
|
||||||
|
|
||||||
|
The second mode could be used to prevent repeated malicious activity.
|
||||||
|
More information is available on configuring Crowdsec in the [official documentation](https://docs.crowdsec.net/docs/next/local_api/profiles/captcha_profile/).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||||
|
volumes:
|
||||||
|
# For captcha and ban mixed decision
|
||||||
|
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||||
|
# For captcha only remediation
|
||||||
|
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||||
|
...
|
||||||
|
```
|
||||||
|
## 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 captcha
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
We will see in the browser the captcha validation page:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_captcha
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note, if we are banned with a "ban" decision from crowdsec a captcha will not be asked and you will have to wait for the decision to expire or remove it manually.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m --type ban
|
||||||
|
```
|
||||||
|
|
||||||
|
## Captcha Workflow
|
||||||
|
|
||||||
|
> Context: The user has no decision attached to his IP
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
> Context: The user has a captcha decision attached to his IP
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
> Context: The user has a ban decision attached to his IP
|
||||||
|
|
||||||
|
```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
|
||||||
|
TraefikPlugin->>User: No, HTTP 403
|
||||||
|
```
|
||||||
|
|
||||||
|
## Captcha Configuration:
|
||||||
|
|
||||||
|
- Recaptcha
|
||||||
|
|
||||||
|
Choose v2 (challenge) and configure the domain to protect:
|
||||||
|

|
||||||
|
|
||||||
|
- Turnstile
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
- Hcatpcha
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CrowdSec Captcha</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>
|
||||||
|
<script src="{{ .FrontendJS }}" async defer></script>
|
||||||
|
</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 Captcha</h1>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
<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>
|
||||||
|
<script>
|
||||||
|
function captchaCallback() {
|
||||||
|
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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-captcha-enabled:/var/log/traefik
|
||||||
|
- './captcha.html:/captcha.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"
|
||||||
|
|
||||||
|
# Choose captcha provider
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=hcaptcha"
|
||||||
|
# Define captcha site key
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
|
||||||
|
# Define captcha secret key
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
|
||||||
|
# Define captcha grade period seconds
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=20"
|
||||||
|
# Define captcha HTML file path
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.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:
|
||||||
|
# For captcha and ban mixed decision
|
||||||
|
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||||
|
# For captcha only remediation
|
||||||
|
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||||
|
- './acquis.yaml:/etc/crowdsec/acquis.yaml:ro'
|
||||||
|
- logs-captcha-enabled:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-captcha-enabled:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-captcha-enabled:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-captcha-enabled:
|
||||||
|
crowdsec-db-captcha-enabled:
|
||||||
|
crowdsec-config-captcha-enabled:
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
name: captcha_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" && GetDecisionsSinceCount(Alert.GetValue(), "24h") <= 3
|
||||||
|
## Same as above but only 3 captcha decision per 24 hours before ban
|
||||||
|
decisions:
|
||||||
|
- type: captcha
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
|
---
|
||||||
|
name: default_ip_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
|
||||||
|
on_success: break
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: captcha_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
|
||||||
|
## Any scenario with http in its name will trigger a captcha challenge
|
||||||
|
decisions:
|
||||||
|
- type: captcha
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
|
---
|
||||||
|
name: default_ip_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
|
||||||
|
on_success: break
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Another thing to note
|
||||||
|
In the html of the ban page, you can use:
|
||||||
|
- {{ .ClientIP }} to display the IP used to ban the request.
|
||||||
|
- {{ .RemediationReason }} that convert on runtime into why the ban page is served. It's an enum with "APPSEC", "LAPI", "TECHNICAL_ISSUE" and it is useful to help user understand why the request is blocked.
|
||||||
|
- {{ .CustomHeader }} value of the specified Request Header (for example X-Request-ID)
|
||||||
|
```
|
||||||
|
<script>var remediation = "{{ .RemediationReason }}"</script>
|
||||||
|
<script>var clientIp = "{{ .ClientIP }}"</script>
|
||||||
|
<script>var traceID = "{{ .TraceID }}"</script>
|
||||||
|
```
|
||||||
|
With the above tweak and some other js, you can customize your ban page on runtime.
|
||||||
@@ -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 |
@@ -0,0 +1,79 @@
|
|||||||
|
# Example
|
||||||
|
|
||||||
|
Read the example captcha before this, to better understand what is done here.
|
||||||
|
|
||||||
|
### Traefik configuration
|
||||||
|
|
||||||
|
The minimal configuration is defined below to implement custom captcha.
|
||||||
|
This documentation use https://github.com/a-ve/wicketkeeper, a self-hosted captcha provider that have a similar API than big providers.
|
||||||
|
|
||||||
|
Minimal API requirement:
|
||||||
|
|
||||||
|
- the JS file URL to load the captcha on the served `captcha.html`
|
||||||
|
- the HTML className to tell to the JS where to display the challenge
|
||||||
|
- the verify URL endpoint to send the field `response` from the captcha with `content-type: application/x-www-form-urlencoded`
|
||||||
|
- the name of the field when you POST the resolved captcha to Traefik
|
||||||
|
|
||||||
|
- the JS file need to respect the `data-callback` on the div that contains the captcha if you use our template, but you can customize it by your side
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik:
|
||||||
|
...
|
||||||
|
labels:
|
||||||
|
# Choose captcha provider
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=custom"
|
||||||
|
# Define captcha grace period seconds
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaCustomJsURL=http://captcha.localhost:8000/fast.js"
|
||||||
|
# Inside Traefik container the plugin must be able to reach wicketkeeper service so we can go through a Traefik localhost
|
||||||
|
# domain which would resolve traefik itself and the port for the dashboard
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomValidateURL=http://wicketkeeper:8080/v0/siteverify"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomKey=wicketkeeper"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomResponse=wicketkeeper_solution"
|
||||||
|
# Define captcha HTML file path
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
wicketkeeper:
|
||||||
|
image: ghcr.io/a-ve/wicketkeeper:latest
|
||||||
|
user: root
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- LISTEN_PORT=8080
|
||||||
|
- REDIS_ADDR=redis:6379
|
||||||
|
- DIFFICULTY=4
|
||||||
|
- ALLOWED_ORIGINS=*
|
||||||
|
- PRIVATE_KEY_PATH=/data/wicketkeeper.key
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis/redis-stack-server:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback" data-challenge-url="http://captcha.localhost:8000/v0/challenge">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exemple navigation
|
||||||
|
|
||||||
|
We can try to query normally the whoami server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/foo
|
||||||
|
```
|
||||||
|
|
||||||
|
We can try to ban ourself and retry.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 10m --type captcha
|
||||||
|
```
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run_custom_captcha
|
||||||
|
```
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>CrowdSec Captcha</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>
|
||||||
|
<script src="{{ .FrontendJS }}" async defer></script>
|
||||||
|
</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 Captcha</h1>
|
||||||
|
</div>
|
||||||
|
<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" data-challenge-url="http://captcha.localhost:8000/v0/challenge">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<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>
|
||||||
|
<script>
|
||||||
|
function captchaCallback() {
|
||||||
|
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v3.5.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"
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.18.0.0/24"
|
||||||
|
|
||||||
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
- "--experimental.plugins.bouncer.version=v1.4.5"
|
||||||
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-custom-captcha-enabled:/var/log/traefik
|
||||||
|
- "./captcha.html:/captcha.html"
|
||||||
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- crowdsec
|
||||||
|
|
||||||
|
whoami-foo:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service-custom-captcha-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.loglevel=DEBUG"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=none"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
|
|
||||||
|
# Choose captcha provider
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=custom"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaCustomJsURL=http://captcha.localhost:8000/fast.js"
|
||||||
|
# Inside Traefik container the plugin must be able to reach wicketkeeper service so we can go through a Traefik localhost
|
||||||
|
# domain which would resolve traefik itself and the port for the dashboard
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomValidateURL=http://wicketkeeper:8080/v0/siteverify"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomKey=wicketkeeper"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomResponse=wicketkeeper_solution"
|
||||||
|
# Define captcha grade period seconds
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=20"
|
||||||
|
# Define captcha HTML file path
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.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:
|
||||||
|
# For captcha and ban mixed decision
|
||||||
|
- "./profiles.yaml:/etc/crowdsec/profiles.yaml:ro"
|
||||||
|
# For captcha only remediation
|
||||||
|
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||||
|
- "./acquis.yaml:/etc/crowdsec/acquis.yaml:ro"
|
||||||
|
- logs-custom-captcha-enabled:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-custom-captcha-enabled:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-custom-captcha-enabled:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
wicketkeeper:
|
||||||
|
image: ghcr.io/a-ve/wicketkeeper:latest
|
||||||
|
container_name: "wicketkeeper"
|
||||||
|
environment:
|
||||||
|
- LISTEN_PORT=8080
|
||||||
|
- REDIS_ADDR=redis:6379
|
||||||
|
- DIFFICULTY=4
|
||||||
|
- ALLOWED_ORIGINS=*
|
||||||
|
- PRIVATE_KEY_PATH=/data/wicketkeeper.key # To override in production environment
|
||||||
|
volumes:
|
||||||
|
- wicketkeeper-custom-captcha-enabled:/data
|
||||||
|
user: root
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Definition of the router
|
||||||
|
- "traefik.http.routers.router-wicketkeeper.rule=Host(`captcha.localhost`)"
|
||||||
|
- "traefik.http.routers.router-wicketkeeper.entrypoints=web"
|
||||||
|
# Definition of the service
|
||||||
|
- "traefik.http.services.service-wicketkeeper.loadbalancer.server.port=8080"
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis/redis-stack-server:latest
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-custom-captcha-enabled:
|
||||||
|
wicketkeeper-custom-captcha-enabled:
|
||||||
|
crowdsec-db-custom-captcha-enabled:
|
||||||
|
crowdsec-config-custom-captcha-enabled:
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: captcha_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" && GetDecisionsSinceCount(Alert.GetValue(), "24h") <= 3
|
||||||
|
## Same as above but only 3 captcha decision per 24 hours before ban
|
||||||
|
decisions:
|
||||||
|
- type: captcha
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
|
---
|
||||||
|
name: default_ip_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
|
||||||
|
on_success: break
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
### Kubernetes Example
|
||||||
|
|
||||||
|
#### Official docs
|
||||||
|
|
||||||
|
##### Install Kubernetes on Docker Desktop
|
||||||
|
|
||||||
|
Install Docker Desktop
|
||||||
|
|
||||||
|
[https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/)
|
||||||
|
|
||||||
|
In settings, click on `Kubernetes` menu, and click `Enable Kubernetes`, then `Apply and Restart`.
|
||||||
|
In case of any issue, you can reset the cluster from this menu and the button `Reset Kubernetes Cluster`.
|
||||||
|
|
||||||
|
|
||||||
|
##### Install Kubernetes on Minikube
|
||||||
|
|
||||||
|
Install Minikube
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
|
||||||
|
&& chmod +x minikube
|
||||||
|
sudo mkdir -p /usr/local/bin/
|
||||||
|
sudo install minikube /usr/local/bin/
|
||||||
|
minikube start
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Install Traefik
|
||||||
|
|
||||||
|
[getting-started/install-traefik/#use-the-helm-chart](https://doc.traefik.io/traefik/getting-started/install-traefik/#use-the-helm-chart)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm repo add traefik https://traefik.github.io/charts
|
||||||
|
helm repo update
|
||||||
|
kubectl create ns traefik
|
||||||
|
helm upgrade --version v28.0.0 --install --namespace=traefik \
|
||||||
|
--values=./traefik/values.yml \
|
||||||
|
traefik traefik/traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
_A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_
|
||||||
|
|
||||||
|
_v28.0.0 of the Traefik helm chart is only compatible with v3 of Traefik_
|
||||||
|
|
||||||
|
#### View the Traefik dashboard
|
||||||
|
|
||||||
|
> Port forward the dashboard:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl --namespace=traefik port-forward $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) 9000:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
Access the dashboard with: [localhost:9000/dashboard/#/](http://localhost:9000/dashboard/#/)
|
||||||
|
|
||||||
|
#### Install the plugin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f traefik/plugin.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install Whoami
|
||||||
|
```bash
|
||||||
|
kubectl apply -f whoami/whoami.yml
|
||||||
|
kubectl apply -f whoami/whoami-services.yml
|
||||||
|
kubectl apply -f whoami/whoami-ingress.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Access Whoami
|
||||||
|
|
||||||
|
> Port forward web port of Traefik
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl --namespace=traefik port-forward $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) 8000:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Access the whoami with: [localhost:8000/](http://localhost:8000/)
|
||||||
|
|
||||||
|
#### Install Crowdsec
|
||||||
|
|
||||||
|
[helm/crowdsec/crowdsec](https://artifacthub.io/packages/helm/crowdsec/crowdsec)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm repo add crowdsec https://crowdsecurity.github.io/helm-charts
|
||||||
|
helm repo update
|
||||||
|
kubectl create ns crowdsec
|
||||||
|
helm upgrade --install --namespace=crowdsec \
|
||||||
|
--values=./crowdsec/values.yml \
|
||||||
|
crowdsec crowdsec/crowdsec
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Read Traefik Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pod --namespace traefik
|
||||||
|
kubectl logs $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) --namespace traefik -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use CSCLI in Crowdsec container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n crowdsec exec -it $(kubectl get pods -n crowdsec --selector "k8s-app=crowdsec,type=lapi" --output=name) bash
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Shell in Traefik container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n traefik exec -it $(kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name) sh
|
||||||
|
```
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
namespace: crowdsec
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: 8080
|
||||||
|
targetPort: web
|
||||||
|
|
||||||
|
selector:
|
||||||
|
app: crowdsec
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
image:
|
||||||
|
tag: v1.6.1-2
|
||||||
|
|
||||||
|
agent:
|
||||||
|
acquisition:
|
||||||
|
- namespace: traefik
|
||||||
|
podName: traefik-*
|
||||||
|
program: traefik
|
||||||
|
env:
|
||||||
|
- name: COLLECTIONS
|
||||||
|
value: "crowdsecurity/traefik"
|
||||||
|
|
||||||
|
lapi:
|
||||||
|
dashboard:
|
||||||
|
enabled: false
|
||||||
|
env:
|
||||||
|
- name: ENROLL_KEY
|
||||||
|
value: 40796d93c2958f9e58345514e67740e5
|
||||||
|
- name: ENROLL_INSTANCE_NAME
|
||||||
|
value: k8s_cluster
|
||||||
|
- name: ENROLL_TAGS
|
||||||
|
value: "k8s linux test"
|
||||||
|
# If it's a test, we don't want to share signals with CrowdSec so disable the Online API.
|
||||||
|
- name: DISABLE_ONLINE_API
|
||||||
|
value: "true"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# dashboard.yaml
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: dashboard
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
routes:
|
||||||
|
- match: Host(`traefik.localhost`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: api@internal
|
||||||
|
kind: TraefikService
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: bouncer
|
||||||
|
namespace: traefik
|
||||||
|
spec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
CrowdsecLapiKey: 40796d93c2958f9e58345514e67740e5
|
||||||
|
Enabled: "true"
|
||||||
|
logLevel: DEBUG
|
||||||
|
crowdsecMode: live
|
||||||
|
crowdsecLapiHost: crowdsec-service.crowdsec.svc.cluster.local:8080
|
||||||
|
crowdsecLapiScheme: http
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
image:
|
||||||
|
tag: v3.0.0
|
||||||
|
|
||||||
|
logs:
|
||||||
|
general:
|
||||||
|
level: DEBUG
|
||||||
|
access:
|
||||||
|
enabled: true
|
||||||
|
format: json
|
||||||
|
fields:
|
||||||
|
headers:
|
||||||
|
defaultmode: keep
|
||||||
|
|
||||||
|
experimental:
|
||||||
|
plugins:
|
||||||
|
bouncer:
|
||||||
|
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
version: "v1.3.0"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
namespace: traefik
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: traefik-bouncer@kubernetescrd
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: whoami
|
||||||
|
port:
|
||||||
|
name: web
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
namespace: traefik
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: 80
|
||||||
|
targetPort: web
|
||||||
|
|
||||||
|
selector:
|
||||||
|
app: whoami
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
kind: Deployment
|
||||||
|
apiVersion: apps/v1
|
||||||
|
metadata:
|
||||||
|
name: whoami
|
||||||
|
namespace: traefik
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: whoami
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: whoami
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: whoami
|
||||||
|
image: traefik/whoami
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
containerPort: 80
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Example
|
||||||
|
## With Redis as an external shared cache
|
||||||
|
|
||||||
|
The plugin must be configured to connect to a redis instance
|
||||||
|
```yaml
|
||||||
|
redisCacheHost: "redis:6379"
|
||||||
|
```
|
||||||
|
Here **redis** is the hostname of a container located in the same network as Traefik and **6379** is the default port of redis
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_cacheredis
|
||||||
|
```
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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-redis:/var/log/traefik
|
||||||
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- crowdsec
|
||||||
|
# - redis-insecure
|
||||||
|
- redis-secure
|
||||||
|
|
||||||
|
# Either use secure, or insecure but do not mix both
|
||||||
|
# whoami-redis-insecure:
|
||||||
|
# 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=Path(`/redis-insecure`)"
|
||||||
|
# - "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.rediscacheenabled=true"
|
||||||
|
# # Contact redis-unsecure without a password
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachehost=redis-insecure:6379"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
|
||||||
|
whoami-redis-secure:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service-bar-redis"
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Definition of the router
|
||||||
|
- "traefik.http.routers.router-bar.rule=Path(`/redis-secure`)"
|
||||||
|
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||||
|
# Definition of the service
|
||||||
|
- "traefik.http.services.service-bar.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.rediscacheenabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachepassword=FIXME"
|
||||||
|
# Contact redis-secure with password
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachehost=redis-secure:6379"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
|
||||||
|
|
||||||
|
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-redis:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-redis:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-redis:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
redis-secure:
|
||||||
|
image: "redis:7.0.12-alpine"
|
||||||
|
container_name: "redis-secure"
|
||||||
|
hostname: redis-secure
|
||||||
|
restart: unless-stopped
|
||||||
|
command: "redis-server --save 60 1 --loglevel debug --requirepass FIXME"
|
||||||
|
volumes:
|
||||||
|
- redis-secure-data:/data
|
||||||
|
|
||||||
|
# redis-insecure:
|
||||||
|
# image: "redis:7.0.12-alpine"
|
||||||
|
# container_name: "redis-insecure"
|
||||||
|
# hostname: redis-unsecure
|
||||||
|
# restart: unless-stopped
|
||||||
|
# command: "redis-server --save 60 1 --loglevel debug"
|
||||||
|
# volumes:
|
||||||
|
# - redis-unsecure-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-redis:
|
||||||
|
crowdsec-db-redis:
|
||||||
|
crowdsec-config-redis:
|
||||||
|
redis-unsecure-data:
|
||||||
|
redis-secure-data:
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#### Generate CAPI credentials (only for `alone` mode)
|
||||||
|
You need to create a crowdsec API credentials for the CAPI.
|
||||||
|
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
whoami:
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiMachineId=LOGIN"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiPassword=PASSWORD"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiScenarios=crowdsecurity/http-generic-bf,crowdsecurity/http-xss-probing,..."
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run all the containers:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 8080:8080
|
||||||
|
|
||||||
|
whoami-foo:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service-foo-alone"
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
||||||
|
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||||
|
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Example
|
||||||
|
|
||||||
|
## Using https communication and tls authentication with Crowdsec
|
||||||
|
|
||||||
|
##### Summary
|
||||||
|
|
||||||
|
This example demonstrates the use of https between the Traefik plugin and the Crowdsec LAPI.
|
||||||
|
|
||||||
|
It is possible to communicate with the LAPI in https and still authenticate with API key.
|
||||||
|
You can add the client TLS certificate generated to authenticate without any Token in the plugin.
|
||||||
|
|
||||||
|
However, note that it is not possible to authenticate with TLS client certificate without https setup for the LAPI.
|
||||||
|
|
||||||
|
##### Details
|
||||||
|
|
||||||
|
Simple HTTPS communication: It is possible to talk to Crowdsec LAPI which is configured with a self-signed certificate
|
||||||
|
In that case the setting **crowdsecLapiTLSInsecureVerify** must be set to true.
|
||||||
|
|
||||||
|
It is recommended to validate the certificate presented by Crowdsec LAPI using the Certificate Authority which created it.
|
||||||
|
|
||||||
|
You can provide the Certificate Authority using:
|
||||||
|
|
||||||
|
- A file path readable by Traefik
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
crowdsec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
crowdsecLapiTlsCertificateAuthorityFile: /etc/traefik/certs/crowdsecCA.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
- The PEM encoded certificate as a text variable
|
||||||
|
|
||||||
|
In the static file configuration of Traefik
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
crowdsec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
crowdsecLapiTlsCertificateAuthority: |-
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||||
|
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
|
||||||
|
...
|
||||||
|
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
|
||||||
|
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
In a dynamic configuration of a provider (ex docker) as a Label
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
whoami-foo:
|
||||||
|
image: traefik/whoami
|
||||||
|
labels:
|
||||||
|
- |
|
||||||
|
traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTlsCertificateAuthority=
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||||
|
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
|
||||||
|
...
|
||||||
|
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
|
||||||
|
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
The example tls-auth presents 2 services, foo and bar which comes with the bouncer.
|
||||||
|
At startup, certificates are created in a shared docker volume by a sidecar container which exits after.
|
||||||
|
|
||||||
|
Traefik will use client and CA certificates.
|
||||||
|
The Bouncer will use server and CA certificates.
|
||||||
|
|
||||||
|
The service `whoami-foo` will authenticate with an **API key** over HTTPS after verifying the server certificate with CA.
|
||||||
|
The service `whoami-bar` will authenticate with a **client certificate** signed by the CA.
|
||||||
|
|
||||||
|
Access to a route that communicate via https and authenticate with API-key:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://localhost:8000/foo
|
||||||
|
```
|
||||||
|
|
||||||
|
Access to a route that communicate via https and authenticate with a client certificate:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://localhost:8000/bar
|
||||||
|
```
|
||||||
|
|
||||||
|
Access to the traefik dashboard
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://localhost:8080/dashboard/#/
|
||||||
|
```
|
||||||
|
|
||||||
|
To play the demo environnement run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run_tlsauth
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
> Traefik need to be restarted if certificates are regenerated after his launch, crowdsec also
|
||||||
|
|
||||||
|
## Separate LAPI and Appsec HTTP/S config
|
||||||
|
|
||||||
|
To separate TLS config for LAPI and Appsec, you can use all the TLS LAPI variable beginning with `CrowdsecLapi...` into `CrowdsecAppsec...`.
|
||||||
|
Don't forget to set `CrowdsecAppsecScheme: HTTP` or `HTTPS` to trigger the separate setup.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
|
---
|
||||||
|
listen_addr: 0.0.0.0:7422
|
||||||
|
appsec_config: crowdsecurity/virtual-patching
|
||||||
|
name: myAppSecComponent
|
||||||
|
source: appsec
|
||||||
|
labels:
|
||||||
|
type: appsec
|
||||||
|
cert_file: /etc/crowdsec/certs/server.pem
|
||||||
|
key_file: /etc/crowdsec/certs/server-key.pem
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v3.5.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.5.0"
|
||||||
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-tls-auth:/var/log/traefik
|
||||||
|
- crowdsec-certs-tls-auth:/etc/traefik/crowdsec-certs
|
||||||
|
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- crowdsec
|
||||||
|
- gencert
|
||||||
|
|
||||||
|
# Use HTTPS scheme but with lapikey authentication
|
||||||
|
# whoami-foo:
|
||||||
|
# image: traefik/whoami
|
||||||
|
# container_name: "simple-service-foo"
|
||||||
|
# restart: unless-stopped
|
||||||
|
# labels:
|
||||||
|
# - "traefik.enable=true"
|
||||||
|
# - "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
||||||
|
# - "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
|
# - "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||||
|
# - "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
|
||||||
|
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
|
||||||
|
|
||||||
|
# Use HTTPS scheme with TLS cert authentication
|
||||||
|
whoami-bar:
|
||||||
|
image: traefik/whoami
|
||||||
|
container_name: "simple-service-bar"
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.router-bar.rule=PathPrefix(`/bar`)"
|
||||||
|
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||||
|
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||||
|
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=none"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecscheme=https"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecAppsecTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
|
||||||
|
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapikey=40796d93c2958f9e58345514e67740e5="
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
|
||||||
|
# Enable AppSec
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||||
|
# Define AppSec host and port informations
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:latest
|
||||||
|
container_name: "crowdsec"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
|
# whoami-foo is authenticating with api key over https
|
||||||
|
# whoami-bar is authenticating with tls cert over https
|
||||||
|
BOUNCER_KEY_TRAEFIK_FOO: 40796d93c2958f9e58345514e67740e5=
|
||||||
|
LOCAL_API_URL: https://127.0.0.1:8080
|
||||||
|
USE_TLS: "true"
|
||||||
|
CERT_FILE: "/etc/crowdsec/certs/server.pem"
|
||||||
|
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
|
||||||
|
CACERT_FILE: "/etc/crowdsec/certs/inter.pem"
|
||||||
|
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
|
||||||
|
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
|
||||||
|
AGENTS_ALLOWED_OU: "agent-ou"
|
||||||
|
BOUNCERS_ALLOWED_OU: "bouncer-ou"
|
||||||
|
LEVEL_DEBUG: "true"
|
||||||
|
# Disabled because it restart in loop otherwise
|
||||||
|
# DISABLE_AGENT: "true"
|
||||||
|
# Disabled for the examples
|
||||||
|
DISABLE_ONLINE_API: "true"
|
||||||
|
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||||
|
volumes:
|
||||||
|
- ./config/acquis.yaml:/etc/crowdsec/acquis.yaml
|
||||||
|
# - ./config/config.yaml:/etc/crowdsec/config_local.yaml
|
||||||
|
# - ./config/local_api_credentials.yaml:/etc/crowdsec/local_api_credentials.yaml:ro
|
||||||
|
- crowdsec-certs-tls-auth:/etc/crowdsec/certs/:ro
|
||||||
|
- logs-tls-auth:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-tls-auth:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-tls-auth:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
depends_on:
|
||||||
|
- gencert
|
||||||
|
|
||||||
|
gencert:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- crowdsec-certs-tls-auth:/out
|
||||||
|
- ./in:/in:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-tls-auth:
|
||||||
|
crowdsec-db-tls-auth:
|
||||||
|
crowdsec-config-tls-auth:
|
||||||
|
crowdsec-certs-tls-auth:
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
stdout=/out/res.log
|
if [ -f "/out/inter-key.pem" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
cfssl gencert --initca /in/ca.json 2>${stdout} | cfssljson --bare "/out/ca" && \
|
cfssl gencert --initca /in/ca.json 2>${stdout} | cfssljson --bare "/out/ca" && \
|
||||||
# Generate an intermediate certificate that will be used to sign the client certificates
|
# Generate an intermediate certificate that will be used to sign the client certificates
|
||||||
cfssl gencert --initca /in/intermediate.json 2>${stdout} | cfssljson --bare "/out/inter" && \
|
cfssl gencert --initca /in/intermediate.json 2>${stdout} | cfssljson --bare "/out/inter" && \
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Example
|
||||||
|
## Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
|
||||||
|
|
||||||
|
You need to configure your Traefik to trust Forwarded headers by your front proxy
|
||||||
|
In the example we use a whoami container protected by crowdsec, and we ban our IP before allowing using TrustedIPs
|
||||||
|
|
||||||
|
If you are using another proxy in front, you need to add its IP in the trusted IP for the forwarded headers.
|
||||||
|
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
|
||||||
|
The "internal" Traefik instance is configured to trust the forward headers
|
||||||
|
```yaml
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
We configure the middleware to trust as well as the IP of the intermediate proxy if needed:
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add your IP to the ban list
|
||||||
|
```bash
|
||||||
|
docker exec crowdsec cscli decisions add --ip 10.0.10.30 -d 10m
|
||||||
|
```
|
||||||
|
You should get a 403 on http://localhost/foo
|
||||||
|
|
||||||
|
> Replace *10.0.10.30* by your IP
|
||||||
|
|
||||||
|
Add the IPs that will not be filtered by the plugin
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clientTrustedips=10.0.10.30/32"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Replace *10.0.10.30/32* by your IP or IP range, so it's not getting checked against ban cache of crowdsec
|
||||||
|
|
||||||
|
You should get a 200 on http://localhost/foo even if you are on the ban cache
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_trustedips
|
||||||
|
```
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
+15
-20
@@ -1,8 +1,6 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.9.4"
|
image: "traefik:v3.0.0"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,12 +13,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.1.3"
|
- "--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-trustedips:/var/log/traefik
|
- logs-trustedips:/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:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
@@ -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"
|
||||||
@@ -40,16 +38,15 @@ services:
|
|||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||||
# Definition of the middleware
|
# Definition of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
# Replace 10.0.10.30/32 by your IP range which is "trusted"
|
# Replace 10.0.10.30/32 by your IP range which is "trusted"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.clienttrustedips=10.0.10.30/32"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.10.30/32"
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
|
||||||
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"
|
||||||
@@ -60,23 +57,21 @@ services:
|
|||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||||
# Definition of the middleware
|
# Definition of the middleware
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
|
||||||
# Replace 10.0.10.30/32 by your IP range which is "trusted"
|
# Replace 10.0.10.30/32 by your IP range which is "trusted"
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.clienttrustedips=10.0.10.30/32"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.10.30/32"
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
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
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
|
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
||||||
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
- logs-trustedips:/var/log/traefik:ro
|
- logs-trustedips:/var/log/traefik:ro
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
traefik:
|
|
||||||
image: "traefik:v2.9.4"
|
|
||||||
container_name: "traefik"
|
|
||||||
restart: unless-stopped
|
|
||||||
command:
|
|
||||||
# - "--log.level=DEBUG"
|
|
||||||
- "--accesslog"
|
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
|
||||||
- "--api.insecure=true"
|
|
||||||
- "--providers.docker=true"
|
|
||||||
- "--providers.docker.exposedbydefault=false"
|
|
||||||
- "--entrypoints.web.address=:80"
|
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.4"
|
|
||||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
- logs-redis:/var/log/traefik
|
|
||||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
|
||||||
ports:
|
|
||||||
- 80:80
|
|
||||||
- 8080:8080
|
|
||||||
depends_on:
|
|
||||||
- crowdsec
|
|
||||||
- redis
|
|
||||||
|
|
||||||
whoami-foo:
|
|
||||||
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=Path(`/foo`)"
|
|
||||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
|
||||||
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
|
||||||
# Definition of the service
|
|
||||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
|
||||||
# Definition of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.rediscacheenabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
|
||||||
|
|
||||||
whoami-bar:
|
|
||||||
image: traefik/whoami
|
|
||||||
container_name: "simple-service-bar"
|
|
||||||
restart: unless-stopped
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
# Definition of the router
|
|
||||||
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
|
||||||
- "traefik.http.routers.router-bar.entrypoints=web"
|
|
||||||
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
|
||||||
# Definition of the service
|
|
||||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
|
||||||
# Definition of the middleware
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
|
||||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscacheenabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
|
|
||||||
|
|
||||||
|
|
||||||
crowdsec:
|
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
|
||||||
container_name: "crowdsec"
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
COLLECTIONS: crowdsecurity/traefik
|
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
|
||||||
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
|
|
||||||
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
|
|
||||||
volumes:
|
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
|
||||||
- logs-redis:/var/log/traefik:ro
|
|
||||||
- crowdsec-db-redis:/var/lib/crowdsec/data/
|
|
||||||
- crowdsec-config-redis:/etc/crowdsec/
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=false"
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: "redis:7.0.5-alpine"
|
|
||||||
container_name: "redis"
|
|
||||||
restart: unless-stopped
|
|
||||||
command: "redis-server --save 60 1"
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
logs-redis:
|
|
||||||
crowdsec-db-redis:
|
|
||||||
crowdsec-config-redis:
|
|
||||||
redis-data:
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
traefik:
|
|
||||||
image: "traefik:v2.9.6"
|
|
||||||
container_name: "traefik"
|
|
||||||
restart: unless-stopped
|
|
||||||
command:
|
|
||||||
# - "--log.level=DEBUG"
|
|
||||||
- "--accesslog"
|
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
|
||||||
- "--api.insecure=true"
|
|
||||||
- "--providers.docker=true"
|
|
||||||
- "--providers.docker.exposedbydefault=false"
|
|
||||||
- "--entrypoints.web.address=:80"
|
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
|
||||||
- "--experimental.plugins.bouncer.version=v1.1.6"
|
|
||||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
- ./LAPIKEY:/etc/traefik/LAPIKEY:ro
|
|
||||||
- logs-tls-auth:/var/log/traefik
|
|
||||||
- crowdsec-certs-tls-auth:/etc/traefik/crowdsec-certs
|
|
||||||
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- 80:80
|
|
||||||
- 8080:8080
|
|
||||||
depends_on:
|
|
||||||
- crowdsec
|
|
||||||
|
|
||||||
whoami-foo:
|
|
||||||
image: traefik/whoami
|
|
||||||
container_name: "simple-service-foo"
|
|
||||||
restart: unless-stopped
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
|
||||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
|
||||||
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
|
||||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapischeme=https"
|
|
||||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
|
|
||||||
|
|
||||||
whoami-bar:
|
|
||||||
image: traefik/whoami
|
|
||||||
container_name: "simple-service-bar"
|
|
||||||
restart: unless-stopped
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
|
||||||
- "traefik.http.routers.router-bar.entrypoints=web"
|
|
||||||
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
|
||||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapischeme=https"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
|
|
||||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
|
|
||||||
|
|
||||||
crowdsec:
|
|
||||||
image: crowdsecurity/crowdsec:v1.4.3
|
|
||||||
container_name: "crowdsec"
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
COLLECTIONS: crowdsecurity/traefik
|
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
|
||||||
# whoami-foo is authenticating with api key over https
|
|
||||||
# whoami-bar is authenticating with tls cert over https
|
|
||||||
BOUNCER_KEY_TRAEFIK_FOO: 40796d93c2958f9e58345514e67740e5
|
|
||||||
LOCAL_API_URL: https://127.0.0.1:8080
|
|
||||||
USE_TLS: "true"
|
|
||||||
CERT_FILE: "/etc/crowdsec/certs/server.pem"
|
|
||||||
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
|
|
||||||
CACERT_FILE: "/etc/crowdsec/certs/inter.pem"
|
|
||||||
AGENTS_ALLOWED_OU: "agent-ou"
|
|
||||||
BOUNCERS_ALLOWED_OU: "bouncer-ou"
|
|
||||||
LEVEL_DEBUG: "true"
|
|
||||||
# Disabled because it restart in loop otherwise
|
|
||||||
DISABLE_AGENT: "true"
|
|
||||||
# Disabled for the examples
|
|
||||||
DISABLE_ONLINE_API: "true"
|
|
||||||
volumes:
|
|
||||||
- ./config/acquis.yaml:/etc/crowdsec/acquis.yaml
|
|
||||||
# - ./config/config.yaml:/etc/crowdsec/config_local.yaml
|
|
||||||
# - ./config/local_api_credentials.yaml:/etc/crowdsec/local_api_credentials.yaml:ro
|
|
||||||
- crowdsec-certs-tls-auth:/etc/crowdsec/certs/:ro
|
|
||||||
- logs-tls-auth:/var/log/traefik:ro
|
|
||||||
- crowdsec-db-tls-auth:/var/lib/crowdsec/data/
|
|
||||||
- crowdsec-config-tls-auth:/etc/crowdsec/
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=false"
|
|
||||||
|
|
||||||
gencert:
|
|
||||||
build: .
|
|
||||||
volumes:
|
|
||||||
- crowdsec-certs-tls-auth:/out
|
|
||||||
- ./in:/in:ro
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
logs-tls-auth:
|
|
||||||
crowdsec-db-tls-auth:
|
|
||||||
crowdsec-config-tls-auth:
|
|
||||||
crowdsec-certs-tls-auth:
|
|
||||||
@@ -1,5 +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 github.com/leprosus/golang-ttl-map v1.1.7
|
require (
|
||||||
|
github.com/leprosus/golang-ttl-map v1.1.7
|
||||||
|
github.com/maxlerebourg/simpleredis v1.0.12
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
|
github.com/leprosus/golang-ttl-map v1.1.7 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.12 h1:VsJpk2l8U9QqxOWbYnEXbrs7iSNZFm2fNmFvlxYlQNk=
|
||||||
|
github.com/maxlerebourg/simpleredis v1.0.12/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
|
||||||
|
|||||||
Vendored
+84
-64
@@ -3,105 +3,125 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
ttl_map "github.com/leprosus/golang-ttl-map"
|
ttl_map "github.com/leprosus/golang-ttl-map"
|
||||||
|
simpleredis "github.com/maxlerebourg/simpleredis"
|
||||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
|
||||||
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cacheBannedValue = "t"
|
// BannedValue Banned string.
|
||||||
cacheNoBannedValue = "f"
|
BannedValue = "t"
|
||||||
|
// NoBannedValue No banned string.
|
||||||
|
NoBannedValue = "f"
|
||||||
|
// CaptchaValue Need captcha string.
|
||||||
|
CaptchaValue = "c"
|
||||||
|
// CaptchaDoneValue Captcha done string.
|
||||||
|
CaptchaDoneValue = "d"
|
||||||
|
// CacheMiss error string when cache is miss.
|
||||||
|
CacheMiss = "cache:miss"
|
||||||
|
// CacheUnreachable error string when cache is unreachable.
|
||||||
|
CacheUnreachable = "cache:unreachable"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
cache = ttl_map.New()
|
redis simpleredis.SimpleRedis
|
||||||
redis simpleredis.SimpleRedis
|
cache = ttl_map.New()
|
||||||
redisEnabled = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileSystem Cache
|
type localCache struct{}
|
||||||
|
|
||||||
func getDecisionLocalCache(clientIP string) (bool, error) {
|
func (localCache) get(key string) (string, error) {
|
||||||
banned, isCached := cache.Get(clientIP)
|
value, isCached := cache.Get(key)
|
||||||
bannedString, isValid := banned.(string)
|
valueString, isValid := value.(string)
|
||||||
if isCached && isValid && len(bannedString) > 0 {
|
if isCached && isValid && len(valueString) > 0 {
|
||||||
return bannedString == cacheBannedValue, nil
|
return valueString, nil
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("cache:miss")
|
return "", errors.New(CacheMiss)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDecisionLocalCache(clientIP string, value string, duration int64) {
|
func (localCache) set(key, value string, duration int64) {
|
||||||
cache.Set(clientIP, value, duration)
|
cache.Set(key, value, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteDecisionLocalCache(clientIP string) {
|
func (localCache) delete(key string) {
|
||||||
cache.Del(clientIP)
|
cache.Del(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redis Cache
|
type redisCache struct {
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
func getDecisionRedisCache(clientIP string) (bool, error) {
|
func (redisCache) get(key string) (string, error) {
|
||||||
banned, err := redis.Get(clientIP)
|
value, err := redis.Get(key)
|
||||||
bannedString := string(banned)
|
valueString := string(value)
|
||||||
if err == nil && len(bannedString) > 0 {
|
if err == nil && len(valueString) > 0 {
|
||||||
return bannedString == cacheBannedValue, nil
|
return valueString, nil
|
||||||
}
|
}
|
||||||
return false, err
|
errRedisMessage := err.Error()
|
||||||
|
if errRedisMessage == simpleredis.RedisMiss {
|
||||||
|
return "", errors.New(CacheMiss)
|
||||||
|
}
|
||||||
|
if errRedisMessage == simpleredis.RedisUnreachable {
|
||||||
|
return "", errors.New(CacheUnreachable)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDecisionRedisCache(clientIP string, value string, duration int64) {
|
func (rc redisCache) set(key, value string, duration int64) {
|
||||||
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
if err := redis.Set(key, []byte(value), duration); err != nil {
|
||||||
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
rc.log.Error("cache:setDecisionRedisCache" + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteDecisionRedisCache(clientIP string) {
|
func (rc redisCache) delete(key string) {
|
||||||
if err := redis.Del(clientIP); err != nil {
|
if err := redis.Del(key); err != nil {
|
||||||
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDecision delete decision in cache.
|
type cacheInterface interface {
|
||||||
func DeleteDecision(clientIP string) {
|
set(key, value string, duration int64)
|
||||||
if redisEnabled {
|
get(key string) (string, error)
|
||||||
deleteDecisionRedisCache(clientIP)
|
delete(key string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client Cache client.
|
||||||
|
type Client struct {
|
||||||
|
cache cacheInterface
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Initialize cache client.
|
||||||
|
func (c *Client) New(log *slog.Logger, isRedis bool, host, pass, database string) {
|
||||||
|
c.log = log
|
||||||
|
if isRedis {
|
||||||
|
redis.Init(host, pass, database)
|
||||||
|
c.cache = &redisCache{log: log}
|
||||||
} else {
|
} else {
|
||||||
deleteDecisionLocalCache(clientIP)
|
c.cache = &localCache{}
|
||||||
}
|
}
|
||||||
|
c.log.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDecision check in the cache if the IP has the banned / not banned value.
|
// Delete delete decision in cache.
|
||||||
|
func (c *Client) Delete(key string) {
|
||||||
|
c.log.Debug(fmt.Sprintf("cache:Delete key:%v", key))
|
||||||
|
c.cache.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get check in the cache if the IP has the banned / not banned value.
|
||||||
// Otherwise return with an error to add the IP in cache if we are on.
|
// Otherwise return with an error to add the IP in cache if we are on.
|
||||||
func GetDecision(clientIP string) (bool, error) {
|
func (c *Client) Get(key string) (string, error) {
|
||||||
if redisEnabled {
|
c.log.Debug(fmt.Sprintf("cache:Get key:%v", key))
|
||||||
return getDecisionRedisCache(clientIP)
|
return c.cache.get(key)
|
||||||
}
|
|
||||||
return getDecisionLocalCache(clientIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
// Set update the cache with the IP as key and the value banned / not banned.
|
||||||
func SetDecision(clientIP string, isBanned bool, duration int64) {
|
func (c *Client) Set(key string, value string, duration int64) {
|
||||||
var value string
|
c.log.Debug(fmt.Sprintf("cache:Set key:%v value:%v duration:%vs", key, value, duration))
|
||||||
if isBanned {
|
c.cache.set(key, value, duration)
|
||||||
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v banned", clientIP))
|
|
||||||
value = cacheBannedValue
|
|
||||||
} else {
|
|
||||||
value = cacheNoBannedValue
|
|
||||||
}
|
|
||||||
if redisEnabled {
|
|
||||||
setDecisionRedisCache(clientIP, value, duration)
|
|
||||||
} else {
|
|
||||||
setDecisionLocalCache(clientIP, value, duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitRedisClient loads variables.
|
|
||||||
func InitRedisClient(host string) {
|
|
||||||
redisEnabled = true
|
|
||||||
redis.Init(host)
|
|
||||||
logger.Debug("cache:InitRedisClient redis:initialized")
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+53
-155
@@ -4,223 +4,121 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_getDecisionLocalCache(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"
|
||||||
setDecisionLocalCache(IPInCache, "t", 10)
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
|
client.Set(IPInCache, BannedValue, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
want bool
|
want string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
valueErr string
|
valueErr string
|
||||||
}{
|
}{
|
||||||
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
|
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: BannedValue, wantErr: false, valueErr: ""},
|
||||||
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
|
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||||
{name: "Fetch invalid value", args: args{clientIP: "zaeaea"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||||
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
|
{name: "Fetch empty value", args: args{clientIP: ""}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||||
}
|
}
|
||||||
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 := getDecisionLocalCache(tt.args.clientIP)
|
got, err := client.Get(tt.args.clientIP)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("getDecisionLocalCache() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("getDecisionLocalCache() = %v, want %v", got, tt.want)
|
t.Errorf("Get() = %v, want %v", got, tt.want)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||||
t.Errorf("getDecisionLocalCache() err = %v, want %v", err.Error(), tt.valueErr)
|
t.Errorf("Get() err = %v, want %v", err.Error(), tt.valueErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_setDecisionLocalCache(t *testing.T) {
|
func Test_Set(t *testing.T) {
|
||||||
IPInCache := "10.0.0.10"
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
|
IPInCache := "10.0.0.11"
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
value string
|
value string
|
||||||
duration int64
|
duration int64
|
||||||
}
|
}
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
{name: "Set valid IP in local cache as t", args: args{clientIP: IPInCache, value: "t", duration: 0}},
|
|
||||||
{name: "Set valid IP in local cache as f", args: args{clientIP: IPInCache, value: "f", duration: 0}},
|
|
||||||
{name: "Set valid IP in local cache as empty str", args: args{clientIP: IPInCache, value: "", duration: 0}},
|
|
||||||
{name: "Set valid IP in local cache as f for -1 sec", args: args{clientIP: IPInCache, value: "f", duration: -1}},
|
|
||||||
{name: "Set valid IP in local cache as f for 10 sec", args: args{clientIP: IPInCache, value: "f", duration: 10}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
setDecisionLocalCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_deleteDecisionLocalCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
valueErr string
|
||||||
}{
|
}{
|
||||||
// TODO: Add test cases.
|
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: BannedValue, duration: 0}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||||
|
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: BannedValue, duration: 10}, want: BannedValue, wantErr: false, valueErr: ""},
|
||||||
|
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: NoBannedValue, duration: 10}, want: NoBannedValue, wantErr: false, valueErr: ""},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
deleteDecisionLocalCache(tt.args.clientIP)
|
client.Set(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||||
})
|
got, err := client.Get(tt.args.clientIP)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_getDecisionRedisCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := getDecisionRedisCache(tt.args.clientIP)
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("getDecisionRedisCache() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("getDecisionRedisCache() = %v, want %v", got, tt.want)
|
t.Errorf("Set() = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||||
|
t.Errorf("Set() err = %v, want %v", err.Error(), tt.valueErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_setDecisionRedisCache(t *testing.T) {
|
func Test_Delete(t *testing.T) {
|
||||||
|
IPInCache := "10.0.0.12"
|
||||||
|
IPNotInCache := "10.0.0.22"
|
||||||
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
|
client.Set(IPInCache, BannedValue, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
value string
|
|
||||||
duration int64
|
|
||||||
}
|
}
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
setDecisionRedisCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_deleteDecisionRedisCache(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
valueErr string
|
||||||
}{
|
}{
|
||||||
// TODO: Add test cases.
|
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||||
|
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
deleteDecisionRedisCache(tt.args.clientIP)
|
client.Delete(tt.args.clientIP)
|
||||||
})
|
got, err := client.Get(tt.args.clientIP)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteDecision(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
DeleteDecision(tt.args.clientIP)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDecision(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := GetDecision(tt.args.clientIP)
|
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
|
t.Errorf("Delete() = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||||
|
t.Errorf("Delete() err = %v, want %v", err.Error(), tt.valueErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetDecision(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
clientIP string
|
|
||||||
isBanned bool
|
|
||||||
duration int64
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
SetDecision(tt.args.clientIP, tt.args.isBanned, tt.args.duration)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitRedisClient(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
host string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
InitRedisClient(tt.args.host)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
// Package captcha implements utility for captcha management.
|
||||||
|
package captcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||||
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client Captcha client.
|
||||||
|
type Client struct {
|
||||||
|
Valid bool
|
||||||
|
siteKey string
|
||||||
|
secretKey string
|
||||||
|
remediationCustomHeader string
|
||||||
|
gracePeriodSeconds int64
|
||||||
|
captchaTemplate *template.Template
|
||||||
|
cacheClient *cache.Client
|
||||||
|
httpClient *http.Client
|
||||||
|
log *slog.Logger
|
||||||
|
infoProvider *infoProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Information for self-hosted provider.
|
||||||
|
type infoProvider struct {
|
||||||
|
js string
|
||||||
|
key string
|
||||||
|
response string
|
||||||
|
validate string
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gochecknoglobals
|
||||||
|
var infoProviders = map[string]*infoProvider{
|
||||||
|
configuration.HcaptchaProvider: {
|
||||||
|
js: "https://hcaptcha.com/1/api.js",
|
||||||
|
key: "h-captcha",
|
||||||
|
response: "h-captcha-response",
|
||||||
|
validate: "https://api.hcaptcha.com/siteverify",
|
||||||
|
},
|
||||||
|
configuration.RecaptchaProvider: {
|
||||||
|
js: "https://www.google.com/recaptcha/api.js",
|
||||||
|
key: "g-recaptcha",
|
||||||
|
response: "g-recaptcha-response",
|
||||||
|
validate: "https://www.google.com/recaptcha/api/siteverify",
|
||||||
|
},
|
||||||
|
configuration.TurnstileProvider: {
|
||||||
|
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||||
|
key: "cf-turnstile",
|
||||||
|
response: "cf-turnstile-response",
|
||||||
|
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Initialize captcha client.
|
||||||
|
func (c *Client) New(log *slog.Logger, cacheClient *cache.Client, httpClient *http.Client, provider, js, key, response, validate, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
|
||||||
|
c.Valid = provider != ""
|
||||||
|
if !c.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var info *infoProvider
|
||||||
|
if provider == configuration.CustomProvider {
|
||||||
|
info = &infoProvider{js: js, key: key, response: response, validate: validate}
|
||||||
|
} else {
|
||||||
|
info = infoProviders[provider]
|
||||||
|
}
|
||||||
|
c.infoProvider = info
|
||||||
|
c.siteKey = siteKey
|
||||||
|
c.secretKey = secretKey
|
||||||
|
c.remediationCustomHeader = remediationCustomHeader
|
||||||
|
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
|
||||||
|
c.captchaTemplate = html
|
||||||
|
c.gracePeriodSeconds = gracePeriodSeconds
|
||||||
|
c.log = log
|
||||||
|
c.httpClient = httpClient
|
||||||
|
c.cacheClient = cacheClient
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP Handle captcha html page or validation.
|
||||||
|
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
|
||||||
|
valid, err := c.Validate(r)
|
||||||
|
if err != nil {
|
||||||
|
c.log.Info("captcha:ServeHTTP:validate " + err.Error())
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
|
c.log.Debug("captcha:ServeHTTP captcha:valid")
|
||||||
|
c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
|
||||||
|
if c.remediationCustomHeader != "" {
|
||||||
|
rw.Header().Set(c.remediationCustomHeader, "solved-captcha")
|
||||||
|
}
|
||||||
|
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
"FrontendJS": c.infoProvider.js,
|
||||||
|
"FrontendKey": c.infoProvider.key,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Verify if the captcha is already done.
|
||||||
|
func (c *Client) Check(remoteIP string) bool {
|
||||||
|
value, _ := c.cacheClient.Get(remoteIP + "_captcha")
|
||||||
|
passed := value == cache.CaptchaDoneValue
|
||||||
|
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
|
||||||
|
return passed
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseProvider struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Verify the captcha from provider API.
|
||||||
|
func (c *Client) Validate(r *http.Request) (bool, error) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
c.log.Debug("captcha:Validate invalid method: " + r.Method)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
var response = r.FormValue(c.infoProvider.response)
|
||||||
|
if response == "" {
|
||||||
|
c.log.Debug("captcha:Validate no captcha response found in request")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
var body = url.Values{}
|
||||||
|
body.Add("secret", c.secretKey)
|
||||||
|
body.Add("response", response)
|
||||||
|
res, err := c.httpClient.PostForm(c.infoProvider.validate, body)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = res.Body.Close(); err != nil {
|
||||||
|
c.log.Error("captcha:Validate " + err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
|
||||||
|
c.log.Debug("captcha:Validate responseType:noJson")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
var captchaResponse responseProvider
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&captchaResponse)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
c.log.Debug(fmt.Sprintf("captcha:Validate success:%v", captchaResponse.Success))
|
||||||
|
return captchaResponse.Success, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user