Compare commits

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

* 🍱 update readme

* 🍱 fix lint

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

* 🚨 Optimize Lint for strings

* 🔒️ Add allow list of packages

* 🚨 Fix final lint

* 👷 Update ci

* 🍱 upgrade dependencies

* 🍱 fix comment

---------

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

* 🍱 Add comment to warn future developer

---------

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

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

* 🐛 Fix config validation for maxFailedStreamUpdate

* 🚨 Fix some lint issue

* 🚨 Bypass lint complexity on ServeHTTP

* 🍱 fix and improve

* 🚨 Fix lint

* 🚨 Fix lint

* 🐛 Fix logic for update max failure

* 📝 Update doc and docker compose local reset

* 🍱 fix log nightmare

* 🍱 fix

---------

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

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

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2024-04-16 13:26:46 +02:00
mathieuHa a0e701f985 📝 Update typo on doc for ban custom page (#147) 2024-04-05 09:30:49 +02:00
36 changed files with 258 additions and 237 deletions
-47
View File
@@ -1,47 +0,0 @@
name: Go Matrix
on: [push, pull_request]
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ 1.19, 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v2
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test
run: go test -v -cover ./...
+3 -3
View File
@@ -12,9 +12,9 @@ jobs:
name: Main Process name: Main Process
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GO_VERSION: 1.19 GO_VERSION: 1.22
GOLANGCI_LINT_VERSION: v1.50.0 GOLANGCI_LINT_VERSION: v1.57.2
YAEGI_VERSION: v0.14.2 YAEGI_VERSION: v0.16.1
CGO_ENABLED: 0 CGO_ENABLED: 0
defaults: defaults:
run: run:
+25 -5
View File
@@ -1,15 +1,11 @@
run: run:
timeout: 3m timeout: 3m
skip-files: []
skip-dirs: []
linters-settings: linters-settings:
govet: govet:
enable-all: true enable-all: true
disable: disable:
- fieldalignment - fieldalignment
golint:
min-confidence: 0
gocyclo: gocyclo:
min-complexity: 15 min-complexity: 15
goconst: goconst:
@@ -25,6 +21,31 @@ linters-settings:
- FIXME - FIXME
gofumpt: gofumpt:
extra-rules: true extra-rules: true
depguard:
rules:
Main:
files:
- $all
- "!$test"
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
- github.com/leprosus/golang-ttl-map
- github.com/maxlerebourg/simpleredis
Test:
files:
- $test
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
linters: linters:
enable-all: true enable-all: true
@@ -67,7 +88,6 @@ linters:
issues: issues:
exclude-use-default: false exclude-use-default: false
max-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclude: exclude:
- "G402: TLS InsecureSkipVerify may be true." - "G402: TLS InsecureSkipVerify may be true."
+27 -16
View File
@@ -26,25 +26,25 @@ run_local:
docker compose -f docker-compose.local.yml up -d --remove-orphans docker compose -f docker-compose.local.yml up -d --remove-orphans
run_behindproxy: run_behindproxy:
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans docker compose -f examples/behind-proxy/docker-compose.yml up -d --remove-orphans
run_cacheredis: run_cacheredis:
docker compose -f examples/redis-cache/docker-compose.redis.yml up -d --remove-orphans docker compose -f examples/redis-cache/docker-compose.yml up -d --remove-orphans
run_trustedips: run_trustedips:
docker compose -f examples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans docker compose -f examples/trusted-ips/docker-compose.yml up -d --remove-orphans
run_binaryvm: run_binaryvm:
cd examples/binary-vm/ && sudo vagrant up cd examples/binary-vm/ && sudo vagrant up
run_tlsauth: run_tlsauth:
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml up -d && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml restart && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml logs -f docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans
run_appsec: run_appsec:
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml up -d docker compose -f examples/appsec-enabled/docker-compose.yml up -d
run_captcha: run_captcha:
docker compose -f examples/captcha/docker-compose.captcha.yml up -d docker compose -f examples/captcha/docker-compose.yml up -d
run_custom_ban_page: run_custom_ban_page:
docker compose -f examples/custom-ban-page/docker-compose.yml up -d docker compose -f examples/custom-ban-page/docker-compose.yml up -d
@@ -62,19 +62,25 @@ restart:
docker compose -f docker-compose.yml restart docker compose -f docker-compose.yml restart
restart_behindproxy: restart_behindproxy:
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml restart docker compose -f examples/behind-proxy/docker-compose.yml restart
restart_cacheredis: restart_cacheredis:
docker compose -f examples/redis-cache/docker-compose.redis.yml restart docker compose -f examples/redis-cache/docker-compose.yml restart
restart_trustedips: restart_trustedips:
docker compose -f examples/trusted-ips/docker-compose.trusted.yml restart docker compose -f examples/trusted-ips/docker-compose.yml restart
restart_tlsauth: restart_tlsauth:
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml docker compose -f examples/tls-auth/docker-compose.yml
restart_appsec: restart_appsec:
docker compose -f examples/tls-auth/docker-compose.appsec-enabled.yml docker compose -f examples/tls-auth/docker-compose.yml
restart_captcha:
docker compose -f examples/captcha/docker-compose.yml
restart_custombanpage:
docker compose -f examples/custom-ban-page/docker-compose.yml
show_logs: show_logs:
docker compose -f docker-compose.yml restart docker compose -f docker-compose.yml restart
@@ -86,11 +92,13 @@ show_dev_logs:
docker compose -f docker-compose.dev.yml logs -f docker compose -f docker-compose.dev.yml logs -f
clean_all_docker: clean_all_docker:
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans docker compose -f examples/behind-proxy/docker-compose.yml down --remove-orphans
docker compose -f examples/redis-cache/docker-compose.redis.yml down --remove-orphans docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans
docker compose -f examples/trusted-ips/docker-compose.trusted.yml down --remove-orphans docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down --remove-orphans docker compose -f examples/tls-auth/docker-compose.yml down --remove-orphans
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml down --remove-orphans docker compose -f examples/appsec-enabled/docker-compose.yml down --remove-orphans
docker compose -f examples/captcha/docker-compose.yml down --remove-orphans
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
docker compose -f docker-compose.local.yml down --remove-orphans docker compose -f docker-compose.local.yml down --remove-orphans
docker compose -f docker-compose.yml down --remove-orphans docker compose -f docker-compose.yml down --remove-orphans
@@ -101,3 +109,6 @@ clean_vagrant:
show_metrics: show_metrics:
docker exec crowdsec cscli metrics docker exec crowdsec cscli metrics
show_decisions:
docker exec crowdsec cscli decisions list
+13 -3
View File
@@ -36,7 +36,7 @@ On successfull completion, he will be cleaned for a specified period of time bef
The following captcha providers are supported now: The following captcha providers are supported now:
- [hcaptcha](https://www.hcaptcha.com/) - [hcaptcha](https://www.hcaptcha.com/)
- [recaptcha](https://www.google.com/recaptcha/about/) - [recaptcha](https://www.google.com/recaptcha/about/)
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/) - [turnstile](https://www.cloudflare.com/products/turnstile/)
There are 5 operating modes (CrowdsecMode) for this plugin: There are 5 operating modes (CrowdsecMode) for this plugin:
@@ -317,7 +317,7 @@ Only one instance of the plugin is *possible*.
- Enable the plugin - Enable the plugin
- LogLevel - LogLevel
- string - string
- default: `INFO`, expected values are: `INFO`, `DEBUG` - default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`, log are written to `stdout` / `stderr`
- CrowdsecMode - CrowdsecMode
- string - string
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec` - default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
@@ -333,6 +333,10 @@ Only one instance of the plugin is *possible*.
- bool - bool
- default: true - default: true
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code). - Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
- CrowdsecAppsecUnreachableBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server is unreachable.
- CrowdsecLapiScheme - CrowdsecLapiScheme
- string - string
- default: `http`, expected values are: `http`, `https` - default: `http`, expected values are: `http`, `https`
@@ -396,6 +400,10 @@ Only one instance of the plugin is *possible*.
- int64 - int64
- default: 60 - default: 60
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI - Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
- UpdateMaxFailure
- int64
- default: 0
- Used only in `stream` and `alone` mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
- DefaultDecisionSeconds - DefaultDecisionSeconds
- int64 - int64
- default: 60 - default: 60
@@ -475,12 +483,14 @@ http:
enabled: false enabled: false
logLevel: DEBUG logLevel: DEBUG
updateIntervalSeconds: 60 updateIntervalSeconds: 60
updateMaxFailure: 0
defaultDecisionSeconds: 60 defaultDecisionSeconds: 60
httpTimeoutSeconds: 10 httpTimeoutSeconds: 10
crowdsecMode: live crowdsecMode: live
crowdsecAppsecEnabled: false crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422 crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecLapiKey: privateKey-foo crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080 crowdsecLapiHost: crowdsec:8080
@@ -529,7 +539,7 @@ http:
captchaSecretKey: FIXME captchaSecretKey: FIXME
captchaGracePeriodSeconds: 1800 captchaGracePeriodSeconds: 1800
captchaHTMLFilePath: /captcha.html captchaHTMLFilePath: /captcha.html
banHTMLFilePath: ban.html banHTMLFilePath: /ban.html
``` ```
#### Fill variable with value of file #### Fill variable with value of file
+44 -26
View File
@@ -7,6 +7,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -28,6 +29,7 @@ const (
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host" crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb" crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key" crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
crowdsecAppsecUserAgent = "X-Crowdsec-Appsec-User-Agent"
crowdsecLapiHeader = "X-Api-Key" crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions" crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream" crowdsecLapiStreamRoute = "v1/decisions/stream"
@@ -42,6 +44,7 @@ const (
var ( var (
isStartup = true isStartup = true
isCrowdsecStreamHealthy = true isCrowdsecStreamHealthy = true
updateFailure = 0
ticker chan bool ticker chan bool
) )
@@ -60,6 +63,7 @@ type Bouncer struct {
appsecEnabled bool appsecEnabled bool
appsecHost string appsecHost string
appsecFailureBlock bool appsecFailureBlock bool
appsecUnreachableBlock bool
crowdsecScheme string crowdsecScheme string
crowdsecHost string crowdsecHost string
crowdsecKey string crowdsecKey string
@@ -68,6 +72,7 @@ type Bouncer struct {
crowdsecPassword string crowdsecPassword string
crowdsecScenarios []string crowdsecScenarios []string
updateInterval int64 updateInterval int64
updateMaxFailure int
defaultDecisionTimeout int64 defaultDecisionTimeout int64
customHeader string customHeader string
crowdsecStreamRoute string crowdsecStreamRoute string
@@ -82,11 +87,11 @@ type Bouncer struct {
} }
// New creates the crowdsec bouncer plugin. // New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) { func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
log := logger.New(config.LogLevel) log := logger.New(config.LogLevel)
err := configuration.ValidateParams(config) err := configuration.ValidateParams(config)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("New:validateParams %s", err.Error())) log.Error("New:validateParams " + err.Error())
return nil, err return nil, err
} }
@@ -109,12 +114,12 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
crowdsecHeader = crowdsecLapiHeader crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log) tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error())) log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
return nil, err return nil, err
} }
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey") apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 { if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error())) log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, err return nil, err
} }
config.CrowdsecLapiKey = apiKey config.CrowdsecLapiKey = apiKey
@@ -126,7 +131,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath) banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
err = banTemplate.Execute(&buf, nil) err = banTemplate.Execute(&buf, nil)
if err != nil { if err != nil {
log.Error(fmt.Sprintf("New:banTemplate is bad formatted %s", err.Error())) log.Error("New:banTemplate is bad formatted " + err.Error())
return nil, err return nil, err
} }
banTemplateString = buf.String() banTemplateString = buf.String()
@@ -142,6 +147,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
appsecEnabled: config.CrowdsecAppsecEnabled, appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost, appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock, appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
crowdsecScheme: config.CrowdsecLapiScheme, crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost, crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey, crowdsecKey: config.CrowdsecLapiKey,
@@ -149,6 +155,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
crowdsecPassword: config.CrowdsecCapiPassword, crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios, crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds, updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure,
customHeader: config.ForwardedHeadersCustomName, customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds, defaultDecisionTimeout: config.DefaultDecisionSeconds,
banTemplateString: banTemplateString, banTemplateString: banTemplateString,
@@ -205,7 +212,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil { if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode { if config.CrowdsecMode == configuration.AloneMode {
if err := getToken(bouncer); err != nil { if err := getToken(bouncer); err != nil {
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error())) bouncer.log.Error("New:getToken " + err.Error())
return nil, err return nil, err
} }
} }
@@ -215,7 +222,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
handleStreamTicker(bouncer) handleStreamTicker(bouncer)
}) })
} }
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode)) bouncer.log.Debug("New initialized mode:" + config.CrowdsecMode)
return bouncer, nil return bouncer, nil
} }
@@ -281,7 +288,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isCrowdsecStreamHealthy { if isCrowdsecStreamHealthy {
handleNextServeHTTP(bouncer, remoteIP, rw, req) handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else { } else {
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP)) bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
handleBanServeHTTP(bouncer, rw) handleBanServeHTTP(bouncer, rw)
} }
} else { } else {
@@ -323,12 +330,15 @@ type Login struct {
Expire string `json:"expire"` Expire string `json:"expire"`
} }
// To append Headers we need to call rw.WriteHeader after set any header.
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) { func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
rw.WriteHeader(http.StatusForbidden) if bouncer.banTemplateString == "" {
if bouncer.banTemplateString != "" { rw.WriteHeader(http.StatusForbidden)
rw.Header().Set("Content-Type", "text/html; charset=utf-8") return
fmt.Fprint(rw, bouncer.banTemplateString)
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, bouncer.banTemplateString)
} }
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) { func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
@@ -357,10 +367,15 @@ func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWrit
func handleStreamTicker(bouncer *Bouncer) { func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil { if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false bouncer.log.Debug(fmt.Sprintf("handleStreamTicker updateFailure:%d isCrowdsecStreamHealthy:%t %s", updateFailure, isCrowdsecStreamHealthy, err.Error()))
bouncer.log.Error(err.Error()) if bouncer.updateMaxFailure != -1 && updateFailure >= bouncer.updateMaxFailure && isCrowdsecStreamHealthy {
isCrowdsecStreamHealthy = false
bouncer.log.Error(fmt.Sprintf("handleStreamTicker:error updateFailure:%d %s", updateFailure, err.Error()))
}
updateFailure++
} else { } else {
isCrowdsecStreamHealthy = true isCrowdsecStreamHealthy = true
updateFailure = 0
} }
} }
@@ -431,7 +446,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
case "captcha": case "captcha":
value = cache.CaptchaValue value = cache.CaptchaValue
default: default:
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type)) bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
} }
if isLiveMode { if isLiveMode {
durationSecond := int64(duration.Seconds()) durationSecond := int64(duration.Seconds())
@@ -440,7 +455,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
} }
bouncer.cacheClient.Set(remoteIP, value, durationSecond) bouncer.cacheClient.Set(remoteIP, value, durationSecond)
} }
return value, fmt.Errorf("handleNoStreamCache:banned") return value, errors.New("handleNoStreamCache:banned")
} }
func getToken(bouncer *Bouncer) error { func getToken(bouncer *Bouncer) error {
@@ -456,7 +471,6 @@ func getToken(bouncer *Bouncer) error {
var login Login var login Login
err = json.Unmarshal(body, &login) err = json.Unmarshal(body, &login)
if err != nil { if err != nil {
isCrowdsecStreamHealthy = false
return fmt.Errorf("getToken:parsingBody %w", err) return fmt.Errorf("getToken:parsingBody %w", err)
} }
if login.Code == 200 && len(login.Token) > 0 { if login.Code == 200 && len(login.Token) > 0 {
@@ -506,7 +520,7 @@ func handleStreamCache(bouncer *Bouncer) error {
case "captcha": case "captcha":
value = cache.CaptchaValue value = cache.CaptchaValue
default: default:
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type)) bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
} }
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds())) bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
} }
@@ -515,7 +529,6 @@ func handleStreamCache(bouncer *Bouncer) error {
bouncer.cacheClient.Delete(decision.Value) bouncer.cacheClient.Delete(decision.Value)
} }
bouncer.log.Debug("handleStreamCache:updated") bouncer.log.Debug("handleStreamCache:updated")
isCrowdsecStreamHealthy = true
return nil return nil
} }
@@ -535,11 +548,11 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey) req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
res, err := bouncer.httpClient.Do(req) res, err := bouncer.httpClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err) return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
} }
defer func() { defer func() {
if err = res.Body.Close(); err != nil { if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error())) bouncer.log.Error("crowdsecQuery:closeBody " + err.Error())
} }
}() }()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode { if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
@@ -586,21 +599,26 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
req.Header.Set(crowdsecAppsecIPHeader, ip) req.Header.Set(crowdsecAppsecIPHeader, ip)
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method) req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host) req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.Path) req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
res, err := bouncer.httpClient.Do(req) res, err := bouncer.httpClient.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("appsecQuery %w", err) bouncer.log.Error("appsecQuery:unreachable")
if bouncer.appsecUnreachableBlock {
return fmt.Errorf("appsecQuery:unreachable %w", err)
}
return nil
} }
defer func() { defer func() {
if err = res.Body.Close(); err != nil { if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error())) bouncer.log.Error("appsecQuery:closeBody " + err.Error())
} }
}() }()
if res.StatusCode == http.StatusInternalServerError { if res.StatusCode == http.StatusInternalServerError {
bouncer.log.Debug("crowdsecQuery statusCode:500") bouncer.log.Info("appsecQuery:failure")
if bouncer.appsecFailureBlock { if bouncer.appsecFailureBlock {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode) return errors.New("appsecQuery statusCode:500")
} }
return nil return nil
} }
+2 -2
View File
@@ -18,7 +18,7 @@ func TestServeHTTP(t *testing.T) {
cfg.CrowdsecLapiKey = "test" cfg.CrowdsecLapiKey = "test"
ctx := context.Background() ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})
handler, err := New(ctx, next, cfg, "demo-plugin") handler, err := New(ctx, next, cfg, "demo-plugin")
if err != nil { if err != nil {
@@ -93,7 +93,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
// TODO: Add test cases. // TODO: Add test cases.
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(_ *testing.T) {
bouncer := &Bouncer{ bouncer := &Bouncer{
next: tt.fields.next, next: tt.fields.next,
name: tt.fields.name, name: tt.fields.name,
+4 -3
View File
@@ -1,6 +1,6 @@
services: services:
traefik: traefik:
image: "traefik:v2.11.0" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -49,14 +49,15 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG" - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=stream"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5=" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.6.0 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5= BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
volumes: volumes:
+4 -4
View File
@@ -1,6 +1,6 @@
services: services:
traefik: traefik:
image: "traefik:v2.11.0" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -12,7 +12,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.3.0-beta1" - "--experimental.plugins.bouncer.version=v1.3.0"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
# - './ban.html:/ban.html:ro' # - './ban.html:/ban.html:ro'
@@ -59,11 +59,11 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5" - "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.6.0 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
COLLECTIONS: crowdsecurity/traefik COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec CUSTOM_HOSTNAME: crowdsec
# We need to register one api key per service we will use # We need to register one api key per service we will use
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1= BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.10.7" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -15,12 +15,12 @@ services:
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.2.0" - "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- logs-appsec-enabled:/var/log/traefik - logs-appsec-enabled:/var/log/traefik
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin # - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports: ports:
- 8000:80 - 8000:80
- 8080:8080 - 8080:8080
@@ -51,11 +51,11 @@ services:
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.6.0 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
COLLECTIONS: crowdsecurity/traefik COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5 BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes: volumes:
@@ -1,8 +1,6 @@
version: "3.8"
services: services:
cloudflare: cloudflare:
image: "traefik:v2.10.4" image: "traefik:v3.0.0"
container_name: "cloudflare" container_name: "cloudflare"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -21,7 +19,7 @@ services:
- 8080:8080 - 8080:8080
traefik: traefik:
image: "traefik:v2.10.4" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -35,7 +33,7 @@ services:
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5" - "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13" - "--experimental.plugins.bouncer.version=v1.3.0"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- logs-traefik:/var/log/traefik - logs-traefik:/var/log/traefik
@@ -47,7 +45,7 @@ services:
whoami-foo: whoami-foo:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service-foo" container_name: "simple-service-foo-cloudflare"
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@@ -66,7 +64,7 @@ services:
whoami-bar: whoami-bar:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service-bar" container_name: "simple-service-bar-cloudflare"
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@@ -81,7 +79,7 @@ services:
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.5.2 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -2,7 +2,7 @@
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
# DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes # DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.10.4/traefik_v2.10.4_linux_amd64.tar.gz" wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v3.0.0/traefik_v3.0.0_linux_amd64.tar.gz"
tar -zxvf traefik.tar.gz tar -zxvf traefik.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85 # inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./traefik /usr/local/bin/ sudo mv ./traefik /usr/local/bin/
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
DEBIAN_FRONTEND=noninteractive sudo apt-get update && apt-get install wget -y DEBIAN_FRONTEND=noninteractive sudo apt-get update && apt-get install wget -y
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.9.0/whoami_v1.9.0_linux_amd64.tar.gz" wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.10.1/whoami_v1.10.1_linux_amd64.tar.gz"
tar -zxvf whoami.tar.gz tar -zxvf whoami.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85 # inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./whoami /usr/local/bin/ sudo mv ./whoami /usr/local/bin/
+2 -2
View File
@@ -32,7 +32,7 @@ It is not directly accessible from Traefik even when importing the plugin, so [d
```yaml ```yaml
... ...
traefik: traefik:
image: "traefik:v2.11.0" image: "traefik:v3.0.0"
volumes: volumes:
- './captcha.html:/captcha.html' - './captcha.html:/captcha.html'
... ...
@@ -52,7 +52,7 @@ More information is available on configuring Crowdsec in the [official documenta
```yaml ```yaml
... ...
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.6.0 image: crowdsecurity/crowdsec:v1.6.1-2
volumes: volumes:
# For captcha and ban mixed decision # For captcha and ban mixed decision
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro' - './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
@@ -1,6 +1,6 @@
services: services:
traefik: traefik:
image: "traefik:v2.11.0" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -12,14 +12,14 @@ services:
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
# - "--experimental.plugins.bouncer.version=v1.2.0" - "--experimental.plugins.bouncer.version=v1.3.0"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- logs-captcha-enabled:/var/log/traefik - logs-captcha-enabled:/var/log/traefik
- './captcha.html:/captcha.html' - './captcha.html:/captcha.html'
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin # - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports: ports:
- 8000:80 - 8000:80
- 8080:8080 - 8080:8080
@@ -55,7 +55,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html" - "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.6.0 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
+1 -1
View File
@@ -10,7 +10,7 @@ This can be usefull as some browser (Firefox for instance) return a 403 blank we
```yaml ```yaml
labels: labels:
# Define ban HTML file path # Define ban HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/ban.html" - "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
``` ```
The ban HTML file must be present in the Traefik container (bind mounted or added during a custom build). The ban HTML file must be present in the Traefik container (bind mounted or added during a custom build).
+6 -6
View File
@@ -1,6 +1,6 @@
services: services:
traefik: traefik:
image: "traefik:v2.11.0" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -12,14 +12,14 @@ services:
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
# - "--experimental.plugins.bouncer.version=v1.2.0" - "--experimental.plugins.bouncer.version=v1.3.0"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- logs-custom-ban-page-enabled:/var/log/traefik - logs-custom-ban-page-enabled:/var/log/traefik
- './ban.html:/ban.html' - './ban.html:/ban.html'
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin # - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports: ports:
- 8000:80 - 8000:80
- 8080:8080 - 8080:8080
@@ -46,7 +46,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html" - "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.6.0 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
+3 -1
View File
@@ -32,13 +32,15 @@ minikube start
helm repo add traefik https://traefik.github.io/charts helm repo add traefik https://traefik.github.io/charts
helm repo update helm repo update
kubectl create ns traefik kubectl create ns traefik
helm upgrade --version v26.1.0 --install --namespace=traefik \ helm upgrade --version v28.0.0 --install --namespace=traefik \
--values=./traefik/values.yml \ --values=./traefik/values.yml \
traefik traefik/traefik traefik traefik/traefik
``` ```
_A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_ _A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_
_v28.0.0 of the Traefik helm chart is only compatible with v3 of Traefik_
#### View the Traefik dashboard #### View the Traefik dashboard
> Port forward the dashboard: > Port forward the dashboard:
+1 -1
View File
@@ -1,5 +1,5 @@
image: image:
tag: v1.6.0 tag: v1.6.1-2
agent: agent:
acquisition: acquisition:
+1 -1
View File
@@ -1,5 +1,5 @@
# dashboard.yaml # dashboard.yaml
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: IngressRoute kind: IngressRoute
metadata: metadata:
name: dashboard name: dashboard
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
name: bouncer name: bouncer
+2 -2
View File
@@ -1,5 +1,5 @@
image: image:
tag: v2.11 tag: v3.0.0
logs: logs:
general: general:
@@ -14,4 +14,4 @@ experimental:
plugins: plugins:
bouncer: bouncer:
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.2.0" version: "v1.3.0"
@@ -1,8 +1,6 @@
version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.10.4" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -15,7 +13,7 @@ services:
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13" - "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
@@ -52,7 +50,7 @@ services:
whoami-redis-secure: whoami-redis-secure:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service-bar" container_name: "simple-service-bar-redis"
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@@ -73,7 +71,7 @@ services:
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.5.2 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -1,8 +1,6 @@
version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.10.4" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -15,16 +13,18 @@ services:
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13" - "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports: ports:
- 80:80 - 80:80
- 8080:8080 - 8080:8080
whoami-foo: whoami-foo:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service-foo" container_name: "simple-service-foo-alone"
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@@ -37,9 +37,7 @@ services:
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG" # - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr" - "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=Password2" - "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
volumes:
logs-local:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ubuntu:22.04 FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl wget RUN apt-get update && apt-get install -y curl wget
RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && VNUMBER=${VERSION#"v"} && wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl && chmod +x cfssl && mv cfssl /usr/local/bin RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && VNUMBER=${VERSION#"v"} && wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl && chmod +x cfssl && mv cfssl /usr/local/bin
+3 -4
View File
@@ -72,11 +72,11 @@ The service `whoami-bar` will authenticate with a **client certificate** signed
Access to a route that communicate via https and authenticate with API-key: Access to a route that communicate via https and authenticate with API-key:
``` ```
curl http://localhost:80/foo curl http://localhost:8000/foo
``` ```
Access to a route that communicate via https and authenticate with a client certificate: Access to a route that communicate via https and authenticate with a client certificate:
``` ```
curl http://localhost:80/bar curl http://localhost:8000/bar
``` ```
Access to the traefik dashboard Access to the traefik dashboard
``` ```
@@ -89,5 +89,4 @@ make run_tlsauth
``` ```
Note: Note:
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing > Traefik need to be restart if certificates are regenerated after his launch
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
@@ -1,8 +1,6 @@
version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.10.4" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -15,7 +13,7 @@ services:
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13" - "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
@@ -25,10 +23,11 @@ services:
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin # - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports: ports:
- 80:80 - 8000:80
- 8080:8080 - 8080:8080
depends_on: depends_on:
- crowdsec - crowdsec
- gencert
# Use HTTPS scheme but with lapikey authentication # Use HTTPS scheme but with lapikey authentication
# whoami-foo: # whoami-foo:
@@ -63,10 +62,10 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.5.2 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -80,11 +79,13 @@ services:
CERT_FILE: "/etc/crowdsec/certs/server.pem" CERT_FILE: "/etc/crowdsec/certs/server.pem"
KEY_FILE: "/etc/crowdsec/certs/server-key.pem" KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
CACERT_FILE: "/etc/crowdsec/certs/inter.pem" CACERT_FILE: "/etc/crowdsec/certs/inter.pem"
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
AGENTS_ALLOWED_OU: "agent-ou" AGENTS_ALLOWED_OU: "agent-ou"
BOUNCERS_ALLOWED_OU: "bouncer-ou" BOUNCERS_ALLOWED_OU: "bouncer-ou"
LEVEL_DEBUG: "true" LEVEL_DEBUG: "true"
# Disabled because it restart in loop otherwise # Disabled because it restart in loop otherwise
DISABLE_AGENT: "true" # DISABLE_AGENT: "true"
# Disabled for the examples # Disabled for the examples
DISABLE_ONLINE_API: "true" DISABLE_ONLINE_API: "true"
volumes: volumes:
@@ -97,7 +98,9 @@ services:
- crowdsec-config-tls-auth:/etc/crowdsec/ - crowdsec-config-tls-auth:/etc/crowdsec/
labels: labels:
- "traefik.enable=false" - "traefik.enable=false"
depends_on:
- gencert
gencert: gencert:
build: . build: .
volumes: volumes:
@@ -1,8 +1,6 @@
version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.10.4" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -15,7 +13,7 @@ services:
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.13" - "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" # - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
@@ -29,7 +27,7 @@ services:
whoami1: whoami1:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service-foo" container_name: "simple-service-foo-trustedips"
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@@ -48,7 +46,7 @@ services:
whoami2: whoami2:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service-bar" container_name: "simple-service-bar-trustedips"
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
@@ -67,7 +65,7 @@ services:
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.5.2 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
+2 -2
View File
@@ -1,8 +1,8 @@
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
go 1.19 go 1.22
require ( require (
github.com/leprosus/golang-ttl-map v1.1.7 github.com/leprosus/golang-ttl-map v1.1.7
github.com/maxlerebourg/simpleredis v1.0.9 github.com/maxlerebourg/simpleredis v1.0.11
) )
+2 -2
View File
@@ -1,4 +1,4 @@
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM= github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0= github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
github.com/maxlerebourg/simpleredis v1.0.9 h1:aj1hKaYPeOVE4Ksu3TV/zsreUDDWOpKXBAvoFysiqII= github.com/maxlerebourg/simpleredis v1.0.11 h1:B33TUeIrHtJH2/Qj2bRdU+UZ1BvZwFyP55JWMxHirWg=
github.com/maxlerebourg/simpleredis v1.0.9/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk= github.com/maxlerebourg/simpleredis v1.0.11/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
+5 -4
View File
@@ -3,6 +3,7 @@
package cache package cache
import ( import (
"errors"
"fmt" "fmt"
ttl_map "github.com/leprosus/golang-ttl-map" ttl_map "github.com/leprosus/golang-ttl-map"
@@ -38,7 +39,7 @@ func (localCache) get(key string) (string, error) {
if isCached && isValid && len(valueString) > 0 { if isCached && isValid && len(valueString) > 0 {
return valueString, nil return valueString, nil
} }
return "", fmt.Errorf(CacheMiss) return "", errors.New(CacheMiss)
} }
func (localCache) set(key, value string, duration int64) { func (localCache) set(key, value string, duration int64) {
@@ -60,20 +61,20 @@ func (redisCache) get(key string) (string, error) {
return valueString, nil return valueString, nil
} }
if err.Error() == simpleredis.RedisMiss { if err.Error() == simpleredis.RedisMiss {
return "", fmt.Errorf(CacheMiss) return "", errors.New(CacheMiss)
} }
return "", err return "", err
} }
func (rc redisCache) set(key, value string, duration int64) { func (rc redisCache) set(key, value string, duration int64) {
if err := redis.Set(key, []byte(value), duration); err != nil { if err := redis.Set(key, []byte(value), duration); err != nil {
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error())) rc.log.Error("cache:setDecisionRedisCache" + err.Error())
} }
} }
func (rc redisCache) delete(key string) { func (rc redisCache) delete(key string) {
if err := redis.Del(key); err != nil { if err := redis.Del(key); err != nil {
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error())) rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
} }
} }
+9 -7
View File
@@ -76,29 +76,31 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) { func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
valid, err := c.Validate(r) valid, err := c.Validate(r)
if err != nil { if err != nil {
c.log.Debug(fmt.Sprintf("captcha:ServeHTTP:validate %s", err.Error())) c.log.Info("captcha:ServeHTTP:validate " + err.Error())
rw.WriteHeader(http.StatusBadRequest) rw.WriteHeader(http.StatusBadRequest)
return return
} }
if valid { if valid {
c.log.Debug("captcha:ServeHTTP captcha:valid") c.log.Debug("captcha:ServeHTTP captcha:valid")
c.cacheClient.Set(fmt.Sprintf("%s_captcha", remoteIP), cache.CaptchaDoneValue, c.gracePeriodSeconds) c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
http.Redirect(rw, r, r.URL.String(), http.StatusFound) http.Redirect(rw, r, r.URL.String(), http.StatusFound)
return return
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{ err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey, "SiteKey": c.siteKey,
"FrontendJS": captcha[c.provider].js, "FrontendJS": captcha[c.provider].js,
"FrontendKey": captcha[c.provider].key, "FrontendKey": captcha[c.provider].key,
}) })
if err != nil { if err != nil {
c.log.Info(fmt.Sprintf("captcha:ServeHTTP captchaTemplateServe %s", err.Error())) c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error())
} }
} }
// Check Verify if the captcha is already done. // Check Verify if the captcha is already done.
func (c *Client) Check(remoteIP string) bool { func (c *Client) Check(remoteIP string) bool {
value, _ := c.cacheClient.Get(fmt.Sprintf("%s_captcha", remoteIP)) value, _ := c.cacheClient.Get(remoteIP + "_captcha")
passed := value == cache.CaptchaDoneValue passed := value == cache.CaptchaDoneValue
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed)) c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
return passed return passed
@@ -111,10 +113,10 @@ type responseProvider struct {
// Validate Verify the captcha from provider API. // Validate Verify the captcha from provider API.
func (c *Client) Validate(r *http.Request) (bool, error) { func (c *Client) Validate(r *http.Request) (bool, error) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
c.log.Debug(fmt.Sprintf("captcha:Validate invalid method: %s", r.Method)) c.log.Debug("captcha:Validate invalid method: " + r.Method)
return false, nil return false, nil
} }
var response = r.FormValue(fmt.Sprintf("%s-response", captcha[c.provider].key)) var response = r.FormValue(captcha[c.provider].key + "-response")
if response == "" { if response == "" {
c.log.Debug("captcha:Validate no captcha response found in request") c.log.Debug("captcha:Validate no captcha response found in request")
return false, nil return false, nil
@@ -128,7 +130,7 @@ func (c *Client) Validate(r *http.Request) (bool, error) {
} }
defer func() { defer func() {
if err = res.Body.Close(); err != nil { if err = res.Body.Close(); err != nil {
c.log.Error(fmt.Sprintf("captcha:Validate %s", err.Error())) c.log.Error("captcha:Validate " + err.Error())
} }
}() }()
if !strings.Contains(res.Header.Get("content-type"), "application/json") { if !strings.Contains(res.Header.Get("content-type"), "application/json") {
+45 -36
View File
@@ -4,6 +4,7 @@ package configuration
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors"
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
@@ -40,6 +41,7 @@ type Config struct {
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"` CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"` CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"` CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"` CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"` CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"` CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
@@ -57,6 +59,7 @@ type Config struct {
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"` CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"` CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"` UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"` HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"` ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
@@ -89,32 +92,34 @@ func contains(source []string, target string) bool {
// New creates the default plugin configuration. // New creates the default plugin configuration.
func New() *Config { func New() *Config {
return &Config{ return &Config{
Enabled: false, Enabled: false,
LogLevel: "INFO", LogLevel: "INFO",
CrowdsecMode: LiveMode, CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false, CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422", CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true, CrowdsecAppsecFailureBlock: true,
CrowdsecLapiScheme: HTTP, CrowdsecAppsecUnreachableBlock: true,
CrowdsecLapiHost: "crowdsec:8080", CrowdsecLapiScheme: HTTP,
CrowdsecLapiKey: "", CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiTLSInsecureVerify: false, CrowdsecLapiKey: "",
UpdateIntervalSeconds: 60, CrowdsecLapiTLSInsecureVerify: false,
DefaultDecisionSeconds: 60, UpdateIntervalSeconds: 60,
HTTPTimeoutSeconds: 10, UpdateMaxFailure: 0,
CaptchaProvider: "", DefaultDecisionSeconds: 60,
CaptchaSiteKey: "", HTTPTimeoutSeconds: 10,
CaptchaSecretKey: "", CaptchaProvider: "",
CaptchaGracePeriodSeconds: 1800, CaptchaSiteKey: "",
CaptchaHTMLFilePath: "/captcha.html", CaptchaSecretKey: "",
BanHTMLFilePath: "", CaptchaGracePeriodSeconds: 1800,
ForwardedHeadersCustomName: "X-Forwarded-For", CaptchaHTMLFilePath: "/captcha.html",
ForwardedHeadersTrustedIPs: []string{}, BanHTMLFilePath: "",
ClientTrustedIPs: []string{}, ForwardedHeadersCustomName: "X-Forwarded-For",
RedisCacheEnabled: false, ForwardedHeadersTrustedIPs: []string{},
RedisCacheHost: "redis:6379", ClientTrustedIPs: []string{},
RedisCachePassword: "", RedisCacheEnabled: false,
RedisCacheDatabase: "", RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
} }
} }
@@ -122,7 +127,7 @@ func New() *Config {
func GetVariable(config *Config, key string) (string, error) { func GetVariable(config *Config, key string) (string, error) {
value := "" value := ""
object := reflect.Indirect(reflect.ValueOf(config)) object := reflect.Indirect(reflect.ValueOf(config))
field := object.FieldByName(fmt.Sprintf("%sFile", key)) field := object.FieldByName(key + "File")
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity // Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
fp := field.String() fp := field.String()
if fp != "" { if fp != "" {
@@ -149,7 +154,7 @@ func GetVariable(config *Config, key string) (string, error) {
func GetHTMLTemplate(path string) (*template.Template, error) { func GetHTMLTemplate(path string) (*template.Template, error) {
var err error var err error
if path == "" { if path == "" {
return nil, fmt.Errorf("no html template provided") return nil, errors.New("no html template provided")
} }
//nolint:gosec //nolint:gosec
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
@@ -232,7 +237,7 @@ func ValidateParams(config *Config) error {
} }
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey // We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") { if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty") return errors.New("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") { } else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
lapiKey = strings.TrimSpace(lapiKey) lapiKey = strings.TrimSpace(lapiKey)
if err = validateParamsAPIKey(lapiKey); err != nil { if err = validateParamsAPIKey(lapiKey); err != nil {
@@ -265,7 +270,7 @@ func validateURL(variable, scheme, host string) error {
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators // See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
func validateParamsAPIKey(lapiKey string) error { func validateParamsAPIKey(lapiKey string) error {
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$") reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
if !reg.Match([]byte(lapiKey)) { if !reg.MatchString(lapiKey) {
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String()) return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
} }
return nil return nil
@@ -277,12 +282,12 @@ func validateParamsTLS(config *Config) error {
return err return err
} }
if certAuth == "" { if certAuth == "" {
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false") return errors.New("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
} }
tlsConfig := new(tls.Config) tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool() tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) { if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
return fmt.Errorf("failed parsing pem file") return errors.New("failed parsing pem file")
} }
return nil return nil
} }
@@ -318,14 +323,18 @@ func validateParamsRequired(config *Config) error {
return fmt.Errorf("%v: cannot be less than 1", key) return fmt.Errorf("%v: cannot be less than 1", key)
} }
} }
if config.UpdateMaxFailure < -1 {
return errors.New("UpdateMaxFailure: cannot be less than -1")
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) { if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'") return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
} }
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) { if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'") return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
} }
if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) { if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'") return errors.New("CaptchaProvider: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
} }
return nil return nil
} }
@@ -354,7 +363,7 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) { if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
// here we return because if CrowdsecLapiTLSInsecureVerify is false // here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https // and CA not load, we can't communicate with https
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled") return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
} }
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully") log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
} }
+2 -1
View File
@@ -3,6 +3,7 @@
package ip package ip
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -45,7 +46,7 @@ func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
// Contains checks if provided address is in the trusted IPs. // Contains checks if provided address is in the trusted IPs.
func (ip *Checker) Contains(addr string) (bool, error) { func (ip *Checker) Contains(addr string) (bool, error) {
if len(addr) == 0 { if len(addr) == 0 {
return false, fmt.Errorf("Contains:noAddress") return false, errors.New("Contains:noAddress")
} }
ipAddr, err := parseIP(addr) ipAddr, err := parseIP(addr)
+1 -2
View File
@@ -18,7 +18,7 @@ const (
RedisMiss = "redis:miss" RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout" RedisTimeout = "redis:timeout"
RedisNoAuth = "redis:noauth" RedisNoAuth = "redis:noauth"
RedisIssue = "redis:issue?" RedisIssue = "redis:issue?"
) )
// A redisCmd is used to communicate with redis at low level using commands. // A redisCmd is used to communicate with redis at low level using commands.
@@ -122,7 +122,6 @@ func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
} }
read, _ = reader.ReadLineBytes() read, _ = reader.ReadLineBytes()
return redisCmd{Data: read} return redisCmd{Data: read}
} }
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
# github.com/leprosus/golang-ttl-map v1.1.7 # github.com/leprosus/golang-ttl-map v1.1.7
## explicit; go 1.15 ## explicit; go 1.15
github.com/leprosus/golang-ttl-map github.com/leprosus/golang-ttl-map
# github.com/maxlerebourg/simpleredis v1.0.9 # github.com/maxlerebourg/simpleredis v1.0.11
## explicit; go 1.19 ## explicit; go 1.22
github.com/maxlerebourg/simpleredis github.com/maxlerebourg/simpleredis