mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
98
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e82ecd1b9 | ||
|
|
4cca0bc2fb | ||
|
|
fe3b3b05fd | ||
|
|
7466dbec6d | ||
|
|
9406b70219 | ||
|
|
a289c93d9b | ||
|
|
b3a874f514 | ||
|
|
b4cb552ee0 | ||
|
|
b3d0203145 | ||
|
|
8f5b7bb9af | ||
|
|
22e3febb45 | ||
|
|
1f094d818a | ||
|
|
a17e082ba3 | ||
|
|
c311d8357d | ||
|
|
7e9044c41d | ||
|
|
988f3ebeae | ||
|
|
91c31e3fa1 | ||
|
|
aa1f00b055 | ||
|
|
c867453ab4 | ||
|
|
f313b478e7 | ||
|
|
2543127abb | ||
|
|
5dd06567f4 | ||
|
|
15baee5c07 | ||
|
|
c29d8a20d3 | ||
|
|
76e5c7497d | ||
|
|
f69faaf66c | ||
|
|
8d9bc33091 | ||
|
|
02e0f532c7 | ||
|
|
a2fe60c621 | ||
|
|
b2ed600d5e | ||
|
|
d80aabaa66 | ||
|
|
16d392ebeb | ||
|
|
70558c781a | ||
|
|
f99dbfc203 | ||
|
|
b37f866ca9 | ||
|
|
1dc50a8a8c | ||
|
|
953e9f6bf4 | ||
|
|
d1d64689af | ||
|
|
80d49e3969 | ||
|
|
72be1a68f5 | ||
|
|
15bca90b18 | ||
|
|
24c7801dac | ||
|
|
be150e8aca | ||
|
|
0a54f7b09f | ||
|
|
2d0bea8eec | ||
|
|
bb714b5dfd | ||
|
|
bba5620187 | ||
|
|
73374ccefe | ||
|
|
6b7f8655ac | ||
|
|
c9a3f1f8a8 | ||
|
|
490e5e934f | ||
|
|
5b0d4b533f | ||
|
|
400de0d552 | ||
|
|
3742b6b540 | ||
|
|
d2f0a9416d | ||
|
|
c1f2131bc2 | ||
|
|
3f70e2b256 | ||
|
|
bf76ca9ef5 | ||
|
|
1baa5d7667 | ||
|
|
18f68de196 | ||
|
|
4a5f1eca6a | ||
|
|
cdda369fb8 | ||
|
|
f0f28fecef | ||
|
|
23620207f7 | ||
|
|
4a674da9f9 | ||
|
|
0dfd18f18e | ||
|
|
d8ee0a34eb | ||
|
|
b50074dca4 | ||
|
|
5044004ec2 | ||
|
|
41a46c0584 | ||
|
|
0a186cf9a9 | ||
|
|
ffcf4356fc | ||
|
|
f94e48aa03 | ||
|
|
a197194591 | ||
|
|
44e329cd57 | ||
|
|
81ffeabcec | ||
|
|
e14d179612 | ||
|
|
4058836678 | ||
|
|
87ed9e9c4e | ||
|
|
395c80dccf | ||
|
|
59268ee33d | ||
|
|
781a83465e | ||
|
|
8696501f61 | ||
|
|
f22fc2cd09 | ||
|
|
8eec1c5656 | ||
|
|
552b30a9ef | ||
|
|
6dde683a0a | ||
|
|
be0306eb49 | ||
|
|
b69bd77409 | ||
|
|
64a117d7b0 | ||
|
|
d0ba71c0c8 | ||
|
|
2e780b304d | ||
|
|
4d7615dd19 | ||
|
|
952fcd844f | ||
|
|
9cf4827768 | ||
|
|
6d8e811a03 | ||
|
|
18e4d42e8e | ||
|
|
54183fbc97 |
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [ 1.17, 1.x ]
|
go-version: [ 1.19, 1.x ]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ jobs:
|
|||||||
name: Main Process
|
name: Main Process
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.17
|
GO_VERSION: 1.19
|
||||||
GOLANGCI_LINT_VERSION: v1.46.2
|
GOLANGCI_LINT_VERSION: v1.50.0
|
||||||
YAEGI_VERSION: v0.13.0
|
YAEGI_VERSION: v0.14.2
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
config
|
./config/
|
||||||
|
conf
|
||||||
db
|
db
|
||||||
logs
|
logs
|
||||||
docker-compose.dev.yml
|
docker-compose.dev.yml
|
||||||
+17
-4
@@ -11,7 +11,7 @@ linters-settings:
|
|||||||
golint:
|
golint:
|
||||||
min-confidence: 0
|
min-confidence: 0
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 12
|
min-complexity: 15
|
||||||
goconst:
|
goconst:
|
||||||
min-len: 5
|
min-len: 5
|
||||||
min-occurrences: 4
|
min-occurrences: 4
|
||||||
@@ -29,11 +29,20 @@ linters-settings:
|
|||||||
linters:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
|
- deadcode # deprecated
|
||||||
|
- exhaustivestruct # deprecated
|
||||||
|
- golint # deprecated
|
||||||
|
- ifshort # deprecated
|
||||||
- interfacer # deprecated
|
- interfacer # deprecated
|
||||||
- maligned # deprecated
|
- maligned # deprecated
|
||||||
|
- nosnakecase # deprecated
|
||||||
- scopelint # deprecated
|
- scopelint # deprecated
|
||||||
- golint # deprecated
|
- scopelint # deprecated
|
||||||
- exhaustivestruct # deprecated
|
- structcheck # deprecated
|
||||||
|
- varcheck # deprecated
|
||||||
|
- sqlclosecheck # 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
|
||||||
@@ -52,12 +61,16 @@ linters:
|
|||||||
- gomnd
|
- gomnd
|
||||||
- forbidigo
|
- forbidigo
|
||||||
- varnamelen
|
- varnamelen
|
||||||
|
- wastedassign # is disabled because of generics
|
||||||
|
- gofumpt
|
||||||
|
- gci
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
max-per-linter: 0
|
max-per-linter: 0
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
exclude: []
|
exclude:
|
||||||
|
- "G402: TLS InsecureSkipVerify may be true."
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
- path: (.+)_test.go
|
- path: (.+)_test.go
|
||||||
linters:
|
linters:
|
||||||
|
|||||||
@@ -18,3 +18,73 @@ vendor:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf ./vendor
|
rm -rf ./vendor
|
||||||
|
|
||||||
|
run_dev:
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_local:
|
||||||
|
docker-compose -f docker-compose.local.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_behindproxy:
|
||||||
|
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_cacheredis:
|
||||||
|
docker-compose -f exemples/redis-cache/docker-compose.redis.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_trustedips:
|
||||||
|
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
run_binaryvm:
|
||||||
|
cd exemples/binary-vm/ && sudo vagrant up
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
run:
|
||||||
|
docker-compose -f docker-compose.yml up -d --remove-orphans
|
||||||
|
|
||||||
|
restart_dev:
|
||||||
|
docker-compose -f docker-compose.dev.yml restart
|
||||||
|
|
||||||
|
restart_local:
|
||||||
|
docker-compose -f docker-compose.local.yml restart
|
||||||
|
|
||||||
|
restart:
|
||||||
|
docker-compose -f docker-compose.yml restart
|
||||||
|
|
||||||
|
restart_behindproxy:
|
||||||
|
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml restart
|
||||||
|
|
||||||
|
restart_cacheredis:
|
||||||
|
docker-compose -f exemples/redis-cache/docker-compose.redis.yml restart
|
||||||
|
|
||||||
|
restart_trustedips:
|
||||||
|
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml restart
|
||||||
|
|
||||||
|
restart_tlsauth:
|
||||||
|
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml
|
||||||
|
|
||||||
|
show_logs:
|
||||||
|
docker-compose -f docker-compose.yml restart
|
||||||
|
|
||||||
|
show_local_logs:
|
||||||
|
docker-compose -f docker-compose.local.yml logs -f
|
||||||
|
|
||||||
|
show_dev_logs:
|
||||||
|
docker-compose -f docker-compose.dev.yml logs -f
|
||||||
|
|
||||||
|
clean_all_docker:
|
||||||
|
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
|
||||||
|
docker-compose -f exemples/redis-cache/docker-compose.redis.yml down --remove-orphans
|
||||||
|
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
|
||||||
|
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.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:
|
||||||
|
cd exemples/binary-vm/ && sudo vagrant destroy -f
|
||||||
|
|
||||||
|
|
||||||
|
show_metrics:
|
||||||
|
docker exec crowdsec cscli metrics
|
||||||
|
|
||||||
|
|||||||
@@ -6,63 +6,111 @@
|
|||||||
|
|
||||||
# Crowdsec Bouncer Traefik plugin
|
# Crowdsec Bouncer Traefik plugin
|
||||||
|
|
||||||
This plugins aims to implement a Crowdsec Bouncer into a traefik plugin.
|
This plugin aims to implement a Crowdsec Bouncer in a traefik plugin.
|
||||||
|
|
||||||
> [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
|
> [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
|
||||||
> We leverage local behavior analysis and crowd power to build the largest CTI network in the world.
|
> We leverage local behavior analysis and crowd power to build the largest CTI network in the world.
|
||||||
|
|
||||||
The purpose is to enable treafik to authorize or block requests from IP based and their reputation and behavior.
|
The purpose is to enable treafik to authorize or block requests from IPs based on their reputation and behavior.
|
||||||
|
|
||||||
The crowdsec utility will provide the community blocklist which contains highly reported and validated IP banned from the crowdsec network.
|
The crowdsec utility will provide the community blocklist which contains highly reported and validated IPs banned from the crowdsec network.
|
||||||
|
|
||||||
When used with crowdsec it will leverage the local API which will analyze traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns against your website.
|
When used with crowdsec it will leverage the local API which will analyze traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns against your website.
|
||||||
|
|
||||||
There are 4 operating modes (CrowdsecMode) for this plugin:
|
There are 3 operating modes (CrowdsecMode) for this plugin:
|
||||||
|
|
||||||
| Mode | Description |
|
| Mode | Description |
|
||||||
|------|------|
|
|------|------|
|
||||||
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
||||||
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
|
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
|
||||||
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
|
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
|
||||||
| alone | Streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI.|
|
|
||||||
|
|
||||||
The recommanded mode for performance is the streaming mode, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every requests that happens hits the cache for quick decisions.
|
The streaming mode is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every request that happens hits the cache for quick decisions.
|
||||||
|
|
||||||
|
The cache can be local to Traefik using the filesystem, or a separate redis instance.
|
||||||
|
Support for Redis is currently in beta (requires version 7.0.X).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
To get started, use the `docker-compose.yml` file.
|
||||||
|
|
||||||
|
You can run it with:
|
||||||
|
```bash
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Note
|
||||||
|
|
||||||
|
**/!\ Since Release 1.1.0, the cache is no longer duplicated but shared by all services**
|
||||||
|
*This lowers the overhead of the cache in memory and the numbers of cache to fetch it from crowdsec in situations with many services*
|
||||||
|
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
- Enabled
|
- Enabled
|
||||||
- bool
|
- bool
|
||||||
|
- default: false
|
||||||
- enable the plugin
|
- enable the plugin
|
||||||
|
- LogLevel
|
||||||
|
- string
|
||||||
|
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
||||||
- CrowdsecMode
|
- CrowdsecMode
|
||||||
- string
|
- string
|
||||||
- default: `stream`, expected value are: `none`, `live`, `stream`, `alone`
|
- default: `live`, expected values are: `none`, `live`, `stream`
|
||||||
- CrowdsecLapiScheme
|
- CrowdsecLapiScheme
|
||||||
- string
|
- string
|
||||||
- default: `http`, expected value are: `http`, `https`
|
- default: `http`, expected values are: `http`, `https`
|
||||||
- CrowdsecLapiHost
|
- CrowdsecLapiHost
|
||||||
- string
|
- string
|
||||||
- default: "crowdsec:8080"
|
- default: "crowdsec:8080"
|
||||||
- Crowdsec LAPI available on which host.
|
- Crowdsec LAPI available on which host and port.
|
||||||
- CrowdsecLapiKey
|
- CrowdsecLapiKey
|
||||||
- string
|
- string
|
||||||
- Crowdsec LAPI generated key for the bouncer.
|
- default: ""
|
||||||
- CrowdsecCapiLogin
|
- Crowdsec LAPI key for the bouncer : **must be unique by service**.
|
||||||
|
- CrowdsecLapiTlsInsecureVerify
|
||||||
|
- bool
|
||||||
|
- default: false
|
||||||
|
- Disable verification of certificate presented by Crowdsec LAPI
|
||||||
|
- CrowdsecLapiTlsCertificateAuthority
|
||||||
- string
|
- string
|
||||||
- Used only in `alone` mode, login for Crowdsec CAPI
|
- default: ""
|
||||||
- CrowdsecCapiPwd
|
- PEM-encoded Certificate Authority of the Crowdsec LAPI
|
||||||
|
- CrowdsecLapiTlsCertificateBouncer
|
||||||
- string
|
- string
|
||||||
- Used only in `alone` mode, password for Crowdsec CAPI
|
- default: ""
|
||||||
- CrowdsecCapiScenarios
|
- PEM-encoded client Certificate of the Bouncer
|
||||||
- []string
|
- CrowdsecLapiTlsCertificateBouncerKey
|
||||||
- Used only in `alone` mode, scenarios for Crowdsec CAPI
|
- string
|
||||||
|
- default: ""
|
||||||
|
- PEM-encoded client private key of the Bouncer
|
||||||
- UpdateIntervalSeconds
|
- UpdateIntervalSeconds
|
||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
- Used only in `stream` mode, interval between fetching blacklisted IPs from LAPI
|
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
||||||
- DefaultDecisionSeconds
|
- DefaultDecisionSeconds
|
||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
- Used only in `live` mode, decision duration of accepted IPs
|
- Used only in `live` mode, decision duration of accepted IPs
|
||||||
|
- ClientTrustedIPs
|
||||||
|
- string
|
||||||
|
- default: []
|
||||||
|
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
|
||||||
|
- ForwardedHeadersTrustedIPs
|
||||||
|
- []string
|
||||||
|
- default: []
|
||||||
|
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
|
||||||
|
- ForwardedHeadersCustomName
|
||||||
|
- string
|
||||||
|
- default: "X-Forwarded-For"
|
||||||
|
- Name of the header where the real IP of the client should be retrieved
|
||||||
|
- RedisCacheEnabled
|
||||||
|
- bool
|
||||||
|
- default: false
|
||||||
|
- enable redis cache instead of filesystem cache
|
||||||
|
- RedisCacheHost
|
||||||
|
- string
|
||||||
|
- default: "redis:6379"
|
||||||
|
- hostname and port for the redis service
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -103,24 +151,64 @@ http:
|
|||||||
plugin:
|
plugin:
|
||||||
bouncer:
|
bouncer:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
logLevel: DEBUG
|
||||||
updateIntervalSeconds: 60
|
updateIntervalSeconds: 60
|
||||||
defaultDecisionSeconds: 60
|
defaultDecisionSeconds: 60
|
||||||
crowdsecMode: stream
|
crowdsecMode: live
|
||||||
crowdsecLapiKey: privateKey
|
crowdsecLapiKey: privateKey-foo
|
||||||
|
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
|
||||||
crowdsecLapiHost: crowdsec:8080
|
crowdsecLapiHost: crowdsec:8080
|
||||||
crowdsecLapiScheme: http
|
crowdsecLapiScheme: http
|
||||||
crowdsecCapiLogin: login
|
crowdsecLapiTLSInsecureVerify: false
|
||||||
crowdsecCapiPwd: password
|
forwardedHeadersTrustedIPs:
|
||||||
crowdsecCapiScenarios:
|
- 10.0.10.23/32
|
||||||
- scenario1
|
- 10.0.20.0/24
|
||||||
- scenario2
|
clientTrustedIPs:
|
||||||
|
- 192.168.1.0/24
|
||||||
|
forwardedHeadersCustomName: X-Custom-Header
|
||||||
|
redisCacheEnabled: false
|
||||||
|
redisCacheHost: "redis:6379"
|
||||||
|
crowdsecLapiTLSCertificateAuthority: |-
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||||
...
|
...
|
||||||
|
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
crowdsecLapiTLSCertificateAuthorityFile: /etc/traefik/crowdsec-certs/ca.pem
|
||||||
|
crowdsecLapiTLSCertificateBouncer: |-
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL
|
||||||
|
...
|
||||||
|
RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz
|
||||||
|
N2U=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
crowdsecLapiTLSCertificateBouncerFile: /etc/traefik/crowdsec-certs/bouncer.pem
|
||||||
|
crowdsecLapiTLSCertificateBouncerKey: |-
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d
|
||||||
|
...
|
||||||
|
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
||||||
|
|
||||||
```
|
```
|
||||||
Except for the crowdsecLapiKey, crowdsecCapiLogin, crowdsecCapiPwd, crowdsecCapiScenarios, these are the default value of the plugin.
|
|
||||||
|
|
||||||
#### Generate LAPI KEY (exept for `alone` mode)
|
#### Fill variable with value of file
|
||||||
You need to generate a crowdsec API key for the LAPI.
|
|
||||||
|
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority` and `CrowdsecLapiKey` can be provided with the content as raw or through a file path that Traefik can read.
|
||||||
|
The file variable will be used as preference if both content and file are provided for the same variable.
|
||||||
|
|
||||||
|
Format is:
|
||||||
|
- Content: VariableName: XXX
|
||||||
|
- File : VariableNameFILE: /path
|
||||||
|
|
||||||
|
#### Authenticate with LAPI
|
||||||
|
|
||||||
|
You can authenticate to the LAPI either with LAPIKEY or by using client certificates.
|
||||||
|
Please see below for more details on each option.
|
||||||
|
|
||||||
|
#### Generate LAPI KEY
|
||||||
|
You can generate a crowdsec API key for the LAPI.
|
||||||
You can follow the documentation here: https://docs.crowdsec.net/docs/user_guides/lapi_mgmt/
|
You can follow the documentation here: https://docs.crowdsec.net/docs/user_guides/lapi_mgmt/
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -128,7 +216,7 @@ docker-compose -f docker-compose-local.yml up -d crowdsec
|
|||||||
docker exec crowdsec cscli bouncers add crowdsecBouncer
|
docker exec crowdsec cscli bouncers add crowdsecBouncer
|
||||||
```
|
```
|
||||||
|
|
||||||
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose-test.yml
|
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml
|
||||||
```yaml
|
```yaml
|
||||||
...
|
...
|
||||||
whoami:
|
whoami:
|
||||||
@@ -143,46 +231,34 @@ crowdsec:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then run all the containers:
|
Note:
|
||||||
```bash
|
> Crowdsec does not require a specific format for la LAPI-key, you may use something like FIXME-LAPI-KEY but that is not recommanded for obvious reasons
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Generate CAPI credentials (only for `alone` mode)
|
|
||||||
You need to create a crowdsec API credentials for the CAPI.
|
|
||||||
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}"
|
|
||||||
```
|
|
||||||
|
|
||||||
These CAPI credentials must be set in your docker-compose.yml or in your config files
|
|
||||||
```yaml
|
|
||||||
...
|
|
||||||
traefik:
|
|
||||||
command:
|
|
||||||
...
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
|
||||||
- "--experimental.plugins.bouncer.version=v1.0.0"
|
|
||||||
...
|
|
||||||
whoami:
|
|
||||||
labels:
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapilogin=LOGIN"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapipwd=PASSWORD"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=scenario1, scenario2, ..."
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
You can then run all the containers:
|
You can then run all the containers:
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add manually an IP to the blocklist (testing purpose)
|
#### Use certificates to authenticate with CrowdSec
|
||||||
|
|
||||||
|
You can follow the example in exemples/tls-auth to view how to authenticate with client certificates with the LAPI.
|
||||||
|
In that case communications with the LAPI must go through HTTPS.
|
||||||
|
|
||||||
|
A script is available to generate certificates in exemples/tls-auth/gencerts.sh and must be in the same directory as the inputs for the PKI creation.
|
||||||
|
|
||||||
|
#### Use HTTPS to communicate with the LAPI
|
||||||
|
|
||||||
|
To communicate with the LAPI in HTTPS you need to either accept any certificates by setting the crowdsecLapiTLSInsecureVerify to true or add the CA used by the server certificate of Crowdsec using crowdsecLapiTLSCertificateAuthority or crowdsecLapiTLSCertificateAuthorityFile.
|
||||||
|
Set the crowdsecLapiScheme to https.
|
||||||
|
|
||||||
|
Crowdsec must be listening in HTTPS for this to work.
|
||||||
|
Please see the tls-auth exemple or the official documentation: [https://docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
|
||||||
|
|
||||||
|
#### Manually add an IP to the blocklist (for testing purposes)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d crowdsec
|
docker-compose up -d crowdsec
|
||||||
docker exec crowdsec cscli decisions add --ip 10.0.0.10 # this will be effective 4h
|
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m # this will be effective 10min
|
||||||
docker exec crowdsec cscli decisions remove --ip 10.0.0.10
|
docker exec crowdsec cscli decisions remove --ip 10.0.0.10
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -191,7 +267,7 @@ docker exec crowdsec cscli decisions remove --ip 10.0.0.10
|
|||||||
Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
|
Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
|
||||||
To use a plugin in local mode, the Traefik static configuration must define the module name (as is usual for Go packages) and a path to a [Go workspace](https://golang.org/doc/gopath_code.html#Workspaces), which can be the local GOPATH or any directory.
|
To use a plugin in local mode, the Traefik static configuration must define the module name (as is usual for Go packages) and a path to a [Go workspace](https://golang.org/doc/gopath_code.html#Workspaces), which can be the local GOPATH or any directory.
|
||||||
|
|
||||||
The plugins must be placed in `./plugins-local` directory,
|
The plugins must be placed in the `./plugins-local` directory,
|
||||||
which should be in the working directory of the process running the Traefik binary.
|
which should be in the working directory of the process running the Traefik binary.
|
||||||
The source code of the plugin should be organized as follows:
|
The source code of the plugin should be organized as follows:
|
||||||
|
|
||||||
@@ -210,19 +286,203 @@ The source code of the plugin should be organized as follows:
|
|||||||
└── vendor/*
|
└── vendor/*
|
||||||
```
|
```
|
||||||
|
|
||||||
For local developpement a docker-compose.local.yml is provided and reproduce the directory layout needed by traefik. This works once you have generated and filled your LAPI-KEY (crowdsecLapiKey), if not look below for informations
|
For local development, a docker-compose.local.yml is provided which reproduces the directory layout needed by traefik. This works once you have generated and filled your LAPI-KEY (crowdsecLapiKey), if not look below for information
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.local.yml up -d
|
docker-compose -f docker-compose.local.yml up -d
|
||||||
```
|
```
|
||||||
|
Equivalent to
|
||||||
|
```bash
|
||||||
|
make run_local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
1. Behind another proxy service (ex: clouflare)
|
||||||
|
|
||||||
|
You need to configure your Traefik to trust Forwarded headers by your front proxy
|
||||||
|
In the example we use another instance of traefik with the container named cloudflare to simulate a front proxy
|
||||||
|
|
||||||
|
The "internal" Traefik instance is configured to trust the cloudflare forward headers
|
||||||
|
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
|
||||||
|
```yaml
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
We configure the middleware to trust as well the IP:
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_behindproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
2. With Redis as an external shared cache
|
||||||
|
|
||||||
|
The plugin must be configured to connect to a redis instance
|
||||||
|
```yaml
|
||||||
|
redisCacheHost: "redis:6379"
|
||||||
|
```
|
||||||
|
Here **redis** is the hostname of a container located in the same network as Traefik and **6379** is the default port of redis
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_cacheredis
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
|
||||||
|
|
||||||
|
You need to configure your Traefik to trust Forwarded headers by your front proxy
|
||||||
|
In the example we use a whoami container protected by crowdsec, and we ban our IP before allowing using TrustedIPs
|
||||||
|
|
||||||
|
If you are using another proxy in front, you need to add its IP in the trusted IP for the forwarded headers.
|
||||||
|
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
|
||||||
|
The "internal" Traefik instance is configured to trust the forward headers
|
||||||
|
```yaml
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
We configure the middleware to trust as well as the IP of the intermediate proxy if needed:
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add your IP to the ban list
|
||||||
|
```bash
|
||||||
|
docker exec crowdsec cscli decisions add --ip 10.0.10.30 -d 10m
|
||||||
|
```
|
||||||
|
You should get a 403 on http://localhost/foo
|
||||||
|
|
||||||
|
> Replace *10.0.10.30* by your IP
|
||||||
|
|
||||||
|
Add the IPs that will not be filtered by the plugin
|
||||||
|
```yaml
|
||||||
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clientTrustedips=10.0.10.30/32"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Replace *10.0.10.30/32* by your IP or IP range, so it's not getting checked against ban cache of crowdsec
|
||||||
|
|
||||||
|
You should get a 200 on http://localhost/foo even if you are on the ban cache
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_trustedips
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Using Crowdsec and Traefik installed as binary in a single VM
|
||||||
|
|
||||||
|
Please see details in `exemples/binary-vm/README.md`
|
||||||
|
|
||||||
|
To play the demo environment run:
|
||||||
|
```bash
|
||||||
|
make run_binaryvm
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Using https communication and tls authentication with Crowdsec
|
||||||
|
|
||||||
|
##### Summary
|
||||||
|
This example demonstrates the use of https between the Traefik plugin and the Crowdsec LAPI.
|
||||||
|
|
||||||
|
It is possible to communicate with the LAPI in https and still authenticate with API key.
|
||||||
|
You can add the client TLS certificate generated to authenticate without any Token in the plugin.
|
||||||
|
|
||||||
|
However, note that it is not possible to authenticate with TLS client certificate without https setup for the LAPI.
|
||||||
|
|
||||||
|
The example is detailed below and will be placed in the `examples/tls-auth/README.md` file.
|
||||||
|
|
||||||
|
##### Details
|
||||||
|
|
||||||
|
Simple HTTPS communication: It is possible to talk to Crowdsec LAPI which is configured with a self-signed certificate
|
||||||
|
In that case the setting **crowdsecLapiTLSInsecureVerify** must be set to true.
|
||||||
|
|
||||||
|
It is recommended to validate the certificate presented by Crowdsec LAPI using the Certificate Authority which created it.
|
||||||
|
|
||||||
|
You can provide the Certificate Authority using:
|
||||||
|
* A file path readable by Traefik
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
crowdsec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
crowdsecLapiTlsCertificateAuthorityFile: /etc/traefik/certs/crowdsecCA.pem
|
||||||
|
```
|
||||||
|
* The PEM encoded certificate as a text variable
|
||||||
|
|
||||||
|
In the static file configuration of Traefik
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
crowdsec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
crowdsecLapiTlsCertificateAuthority: |-
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||||
|
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
|
||||||
|
...
|
||||||
|
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
|
||||||
|
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
In a dynamic configuration of a provider (ex docker) as a Label
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
whoami-foo:
|
||||||
|
image: traefik/whoami
|
||||||
|
labels:
|
||||||
|
- |
|
||||||
|
traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTlsCertificateAuthority=
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||||
|
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
|
||||||
|
...
|
||||||
|
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
|
||||||
|
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
The example tls-auth presents 2 services, foo and bar which comes with the bouncer.
|
||||||
|
At startup, certificates are created in a shared docker volume by a sidecar container which exits after.
|
||||||
|
|
||||||
|
Traefik will use client and CA certificates.
|
||||||
|
The Bouncer will use server and CA certificates.
|
||||||
|
|
||||||
|
The service `whoami-foo` will authenticate with an **API key** over HTTPS after verifying the server certificate with CA.
|
||||||
|
The service `whoami-bar` will authenticate with a **client certificate** signed by the CA.
|
||||||
|
|
||||||
|
Access to a route that communicate via https and authenticate with API-key:
|
||||||
|
```
|
||||||
|
curl http://localhost:80/foo
|
||||||
|
```
|
||||||
|
Access to a route that communicate via https and authenticate with a client certificate:
|
||||||
|
```
|
||||||
|
curl http://localhost:80/bar
|
||||||
|
```
|
||||||
|
Access to the traefik dashboard
|
||||||
|
```
|
||||||
|
curl http://localhost:8080/dashboard/#/
|
||||||
|
```
|
||||||
|
|
||||||
|
To play the demo environnement run:
|
||||||
|
```bash
|
||||||
|
make run_tlsauth
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing
|
||||||
|
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
|
||||||
|
|
||||||
|
|
||||||
### About
|
### About
|
||||||
|
|
||||||
Me and [mathieuHa](https://github.com/mathieuHa) have been using traefik since 2020 at [Primadviz](https://primadviz.com).
|
Me and [mathieuHa](https://github.com/mathieuHa) have been using traefik since 2020 at [Primadviz](https://primadviz.com).
|
||||||
We come from web developper and security engineer background and wanted to add the power of a very promesing technology (Crowdsec) into the edge router we love.
|
We come from a web development and security engineer background and wanted to add the power of a very promising technology (Crowdsec) to the edge router we love.
|
||||||
|
|
||||||
We initially run into this project: https://github.com/fbonalair/traefik-crowdsec-bouncer
|
We initially ran into this project: https://github.com/fbonalair/traefik-crowdsec-bouncer
|
||||||
It was using traefik and forward auth middleware to verify every requests.
|
It was using traefik and forward auth middleware to verify every request.
|
||||||
They had to go through a webserver which then contacts of another webservice (the crowdsec LAPI) to make a decision based on the source IP.
|
They had to go through a webserver which then contacts another webservice (the crowdsec LAPI) to make a decision based on the source IP.
|
||||||
We initially proposed some improvement by implementing a streaming mode and a local cache.
|
We initially proposed some improvements by implementing a streaming mode and a local cache.
|
||||||
With the Traefik hackathon we deciced to implement our solution directly as a traefik plugin which could be found by every one on plugins.traefik.io and be more performant.
|
With the Traefik hackathon we decided to implement our solution directly as a traefik plugin which could be found by everyone on plugins.traefik.io and be more performant.
|
||||||
|
|||||||
+174
-275
@@ -1,4 +1,6 @@
|
|||||||
package crowdsec_bouncer_traefik_plugin
|
// 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.
|
||||||
|
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -6,88 +8,75 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ttl_map "github.com/leprosus/golang-ttl-map"
|
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||||
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
|
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
aloneMode = "alone"
|
|
||||||
streamMode = "stream"
|
|
||||||
liveMode = "live"
|
|
||||||
noneMode = "none"
|
|
||||||
crowdsecLapiHeader = "X-Api-Key"
|
crowdsecLapiHeader = "X-Api-Key"
|
||||||
crowdsecCapiHeader = "Authorization"
|
|
||||||
crowdsecLapiRoute = "v1/decisions"
|
crowdsecLapiRoute = "v1/decisions"
|
||||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||||
crowdsecCapiLogin = "v2/watchers/login"
|
cacheTimeoutKey = "updated"
|
||||||
crowdsecCapiDecisions = "v2/decisions/stream"
|
|
||||||
cacheBannedValue = "t"
|
|
||||||
cacheNoBannedValue = "f"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config the plugin configuration.
|
//nolint:gochecknoglobals
|
||||||
type Config struct {
|
var (
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
isCrowdsecStreamHealthy = false
|
||||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
ticker chan bool
|
||||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
)
|
||||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
|
||||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
|
||||||
CrowdsecCapiLogin string `json:"crowdsecCapiLogin,omitempty"`
|
|
||||||
CrowdsecCapiPwd string `json:"crowdsecCapiPwd,omitempty"`
|
|
||||||
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
|
||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateConfig creates the default plugin configuration.
|
// CreateConfig creates the default plugin configuration.
|
||||||
func CreateConfig() *Config {
|
func CreateConfig() *configuration.Config {
|
||||||
return &Config{
|
return configuration.New()
|
||||||
Enabled: false,
|
|
||||||
CrowdsecMode: streamMode,
|
|
||||||
CrowdsecLapiScheme: "http",
|
|
||||||
CrowdsecLapiHost: "crowdsec:8080",
|
|
||||||
CrowdsecLapiKey: "",
|
|
||||||
CrowdsecCapiLogin: "",
|
|
||||||
CrowdsecCapiPwd: "",
|
|
||||||
CrowdsecCapiScenarios: []string{},
|
|
||||||
UpdateIntervalSeconds: 60,
|
|
||||||
DefaultDecisionSeconds: 60,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bouncer a Bouncer plugin.
|
// Bouncer a Bouncer struct.
|
||||||
type Bouncer struct {
|
type Bouncer struct {
|
||||||
next http.Handler
|
next http.Handler
|
||||||
name string
|
name string
|
||||||
template *template.Template
|
template *template.Template
|
||||||
|
|
||||||
enabled bool
|
enabled bool
|
||||||
crowdsecStreamHealthy bool
|
|
||||||
crowdsecScheme string
|
crowdsecScheme string
|
||||||
crowdsecHost string
|
crowdsecHost string
|
||||||
crowdsecKey string
|
crowdsecKey string
|
||||||
crowdsecMode string
|
crowdsecMode string
|
||||||
updateInterval int64
|
updateInterval int64
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
crowdsecLogin string
|
customHeader string
|
||||||
crowdsecPwd string
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
crowdsecScenarios []string
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
client *http.Client
|
client *http.Client
|
||||||
cache *ttl_map.Heap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
||||||
err := validateParams(config)
|
logger.Init(config.LogLevel)
|
||||||
|
err := configuration.ValidateParams(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||||
|
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
||||||
|
|
||||||
|
tlsConfig, err := configuration.GetTLSConfigCrowdsec(config)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
apiKey, err := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||||
|
if err != nil && len(tlsConfig.Certificates) == 0 {
|
||||||
|
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,76 +86,106 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
|
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
|
||||||
|
|
||||||
enabled: config.Enabled,
|
enabled: config.Enabled,
|
||||||
crowdsecStreamHealthy: false,
|
|
||||||
crowdsecMode: config.CrowdsecMode,
|
crowdsecMode: config.CrowdsecMode,
|
||||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||||
crowdsecHost: config.CrowdsecLapiHost,
|
crowdsecHost: config.CrowdsecLapiHost,
|
||||||
crowdsecKey: config.CrowdsecLapiKey,
|
crowdsecKey: apiKey,
|
||||||
crowdsecLogin: config.CrowdsecCapiLogin,
|
|
||||||
crowdsecPwd: config.CrowdsecCapiPwd,
|
|
||||||
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
|
||||||
updateInterval: config.UpdateIntervalSeconds,
|
updateInterval: config.UpdateIntervalSeconds,
|
||||||
|
customHeader: config.ForwardedHeadersCustomName,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
|
serverPoolStrategy: &ip.PoolStrategy{
|
||||||
|
Checker: serverChecker,
|
||||||
|
},
|
||||||
|
clientPoolStrategy: &ip.PoolStrategy{
|
||||||
|
Checker: clientChecker,
|
||||||
|
},
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
},
|
},
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
},
|
},
|
||||||
cache: ttl_map.New(),
|
|
||||||
}
|
}
|
||||||
if config.CrowdsecMode == streamMode || config.CrowdsecMode == aloneMode {
|
if config.RedisCacheEnabled {
|
||||||
if config.CrowdsecMode == aloneMode {
|
cache.InitRedisClient(config.RedisCacheHost)
|
||||||
getToken(bouncer)
|
|
||||||
}
|
}
|
||||||
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
|
||||||
go func() {
|
ticker = startTicker(config, func() {
|
||||||
go handleStreamCache(bouncer)
|
handleStreamCache(bouncer)
|
||||||
for range ticker.C {
|
})
|
||||||
go handleStreamCache(bouncer)
|
go handleStreamCache(bouncer)
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
|
||||||
return bouncer, nil
|
return bouncer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP principal function of plugin.
|
// ServeHTTP principal function of plugin.
|
||||||
func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
//
|
||||||
if !a.enabled {
|
//nolint:nestif
|
||||||
a.next.ServeHTTP(rw, req)
|
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if !bouncer.enabled {
|
||||||
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Make sur remote address does not include the port.
|
// Here we check for the trusted IPs in the customHeader
|
||||||
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("failed to extract ip from remote address: %v", err))
|
logger.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
||||||
a.next.ServeHTTP(rw, req)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if our IP is in the trusted list we bypass the next checks
|
||||||
|
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
|
||||||
|
if isTrusted {
|
||||||
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.crowdsecMode == streamMode || a.crowdsecMode == aloneMode || a.crowdsecMode == liveMode {
|
// TODO This should be simplified
|
||||||
isBanned, err := getDecision(a, remoteHost)
|
if bouncer.crowdsecMode != configuration.NoneMode {
|
||||||
if err == nil {
|
isBanned, erro := cache.GetDecision(remoteIP)
|
||||||
|
if erro != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
|
||||||
|
if erro.Error() == simpleredis.RedisUnreachable {
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
|
||||||
if isBanned {
|
if isBanned {
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
} else {
|
} else {
|
||||||
a.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
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 a.crowdsecMode == streamMode || a.crowdsecMode == aloneMode {
|
if bouncer.crowdsecMode == configuration.StreamMode {
|
||||||
if a.crowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
a.next.ServeHTTP(rw, req)
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
} else {
|
} else {
|
||||||
|
logger.Error(fmt.Sprintf("ServeHTTP:isCrowdsecStreamHealthy ip:%s", remoteIP))
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handleNoStreamCache(a, rw, req, remoteHost)
|
err = handleNoStreamCache(bouncer, remoteIP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s %s", remoteIP, err.Error()))
|
||||||
|
rw.WriteHeader(http.StatusForbidden)
|
||||||
|
} else {
|
||||||
|
bouncer.next.ServeHTTP(rw, req)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,247 +210,127 @@ type Stream struct {
|
|||||||
New []Decision `json:"new"`
|
New []Decision `json:"new"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login Body returned from Crowdsec Login CAPI.
|
func startTicker(config *configuration.Config, work func()) chan bool {
|
||||||
type Login struct {
|
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
|
||||||
Code int `json:"code"`
|
stop := make(chan bool, 1)
|
||||||
Token string `json:"token"`
|
go func() {
|
||||||
Expire string `json:"expire"`
|
defer logger.Debug("ticker:stopped")
|
||||||
}
|
for {
|
||||||
|
select {
|
||||||
func logger(str string) {
|
case <-ticker.C:
|
||||||
log.Printf("Crowdsec Bouncer Traefik Plugin - %s", str)
|
go work()
|
||||||
}
|
case <-stop:
|
||||||
|
|
||||||
func contains(source []string, target string) bool {
|
|
||||||
for _, a := range source {
|
|
||||||
if a == target {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Decision check in the cache if the IP has the banned / not banned value.
|
|
||||||
// Otherwise return with an error to add the IP in cache if we are on.
|
|
||||||
func getDecision(a *Bouncer, clientIP string) (bool, error) {
|
|
||||||
banned, isCached := a.cache.Get(clientIP)
|
|
||||||
bannedString, isValid := banned.(string)
|
|
||||||
if isCached && isValid && len(bannedString) > 0 {
|
|
||||||
return bannedString == cacheBannedValue, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("no cache data")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDecision(a *Bouncer, clientIP string, isBanned bool, duration int64) {
|
|
||||||
if a.crowdsecMode == noneMode {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isBanned {
|
|
||||||
logger(fmt.Sprintf("%v banned", clientIP))
|
|
||||||
a.cache.Set(clientIP, cacheBannedValue, duration)
|
|
||||||
} else {
|
|
||||||
a.cache.Set(clientIP, cacheNoBannedValue, duration)
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
return stop
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNoStreamCache(a *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
|
// We are now in none or live mode.
|
||||||
// We are now in none or live mode.
|
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
||||||
|
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
|
||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: a.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: a.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: crowdsecLapiRoute,
|
Path: crowdsecLapiRoute,
|
||||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost),
|
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
||||||
|
}
|
||||||
|
body, err := crowdsecQuery(bouncer, routeURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
body := crowdsecQuery(a, routeURL.String(), false)
|
|
||||||
|
|
||||||
if bytes.Equal(body, []byte("null")) {
|
if bytes.Equal(body, []byte("null")) {
|
||||||
setDecision(a, remoteHost, false, a.defaultDecisionTimeout)
|
if isLiveMode {
|
||||||
a.next.ServeHTTP(rw, req)
|
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||||
return
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var decisions []Decision
|
var decisions []Decision
|
||||||
err := json.Unmarshal(body, &decisions)
|
err = json.Unmarshal(body, &decisions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("failed to parse body: %s", err))
|
return fmt.Errorf("handleNoStreamCache:parseBody %w", err)
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if len(decisions) == 0 {
|
if len(decisions) == 0 {
|
||||||
setDecision(a, remoteHost, false, a.defaultDecisionTimeout)
|
if isLiveMode {
|
||||||
a.next.ServeHTTP(rw, req)
|
cache.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||||
return
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
duration, err := time.ParseDuration(decisions[0].Duration)
|
duration, err := time.ParseDuration(decisions[0].Duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("failed to parse duration: %s", err))
|
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
setDecision(a, remoteHost, true, int64(duration.Seconds()))
|
if isLiveMode {
|
||||||
|
cache.SetDecision(remoteIP, true, int64(duration.Seconds()))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("handleNoStreamCache:banned")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStreamCache(a *Bouncer) {
|
func handleStreamCache(bouncer *Bouncer) {
|
||||||
logger(fmt.Sprintf("Start handleStreamCache with health=%v", a.crowdsecStreamHealthy))
|
|
||||||
// TODO clean properly on exit.
|
// TODO clean properly on exit.
|
||||||
var rawQuery string
|
// Instead of blocking the goroutine interval for all the secondary node,
|
||||||
var path string
|
// if the master service is shut down, other goroutine can take the lead
|
||||||
if a.crowdsecMode == aloneMode {
|
// because updated routine information is in the cache
|
||||||
rawQuery = ""
|
_, err := cache.GetDecision(cacheTimeoutKey)
|
||||||
path = crowdsecCapiDecisions
|
if err == nil {
|
||||||
} else {
|
logger.Debug("handleStreamCache:alreadyUpdated")
|
||||||
rawQuery = fmt.Sprintf("startup=%t", !a.crowdsecStreamHealthy)
|
return
|
||||||
path = crowdsecLapiStreamRoute
|
|
||||||
}
|
}
|
||||||
|
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
||||||
streamRouteURL := url.URL{
|
streamRouteURL := url.URL{
|
||||||
Scheme: a.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: a.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: path,
|
Path: crowdsecLapiStreamRoute,
|
||||||
RawQuery: rawQuery,
|
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
|
||||||
}
|
}
|
||||||
body := crowdsecQuery(a, streamRouteURL.String(), false)
|
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
|
||||||
var stream Stream
|
|
||||||
err := json.Unmarshal(body, &stream)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("error while parsing body: %s", err))
|
logger.Error(err.Error())
|
||||||
a.crowdsecStreamHealthy = false
|
isCrowdsecStreamHealthy = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var stream Stream
|
||||||
|
err = json.Unmarshal(body, &stream)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("handleStreamCache:parsingBody %s", err.Error()))
|
||||||
|
isCrowdsecStreamHealthy = false
|
||||||
return
|
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 {
|
||||||
setDecision(a, decision.Value, true, int64(duration.Seconds()))
|
cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, decision := range stream.Deleted {
|
for _, decision := range stream.Deleted {
|
||||||
a.cache.Del(decision.Value)
|
cache.DeleteDecision(decision.Value)
|
||||||
}
|
}
|
||||||
a.crowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getToken(a *Bouncer) {
|
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
|
||||||
loginURL := url.URL{
|
|
||||||
Scheme: a.crowdsecScheme,
|
|
||||||
Host: a.crowdsecHost,
|
|
||||||
Path: crowdsecCapiLogin,
|
|
||||||
}
|
|
||||||
body := crowdsecQuery(a, loginURL.String(), true)
|
|
||||||
var login Login
|
|
||||||
err := json.Unmarshal(body, &login)
|
|
||||||
if err != nil {
|
|
||||||
logger(fmt.Sprintf("error while parsing body: %s", err))
|
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if login.Code == 200 && len(login.Token) > 0 {
|
|
||||||
a.crowdsecKey = login.Token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func crowdsecQuery(a *Bouncer, stringURL string, isPost bool) []byte {
|
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
if isPost {
|
|
||||||
data := []byte(fmt.Sprintf(
|
|
||||||
`{"machine_id": "%v","password": "%v","scenarios": ["%v"]}`,
|
|
||||||
a.crowdsecLogin,
|
|
||||||
a.crowdsecPwd,
|
|
||||||
strings.Join(a.crowdsecScenarios, `","`),
|
|
||||||
))
|
|
||||||
req, _ = http.NewRequest(http.MethodPost, stringURL, bytes.NewBuffer(data))
|
|
||||||
} else {
|
|
||||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||||
}
|
req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey)
|
||||||
if a.crowdsecMode == aloneMode {
|
res, err := bouncer.client.Do(req)
|
||||||
req.Header.Add(crowdsecCapiHeader, a.crowdsecKey)
|
|
||||||
} else {
|
|
||||||
req.Header.Add(crowdsecLapiHeader, a.crowdsecKey)
|
|
||||||
}
|
|
||||||
res, err := a.client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger(fmt.Sprintf("error while fetching %v: %s", stringURL, err))
|
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if res.StatusCode == http.StatusUnauthorized && a.crowdsecMode == aloneMode {
|
|
||||||
oldToken := a.crowdsecKey
|
|
||||||
getToken(a)
|
|
||||||
if oldToken == a.crowdsecKey {
|
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return crowdsecQuery(a, stringURL, false)
|
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
logger(fmt.Sprintf("error while fetching %v, status code: %d", stringURL, res.StatusCode))
|
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
defer func(body io.ReadCloser) {
|
defer func() {
|
||||||
err = body.Close()
|
if err = res.Body.Close(); err != nil {
|
||||||
if err != nil {
|
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
|
||||||
logger(fmt.Sprintf("failed to close body reader: %s", err))
|
|
||||||
}
|
}
|
||||||
}(res.Body)
|
}()
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
|
||||||
logger(fmt.Sprintf("error while reading body: %s", err))
|
|
||||||
a.crowdsecStreamHealthy = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateParams(config *Config) error {
|
|
||||||
var requiredStrings map[string]string
|
|
||||||
if config.CrowdsecMode == aloneMode {
|
|
||||||
requiredStrings = map[string]string{
|
|
||||||
"CrowdsecCapiLogin": config.CrowdsecLapiScheme,
|
|
||||||
"CrowdsecCapiPwd": config.CrowdsecLapiHost,
|
|
||||||
}
|
|
||||||
for _, val := range config.CrowdsecCapiScenarios {
|
|
||||||
if len(val) == 0 {
|
|
||||||
return fmt.Errorf("CrowdsecCapiScenarios: one or more scenario are empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.UpdateIntervalSeconds = 7200
|
|
||||||
config.CrowdsecLapiKey = ""
|
|
||||||
config.CrowdsecLapiScheme = "https"
|
|
||||||
config.CrowdsecLapiHost = "api.crowdsec.net"
|
|
||||||
} else {
|
|
||||||
requiredStrings = map[string]string{
|
|
||||||
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
|
|
||||||
"CrowdsecLapiHost": config.CrowdsecLapiHost,
|
|
||||||
"CrowdsecLapiKey": config.CrowdsecLapiKey,
|
|
||||||
"CrowdsecMode": config.CrowdsecMode,
|
|
||||||
}
|
|
||||||
requiredInt := map[string]int64{
|
|
||||||
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
|
|
||||||
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
|
|
||||||
}
|
|
||||||
for key, val := range requiredInt {
|
|
||||||
if val < 1 {
|
|
||||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for key, val := range requiredStrings {
|
|
||||||
if len(val) == 0 {
|
|
||||||
return fmt.Errorf("%v: cannot be empty", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !contains([]string{noneMode, liveMode, streamMode, aloneMode}, config.CrowdsecMode) {
|
|
||||||
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
|
|
||||||
}
|
|
||||||
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
|
|
||||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
|
||||||
}
|
|
||||||
testURL := url.URL{
|
|
||||||
Scheme: config.CrowdsecLapiScheme,
|
|
||||||
Host: config.CrowdsecLapiHost,
|
|
||||||
}
|
|
||||||
_, err := http.NewRequest(http.MethodGet, testURL.String(), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
return nil, fmt.Errorf("crowdsecQuery:readBody %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+154
-7
@@ -1,28 +1,30 @@
|
|||||||
package crowdsec_bouncer_traefik_plugin_test
|
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
crowdsec_bouncer_traefik_plugin "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||||
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCrowdSec(t *testing.T) {
|
func TestServeHTTP(t *testing.T) {
|
||||||
cfg := crowdsec_bouncer_traefik_plugin.CreateConfig()
|
cfg := CreateConfig()
|
||||||
cfg.CrowdsecLapiKey = "caca"
|
cfg.CrowdsecLapiKey = "test"
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
|
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
|
||||||
|
|
||||||
handler, err := crowdsec_bouncer_traefik_plugin.New(ctx, next, cfg, "demo-plugin")
|
handler, err := New(ctx, next, cfg, "demo-plugin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -30,3 +32,148 @@ func TestCrowdSec(t *testing.T) {
|
|||||||
|
|
||||||
handler.ServeHTTP(recorder, req)
|
handler.ServeHTTP(recorder, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context //nolint:containedctx
|
||||||
|
next http.Handler
|
||||||
|
config *configuration.Config
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want http.Handler
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := New(tt.args.ctx, tt.args.next, tt.args.config, tt.args.name)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBouncer_ServeHTTP(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
next http.Handler
|
||||||
|
name string
|
||||||
|
template *template.Template
|
||||||
|
enabled bool
|
||||||
|
crowdsecScheme string
|
||||||
|
crowdsecHost string
|
||||||
|
crowdsecKey string
|
||||||
|
crowdsecMode string
|
||||||
|
updateInterval int64
|
||||||
|
defaultDecisionTimeout int64
|
||||||
|
customHeader string
|
||||||
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
rw http.ResponseWriter
|
||||||
|
req *http.Request
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
bouncer := &Bouncer{
|
||||||
|
next: tt.fields.next,
|
||||||
|
name: tt.fields.name,
|
||||||
|
template: tt.fields.template,
|
||||||
|
enabled: tt.fields.enabled,
|
||||||
|
crowdsecScheme: tt.fields.crowdsecScheme,
|
||||||
|
crowdsecHost: tt.fields.crowdsecHost,
|
||||||
|
crowdsecKey: tt.fields.crowdsecKey,
|
||||||
|
crowdsecMode: tt.fields.crowdsecMode,
|
||||||
|
updateInterval: tt.fields.updateInterval,
|
||||||
|
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
|
||||||
|
customHeader: tt.fields.customHeader,
|
||||||
|
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
||||||
|
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
||||||
|
client: tt.fields.client,
|
||||||
|
}
|
||||||
|
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_handleNoStreamCache(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
bouncer *Bouncer
|
||||||
|
remoteIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := handleNoStreamCache(tt.args.bouncer, tt.args.remoteIP); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("handleNoStreamCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_handleStreamCache(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
bouncer *Bouncer
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
handleStreamCache(tt.args.bouncer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_crowdsecQuery(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
bouncer *Bouncer
|
||||||
|
stringURL string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("crowdsecQuery() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+34
-31
@@ -2,10 +2,11 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.8.7"
|
image: "traefik:v2.9.4"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
# - "--log.level=DEBUG"
|
- "--log.level=DEBUG"
|
||||||
- "--accesslog"
|
- "--accesslog"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
@@ -16,55 +17,57 @@ services:
|
|||||||
- "--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:/var/log/traefik
|
- logs-local:/var/log/traefik
|
||||||
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapilogin=3829a6c9870e4726a377d8951ebb64a1m8psGvMaq1ykJ3zX"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapipwd=Q5pgN8bRNInHGdx6QCksdPOJVLeLQ7ipJntSeuP3r8088zXzRVs4G8liXAKfI1k6"
|
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/http-backdoors-attempts,baudneo/zoneminder-bf"
|
|
||||||
ports:
|
ports:
|
||||||
- 8000:80
|
- 80:80
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- crowdsec
|
- crowdsec
|
||||||
|
|
||||||
whoami1:
|
whoami-foo:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service1"
|
container_name: "simple-service-foo"
|
||||||
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
|
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
||||||
- "traefik.http.routers.whoami.entrypoints=web"
|
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
- "traefik.http.routers.whoami.middlewares=crowdsec@docker"
|
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
||||||
- "traefik.http.services.whoami.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:
|
whoami2:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service2"
|
container_name: "simple-service-bar"
|
||||||
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
|
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
|
||||||
- "traefik.http.routers.whoami.entrypoints=web"
|
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||||
- "traefik.http.routers.whoami.middlewares=crowdsec@docker"
|
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
||||||
- "traefik.http.services.whoami.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-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
command: rm -rf /etc/crowdsec/acquis.yaml
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
COLLECTIONS: crowdsecurity/traefik
|
COLLECTIONS: crowdsecurity/traefik
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5
|
BOUNCER_KEY_TRAEFIK_1: 40796d93c2958f9e58345514e67740e5
|
||||||
|
BOUNCER_KEY_TRAEFIK_2: 44c36dac5c4140af9f06f397508e82c7
|
||||||
volumes:
|
volumes:
|
||||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||||
- logs:/var/log/traefik:ro
|
- logs-local:/var/log/traefik:ro
|
||||||
- crowdsec-db:/var/lib/crowdsec/data/
|
- crowdsec-db-local:/var/lib/crowdsec/data/
|
||||||
- crowdsec-config:/etc/crowdsec/
|
- crowdsec-config-local:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
volumes:
|
volumes:
|
||||||
logs:
|
logs-local:
|
||||||
crowdsec-db:
|
crowdsec-db-local:
|
||||||
crowdsec-config:
|
crowdsec-config-local:
|
||||||
|
|||||||
+43
-11
@@ -2,10 +2,10 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: "traefik:v2.8.7"
|
image: "traefik:v2.9.4"
|
||||||
container_name: "traefik"
|
container_name: "traefik"
|
||||||
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
# - "--log.level=DEBUG"
|
|
||||||
- "--accesslog"
|
- "--accesslog"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
@@ -14,6 +14,7 @@ services:
|
|||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
|
|
||||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
- "--experimental.plugins.bouncer.version=v1.1.3"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
- "logs:/var/log/traefik"
|
- "logs:/var/log/traefik"
|
||||||
@@ -23,30 +24,61 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- 'crowdsec'
|
- 'crowdsec'
|
||||||
|
|
||||||
whoami:
|
whoami1:
|
||||||
image: traefik/whoami
|
image: traefik/whoami
|
||||||
container_name: "simple-service"
|
container_name: "simple-service-foo"
|
||||||
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
|
# Definition of the router
|
||||||
- "traefik.http.routers.whoami.entrypoints=web"
|
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
|
||||||
- "traefik.http.routers.whoami.middlewares=crowdsec@docker"
|
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
|
# 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 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:
|
||||||
|
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"
|
||||||
|
# Definitin of the middleware
|
||||||
|
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
||||||
|
# crowdseclapikey must be unique to the middleware attached to the service
|
||||||
|
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
|
||||||
|
# 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"
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: crowdsecurity/crowdsec:v1.4.1
|
image: crowdsecurity/crowdsec:v1.4.1
|
||||||
container_name: "crowdsec"
|
container_name: "crowdsec"
|
||||||
command: rm -rf /etc/crowdsec/acquis.yaml
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
COLLECTIONS: crowdsecurity/traefik
|
COLLECTIONS: crowdsecurity/traefik
|
||||||
CUSTOM_HOSTNAME: crowdsec
|
CUSTOM_HOSTNAME: crowdsec
|
||||||
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
# We need to register one api key per service we will use
|
||||||
|
BOUNCER_KEY_TRAEFIK_1: 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
|
||||||
- crowdsec-db:/var/lib/crowdsec/data/
|
- crowdsec-db:/var/lib/crowdsec/data/
|
||||||
- crowdsec-config:/etc/crowdsec/
|
- crowdsec-config:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
logs:
|
logs:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
http:
|
||||||
|
# Add the router
|
||||||
|
routers:
|
||||||
|
router0:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
service: service-foo
|
||||||
|
rule: Path(`/foo`)
|
||||||
|
|
||||||
|
# Add the service
|
||||||
|
services:
|
||||||
|
service-foo:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://traefik/foo:80
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
cloudflare:
|
||||||
|
image: "traefik:v2.9.4"
|
||||||
|
container_name: "cloudflare"
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
# - "--log.level=DEBUG"
|
||||||
|
- "--accesslog"
|
||||||
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--entrypoints.web.address=:80"
|
||||||
|
- "--providers.file.filename=/cloud.yaml"
|
||||||
|
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./cloudflare-exemple.yaml:/cloud.yaml:ro
|
||||||
|
- logs-cloudflare:/var/log/traefik
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 8080:8080
|
||||||
|
|
||||||
|
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"
|
||||||
|
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||||
|
|
||||||
|
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
- "--experimental.plugins.bouncer.version=v1.1.3"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-traefik:/var/log/traefik
|
||||||
|
ports:
|
||||||
|
- 90:80
|
||||||
|
- 9080: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=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"
|
||||||
|
# Definitin 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.crowdsecmode=live"
|
||||||
|
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
|
||||||
|
whoami2:
|
||||||
|
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"
|
||||||
|
# Definitin 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.crowdsecmode=live"
|
||||||
|
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||||
|
- "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-cloudflare:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-cloudflare:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-cloudflare:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-traefik:
|
||||||
|
logs-cloudflare:
|
||||||
|
crowdsec-db-cloudflare:
|
||||||
|
crowdsec-config-cloudflare:
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.vagrant/
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
### Install vagrant
|
||||||
|
|
||||||
|
##### On linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.asc
|
||||||
|
echo "deb [ signed-by=/usr/share/keyrings/hashicorp-archive-keyring.asc ] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install vagrant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install libvirt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y qemu-kvm virt-manager libvirt-daemon-system virtinst libvirt-clients bridge-utils
|
||||||
|
sudo systemctl enable --now libvirtd
|
||||||
|
sudo systemctl start libvirtd
|
||||||
|
sudo usermod -aG kvm $USER
|
||||||
|
sudo usermod -aG libvirt $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install the plugin vagrant-libvirt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vagrant plugin install vagrant-libvirt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Start the VM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo vagrant up --provider=libvirt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Destroy the VM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo vagrant destroy -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SSH in the VM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo vagrant ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
Traefik is installed as a systemd service.
|
||||||
|
It is configured with the dashboard activated and listening on port 8081 and port 80 for the web
|
||||||
|
|
||||||
|
Crowdsec is started and listening on port 8080.
|
||||||
|
Certificates are generated on the provision step of vagrant.
|
||||||
|
|
||||||
|
Whoami is installed as a systemd service.
|
||||||
|
It is configured to listen on port 9000.
|
||||||
|
|
||||||
|
Whoami is accessible from traefik on port 80 at any domain and path
|
||||||
|
|
||||||
|
For example: curl http://localhost:80/test
|
||||||
|
|
||||||
|
The Plugin / Bouncer use certificates to validate the server certificates and authenticates with the Crowdsec local api.
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
# -*- mode: ruby -*-
|
||||||
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
|
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||||
|
# configures the configuration version (we support older styles for
|
||||||
|
# backwards compatibility). Please don't change it unless you know what
|
||||||
|
# you're doing.
|
||||||
|
Vagrant.configure("2") do |config|
|
||||||
|
# The most common configuration options are documented and commented below.
|
||||||
|
# For a complete reference, please see the online documentation at
|
||||||
|
# https://docs.vagrantup.com.
|
||||||
|
|
||||||
|
# Every Vagrant development environment requires a box. You can search for
|
||||||
|
# boxes at https://vagrantcloud.com/search.
|
||||||
|
config.vm.box = "generic/debian11"
|
||||||
|
|
||||||
|
# Disable automatic box update checking. If you disable this, then
|
||||||
|
# boxes will only be checked for updates when the user runs
|
||||||
|
# `vagrant box outdated`. This is not recommended.
|
||||||
|
# config.vm.box_check_update = false
|
||||||
|
|
||||||
|
# Create a forwarded port mapping which allows access to a specific port
|
||||||
|
# within the machine from a port on the host machine. In the example below,
|
||||||
|
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||||
|
# NOTE: This will enable public access to the opened port
|
||||||
|
# config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||||
|
|
||||||
|
# Create a forwarded port mapping which allows access to a specific port
|
||||||
|
# within the machine from a port on the host machine and only allow access
|
||||||
|
# via 127.0.0.1 to disable public access
|
||||||
|
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
|
||||||
|
config.vm.network "forwarded_port", guest: 8081, host: 8081
|
||||||
|
config.vm.network "forwarded_port", guest: 80, host: 80
|
||||||
|
|
||||||
|
|
||||||
|
# Create a private network, which allows host-only access to the machine
|
||||||
|
# using a specific IP.
|
||||||
|
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||||
|
|
||||||
|
# Create a public network, which generally matched to bridged network.
|
||||||
|
# Bridged networks make the machine appear as another physical device on
|
||||||
|
# your network.
|
||||||
|
# config.vm.network "public_network"
|
||||||
|
|
||||||
|
# Share an additional folder to the guest VM. The first argument is
|
||||||
|
# the path on the host to the actual folder. The second argument is
|
||||||
|
# the path on the guest to mount the folder. And the optional third
|
||||||
|
# argument is a set of non-required options.
|
||||||
|
# Provider-specific configuration so you can fine-tune various
|
||||||
|
# backing providers for Vagrant. These expose provider-specific options.
|
||||||
|
# Example for VirtualBox:
|
||||||
|
#
|
||||||
|
|
||||||
|
config.vm.provider "libvirt" do |lv|
|
||||||
|
graphics_type = "none"
|
||||||
|
lv.cpus = 1
|
||||||
|
# lv.vm.network :private_network, :ip => "10.20.30.40"
|
||||||
|
# Customize the amount of memory on the VM:
|
||||||
|
lv.memory = "2048"
|
||||||
|
end
|
||||||
|
|
||||||
|
# config.vm.provider "virtualbox" do |vb|
|
||||||
|
# vb.gui = false
|
||||||
|
# vb.cpus = 1
|
||||||
|
# # vb.vm.network :private_network, :ip => "10.20.30.40"
|
||||||
|
# # Customize the amount of memory on the VM:
|
||||||
|
# vb.memory = "2048"
|
||||||
|
# end
|
||||||
|
config.vm.provision "file", source: "./files", destination: "/home/vagrant/vagrant_data"
|
||||||
|
|
||||||
|
config.vm.provision "shell", path: "scripts/install_traefik.sh"
|
||||||
|
config.vm.provision "shell", path: "scripts/configure_traefik.sh"
|
||||||
|
|
||||||
|
config.vm.provision "shell", path: "scripts/install_whoami.sh"
|
||||||
|
|
||||||
|
config.vm.provision "shell", path: "scripts/install_crowdsec.sh"
|
||||||
|
|
||||||
|
config.vm.provision "shell", path: "scripts/configure_crowdsec_certs.sh"
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"CN": "myagent",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "agent-ou",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"CN": "whoami",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "bouncer-ou",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"CN": "CrowdSec Test CA",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "Crowdsec",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"CN": "CrowdSec Test CA Intermediate",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "Crowdsec Intermediate",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ca": {
|
||||||
|
"expiry": "42720h"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"signing": {
|
||||||
|
"default": {
|
||||||
|
"expiry": "8760h"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"intermediate_ca": {
|
||||||
|
"usages": [
|
||||||
|
"signing",
|
||||||
|
"digital signature",
|
||||||
|
"key encipherment",
|
||||||
|
"cert sign",
|
||||||
|
"crl sign",
|
||||||
|
"server auth",
|
||||||
|
"client auth"
|
||||||
|
],
|
||||||
|
"expiry": "8760h",
|
||||||
|
"ca_constraint": {
|
||||||
|
"is_ca": true,
|
||||||
|
"max_path_len": 0,
|
||||||
|
"max_path_len_zero": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"usages": [
|
||||||
|
"signing",
|
||||||
|
"digital signing",
|
||||||
|
"key encipherment",
|
||||||
|
"server auth"
|
||||||
|
],
|
||||||
|
"expiry": "8760h"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"usages": [
|
||||||
|
"signing",
|
||||||
|
"digital signature",
|
||||||
|
"key encipherment",
|
||||||
|
"client auth"
|
||||||
|
],
|
||||||
|
"expiry": "8760h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"CN": "localhost",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "Crowdsec Server",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hosts": [
|
||||||
|
"127.0.0.1",
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
common:
|
||||||
|
daemonize: true
|
||||||
|
pid_dir: /var/run/
|
||||||
|
log_media: file
|
||||||
|
log_level: debug
|
||||||
|
log_dir: /var/log/
|
||||||
|
log_max_size: 20
|
||||||
|
compress_logs: true
|
||||||
|
log_max_files: 10
|
||||||
|
working_dir: .
|
||||||
|
config_paths:
|
||||||
|
config_dir: /etc/crowdsec/
|
||||||
|
data_dir: /var/lib/crowdsec/data/
|
||||||
|
simulation_path: /etc/crowdsec/simulation.yaml
|
||||||
|
hub_dir: /etc/crowdsec/hub/
|
||||||
|
index_path: /etc/crowdsec/hub/.index.json
|
||||||
|
notification_dir: /etc/crowdsec/notifications/
|
||||||
|
plugin_dir: /usr/lib/crowdsec/plugins/
|
||||||
|
crowdsec_service:
|
||||||
|
acquisition_path: /etc/crowdsec/acquis.yaml
|
||||||
|
acquisition_dir: /etc/crowdsec/acquis.d
|
||||||
|
parser_routines: 1
|
||||||
|
cscli:
|
||||||
|
output: human
|
||||||
|
color: auto
|
||||||
|
db_config:
|
||||||
|
log_level: info
|
||||||
|
type: sqlite
|
||||||
|
db_path: /var/lib/crowdsec/data/crowdsec.db
|
||||||
|
#max_open_conns: 100
|
||||||
|
#user:
|
||||||
|
#password:
|
||||||
|
#db_name:
|
||||||
|
#host:
|
||||||
|
#port:
|
||||||
|
flush:
|
||||||
|
max_items: 5000
|
||||||
|
max_age: 7d
|
||||||
|
plugin_config:
|
||||||
|
user: nobody # plugin process would be ran on behalf of this user
|
||||||
|
group: nogroup # plugin process would be ran on behalf of this group
|
||||||
|
api:
|
||||||
|
client:
|
||||||
|
insecure_skip_verify: false
|
||||||
|
credentials_path: /etc/crowdsec/local_api_credentials.yaml
|
||||||
|
server:
|
||||||
|
log_level: debug
|
||||||
|
listen_uri: 127.0.0.1:8080
|
||||||
|
profiles_path: /etc/crowdsec/profiles.yaml
|
||||||
|
console_path: /etc/crowdsec/console.yaml
|
||||||
|
online_client: # Central API credentials (to push signals and receive bad IPs)
|
||||||
|
credentials_path: /etc/crowdsec/online_api_credentials.yaml
|
||||||
|
trusted_ips: # IP ranges, or IPs which can have admin API access
|
||||||
|
- 127.0.0.1
|
||||||
|
tls:
|
||||||
|
cert_file: /etc/crowdsec/certs/server.pem #Server side cert
|
||||||
|
key_file: /etc/crowdsec/certs/server-key.pem #Server side key
|
||||||
|
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA used to verify the client certs
|
||||||
|
bouncers_allowed_ou: #OU allowed for bouncers
|
||||||
|
- bouncer-ou
|
||||||
|
agents_allowed_ou: #OU allowed for agents
|
||||||
|
- agent-ou
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
url: https://localhost:8080
|
||||||
|
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA to trust the server certificate
|
||||||
|
key_path: /etc/crowdsec/certs/agent-key.pem #Client key
|
||||||
|
cert_path: /etc/crowdsec/certs/agent.pem #Client cert
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
to-whoami-http-service:
|
||||||
|
rule: "PathPrefix(`/`)"
|
||||||
|
service: whoami-service
|
||||||
|
middlewares:
|
||||||
|
- "crowdsec-whoami"
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
|
||||||
|
services:
|
||||||
|
whoami-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://localhost:9000/"
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
crowdsec-whoami:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
enabled: true
|
||||||
|
crowdseclapikey: whoami-demo
|
||||||
|
updateintervalseconds: 60
|
||||||
|
crowdsecmode: live
|
||||||
|
loglevel: "DEBUG"
|
||||||
|
crowdsecLapiScheme: https
|
||||||
|
crowdsecLapiHost: localhost:8080
|
||||||
|
crowdsecLapiTLSInsecureVerify: false
|
||||||
|
crowdsecLapiTLSCertificateAuthorityFile: /etc/traefik/crowdsec-certs/inter.pem
|
||||||
|
crowdsecLapiTLSCertificateBouncerFile: /etc/traefik/crowdsec-certs/bouncer.pem
|
||||||
|
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=traefik proxy
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target systemd-networkd-wait-online.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=on-abnormal
|
||||||
|
|
||||||
|
; User and group the process will run as.
|
||||||
|
User=traefik
|
||||||
|
Group=traefik
|
||||||
|
|
||||||
|
; Always set "-root" to something safe in case it gets forgotten in the traefikfile.
|
||||||
|
ExecStart=/usr/local/bin/traefik --configfile=/etc/traefik/traefik.yml
|
||||||
|
WorkingDirectory=/etc/traefik
|
||||||
|
; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
|
||||||
|
; Use private /tmp and /var/tmp, which are discarded after traefik stops.
|
||||||
|
PrivateTmp=true
|
||||||
|
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
|
||||||
|
PrivateDevices=false
|
||||||
|
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
|
||||||
|
ProtectHome=true
|
||||||
|
; Make /usr, /boot, /etc and possibly some more folders read-only.
|
||||||
|
;ProtectSystem=full
|
||||||
|
; … except /etc/ssl/traefik, because we want Letsencrypt-certificates there.
|
||||||
|
; This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
|
||||||
|
;ReadWriteDirectories=/etc/traefik/acme
|
||||||
|
;ReadWriteDirectories=/etc/traefik/plugins-storage
|
||||||
|
|
||||||
|
; The following additional security directives only work with systemd v229 or later.
|
||||||
|
; They further restrict privileges that can be gained by traefik. Uncomment if you like.
|
||||||
|
; Note that you may have to add capabilities required by any plugins in use.
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
################################################################
|
||||||
|
# Global configuration
|
||||||
|
################################################################
|
||||||
|
global:
|
||||||
|
checkNewVersion: false
|
||||||
|
sendAnonymousUsage: false
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# EntryPoints configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: :80
|
||||||
|
traefik:
|
||||||
|
address: :8081
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Provider file configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
providers:
|
||||||
|
file:
|
||||||
|
directory: "/etc/traefik/conf"
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Plugin configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
experimental:
|
||||||
|
plugins:
|
||||||
|
bouncer:
|
||||||
|
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
version: v1.1.5
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Certificate Resolver
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
serversTransport:
|
||||||
|
insecureSkipVerify: false
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Traefik logs configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Traefik logs
|
||||||
|
# Enabled by default and log to stdout
|
||||||
|
log:
|
||||||
|
filePath: /var/log/traefik/traefik.log
|
||||||
|
level: DEBUG
|
||||||
|
# format: json
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# Access logs configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable access logs
|
||||||
|
# By default it will write to stdout and produce logs in the textual
|
||||||
|
# Common Log Format (CLF), extended with additional fields.
|
||||||
|
accessLog:
|
||||||
|
# Sets the file path for the access log. If not specified, stdout will be used.
|
||||||
|
# Intermediate directories are created if necessary.
|
||||||
|
filePath: /var/log/traefik/access.log
|
||||||
|
fields:
|
||||||
|
defaultMode: keep
|
||||||
|
names:
|
||||||
|
ClientUsername: keep
|
||||||
|
headers:
|
||||||
|
defaultMode: keep
|
||||||
|
|
||||||
|
# Format is either "json" or "common".
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "common"
|
||||||
|
#
|
||||||
|
# format: json
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# API and dashboard configuration
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable API and dashboard
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
api:
|
||||||
|
# Enable the API in insecure mode
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: false
|
||||||
|
#
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
# Enabled Dashboard
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
dashboard: true
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=whoami web server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target systemd-networkd-wait-online.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=on-abnormal
|
||||||
|
|
||||||
|
; User and group the process will run as.
|
||||||
|
User=whoami
|
||||||
|
Group=whoami
|
||||||
|
|
||||||
|
; Always set "-root" to something safe in case it gets forgotten in the traefikfile.
|
||||||
|
ExecStart=/usr/local/bin/whoami --port=9000
|
||||||
|
|
||||||
|
; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
|
||||||
|
; Use private /tmp and /var/tmp, which are discarded after traefik stops.
|
||||||
|
PrivateTmp=true
|
||||||
|
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
|
||||||
|
PrivateDevices=false
|
||||||
|
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
|
||||||
|
ProtectHome=true
|
||||||
|
; Make /usr, /boot, /etc and possibly some more folders read-only.
|
||||||
|
ProtectSystem=full
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
basepath="/home/vagrant/vagrant_data/crowdsec/certs"
|
||||||
|
|
||||||
|
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
|
||||||
|
sudo mv cfssl /usr/local/bin
|
||||||
|
|
||||||
|
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}/cfssljson_${VNUMBER}_linux_amd64 -O cfssljson
|
||||||
|
chmod +x cfssljson
|
||||||
|
sudo mv cfssljson /usr/local/bin
|
||||||
|
cfssljson -version
|
||||||
|
|
||||||
|
mkdir -p /etc/crowdsec/certs
|
||||||
|
|
||||||
|
# Generate the CA
|
||||||
|
cfssl gencert --initca ${basepath}/ca.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/ca"
|
||||||
|
# Generate an intermediate certificate that will be used to sign the client certificates
|
||||||
|
cfssl gencert --initca ${basepath}/intermediate.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/inter"
|
||||||
|
cfssl sign -ca "/etc/crowdsec/certs/ca.pem" -ca-key "/etc/crowdsec/certs/ca-key.pem" -config ${basepath}/profiles.json -profile intermediate_ca "/etc/crowdsec/certs/inter.csr" 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/inter"
|
||||||
|
# Generate a server side certificate
|
||||||
|
cfssl gencert -ca "/etc/crowdsec/certs/inter.pem" -ca-key "/etc/crowdsec/certs/inter-key.pem" -config ${basepath}/profiles.json -profile=server ${basepath}/server.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/server"
|
||||||
|
# Generate a client certificate for the bouncer whoami
|
||||||
|
cfssl gencert -ca "/etc/crowdsec/certs/inter.pem" -ca-key "/etc/crowdsec/certs/inter-key.pem" -config ${basepath}/profiles.json -profile=client ${basepath}/bouncer.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/bouncer"
|
||||||
|
# Generate a client certificate for the agent
|
||||||
|
cfssl gencert -ca "/etc/crowdsec/certs/inter.pem" -ca-key "/etc/crowdsec/certs/inter-key.pem" -config ${basepath}/profiles.json -profile=client ${basepath}/agent.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/agent"
|
||||||
|
|
||||||
|
|
||||||
|
cp /home/vagrant/vagrant_data/crowdsec/config/config.yaml /etc/crowdsec/config.yaml.local
|
||||||
|
cp /home/vagrant/vagrant_data/crowdsec/config/local_api_credentials.yaml /etc/crowdsec/
|
||||||
|
chmod +r /etc/crowdsec/config.yaml
|
||||||
|
chmod +r /etc/crowdsec/local_api_credentials.yaml
|
||||||
|
|
||||||
|
systemctl restart crowdsec
|
||||||
|
|
||||||
|
mkdir /etc/traefik/crowdsec-certs
|
||||||
|
cp /etc/crowdsec/certs/inter.pem /etc/traefik/crowdsec-certs/inter.pem
|
||||||
|
cp /etc/crowdsec/certs/bouncer.pem /etc/traefik/crowdsec-certs/bouncer.pem
|
||||||
|
cp /etc/crowdsec/certs/bouncer-key.pem /etc/traefik/crowdsec-certs/bouncer-key.pem
|
||||||
|
chown -R traefik:traefik /etc/traefik/crowdsec-certs/
|
||||||
|
|
||||||
|
systemctl restart traefik
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
|
||||||
|
sudo cp /home/vagrant/vagrant_data/traefik/traefik.yml /etc/traefik/traefik.yml
|
||||||
|
sudo cp -a /home/vagrant/vagrant_data/traefik/conf /etc/traefik/
|
||||||
|
sudo chown -R traefik:traefik /etc/traefik
|
||||||
|
sudo mkdir /var/log/traefik
|
||||||
|
sudo chown -R traefik:traefik /var/log/traefik
|
||||||
|
|
||||||
|
sudo systemctl restart traefik.service
|
||||||
|
sudo systemctl status traefik.service
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
|
||||||
|
sudo apt install crowdsec -y
|
||||||
|
sudo cp /home/vagrant/vagrant_data/crowdsec/acquis.yaml /etc/crowdsec/acquis.yaml
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sudo apt-get update && apt-get install wget -y && apt-get upgrade -y
|
||||||
|
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.9.5/traefik_v2.9.5_linux_amd64.tar.gz"
|
||||||
|
tar -zxvf traefik.tar.gz
|
||||||
|
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
||||||
|
sudo mv ./traefik /usr/local/bin/
|
||||||
|
sudo chown root:root /usr/local/bin/traefik
|
||||||
|
sudo chmod 755 /usr/local/bin/traefik
|
||||||
|
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/traefik
|
||||||
|
|
||||||
|
sudo groupadd -g 321 traefik
|
||||||
|
sudo useradd \
|
||||||
|
-g traefik --no-user-group \
|
||||||
|
--home-dir /var/www --no-create-home \
|
||||||
|
--shell /usr/sbin/nologin \
|
||||||
|
--system --uid 321 traefik
|
||||||
|
|
||||||
|
sudo mkdir /etc/traefik
|
||||||
|
sudo mkdir /etc/traefik/acme
|
||||||
|
sudo mkdir /etc/traefik/plugins-storage
|
||||||
|
sudo chown -R root:root /etc/traefik
|
||||||
|
sudo chown -R traefik:traefik /etc/traefik/acme
|
||||||
|
sudo chown -R traefik:traefik /etc/traefik/plugins-storage
|
||||||
|
|
||||||
|
sudo cp /home/vagrant/vagrant_data/traefik/traefik.service /etc/systemd/system/
|
||||||
|
sudo chown root:root /etc/systemd/system/traefik.service
|
||||||
|
sudo chmod 644 /etc/systemd/system/traefik.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl start traefik.service
|
||||||
|
sudo systemctl enable traefik.service
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sudo apt-get update && apt-get install wget -y
|
||||||
|
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.8.7/whoami_v1.8.7_linux_amd64.tar.gz"
|
||||||
|
tar -zxvf whoami.tar.gz
|
||||||
|
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
||||||
|
sudo mv ./whoami /usr/local/bin/
|
||||||
|
sudo chown root:root /usr/local/bin/whoami
|
||||||
|
sudo chmod 755 /usr/local/bin/whoami
|
||||||
|
|
||||||
|
sudo groupadd -g 322 whoami
|
||||||
|
sudo useradd \
|
||||||
|
-g whoami --no-user-group \
|
||||||
|
--home-dir /var/www --no-create-home \
|
||||||
|
--shell /usr/sbin/nologin \
|
||||||
|
--system --uid 322 whoami
|
||||||
|
|
||||||
|
sudo cp /home/vagrant/vagrant_data/whoami.service /etc/systemd/system/
|
||||||
|
sudo chown root:root /etc/systemd/system/whoami.service
|
||||||
|
sudo chmod 644 /etc/systemd/system/whoami.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl start whoami.service
|
||||||
|
sudo systemctl enable whoami.service
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
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:
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y curl wget
|
||||||
|
RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && VNUMBER=${VERSION#"v"} && wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl && chmod +x cfssl && mv cfssl /usr/local/bin
|
||||||
|
|
||||||
|
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}/cfssljson_${VNUMBER}_linux_amd64 -O cfssljson && \
|
||||||
|
chmod +x cfssljson && \
|
||||||
|
mv cfssljson /usr/local/bin && \
|
||||||
|
cfssljson -version
|
||||||
|
|
||||||
|
COPY gencerts.sh /gencerts.sh
|
||||||
|
RUN chmod +x /gencerts.sh
|
||||||
|
|
||||||
|
CMD [ "/gencerts.sh" ]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
common:
|
||||||
|
daemonize: false
|
||||||
|
pid_dir: /var/run/
|
||||||
|
log_media: stdout
|
||||||
|
log_level: info
|
||||||
|
log_dir: /var/log/
|
||||||
|
working_dir: .
|
||||||
|
config_paths:
|
||||||
|
config_dir: /etc/crowdsec/
|
||||||
|
data_dir: /var/lib/crowdsec/data/
|
||||||
|
simulation_path: /etc/crowdsec/simulation.yaml
|
||||||
|
hub_dir: /etc/crowdsec/hub/
|
||||||
|
index_path: /etc/crowdsec/hub/.index.json
|
||||||
|
notification_dir: /etc/crowdsec/notifications/
|
||||||
|
plugin_dir: /usr/local/lib/crowdsec/plugins/
|
||||||
|
crowdsec_service:
|
||||||
|
acquisition_path: /etc/crowdsec/acquis.yaml
|
||||||
|
parser_routines: 1
|
||||||
|
plugin_config:
|
||||||
|
user: nobody
|
||||||
|
group: nobody
|
||||||
|
cscli:
|
||||||
|
output: human
|
||||||
|
db_config:
|
||||||
|
log_level: info
|
||||||
|
type: sqlite
|
||||||
|
db_path: /var/lib/crowdsec/data/crowdsec.db
|
||||||
|
#user:
|
||||||
|
#password:
|
||||||
|
#db_name:
|
||||||
|
#host:
|
||||||
|
#port:
|
||||||
|
flush:
|
||||||
|
max_items: 5000
|
||||||
|
max_age: 7d
|
||||||
|
api:
|
||||||
|
client:
|
||||||
|
insecure_skip_verify: false
|
||||||
|
credentials_path: /etc/crowdsec/local_api_credentials.yaml
|
||||||
|
server:
|
||||||
|
log_level: info
|
||||||
|
listen_uri: 0.0.0.0:8080
|
||||||
|
profiles_path: /etc/crowdsec/profiles.yaml
|
||||||
|
trusted_ips: # IP ranges, or IPs which can have admin API access
|
||||||
|
- 127.0.0.1
|
||||||
|
- ::1
|
||||||
|
online_client: # Central API credentials (to push signals and receive bad IPs)
|
||||||
|
#credentials_path: /etc/crowdsec/online_api_credentials.yaml
|
||||||
|
tls:
|
||||||
|
cert_file: /etc/crowdsec/certs/server.pem #Server side cert
|
||||||
|
key_file: /etc/crowdsec/certs/server-key.pem #Server side key
|
||||||
|
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA used to verify the client certs
|
||||||
|
bouncers_allowed_ou: #OU allowed for bouncers
|
||||||
|
- bouncer-ou
|
||||||
|
agents_allowed_ou: #OU allowed for agents
|
||||||
|
- agent-ou
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
enabled: true
|
||||||
|
level: full
|
||||||
|
listen_addr: 0.0.0.0
|
||||||
|
listen_port: 6060
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
url: https://localhost:8080
|
||||||
|
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA to trust the server certificate
|
||||||
|
key_path: /etc/crowdsec/certs/agent-key.pem #Client key
|
||||||
|
cert_path: /etc/crowdsec/certs/agent.pem #Client cert
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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.5"
|
||||||
|
- "--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:
|
||||||
|
- 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:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
stdout=/out/res.log
|
||||||
|
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
|
||||||
|
cfssl gencert --initca /in/intermediate.json 2>${stdout} | cfssljson --bare "/out/inter" && \
|
||||||
|
cfssl sign -ca "/out/ca.pem" -ca-key "/out/ca-key.pem" -config /in/profiles.json -profile intermediate_ca "/out/inter.csr" 2>${stdout} | cfssljson --bare "/out/inter" && \
|
||||||
|
# Generate a server side certificate
|
||||||
|
cfssl gencert -ca "/out/inter.pem" -ca-key "/out/inter-key.pem" -config /in/profiles.json -profile=server /in/server.json 2>${stdout} | cfssljson --bare "/out/server" && \
|
||||||
|
# Generate a client certificate for the bouncer whoami
|
||||||
|
cfssl gencert -ca "/out/inter.pem" -ca-key "/out/inter-key.pem" -config /in/profiles.json -profile=client /in/bouncer.json 2>${stdout} | cfssljson --bare "/out/bouncer" && \
|
||||||
|
# Generate a client certificate for the agent
|
||||||
|
cfssl gencert -ca "/out/inter.pem" -ca-key "/out/inter-key.pem" -config /in/profiles.json -profile=client /in/agent.json 2>${stdout} | cfssljson --bare "/out/agent"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"CN": "myagent",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "agent-ou",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"CN": "crowdsec",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "bouncer-ou",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hosts": [
|
||||||
|
"127.0.0.1",
|
||||||
|
"localhost",
|
||||||
|
"crowdsec"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"CN": "CrowdSec Test CA",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "Crowdsec",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"CN": "CrowdSec Test CA Intermediate",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "Crowdsec Intermediate",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ca": {
|
||||||
|
"expiry": "42720h"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"signing": {
|
||||||
|
"default": {
|
||||||
|
"expiry": "8760h"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"intermediate_ca": {
|
||||||
|
"usages": [
|
||||||
|
"signing",
|
||||||
|
"digital signature",
|
||||||
|
"key encipherment",
|
||||||
|
"cert sign",
|
||||||
|
"crl sign",
|
||||||
|
"server auth",
|
||||||
|
"client auth"
|
||||||
|
],
|
||||||
|
"expiry": "8760h",
|
||||||
|
"ca_constraint": {
|
||||||
|
"is_ca": true,
|
||||||
|
"max_path_len": 0,
|
||||||
|
"max_path_len_zero": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"usages": [
|
||||||
|
"signing",
|
||||||
|
"digital signing",
|
||||||
|
"key encipherment",
|
||||||
|
"server auth"
|
||||||
|
],
|
||||||
|
"expiry": "8760h"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"usages": [
|
||||||
|
"signing",
|
||||||
|
"digital signature",
|
||||||
|
"key encipherment",
|
||||||
|
"client auth"
|
||||||
|
],
|
||||||
|
"expiry": "8760h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"CN": "crowdsec",
|
||||||
|
"key": {
|
||||||
|
"algo": "rsa",
|
||||||
|
"size": 2048
|
||||||
|
},
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"C": "FR",
|
||||||
|
"L": "Paris",
|
||||||
|
"O": "Crowdsec",
|
||||||
|
"OU": "Crowdsec Server",
|
||||||
|
"ST": "France"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hosts": [
|
||||||
|
"127.0.0.1",
|
||||||
|
"localhost",
|
||||||
|
"crowdsec"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
filenames:
|
||||||
|
- /var/log/traefik/access.log
|
||||||
|
labels:
|
||||||
|
type: traefik
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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.3"
|
||||||
|
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- logs-trustedips:/var/log/traefik
|
||||||
|
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
ports:
|
||||||
|
- 80: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=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"
|
||||||
|
# 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-foo.plugin.bouncer.loglevel=DEBUG"
|
||||||
|
|
||||||
|
whoami2:
|
||||||
|
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"
|
||||||
|
# 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-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-trustedips:/var/log/traefik:ro
|
||||||
|
- crowdsec-db-trustedips:/var/lib/crowdsec/data/
|
||||||
|
- crowdsec-config-trustedips:/etc/crowdsec/
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs-trustedips:
|
||||||
|
crowdsec-db-trustedips:
|
||||||
|
crowdsec-config-trustedips:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||||
|
|
||||||
go 1.17
|
go 1.19
|
||||||
|
|
||||||
require github.com/leprosus/golang-ttl-map v1.1.7
|
require github.com/leprosus/golang-ttl-map v1.1.7
|
||||||
|
|||||||
Vendored
+107
@@ -0,0 +1,107 @@
|
|||||||
|
// Package cache implements utility routines for manipulating cache.
|
||||||
|
// It supports currently local file and redis cache.
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
ttl_map "github.com/leprosus/golang-ttl-map"
|
||||||
|
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
|
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cacheBannedValue = "t"
|
||||||
|
cacheNoBannedValue = "f"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:gochecknoglobals
|
||||||
|
var (
|
||||||
|
cache = ttl_map.New()
|
||||||
|
redis simpleredis.SimpleRedis
|
||||||
|
redisEnabled = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileSystem Cache
|
||||||
|
|
||||||
|
func getDecisionLocalCache(clientIP string) (bool, error) {
|
||||||
|
banned, isCached := cache.Get(clientIP)
|
||||||
|
bannedString, isValid := banned.(string)
|
||||||
|
if isCached && isValid && len(bannedString) > 0 {
|
||||||
|
return bannedString == cacheBannedValue, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("cache:miss")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDecisionLocalCache(clientIP string, value string, duration int64) {
|
||||||
|
cache.Set(clientIP, value, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDecisionLocalCache(clientIP string) {
|
||||||
|
cache.Del(clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis Cache
|
||||||
|
|
||||||
|
func getDecisionRedisCache(clientIP string) (bool, error) {
|
||||||
|
banned, err := redis.Get(clientIP)
|
||||||
|
bannedString := string(banned)
|
||||||
|
if err == nil && len(bannedString) > 0 {
|
||||||
|
return bannedString == cacheBannedValue, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDecisionRedisCache(clientIP string, value string, duration int64) {
|
||||||
|
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDecisionRedisCache(clientIP string) {
|
||||||
|
if err := redis.Del(clientIP); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDecision delete decision in cache.
|
||||||
|
func DeleteDecision(clientIP string) {
|
||||||
|
if redisEnabled {
|
||||||
|
deleteDecisionRedisCache(clientIP)
|
||||||
|
} else {
|
||||||
|
deleteDecisionLocalCache(clientIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDecision check in the cache if the IP has the banned / not banned value.
|
||||||
|
// Otherwise return with an error to add the IP in cache if we are on.
|
||||||
|
func GetDecision(clientIP string) (bool, error) {
|
||||||
|
if redisEnabled {
|
||||||
|
return getDecisionRedisCache(clientIP)
|
||||||
|
}
|
||||||
|
return getDecisionLocalCache(clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
||||||
|
func SetDecision(clientIP string, isBanned bool, duration int64) {
|
||||||
|
var value string
|
||||||
|
if isBanned {
|
||||||
|
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v banned", clientIP))
|
||||||
|
value = cacheBannedValue
|
||||||
|
} else {
|
||||||
|
value = cacheNoBannedValue
|
||||||
|
}
|
||||||
|
if redisEnabled {
|
||||||
|
setDecisionRedisCache(clientIP, value, duration)
|
||||||
|
} else {
|
||||||
|
setDecisionLocalCache(clientIP, value, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitRedisClient loads variables.
|
||||||
|
func InitRedisClient(host string) {
|
||||||
|
redisEnabled = true
|
||||||
|
redis.Init(host)
|
||||||
|
logger.Debug("cache:InitRedisClient redis:initialized")
|
||||||
|
}
|
||||||
Vendored
+226
@@ -0,0 +1,226 @@
|
|||||||
|
// Package cache implements utility routines for manipulating cache.
|
||||||
|
// It supports currently local file and redis cache.
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getDecisionLocalCache(t *testing.T) {
|
||||||
|
IPInCache := "10.0.0.10"
|
||||||
|
IPNotInCache := "10.0.0.20"
|
||||||
|
setDecisionLocalCache(IPInCache, "t", 10)
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr bool
|
||||||
|
valueErr string
|
||||||
|
}{
|
||||||
|
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
|
||||||
|
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||||
|
{name: "Fetch invalid value", args: args{clientIP: "zaeaea"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||||
|
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := getDecisionLocalCache(tt.args.clientIP)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("getDecisionLocalCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getDecisionLocalCache() = %v, want %v", got, tt.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||||
|
t.Errorf("getDecisionLocalCache() err = %v, want %v", err.Error(), tt.valueErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_setDecisionLocalCache(t *testing.T) {
|
||||||
|
IPInCache := "10.0.0.10"
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
value string
|
||||||
|
duration int64
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
{name: "Set valid IP in local cache as t", args: args{clientIP: IPInCache, value: "t", duration: 0}},
|
||||||
|
{name: "Set valid IP in local cache as f", args: args{clientIP: IPInCache, value: "f", duration: 0}},
|
||||||
|
{name: "Set valid IP in local cache as empty str", args: args{clientIP: IPInCache, value: "", duration: 0}},
|
||||||
|
{name: "Set valid IP in local cache as f for -1 sec", args: args{clientIP: IPInCache, value: "f", duration: -1}},
|
||||||
|
{name: "Set valid IP in local cache as f for 10 sec", args: args{clientIP: IPInCache, value: "f", duration: 10}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setDecisionLocalCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_deleteDecisionLocalCache(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
deleteDecisionLocalCache(tt.args.clientIP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getDecisionRedisCache(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := getDecisionRedisCache(tt.args.clientIP)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("getDecisionRedisCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getDecisionRedisCache() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_setDecisionRedisCache(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
value string
|
||||||
|
duration int64
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setDecisionRedisCache(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_deleteDecisionRedisCache(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
deleteDecisionRedisCache(tt.args.clientIP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteDecision(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
DeleteDecision(tt.args.clientIP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDecision(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
clientIP string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := GetDecision(tt.args.clientIP)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,259 @@
|
|||||||
|
// Package configuration implements plugin Config, default Config values and validation param functions.
|
||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enums for crowdsec mode.
|
||||||
|
const (
|
||||||
|
StreamMode = "stream"
|
||||||
|
LiveMode = "live"
|
||||||
|
NoneMode = "none"
|
||||||
|
HTTPS = "https"
|
||||||
|
HTTP = "http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config the plugin configuration.
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
|
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||||
|
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||||
|
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||||
|
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||||
|
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
||||||
|
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
||||||
|
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
|
||||||
|
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
|
||||||
|
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
|
||||||
|
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
||||||
|
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
||||||
|
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
||||||
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
|
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
||||||
|
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||||
|
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||||
|
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
||||||
|
RedisCacheHost string `json:"redisCacheHost,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(source []string, target string) bool {
|
||||||
|
for _, item := range source {
|
||||||
|
if item == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates the default plugin configuration.
|
||||||
|
func New() *Config {
|
||||||
|
return &Config{
|
||||||
|
Enabled: false,
|
||||||
|
LogLevel: "INFO",
|
||||||
|
CrowdsecMode: LiveMode,
|
||||||
|
CrowdsecLapiScheme: HTTP,
|
||||||
|
CrowdsecLapiHost: "crowdsec:8080",
|
||||||
|
CrowdsecLapiKey: "",
|
||||||
|
CrowdsecLapiTLSInsecureVerify: false,
|
||||||
|
UpdateIntervalSeconds: 60,
|
||||||
|
DefaultDecisionSeconds: 60,
|
||||||
|
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||||
|
ForwardedHeadersTrustedIPs: []string{},
|
||||||
|
ClientTrustedIPs: []string{},
|
||||||
|
RedisCacheEnabled: false,
|
||||||
|
RedisCacheHost: "redis:6379",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVariable get variable from file and after in the variables gave by user.
|
||||||
|
func GetVariable(config *Config, key string) (string, error) {
|
||||||
|
value := ""
|
||||||
|
object := reflect.Indirect(reflect.ValueOf(config))
|
||||||
|
field := object.FieldByName(fmt.Sprintf("%sFile", key))
|
||||||
|
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
|
||||||
|
fp := field.String()
|
||||||
|
if fp != "" {
|
||||||
|
file, err := os.Stat(fp)
|
||||||
|
if err != nil {
|
||||||
|
return value, fmt.Errorf("%s:%s invalid path %w", key, fp, err)
|
||||||
|
}
|
||||||
|
if file.IsDir() {
|
||||||
|
return value, fmt.Errorf("%s:%s path must be a file", key, fp)
|
||||||
|
}
|
||||||
|
fileValue, err := os.ReadFile(filepath.Clean(fp))
|
||||||
|
if err != nil {
|
||||||
|
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
|
||||||
|
}
|
||||||
|
value = string(fileValue)
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
field = object.FieldByName(key)
|
||||||
|
value = field.String()
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateParams validate all the param gave by user.
|
||||||
|
func ValidateParams(config *Config) error {
|
||||||
|
if err := validateParamsRequired(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This only check that the format of the URL scheme:// is correct and do not make requests
|
||||||
|
testURL := url.URL{
|
||||||
|
Scheme: config.CrowdsecLapiScheme,
|
||||||
|
Host: config.CrowdsecLapiHost,
|
||||||
|
}
|
||||||
|
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
|
||||||
|
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
|
||||||
|
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
|
||||||
|
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be both empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case https to contact Crowdsec LAPI and certificate must be provided
|
||||||
|
if config.CrowdsecLapiScheme == HTTPS && !config.CrowdsecLapiTLSInsecureVerify {
|
||||||
|
err = validateParamsTLS(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateParamsTLS(config *Config) error {
|
||||||
|
certAuth, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if certAuth == "" {
|
||||||
|
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
|
||||||
|
}
|
||||||
|
tlsConfig := new(tls.Config)
|
||||||
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
|
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
|
||||||
|
return fmt.Errorf("failed parsing pem file")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateParamsIPs(listIP []string, key string) error {
|
||||||
|
if len(listIP) > 0 {
|
||||||
|
if _, err := ip.NewChecker(listIP); err != nil {
|
||||||
|
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Debug(fmt.Sprintf("No IP provided for %s", key))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateParamsRequired(config *Config) error {
|
||||||
|
requiredStrings := map[string]string{
|
||||||
|
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
|
||||||
|
"CrowdsecLapiHost": config.CrowdsecLapiHost,
|
||||||
|
"CrowdsecMode": config.CrowdsecMode,
|
||||||
|
}
|
||||||
|
for key, val := range requiredStrings {
|
||||||
|
if len(val) == 0 {
|
||||||
|
return fmt.Errorf("%v: cannot be empty", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requiredInt := map[string]int64{
|
||||||
|
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
|
||||||
|
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
|
||||||
|
}
|
||||||
|
for key, val := range requiredInt {
|
||||||
|
if val < 1 {
|
||||||
|
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !contains([]string{NoneMode, LiveMode, StreamMode}, config.CrowdsecMode) {
|
||||||
|
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
|
||||||
|
}
|
||||||
|
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
||||||
|
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTLSConfigCrowdsec get TLS config from Config.
|
||||||
|
func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
|
||||||
|
tlsConfig := new(tls.Config)
|
||||||
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
|
//nolint:gocritic
|
||||||
|
if config.CrowdsecLapiScheme != HTTPS {
|
||||||
|
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme not https")
|
||||||
|
return tlsConfig, nil
|
||||||
|
} else if config.CrowdsecLapiTLSInsecureVerify {
|
||||||
|
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify is true")
|
||||||
|
tlsConfig.InsecureSkipVerify = true
|
||||||
|
// If we return here and still want to use client auth this won't work
|
||||||
|
// return tlsConfig, nil
|
||||||
|
} else {
|
||||||
|
certAuthority, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cert := []byte(certAuthority)
|
||||||
|
if !tlsConfig.RootCAs.AppendCertsFromPEM(cert) {
|
||||||
|
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority read cert failed")
|
||||||
|
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
||||||
|
// and CA not load, we can't communicate with https
|
||||||
|
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if certBouncer == "" || certBouncerKey == "" {
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
||||||
|
clientCert, err := tls.X509KeyPair([]byte(certBouncer), []byte(certBouncerKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getTLSClientConfigCrowdsec impossible to generate ClientCert %w", err)
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCert)
|
||||||
|
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getMinimalConfig() *Config {
|
||||||
|
cfg := New()
|
||||||
|
cfg.CrowdsecLapiKey = "test"
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_contains(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
source []string
|
||||||
|
target string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "Contain in the list", args: args{source: []string{"a", "b"}, target: "a"}, want: true},
|
||||||
|
{name: "Contain not in the list", args: args{source: []string{"a", "b"}, target: "c"}, want: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := contains(tt.args.source, tt.args.target); got != tt.want {
|
||||||
|
t.Errorf("contains() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetVariable(t *testing.T) {
|
||||||
|
cfg1 := New()
|
||||||
|
cfg1.CrowdsecLapiKey = "test"
|
||||||
|
cfg2 := New()
|
||||||
|
cfg2.CrowdsecLapiKeyFile = "../../tests/.keytest"
|
||||||
|
cfg3 := New()
|
||||||
|
cfg3.CrowdsecLapiKeyFile = "../../tests/.bad"
|
||||||
|
type args struct {
|
||||||
|
config *Config
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "Validate a key string", args: args{config: cfg1, key: "CrowdsecLapiKey"}, want: "test", wantErr: false},
|
||||||
|
{name: "Validate a key file", args: args{config: cfg2, key: "CrowdsecLapiKey"}, want: "test", wantErr: false},
|
||||||
|
{name: "Not validate an invalid file", args: args{config: cfg3, key: "CrowdsecLapiKey"}, want: "", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := GetVariable(tt.args.config, tt.args.key)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("getVariable() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getVariable() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ValidateParams(t *testing.T) {
|
||||||
|
cfg3 := getMinimalConfig()
|
||||||
|
cfg3.CrowdsecMode = "bad"
|
||||||
|
cfg4 := getMinimalConfig()
|
||||||
|
cfg4.UpdateIntervalSeconds = 0
|
||||||
|
cfg5 := getMinimalConfig()
|
||||||
|
cfg5.ClientTrustedIPs = []string{0: "bad"}
|
||||||
|
cfg6 := getMinimalConfig()
|
||||||
|
cfg6.CrowdsecLapiScheme = HTTPS
|
||||||
|
cfg6.CrowdsecLapiTLSInsecureVerify = true
|
||||||
|
cfg8 := getMinimalConfig()
|
||||||
|
cfg8.CrowdsecLapiScheme = HTTPS
|
||||||
|
type args struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
|
||||||
|
{name: "Not validate an absent crowdsec lapi key", args: args{config: New()}, wantErr: true},
|
||||||
|
{name: "Not validate a not listed item", args: args{config: cfg3}, wantErr: true},
|
||||||
|
{name: "Not validate a bad number", args: args{config: cfg4}, wantErr: true},
|
||||||
|
{name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true},
|
||||||
|
// HTTPS enabled
|
||||||
|
{name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false},
|
||||||
|
{name: "Not validate https without cert authority", args: args{config: cfg8}, wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := ValidateParams(tt.args.config); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateParams() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validateParamsTLS(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := validateParamsTLS(tt.args.config); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateParamsTLS() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validateParamsIPs(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
listIP []string
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "Not validate a non ip", args: args{listIP: []string{0: "bad"}}, wantErr: true},
|
||||||
|
{name: "Not validate localhost", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
|
||||||
|
{name: "Not validate a weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
|
||||||
|
{name: "Not validate a weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
|
||||||
|
{name: "Validate an ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
|
||||||
|
{name: "Validate a ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := validateParamsIPs(tt.args.listIP, tt.args.key); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateParamsIPs() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validateParamsRequired(t *testing.T) {
|
||||||
|
cfg2 := getMinimalConfig()
|
||||||
|
cfg2.CrowdsecLapiScheme = "bad"
|
||||||
|
cfg3 := getMinimalConfig()
|
||||||
|
cfg3.CrowdsecMode = "bad"
|
||||||
|
cfg4 := getMinimalConfig()
|
||||||
|
cfg4.UpdateIntervalSeconds = 0
|
||||||
|
cfg5 := getMinimalConfig()
|
||||||
|
cfg5.DefaultDecisionSeconds = 0
|
||||||
|
type args struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
|
||||||
|
{name: "Not validate a bad crowdsec scheme", args: args{config: cfg2}, wantErr: true},
|
||||||
|
{name: "Not validate a bad crowdsec mode", args: args{config: cfg3}, wantErr: true},
|
||||||
|
{name: "Not validate a bad update interval seconds", args: args{config: cfg4}, wantErr: true},
|
||||||
|
{name: "Not validate a bad default decision seconds", args: args{config: cfg5}, wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := validateParamsRequired(tt.args.config); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateParamsRequired() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetTLSConfigCrowdsec(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *tls.Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := GetTLSConfigCrowdsec(tt.args.config)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("getTLSConfigCrowdsec() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
// Package ip implements utility routines to manipulate IP and CIDR.
|
||||||
|
// It allows searching an IP on a list, and find if an IP is part of a list of CIDR.
|
||||||
|
package ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CHECKER
|
||||||
|
|
||||||
|
// Checker allows to check that addresses are in a trusted IPs.
|
||||||
|
type Checker struct {
|
||||||
|
authorizedIPs []*net.IP
|
||||||
|
authorizedIPsNet []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
|
||||||
|
func NewChecker(trustedIPs []string) (*Checker, error) {
|
||||||
|
checker := &Checker{}
|
||||||
|
|
||||||
|
for _, ipMask := range trustedIPs {
|
||||||
|
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
||||||
|
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
||||||
|
logger.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ipAddr, err := net.ParseCIDR(ipMask)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
||||||
|
}
|
||||||
|
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
||||||
|
logger.Debug(fmt.Sprintf("IP network %v is trusted", ipAddr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains checks if provided address is in the trusted IPs.
|
||||||
|
func (ip *Checker) Contains(addr string) (bool, error) {
|
||||||
|
if len(addr) == 0 {
|
||||||
|
return false, fmt.Errorf("Contains:noAddress")
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddr, err := parseIP(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Contains:parseAddress addr:%s %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.ContainsIP(ipAddr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsIP checks if provided address is in the trusted IPs.
|
||||||
|
func (ip *Checker) ContainsIP(addr net.IP) bool {
|
||||||
|
for _, authorizedIP := range ip.authorizedIPs {
|
||||||
|
if authorizedIP.Equal(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, authorizedNet := range ip.authorizedIPsNet {
|
||||||
|
if authorizedNet.Contains(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIP(addr string) (net.IP, error) {
|
||||||
|
userIP := net.ParseIP(addr)
|
||||||
|
if userIP == nil {
|
||||||
|
return nil, fmt.Errorf("parseIP:parseAddress %s", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// STRATEGY
|
||||||
|
|
||||||
|
// PoolStrategy is a strategy based on an IP Checker.
|
||||||
|
// It allows to check whether addresses are in a given pool of IPs.
|
||||||
|
type PoolStrategy struct {
|
||||||
|
Checker *Checker
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIP checks the list of Forwarded IPs (most recent first) against the
|
||||||
|
// Checker pool of IPs. It returns the first IP that is not in the pool, or the
|
||||||
|
// empty string otherwise.
|
||||||
|
func (s *PoolStrategy) getIP(req *http.Request, customHeader string) string {
|
||||||
|
if s.Checker == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
xff := req.Header.Get(customHeader)
|
||||||
|
|
||||||
|
xffs := strings.Split(xff, ",")
|
||||||
|
|
||||||
|
for i := len(xffs) - 1; i >= 0; i-- {
|
||||||
|
xffTrimmed := strings.TrimSpace(xffs[i])
|
||||||
|
if len(xffTrimmed) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if contain, _ := s.Checker.Contains(xffTrimmed); !contain {
|
||||||
|
return xffTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRemoteIP It returns the first IP that is not in the pool, or the empty string otherwise.
|
||||||
|
func GetRemoteIP(req *http.Request, strategy *PoolStrategy, customHeader string) (string, error) {
|
||||||
|
remoteIP := strategy.getIP(req, customHeader)
|
||||||
|
if len(remoteIP) != 0 {
|
||||||
|
return remoteIP, nil
|
||||||
|
}
|
||||||
|
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("GetRemoteIP:extractIP: %w", err)
|
||||||
|
}
|
||||||
|
return remoteIP, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Package logger implements utility routines to write to stdout and stderr.
|
||||||
|
// It supports debug, info and error level
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loggerInfo = log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
|
||||||
|
loggerDebug = log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
|
||||||
|
loggerError = log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init Set Default log level to info in case log level to defined.
|
||||||
|
func Init(logLevel string) {
|
||||||
|
loggerError.SetOutput(os.Stderr)
|
||||||
|
loggerInfo.SetOutput(os.Stdout)
|
||||||
|
if logLevel == "DEBUG" {
|
||||||
|
loggerDebug.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info log to Stdout.
|
||||||
|
func Info(str string) {
|
||||||
|
loggerInfo.Printf(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug log to Stdout.
|
||||||
|
func Debug(str string) {
|
||||||
|
loggerDebug.Printf(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error log to Stderr.
|
||||||
|
func Error(str string) {
|
||||||
|
loggerError.Printf(str)
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Package simpleredis implements utility routines for interacting.
|
||||||
|
// It supports currently the following operations: GET, SET, DELETE,
|
||||||
|
// and support timetoleave for keys.
|
||||||
|
package simpleredis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error strings for redis.
|
||||||
|
const (
|
||||||
|
RedisUnreachable = "redis:unreachable"
|
||||||
|
RedisMiss = "redis:miss"
|
||||||
|
RedisTimeout = "redis:timeout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A RedisCmd is used to communicate with redis at low level using commands.
|
||||||
|
type RedisCmd struct {
|
||||||
|
Command string
|
||||||
|
Name string
|
||||||
|
Data []byte
|
||||||
|
Duration int64
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SimpleRedis is used to communicate with redis.
|
||||||
|
type SimpleRedis struct {
|
||||||
|
redisHost string
|
||||||
|
}
|
||||||
|
|
||||||
|
func genRedisArray(params ...[]byte) []byte {
|
||||||
|
MSG := ""
|
||||||
|
for cntr := 0; cntr < len(params); cntr++ {
|
||||||
|
MSG = strings.Join([]string{MSG, string(params[cntr])}, " ")
|
||||||
|
}
|
||||||
|
MSG = strings.Trim(MSG, " ")
|
||||||
|
MSG = strings.Join([]string{MSG, "\r\n"}, "")
|
||||||
|
return []byte(MSG)
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(wr *textproto.Writer, method string, data []byte) {
|
||||||
|
if err := wr.PrintfLine(string(data)); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("redis:%s %s", method, err.Error()))
|
||||||
|
} else {
|
||||||
|
logger.Debug(fmt.Sprintf("redis:%s", method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
|
||||||
|
dialer := net.Dialer{Timeout: 2 * time.Second}
|
||||||
|
conn, err := dialer.Dial("tcp", hostnamePort)
|
||||||
|
if err != nil {
|
||||||
|
channel <- RedisCmd{Error: fmt.Errorf(RedisUnreachable)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("redis:connClose %s", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
writer := textproto.NewWriter(bufio.NewWriter(conn))
|
||||||
|
reader := textproto.NewReader(bufio.NewReader(conn))
|
||||||
|
|
||||||
|
switch cmd.Command {
|
||||||
|
case "SET":
|
||||||
|
data := genRedisArray([]byte("SET"), []byte(cmd.Name), cmd.Data, []byte("EX"), []byte(fmt.Sprintf("%d", cmd.Duration)))
|
||||||
|
send(writer, "set", data)
|
||||||
|
case "DEL":
|
||||||
|
data := genRedisArray([]byte("DEL"), []byte(cmd.Name))
|
||||||
|
send(writer, "del", data)
|
||||||
|
case "GET":
|
||||||
|
data := genRedisArray([]byte("GET"), []byte(cmd.Name))
|
||||||
|
send(writer, "get", data)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
channel <- RedisCmd{Error: fmt.Errorf(RedisTimeout)}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
read, _ := reader.ReadLineBytes()
|
||||||
|
if string(read) != "$1" {
|
||||||
|
channel <- RedisCmd{Error: fmt.Errorf(RedisMiss)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
read, _ = reader.ReadLineBytes()
|
||||||
|
channel <- RedisCmd{Data: read}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init sets the redisHost used to connect to redis.
|
||||||
|
func (sr *SimpleRedis) Init(redisHost string) {
|
||||||
|
sr.redisHost = redisHost
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get fetches the value for key name in redis.
|
||||||
|
func (sr *SimpleRedis) Get(name string) ([]byte, error) {
|
||||||
|
redisCmd := RedisCmd{
|
||||||
|
Command: "GET",
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
channel := make(chan RedisCmd)
|
||||||
|
go askRedis(sr.redisHost, redisCmd, channel)
|
||||||
|
resp := <-channel
|
||||||
|
if resp.Error != nil {
|
||||||
|
return nil, resp.Error
|
||||||
|
}
|
||||||
|
return resp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set updates the value for key name in redis with value data for duration.
|
||||||
|
func (sr *SimpleRedis) Set(name string, data []byte, duration int64) error {
|
||||||
|
redisCmd := RedisCmd{
|
||||||
|
Command: "SET",
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
Duration: duration,
|
||||||
|
}
|
||||||
|
go askRedis(sr.redisHost, redisCmd, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del removes the key name in redis.
|
||||||
|
func (sr *SimpleRedis) Del(name string) error {
|
||||||
|
redisCmd := RedisCmd{
|
||||||
|
Command: "DEL",
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
go askRedis(sr.redisHost, redisCmd, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
test
|
||||||
Reference in New Issue
Block a user