mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4708d76854 | ||
|
|
c34d7f4617 | ||
|
|
0e9620bfe9 | ||
|
|
92f05b0ba5 | ||
|
|
980a7dd05e | ||
|
|
5c8a60118f | ||
|
|
8fb0a016b6 | ||
|
|
45d5f38c4d | ||
|
|
f1de1c924e | ||
|
|
36e6043c32 | ||
|
|
123cf15434 | ||
|
|
f89c5e25a9 | ||
|
|
58946d9fa2 | ||
|
|
6187a722ca | ||
|
|
70ad0365f0 | ||
|
|
8975216af8 | ||
|
|
e6b10b8ac4 | ||
|
|
1f11531509 | ||
|
|
ee97250acf |
@@ -0,0 +1,24 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for Go
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Maintain dependencies for build tools
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/tools"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Go Matrix
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
||||
cross:
|
||||
name: Go
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [ 1.19, 1.x ]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
# https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
# https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# https://github.com/marketplace/actions/cache
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
# In order:
|
||||
# * Module download cache
|
||||
# * Build cache (Linux)
|
||||
# * Build cache (Mac)
|
||||
# * Build cache (Windows)
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
~/Library/Caches/go-build
|
||||
%LocalAppData%\go-build
|
||||
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||
|
||||
- name: Test
|
||||
run: go test -v -cover ./...
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
name: Main Process
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO_VERSION: 1.19
|
||||
GOLANGCI_LINT_VERSION: v1.50.0
|
||||
YAEGI_VERSION: v0.14.2
|
||||
GO_VERSION: 1.23
|
||||
GOLANGCI_LINT_VERSION: v1.63.4
|
||||
YAEGI_VERSION: v0.16.1
|
||||
CGO_ENABLED: 0
|
||||
defaults:
|
||||
run:
|
||||
@@ -24,20 +24,20 @@ jobs:
|
||||
|
||||
# https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
# https://github.com/marketplace/actions/checkout
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: go/src/github.com/${{ github.repository }}
|
||||
fetch-depth: 0
|
||||
|
||||
# https://github.com/marketplace/actions/cache
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
+27
-20
@@ -1,15 +1,11 @@
|
||||
run:
|
||||
timeout: 3m
|
||||
skip-files: []
|
||||
skip-dirs: []
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
golint:
|
||||
min-confidence: 0
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
goconst:
|
||||
@@ -25,24 +21,37 @@ linters-settings:
|
||||
- FIXME
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
depguard:
|
||||
rules:
|
||||
Main:
|
||||
files:
|
||||
- $all
|
||||
- "!$test"
|
||||
allow:
|
||||
- $gostd
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
|
||||
- github.com/leprosus/golang-ttl-map
|
||||
- github.com/maxlerebourg/simpleredis
|
||||
Test:
|
||||
files:
|
||||
- $test
|
||||
allow:
|
||||
- $gostd
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
|
||||
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- deadcode # deprecated
|
||||
- exhaustivestruct # deprecated
|
||||
- golint # deprecated
|
||||
- ifshort # deprecated
|
||||
- interfacer # deprecated
|
||||
- maligned # deprecated
|
||||
- nosnakecase # deprecated
|
||||
- scopelint # deprecated
|
||||
- scopelint # deprecated
|
||||
- structcheck # deprecated
|
||||
- varcheck # deprecated
|
||||
- sqlclosecheck # not relevant (SQL)
|
||||
- rowserrcheck # not relevant (SQL)
|
||||
- execinquery # not relevant (SQL)
|
||||
- cyclop # duplicate of gocyclo
|
||||
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
|
||||
- dupl
|
||||
@@ -53,21 +62,19 @@ linters:
|
||||
- wsl
|
||||
- exhaustive
|
||||
- exhaustruct
|
||||
- goerr113
|
||||
- err113
|
||||
- wrapcheck
|
||||
- ifshort
|
||||
- noctx
|
||||
- lll
|
||||
- gomnd
|
||||
- forbidigo
|
||||
- varnamelen
|
||||
- wastedassign # is disabled because of generics
|
||||
- gofumpt
|
||||
- gci
|
||||
- mnd
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
max-per-linter: 0
|
||||
max-same-issues: 0
|
||||
exclude:
|
||||
- "G402: TLS InsecureSkipVerify may be true."
|
||||
|
||||
@@ -26,25 +26,25 @@ run_local:
|
||||
docker compose -f docker-compose.local.yml up -d --remove-orphans
|
||||
|
||||
run_behindproxy:
|
||||
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
|
||||
docker compose -f examples/behind-proxy/docker-compose.yml up -d --remove-orphans
|
||||
|
||||
run_cacheredis:
|
||||
docker compose -f examples/redis-cache/docker-compose.redis.yml up -d --remove-orphans
|
||||
docker compose -f examples/redis-cache/docker-compose.yml up -d --remove-orphans
|
||||
|
||||
run_trustedips:
|
||||
docker compose -f examples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans
|
||||
docker compose -f examples/trusted-ips/docker-compose.yml up -d --remove-orphans
|
||||
|
||||
run_binaryvm:
|
||||
cd examples/binary-vm/ && sudo vagrant up
|
||||
|
||||
run_tlsauth:
|
||||
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml up -d && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml restart && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml logs -f
|
||||
docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans
|
||||
|
||||
run_appsec:
|
||||
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml up -d
|
||||
docker compose -f examples/appsec-enabled/docker-compose.yml up -d
|
||||
|
||||
run_captcha:
|
||||
docker compose -f examples/captcha/docker-compose.captcha.yml up -d
|
||||
docker compose -f examples/captcha/docker-compose.yml up -d
|
||||
|
||||
run_custom_ban_page:
|
||||
docker compose -f examples/custom-ban-page/docker-compose.yml up -d
|
||||
@@ -62,19 +62,25 @@ restart:
|
||||
docker compose -f docker-compose.yml restart
|
||||
|
||||
restart_behindproxy:
|
||||
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml restart
|
||||
docker compose -f examples/behind-proxy/docker-compose.yml restart
|
||||
|
||||
restart_cacheredis:
|
||||
docker compose -f examples/redis-cache/docker-compose.redis.yml restart
|
||||
docker compose -f examples/redis-cache/docker-compose.yml restart
|
||||
|
||||
restart_trustedips:
|
||||
docker compose -f examples/trusted-ips/docker-compose.trusted.yml restart
|
||||
docker compose -f examples/trusted-ips/docker-compose.yml restart
|
||||
|
||||
restart_tlsauth:
|
||||
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml
|
||||
docker compose -f examples/tls-auth/docker-compose.yml
|
||||
|
||||
restart_appsec:
|
||||
docker compose -f examples/tls-auth/docker-compose.appsec-enabled.yml
|
||||
docker compose -f examples/tls-auth/docker-compose.yml
|
||||
|
||||
restart_captcha:
|
||||
docker compose -f examples/captcha/docker-compose.yml
|
||||
|
||||
restart_custombanpage:
|
||||
docker compose -f examples/custom-ban-page/docker-compose.yml
|
||||
|
||||
show_logs:
|
||||
docker compose -f docker-compose.yml restart
|
||||
@@ -86,11 +92,13 @@ show_dev_logs:
|
||||
docker compose -f docker-compose.dev.yml logs -f
|
||||
|
||||
clean_all_docker:
|
||||
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
|
||||
docker compose -f examples/redis-cache/docker-compose.redis.yml down --remove-orphans
|
||||
docker compose -f examples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
|
||||
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down --remove-orphans
|
||||
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml down --remove-orphans
|
||||
docker compose -f examples/behind-proxy/docker-compose.yml down --remove-orphans
|
||||
docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans
|
||||
docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans
|
||||
docker compose -f examples/tls-auth/docker-compose.yml down --remove-orphans
|
||||
docker compose -f examples/appsec-enabled/docker-compose.yml down --remove-orphans
|
||||
docker compose -f examples/captcha/docker-compose.yml down --remove-orphans
|
||||
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
|
||||
docker compose -f docker-compose.local.yml down --remove-orphans
|
||||
docker compose -f docker-compose.yml down --remove-orphans
|
||||
|
||||
@@ -101,3 +109,6 @@ clean_vagrant:
|
||||
show_metrics:
|
||||
docker exec crowdsec cscli metrics
|
||||
|
||||
show_decisions:
|
||||
docker exec crowdsec cscli decisions list
|
||||
|
||||
|
||||
@@ -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:
|
||||
- [hcaptcha](https://www.hcaptcha.com/)
|
||||
- [recaptcha](https://www.google.com/recaptcha/about/)
|
||||
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
|
||||
- [turnstile](https://www.cloudflare.com/products/turnstile/)
|
||||
|
||||
|
||||
There are 5 operating modes (CrowdsecMode) for this plugin:
|
||||
@@ -51,7 +51,7 @@ There are 5 operating modes (CrowdsecMode) for this plugin:
|
||||
|
||||
The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.
|
||||
|
||||
The cache can be local to Traefik using the filesystem, or a separate Redis instance.
|
||||
The cache can be local to Traefik in memory or using a separate Redis instance.
|
||||
|
||||
Below are Mermaid diagrams detailling how each mode work:
|
||||
|
||||
@@ -310,6 +310,9 @@ make run
|
||||
*This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP*
|
||||
Only one instance of the plugin is *possible*.
|
||||
|
||||
**/!\ Appsec maximum body limit is defaulted to 10MB**
|
||||
*By careful when you upgrade to >1.4.x*
|
||||
|
||||
### Variables
|
||||
- Enabled
|
||||
- bool
|
||||
@@ -317,7 +320,7 @@ Only one instance of the plugin is *possible*.
|
||||
- Enable the plugin
|
||||
- LogLevel
|
||||
- string
|
||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`
|
||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`, log are written to `stdout` / `stderr`
|
||||
- CrowdsecMode
|
||||
- string
|
||||
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
|
||||
@@ -329,10 +332,22 @@ Only one instance of the plugin is *possible*.
|
||||
- string
|
||||
- default: "crowdsec:7422"
|
||||
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
|
||||
- CrowdsecAppsecPath
|
||||
- string
|
||||
- default: "/"
|
||||
- Crowdsec Appsec Server available on this path. Will be appended to CrowdsecAppsecHost. Need to finish with "/".
|
||||
- CrowdsecAppsecFailureBlock
|
||||
- bool
|
||||
- default: true
|
||||
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
|
||||
- CrowdsecAppsecUnreachableBlock
|
||||
- bool
|
||||
- default: true
|
||||
- Block request when Crowdsec Appsec Server is unreachable.
|
||||
- CrowdsecAppsecBodyLimit
|
||||
- int64
|
||||
- default: 10485760 (= 10MB)
|
||||
- Transmit only the first number of bytes to Crowdsec Appsec Server.
|
||||
- CrowdsecLapiScheme
|
||||
- string
|
||||
- default: `http`, expected values are: `http`, `https`
|
||||
@@ -340,6 +355,10 @@ Only one instance of the plugin is *possible*.
|
||||
- string
|
||||
- default: "crowdsec:8080"
|
||||
- Crowdsec LAPI available on which host and port.
|
||||
- CrowdsecLapiPath
|
||||
- string
|
||||
- default: "/"
|
||||
- Crowdsec LAPI Server available on this path. Will be appended to CrowdsecLapiHost. Need to finish with "/".
|
||||
- CrowdsecLapiKey
|
||||
- string
|
||||
- default: ""
|
||||
@@ -364,18 +383,22 @@ Only one instance of the plugin is *possible*.
|
||||
- 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)
|
||||
- RemediationHeadersCustomName
|
||||
- string
|
||||
- default: ""
|
||||
- Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
|
||||
- ForwardedHeadersCustomName
|
||||
- string
|
||||
- default: "X-Forwarded-For"
|
||||
- Name of the header where the real IP of the client should be retrieved
|
||||
- ForwardedHeadersTrustedIPs
|
||||
- []string
|
||||
- default: []
|
||||
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
|
||||
- RedisCacheEnabled
|
||||
- bool
|
||||
- default: false
|
||||
- enable Redis cache instead of filesystem cache
|
||||
- enable Redis cache instead of in-memory cache
|
||||
- RedisCacheHost
|
||||
- string
|
||||
- default: "redis:6379"
|
||||
@@ -388,6 +411,10 @@ Only one instance of the plugin is *possible*.
|
||||
- string
|
||||
- default: ""
|
||||
- Database selection for the Redis service
|
||||
- RedisUnreachableBlock
|
||||
- bool
|
||||
- default: true
|
||||
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
|
||||
- HTTPTimeoutSeconds
|
||||
- int64
|
||||
- default: 10
|
||||
@@ -396,6 +423,10 @@ Only one instance of the plugin is *possible*.
|
||||
- int64
|
||||
- default: 60
|
||||
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
|
||||
- UpdateMaxFailure
|
||||
- int64
|
||||
- default: 0
|
||||
- Used only in `stream` and `alone` mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
|
||||
- DefaultDecisionSeconds
|
||||
- int64
|
||||
- default: 60
|
||||
@@ -475,16 +506,21 @@ http:
|
||||
enabled: false
|
||||
logLevel: DEBUG
|
||||
updateIntervalSeconds: 60
|
||||
updateMaxFailure: 0
|
||||
defaultDecisionSeconds: 60
|
||||
httpTimeoutSeconds: 10
|
||||
crowdsecMode: live
|
||||
crowdsecAppsecEnabled: false
|
||||
crowdsecAppsecHost: crowdsec:7422
|
||||
crowdsecAppsecPath: "/"
|
||||
crowdsecAppsecFailureBlock: true
|
||||
crowdsecAppsecUnreachableBlock: true
|
||||
crowdsecAppsecBodyLimit: 10485760
|
||||
crowdsecLapiKey: privateKey-foo
|
||||
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
|
||||
crowdsecLapiHost: crowdsec:8080
|
||||
crowdsecLapiScheme: http
|
||||
crowdsecLapiHost: crowdsec:8080
|
||||
crowdsecLapiPath: "/"
|
||||
crowdsecLapiTLSInsecureVerify: false
|
||||
crowdsecCapiMachineId: login
|
||||
crowdsecCapiPassword: password
|
||||
@@ -498,10 +534,12 @@ http:
|
||||
clientTrustedIPs:
|
||||
- 192.168.1.0/24
|
||||
forwardedHeadersCustomName: X-Custom-Header
|
||||
remediationHeadersCustomName: cs-remediation
|
||||
redisCacheEnabled: false
|
||||
redisCacheHost: "redis:6379"
|
||||
redisCachePassword: password
|
||||
redisCacheDatabase: "5"
|
||||
redisCacheUnreachableBlock: true
|
||||
crowdsecLapiTLSCertificateAuthority: |-
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
|
||||
|
||||
+82
-36
@@ -1,5 +1,5 @@
|
||||
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
|
||||
// It can cache results to filesystem or redis, or even ask crowdsec for every requests.
|
||||
// It can cache results in memory or using redis, or even ask crowdsec for every requests.
|
||||
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||
|
||||
import (
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -43,6 +44,7 @@ const (
|
||||
var (
|
||||
isStartup = true
|
||||
isCrowdsecStreamHealthy = true
|
||||
updateFailure = 0
|
||||
ticker chan bool
|
||||
)
|
||||
|
||||
@@ -60,19 +62,26 @@ type Bouncer struct {
|
||||
enabled bool
|
||||
appsecEnabled bool
|
||||
appsecHost string
|
||||
appsecPath string
|
||||
appsecFailureBlock bool
|
||||
appsecUnreachableBlock bool
|
||||
appsecBodyLimit int64
|
||||
crowdsecScheme string
|
||||
crowdsecHost string
|
||||
crowdsecPath string
|
||||
crowdsecKey string
|
||||
crowdsecMode string
|
||||
crowdsecMachineID string
|
||||
crowdsecPassword string
|
||||
crowdsecScenarios []string
|
||||
updateInterval int64
|
||||
updateMaxFailure int
|
||||
defaultDecisionTimeout int64
|
||||
customHeader string
|
||||
remediationCustomHeader string
|
||||
forwardedCustomHeader string
|
||||
crowdsecStreamRoute string
|
||||
crowdsecHeader string
|
||||
redisUnreachableBlock bool
|
||||
banTemplateString string
|
||||
clientPoolStrategy *ip.PoolStrategy
|
||||
serverPoolStrategy *ip.PoolStrategy
|
||||
@@ -83,11 +92,11 @@ type Bouncer struct {
|
||||
}
|
||||
|
||||
// New creates the crowdsec bouncer plugin.
|
||||
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
||||
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
||||
log := logger.New(config.LogLevel)
|
||||
err := configuration.ValidateParams(config)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||
log.Error("New:validateParams " + err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -100,8 +109,10 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
if config.CrowdsecMode == configuration.AloneMode {
|
||||
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
|
||||
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
|
||||
config.CrowdsecLapiScheme = configuration.HTTPS
|
||||
config.CrowdsecLapiHost = crowdsecCapiHost
|
||||
config.CrowdsecLapiScheme = "https"
|
||||
config.CrowdsecLapiPath = "/"
|
||||
config.CrowdsecAppsecEnabled = false
|
||||
config.UpdateIntervalSeconds = 7200 // 2 hours
|
||||
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
||||
crowdsecHeader = crowdsecCapiHeader
|
||||
@@ -110,13 +121,13 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
crowdsecHeader = crowdsecLapiHeader
|
||||
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
|
||||
return nil, err
|
||||
}
|
||||
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
|
||||
log.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
|
||||
return nil, err
|
||||
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
|
||||
return nil, errAPIKey
|
||||
}
|
||||
config.CrowdsecLapiKey = apiKey
|
||||
}
|
||||
@@ -127,7 +138,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
|
||||
err = banTemplate.Execute(&buf, 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
|
||||
}
|
||||
banTemplateString = buf.String()
|
||||
@@ -142,16 +153,23 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
crowdsecMode: config.CrowdsecMode,
|
||||
appsecEnabled: config.CrowdsecAppsecEnabled,
|
||||
appsecHost: config.CrowdsecAppsecHost,
|
||||
appsecPath: config.CrowdsecAppsecPath,
|
||||
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
||||
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
||||
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||
crowdsecHost: config.CrowdsecLapiHost,
|
||||
crowdsecPath: config.CrowdsecLapiPath,
|
||||
crowdsecKey: config.CrowdsecLapiKey,
|
||||
crowdsecMachineID: config.CrowdsecCapiMachineID,
|
||||
crowdsecPassword: config.CrowdsecCapiPassword,
|
||||
crowdsecScenarios: config.CrowdsecCapiScenarios,
|
||||
updateInterval: config.UpdateIntervalSeconds,
|
||||
customHeader: config.ForwardedHeadersCustomName,
|
||||
updateMaxFailure: config.UpdateMaxFailure,
|
||||
remediationCustomHeader: config.RemediationHeadersCustomName,
|
||||
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||
banTemplateString: banTemplateString,
|
||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||
crowdsecHeader: crowdsecHeader,
|
||||
@@ -196,6 +214,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
config.CaptchaProvider,
|
||||
config.CaptchaSiteKey,
|
||||
config.CaptchaSecretKey,
|
||||
config.RemediationHeadersCustomName,
|
||||
config.CaptchaHTMLFilePath,
|
||||
config.CaptchaGracePeriodSeconds,
|
||||
)
|
||||
@@ -206,7 +225,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
||||
if config.CrowdsecMode == configuration.AloneMode {
|
||||
if err := getToken(bouncer); err != nil {
|
||||
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
||||
bouncer.log.Error("New:getToken " + err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -216,22 +235,22 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
|
||||
handleStreamTicker(bouncer)
|
||||
})
|
||||
}
|
||||
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
|
||||
bouncer.log.Debug("New initialized mode:" + config.CrowdsecMode)
|
||||
|
||||
return bouncer, nil
|
||||
}
|
||||
|
||||
// ServeHTTP principal function of plugin.
|
||||
//
|
||||
//nolint:nestif
|
||||
//nolint:nestif,gocyclo
|
||||
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if !bouncer.enabled {
|
||||
bouncer.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Here we check for the trusted IPs in the customHeader
|
||||
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
|
||||
// Here we check for the trusted IPs in the forwardedCustomHeader
|
||||
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
|
||||
if err != nil {
|
||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
|
||||
handleBanServeHTTP(bouncer, rw)
|
||||
@@ -261,6 +280,11 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if cacheErr != nil {
|
||||
cacheErrString := cacheErr.Error()
|
||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
||||
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||
return
|
||||
}
|
||||
if cacheErrString != cache.CacheMiss {
|
||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
||||
handleBanServeHTTP(bouncer, rw)
|
||||
@@ -282,7 +306,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if isCrowdsecStreamHealthy {
|
||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||
} else {
|
||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
|
||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
||||
handleBanServeHTTP(bouncer, rw)
|
||||
}
|
||||
} else {
|
||||
@@ -324,11 +348,20 @@ type Login struct {
|
||||
Expire string `json:"expire"`
|
||||
}
|
||||
|
||||
// To append Headers we need to call rw.WriteHeader after set any header.
|
||||
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
|
||||
if bouncer.banTemplateString == "" {
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
if bouncer.banTemplateString != "" {
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(rw, bouncer.banTemplateString)
|
||||
if bouncer.remediationCustomHeader != "" {
|
||||
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
||||
}
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
|
||||
if err != nil {
|
||||
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,10 +391,15 @@ func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWrit
|
||||
|
||||
func handleStreamTicker(bouncer *Bouncer) {
|
||||
if err := handleStreamCache(bouncer); err != nil {
|
||||
bouncer.log.Debug(fmt.Sprintf("handleStreamTicker updateFailure:%d isCrowdsecStreamHealthy:%t %s", updateFailure, isCrowdsecStreamHealthy, err.Error()))
|
||||
if bouncer.updateMaxFailure != -1 && updateFailure >= bouncer.updateMaxFailure && isCrowdsecStreamHealthy {
|
||||
isCrowdsecStreamHealthy = false
|
||||
bouncer.log.Error(err.Error())
|
||||
bouncer.log.Error(fmt.Sprintf("handleStreamTicker:error updateFailure:%d %s", updateFailure, err.Error()))
|
||||
}
|
||||
updateFailure++
|
||||
} else {
|
||||
isCrowdsecStreamHealthy = true
|
||||
updateFailure = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +426,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
||||
routeURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
Host: bouncer.crowdsecHost,
|
||||
Path: crowdsecLapiRoute,
|
||||
Path: bouncer.crowdsecPath + crowdsecLapiRoute,
|
||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
||||
}
|
||||
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
||||
@@ -432,7 +470,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
||||
case "captcha":
|
||||
value = cache.CaptchaValue
|
||||
default:
|
||||
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
|
||||
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
|
||||
}
|
||||
if isLiveMode {
|
||||
durationSecond := int64(duration.Seconds())
|
||||
@@ -441,7 +479,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
||||
}
|
||||
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
|
||||
}
|
||||
return value, fmt.Errorf("handleNoStreamCache:banned")
|
||||
return value, errors.New("handleNoStreamCache:banned")
|
||||
}
|
||||
|
||||
func getToken(bouncer *Bouncer) error {
|
||||
@@ -457,7 +495,6 @@ func getToken(bouncer *Bouncer) error {
|
||||
var login Login
|
||||
err = json.Unmarshal(body, &login)
|
||||
if err != nil {
|
||||
isCrowdsecStreamHealthy = false
|
||||
return fmt.Errorf("getToken:parsingBody %w", err)
|
||||
}
|
||||
if login.Code == 200 && len(login.Token) > 0 {
|
||||
@@ -485,7 +522,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
||||
streamRouteURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
Host: bouncer.crowdsecHost,
|
||||
Path: bouncer.crowdsecStreamRoute,
|
||||
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
|
||||
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
|
||||
}
|
||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
|
||||
@@ -507,7 +544,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
||||
case "captcha":
|
||||
value = cache.CaptchaValue
|
||||
default:
|
||||
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
|
||||
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
|
||||
}
|
||||
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
|
||||
}
|
||||
@@ -516,7 +553,6 @@ func handleStreamCache(bouncer *Bouncer) error {
|
||||
bouncer.cacheClient.Delete(decision.Value)
|
||||
}
|
||||
bouncer.log.Debug("handleStreamCache:updated")
|
||||
isCrowdsecStreamHealthy = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -534,13 +570,15 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
|
||||
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
|
||||
}
|
||||
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
|
||||
req.Header.Add("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/1.X.X")
|
||||
|
||||
res, err := bouncer.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
|
||||
return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
|
||||
}
|
||||
defer func() {
|
||||
if err = res.Body.Close(); err != nil {
|
||||
bouncer.log.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
|
||||
bouncer.log.Error("crowdsecQuery:closeBody " + err.Error())
|
||||
}
|
||||
}()
|
||||
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
|
||||
@@ -564,15 +602,19 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
||||
routeURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
Host: bouncer.appsecHost,
|
||||
Path: "/",
|
||||
Path: bouncer.appsecPath,
|
||||
}
|
||||
var req *http.Request
|
||||
if httpReq.Body != nil && httpReq.ContentLength > 0 {
|
||||
bodyBytes, err := io.ReadAll(httpReq.Body)
|
||||
if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil && httpReq.ContentLength > 0 {
|
||||
var bodyBuffer bytes.Buffer
|
||||
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
|
||||
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
|
||||
bodyBytes, err := io.ReadAll(teeReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("appsecQuery:GetBody %w", err)
|
||||
}
|
||||
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// Conserve body intact after reading it for other middlewares and service
|
||||
httpReq.Body = io.NopCloser(io.MultiReader(&bodyBuffer, httpReq.Body))
|
||||
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
|
||||
} else {
|
||||
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
|
||||
@@ -592,17 +634,21 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
||||
|
||||
res, err := bouncer.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("appsecQuery %w", err)
|
||||
bouncer.log.Error("appsecQuery:unreachable")
|
||||
if bouncer.appsecUnreachableBlock {
|
||||
return fmt.Errorf("appsecQuery:unreachable %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err = res.Body.Close(); err != nil {
|
||||
bouncer.log.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
|
||||
bouncer.log.Error("appsecQuery:closeBody " + err.Error())
|
||||
}
|
||||
}()
|
||||
if res.StatusCode == http.StatusInternalServerError {
|
||||
bouncer.log.Debug("crowdsecQuery statusCode:500")
|
||||
bouncer.log.Info("appsecQuery:failure")
|
||||
if bouncer.appsecFailureBlock {
|
||||
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
|
||||
return errors.New("appsecQuery statusCode:500")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+4
-4
@@ -18,7 +18,7 @@ func TestServeHTTP(t *testing.T) {
|
||||
cfg.CrowdsecLapiKey = "test"
|
||||
|
||||
ctx := context.Background()
|
||||
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
|
||||
next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})
|
||||
|
||||
handler, err := New(ctx, next, cfg, "demo-plugin")
|
||||
if err != nil {
|
||||
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
||||
crowdsecMode string
|
||||
updateInterval int64
|
||||
defaultDecisionTimeout int64
|
||||
customHeader string
|
||||
forwardedCustomHeader string
|
||||
clientPoolStrategy *ip.PoolStrategy
|
||||
serverPoolStrategy *ip.PoolStrategy
|
||||
httpClient *http.Client
|
||||
@@ -93,7 +93,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
bouncer := &Bouncer{
|
||||
next: tt.fields.next,
|
||||
name: tt.fields.name,
|
||||
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
|
||||
crowdsecMode: tt.fields.crowdsecMode,
|
||||
updateInterval: tt.fields.updateInterval,
|
||||
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
|
||||
customHeader: tt.fields.customHeader,
|
||||
forwardedCustomHeader: tt.fields.forwardedCustomHeader,
|
||||
clientPoolStrategy: tt.fields.clientPoolStrategy,
|
||||
serverPoolStrategy: tt.fields.serverPoolStrategy,
|
||||
httpClient: tt.fields.httpClient,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -49,14 +49,15 @@ services:
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=stream"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching
|
||||
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||
CUSTOM_HOSTNAME: crowdsec
|
||||
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
|
||||
volumes:
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0-beta3"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
# - './ban.html:/ban.html:ro'
|
||||
@@ -59,11 +59,11 @@ services:
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: crowdsecurity/traefik
|
||||
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||
CUSTOM_HOSTNAME: crowdsec
|
||||
# We need to register one api key per service we will use
|
||||
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.10.7"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -15,12 +15,12 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.2.0"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-appsec-enabled:/var/log/traefik
|
||||
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
ports:
|
||||
- 8000:80
|
||||
- 8080:8080
|
||||
@@ -51,11 +51,11 @@ services:
|
||||
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: crowdsecurity/traefik
|
||||
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
|
||||
CUSTOM_HOSTNAME: crowdsec
|
||||
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
||||
volumes:
|
||||
|
||||
+6
-8
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
cloudflare:
|
||||
image: "traefik:v2.10.4"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "cloudflare"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -21,7 +19,7 @@ services:
|
||||
- 8080:8080
|
||||
|
||||
traefik:
|
||||
image: "traefik:v2.10.4"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -35,7 +33,7 @@ services:
|
||||
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-traefik:/var/log/traefik
|
||||
@@ -47,7 +45,7 @@ services:
|
||||
|
||||
whoami-foo:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-foo"
|
||||
container_name: "simple-service-foo-cloudflare"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -66,7 +64,7 @@ services:
|
||||
|
||||
whoami-bar:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-bar"
|
||||
container_name: "simple-service-bar-cloudflare"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -81,7 +79,7 @@ services:
|
||||
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.5.2
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
|
||||
# DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes
|
||||
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.10.4/traefik_v2.10.4_linux_amd64.tar.gz"
|
||||
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v3.0.0/traefik_v3.0.0_linux_amd64.tar.gz"
|
||||
tar -zxvf traefik.tar.gz
|
||||
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
||||
sudo mv ./traefik /usr/local/bin/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update && apt-get install wget -y
|
||||
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.9.0/whoami_v1.9.0_linux_amd64.tar.gz"
|
||||
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.10.1/whoami_v1.10.1_linux_amd64.tar.gz"
|
||||
tar -zxvf whoami.tar.gz
|
||||
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
|
||||
sudo mv ./whoami /usr/local/bin/
|
||||
|
||||
@@ -32,7 +32,7 @@ It is not directly accessible from Traefik even when importing the plugin, so [d
|
||||
```yaml
|
||||
...
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
image: "traefik:v3.0.0"
|
||||
volumes:
|
||||
- './captcha.html:/captcha.html'
|
||||
...
|
||||
@@ -52,7 +52,7 @@ More information is available on configuring Crowdsec in the [official documenta
|
||||
```yaml
|
||||
...
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
volumes:
|
||||
# For captcha and ban mixed decision
|
||||
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -12,14 +12,14 @@ services:
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
# - "--experimental.plugins.bouncer.version=v1.2.0"
|
||||
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-captcha-enabled:/var/log/traefik
|
||||
- './captcha.html:/captcha.html'
|
||||
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
ports:
|
||||
- 8000:80
|
||||
- 8080:8080
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -12,14 +12,14 @@ services:
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
# - "--experimental.plugins.bouncer.version=v1.3.0-beta3"
|
||||
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-custom-ban-page-enabled:/var/log/traefik
|
||||
- './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:
|
||||
- 8000:80
|
||||
- 8080:8080
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
|
||||
@@ -32,13 +32,15 @@ minikube start
|
||||
helm repo add traefik https://traefik.github.io/charts
|
||||
helm repo update
|
||||
kubectl create ns traefik
|
||||
helm upgrade --version v26.1.0 --install --namespace=traefik \
|
||||
helm upgrade --version v28.0.0 --install --namespace=traefik \
|
||||
--values=./traefik/values.yml \
|
||||
traefik traefik/traefik
|
||||
```
|
||||
|
||||
_A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_
|
||||
|
||||
_v28.0.0 of the Traefik helm chart is only compatible with v3 of Traefik_
|
||||
|
||||
#### View the Traefik dashboard
|
||||
|
||||
> Port forward the dashboard:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
tag: v1.6.0
|
||||
tag: v1.6.1-2
|
||||
|
||||
agent:
|
||||
acquisition:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dashboard.yaml
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: dashboard
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: bouncer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
tag: v2.11
|
||||
tag: v3.0.0
|
||||
|
||||
logs:
|
||||
general:
|
||||
@@ -14,4 +14,4 @@ experimental:
|
||||
plugins:
|
||||
bouncer:
|
||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
version: "v1.2.0"
|
||||
version: "v1.3.0"
|
||||
|
||||
+4
-6
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.10.4"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -15,7 +13,7 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
@@ -52,7 +50,7 @@ services:
|
||||
|
||||
whoami-redis-secure:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-bar"
|
||||
container_name: "simple-service-bar-redis"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -73,7 +71,7 @@ services:
|
||||
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.5.2
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -3,7 +3,9 @@ You need to create a crowdsec API credentials for the CAPI.
|
||||
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}"
|
||||
LOGIN=...
|
||||
PASSWORD=...
|
||||
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"$PASSWORD\", \"machine_id\": \"$LOGIN\"}"
|
||||
```
|
||||
|
||||
These CAPI credentials must be set in your docker-compose.yml or in your config files
|
||||
|
||||
+8
-11
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.10.4"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -15,16 +13,18 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
ports:
|
||||
- 80:80
|
||||
- 8080:8080
|
||||
|
||||
whoami-foo:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-foo"
|
||||
container_name: "simple-service-foo-alone"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -35,11 +35,8 @@ services:
|
||||
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=Password2"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
|
||||
|
||||
volumes:
|
||||
logs-local:
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && apt-get install -y curl wget
|
||||
RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && VNUMBER=${VERSION#"v"} && wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl && chmod +x cfssl && mv cfssl /usr/local/bin
|
||||
|
||||
@@ -72,11 +72,11 @@ The service `whoami-bar` will authenticate with a **client certificate** signed
|
||||
|
||||
Access to a route that communicate via https and authenticate with API-key:
|
||||
```
|
||||
curl http://localhost:80/foo
|
||||
curl http://localhost:8000/foo
|
||||
```
|
||||
Access to a route that communicate via https and authenticate with a client certificate:
|
||||
```
|
||||
curl http://localhost:80/bar
|
||||
curl http://localhost:8000/bar
|
||||
```
|
||||
Access to the traefik dashboard
|
||||
```
|
||||
@@ -89,5 +89,4 @@ make run_tlsauth
|
||||
```
|
||||
|
||||
Note:
|
||||
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing
|
||||
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
|
||||
> Traefik need to be restart if certificates are regenerated after his launch
|
||||
+10
-7
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.10.4"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -15,7 +13,7 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
@@ -25,10 +23,11 @@ services:
|
||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
|
||||
ports:
|
||||
- 80:80
|
||||
- 8000:80
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
- crowdsec
|
||||
- gencert
|
||||
|
||||
# Use HTTPS scheme but with lapikey authentication
|
||||
# whoami-foo:
|
||||
@@ -66,7 +65,7 @@ services:
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.5.2
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -80,11 +79,13 @@ services:
|
||||
CERT_FILE: "/etc/crowdsec/certs/server.pem"
|
||||
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
|
||||
CACERT_FILE: "/etc/crowdsec/certs/inter.pem"
|
||||
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
|
||||
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
|
||||
AGENTS_ALLOWED_OU: "agent-ou"
|
||||
BOUNCERS_ALLOWED_OU: "bouncer-ou"
|
||||
LEVEL_DEBUG: "true"
|
||||
# Disabled because it restart in loop otherwise
|
||||
DISABLE_AGENT: "true"
|
||||
# DISABLE_AGENT: "true"
|
||||
# Disabled for the examples
|
||||
DISABLE_ONLINE_API: "true"
|
||||
volumes:
|
||||
@@ -97,6 +98,8 @@ services:
|
||||
- crowdsec-config-tls-auth:/etc/crowdsec/
|
||||
labels:
|
||||
- "traefik.enable=false"
|
||||
depends_on:
|
||||
- gencert
|
||||
|
||||
gencert:
|
||||
build: .
|
||||
+5
-7
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.10.4"
|
||||
image: "traefik:v3.0.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -15,7 +13,7 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
- "--experimental.plugins.bouncer.version=v1.3.0"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
@@ -29,7 +27,7 @@ services:
|
||||
|
||||
whoami1:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-foo"
|
||||
container_name: "simple-service-foo-trustedips"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -48,7 +46,7 @@ services:
|
||||
|
||||
whoami2:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-bar"
|
||||
container_name: "simple-service-bar-trustedips"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -67,7 +65,7 @@ services:
|
||||
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.5.2
|
||||
image: crowdsecurity/crowdsec:v1.6.1-2
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -1,8 +1,8 @@
|
||||
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
|
||||
go 1.19
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/leprosus/golang-ttl-map v1.1.7
|
||||
github.com/maxlerebourg/simpleredis v1.0.9
|
||||
github.com/maxlerebourg/simpleredis v1.0.11
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
|
||||
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
|
||||
github.com/maxlerebourg/simpleredis v1.0.9 h1:aj1hKaYPeOVE4Ksu3TV/zsreUDDWOpKXBAvoFysiqII=
|
||||
github.com/maxlerebourg/simpleredis v1.0.9/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
|
||||
github.com/maxlerebourg/simpleredis v1.0.11 h1:B33TUeIrHtJH2/Qj2bRdU+UZ1BvZwFyP55JWMxHirWg=
|
||||
github.com/maxlerebourg/simpleredis v1.0.11/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
|
||||
|
||||
Vendored
+12
-5
@@ -3,6 +3,7 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ttl_map "github.com/leprosus/golang-ttl-map"
|
||||
@@ -22,6 +23,8 @@ const (
|
||||
CaptchaDoneValue = "d"
|
||||
// CacheMiss error string when cache is miss.
|
||||
CacheMiss = "cache:miss"
|
||||
// CacheUnreachable error string when cache is unreachable.
|
||||
CacheUnreachable = "cache:unreachable"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
@@ -38,7 +41,7 @@ func (localCache) get(key string) (string, error) {
|
||||
if isCached && isValid && len(valueString) > 0 {
|
||||
return valueString, nil
|
||||
}
|
||||
return "", fmt.Errorf(CacheMiss)
|
||||
return "", errors.New(CacheMiss)
|
||||
}
|
||||
|
||||
func (localCache) set(key, value string, duration int64) {
|
||||
@@ -59,21 +62,25 @@ func (redisCache) get(key string) (string, error) {
|
||||
if err == nil && len(valueString) > 0 {
|
||||
return valueString, nil
|
||||
}
|
||||
if err.Error() == simpleredis.RedisMiss {
|
||||
return "", fmt.Errorf(CacheMiss)
|
||||
errRedisMessage := err.Error()
|
||||
if errRedisMessage == simpleredis.RedisMiss {
|
||||
return "", errors.New(CacheMiss)
|
||||
}
|
||||
if errRedisMessage == simpleredis.RedisUnreachable {
|
||||
return "", errors.New(CacheUnreachable)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (rc redisCache) set(key, value string, duration int64) {
|
||||
if err := redis.Set(key, []byte(value), duration); err != nil {
|
||||
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||
rc.log.Error("cache:setDecisionRedisCache" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (rc redisCache) delete(key string) {
|
||||
if err := redis.Del(key); err != nil {
|
||||
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-10
@@ -20,6 +20,7 @@ type Client struct {
|
||||
provider string
|
||||
siteKey string
|
||||
secretKey string
|
||||
remediationCustomHeader string
|
||||
gracePeriodSeconds int64
|
||||
captchaTemplate *template.Template
|
||||
cacheClient *cache.Client
|
||||
@@ -48,14 +49,14 @@ var (
|
||||
},
|
||||
configuration.TurnstileProvider: {
|
||||
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
key: "cf-captcha",
|
||||
key: "cf-turnstile",
|
||||
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// New Initialize captcha client.
|
||||
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, captchaTemplatePath string, gracePeriodSeconds int64) error {
|
||||
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
|
||||
c.Valid = provider != ""
|
||||
if !c.Valid {
|
||||
return nil
|
||||
@@ -63,6 +64,7 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
|
||||
c.siteKey = siteKey
|
||||
c.secretKey = secretKey
|
||||
c.provider = provider
|
||||
c.remediationCustomHeader = remediationCustomHeader
|
||||
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
|
||||
c.captchaTemplate = html
|
||||
c.gracePeriodSeconds = gracePeriodSeconds
|
||||
@@ -76,29 +78,34 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
|
||||
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
|
||||
valid, err := c.Validate(r)
|
||||
if err != nil {
|
||||
c.log.Debug(fmt.Sprintf("captcha:ServeHTTP:validate %s", err.Error()))
|
||||
c.log.Info("captcha:ServeHTTP:validate " + err.Error())
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if valid {
|
||||
c.log.Debug("captcha:ServeHTTP captcha:valid")
|
||||
c.cacheClient.Set(fmt.Sprintf("%s_captcha", remoteIP), cache.CaptchaDoneValue, c.gracePeriodSeconds)
|
||||
c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
|
||||
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if c.remediationCustomHeader != "" {
|
||||
rw.Header().Set(c.remediationCustomHeader, "captcha")
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
err = c.captchaTemplate.Execute(rw, map[string]string{
|
||||
"SiteKey": c.siteKey,
|
||||
"FrontendJS": captcha[c.provider].js,
|
||||
"FrontendKey": captcha[c.provider].key,
|
||||
})
|
||||
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.
|
||||
func (c *Client) Check(remoteIP string) bool {
|
||||
value, _ := c.cacheClient.Get(fmt.Sprintf("%s_captcha", remoteIP))
|
||||
value, _ := c.cacheClient.Get(remoteIP + "_captcha")
|
||||
passed := value == cache.CaptchaDoneValue
|
||||
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
|
||||
return passed
|
||||
@@ -111,10 +118,10 @@ type responseProvider struct {
|
||||
// Validate Verify the captcha from provider API.
|
||||
func (c *Client) Validate(r *http.Request) (bool, error) {
|
||||
if r.Method != http.MethodPost {
|
||||
c.log.Debug(fmt.Sprintf("captcha:Validate invalid method: %s", r.Method))
|
||||
c.log.Debug("captcha:Validate invalid method: " + r.Method)
|
||||
return false, nil
|
||||
}
|
||||
var response = r.FormValue(fmt.Sprintf("%s-response", captcha[c.provider].key))
|
||||
var response = r.FormValue(captcha[c.provider].key + "-response")
|
||||
if response == "" {
|
||||
c.log.Debug("captcha:Validate no captcha response found in request")
|
||||
return false, nil
|
||||
@@ -128,10 +135,10 @@ func (c *Client) Validate(r *http.Request) (bool, error) {
|
||||
}
|
||||
defer func() {
|
||||
if err = res.Body.Close(); err != nil {
|
||||
c.log.Error(fmt.Sprintf("captcha:Validate %s", err.Error()))
|
||||
c.log.Error("captcha:Validate " + err.Error())
|
||||
}
|
||||
}()
|
||||
if !strings.Contains(res.Header.Get("content-type"), "application/json") {
|
||||
if !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
|
||||
c.log.Debug("captcha:Validate responseType:noJson")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package configuration
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
@@ -39,9 +40,13 @@ type Config struct {
|
||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
|
||||
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
|
||||
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
|
||||
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
||||
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
|
||||
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
|
||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
|
||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
||||
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
||||
@@ -57,8 +62,10 @@ type Config struct {
|
||||
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
||||
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
|
||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
||||
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
|
||||
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
||||
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||
@@ -67,6 +74,7 @@ type Config struct {
|
||||
RedisCachePassword string `json:"redisCachePassword,omitempty"`
|
||||
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
|
||||
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
|
||||
RedisCacheUnreachableBlock bool `json:"redisCacheUnreachableBlock,omitempty"`
|
||||
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
|
||||
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
|
||||
CaptchaProvider string `json:"captchaProvider,omitempty"`
|
||||
@@ -94,12 +102,17 @@ func New() *Config {
|
||||
CrowdsecMode: LiveMode,
|
||||
CrowdsecAppsecEnabled: false,
|
||||
CrowdsecAppsecHost: "crowdsec:7422",
|
||||
CrowdsecAppsecPath: "/",
|
||||
CrowdsecAppsecFailureBlock: true,
|
||||
CrowdsecAppsecUnreachableBlock: true,
|
||||
CrowdsecAppsecBodyLimit: 10485760,
|
||||
CrowdsecLapiScheme: HTTP,
|
||||
CrowdsecLapiHost: "crowdsec:8080",
|
||||
CrowdsecLapiPath: "/",
|
||||
CrowdsecLapiKey: "",
|
||||
CrowdsecLapiTLSInsecureVerify: false,
|
||||
UpdateIntervalSeconds: 60,
|
||||
UpdateMaxFailure: 0,
|
||||
DefaultDecisionSeconds: 60,
|
||||
HTTPTimeoutSeconds: 10,
|
||||
CaptchaProvider: "",
|
||||
@@ -108,6 +121,7 @@ func New() *Config {
|
||||
CaptchaGracePeriodSeconds: 1800,
|
||||
CaptchaHTMLFilePath: "/captcha.html",
|
||||
BanHTMLFilePath: "",
|
||||
RemediationHeadersCustomName: "",
|
||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||
ForwardedHeadersTrustedIPs: []string{},
|
||||
ClientTrustedIPs: []string{},
|
||||
@@ -115,6 +129,7 @@ func New() *Config {
|
||||
RedisCacheHost: "redis:6379",
|
||||
RedisCachePassword: "",
|
||||
RedisCacheDatabase: "",
|
||||
RedisCacheUnreachableBlock: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +137,7 @@ func New() *Config {
|
||||
func GetVariable(config *Config, key string) (string, error) {
|
||||
value := ""
|
||||
object := reflect.Indirect(reflect.ValueOf(config))
|
||||
field := object.FieldByName(fmt.Sprintf("%sFile", key))
|
||||
field := object.FieldByName(key + "File")
|
||||
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
|
||||
fp := field.String()
|
||||
if fp != "" {
|
||||
@@ -149,7 +164,7 @@ func GetVariable(config *Config, key string) (string, error) {
|
||||
func GetHTMLTemplate(path string) (*template.Template, error) {
|
||||
var err error
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("no html template provided")
|
||||
return nil, errors.New("no html template provided")
|
||||
}
|
||||
//nolint:gosec
|
||||
b, err := os.ReadFile(path)
|
||||
@@ -210,11 +225,11 @@ func ValidateParams(config *Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
|
||||
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost, config.CrowdsecLapiPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
|
||||
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost, config.CrowdsecAppsecPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -232,7 +247,7 @@ func ValidateParams(config *Config) error {
|
||||
}
|
||||
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
|
||||
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
|
||||
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
|
||||
return errors.New("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
|
||||
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
|
||||
lapiKey = strings.TrimSpace(lapiKey)
|
||||
if err = validateParamsAPIKey(lapiKey); err != nil {
|
||||
@@ -250,11 +265,11 @@ func ValidateParams(config *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateURL(variable, scheme, host string) error {
|
||||
// This only check that the format of the URL scheme://host is correct and do not make requests
|
||||
testURL := url.URL{Scheme: scheme, Host: host}
|
||||
func validateURL(variable, scheme, host, path string) error {
|
||||
// This only check that the format of the URL scheme://host/path is correct and do not make requests
|
||||
testURL := url.URL{Scheme: scheme, Host: host, Path: path}
|
||||
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
|
||||
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v' must be an URL", variable, scheme, host)
|
||||
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v%v' must be a valid URL", variable, scheme, host, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -265,7 +280,7 @@ func validateURL(variable, scheme, host string) error {
|
||||
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
|
||||
func validateParamsAPIKey(lapiKey string) error {
|
||||
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
|
||||
if !reg.Match([]byte(lapiKey)) {
|
||||
if !reg.MatchString(lapiKey) {
|
||||
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
|
||||
}
|
||||
return nil
|
||||
@@ -277,12 +292,12 @@ func validateParamsTLS(config *Config) error {
|
||||
return err
|
||||
}
|
||||
if certAuth == "" {
|
||||
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
|
||||
return errors.New("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
|
||||
}
|
||||
tlsConfig := new(tls.Config)
|
||||
tlsConfig.RootCAs = x509.NewCertPool()
|
||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
|
||||
return fmt.Errorf("failed parsing pem file")
|
||||
return errors.New("failed parsing pem file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -318,14 +333,21 @@ func validateParamsRequired(config *Config) error {
|
||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||
}
|
||||
}
|
||||
if config.UpdateMaxFailure < -1 {
|
||||
return errors.New("UpdateMaxFailure: cannot be less than -1")
|
||||
}
|
||||
if config.CrowdsecAppsecBodyLimit < 0 {
|
||||
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
|
||||
}
|
||||
|
||||
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
|
||||
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
|
||||
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
|
||||
}
|
||||
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||
}
|
||||
if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) {
|
||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
|
||||
return errors.New("CaptchaProvider: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -354,7 +376,7 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
|
||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
|
||||
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
||||
// and CA not load, we can't communicate with https
|
||||
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
||||
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
||||
}
|
||||
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -45,7 +46,7 @@ func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
|
||||
// Contains checks if provided address is in the trusted IPs.
|
||||
func (ip *Checker) Contains(addr string) (bool, error) {
|
||||
if len(addr) == 0 {
|
||||
return false, fmt.Errorf("Contains:noAddress")
|
||||
return false, errors.New("Contains:noAddress")
|
||||
}
|
||||
|
||||
ipAddr, err := parseIP(addr)
|
||||
|
||||
@@ -34,15 +34,15 @@ func New(logLevel string) *Log {
|
||||
|
||||
// Info log to Stdout.
|
||||
func (l *Log) Info(str string) {
|
||||
l.logInfo.Printf(str)
|
||||
l.logInfo.Printf("%s", str)
|
||||
}
|
||||
|
||||
// Debug log to Stdout.
|
||||
func (l *Log) Debug(str string) {
|
||||
l.logDebug.Printf(str)
|
||||
l.logDebug.Printf("%s", str)
|
||||
}
|
||||
|
||||
// Error log to Stderr.
|
||||
func (l *Log) Error(str string) {
|
||||
l.logError.Printf(str)
|
||||
l.logError.Printf("%s", str)
|
||||
}
|
||||
|
||||
-1
@@ -122,7 +122,6 @@ func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
|
||||
}
|
||||
read, _ = reader.ReadLineBytes()
|
||||
return redisCmd{Data: read}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
||||
# github.com/leprosus/golang-ttl-map v1.1.7
|
||||
## explicit; go 1.15
|
||||
github.com/leprosus/golang-ttl-map
|
||||
# github.com/maxlerebourg/simpleredis v1.0.9
|
||||
## explicit; go 1.19
|
||||
# github.com/maxlerebourg/simpleredis v1.0.11
|
||||
## explicit; go 1.22
|
||||
github.com/maxlerebourg/simpleredis
|
||||
|
||||
Reference in New Issue
Block a user