Compare commits

..
5 Commits
Author SHA1 Message Date
maxlerebourgandmhx 892909b9b8 🐛 fix start up config error for appsec and review doc for appsec tls (#300)
* 🐛 fix start up config error for appsec

* :doc: add documentation on appsec variables and missing conf parameter

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix after lot of tests

* update exemple tls with new variables tested

* fix exemple appsec with release and not localplugin

---------

Co-authored-by: mhx <mathieu@hanotaux.fr>
2025-12-21 21:52:19 +01:00
dependabot[bot] c26923dee5 ⬆️ Bump actions/cache from 4 to 5 (#303)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 14:30:49 +01:00
maxlerebourg a9d83f2097 Separate TLS conf for LAPI and Appsec (#293)
*   Separate TLS conf for LAPI and Appsec

* 🍱 fix lint

* 🍱 fix test

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint ?

* 🍱 fix lint

* 🍱 Add default for traceCustomHeaders
2025-12-11 07:58:12 +01:00
omer e20ccc5d0c Add support for injecting request header value into ban HTML template (#296)
* Add parameter to specify Request header value to inject in HTML template

* Add documentation for specifying request header value

* Fix example in docs for CustomHeader

* Update parameter name to traceCustomHeader

* Ensure naming is consitent with project
2025-12-10 07:54:07 +01:00
dependabot[bot] 50beb4294f ⬆️ Bump actions/checkout from 5 to 6 (#294)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 15:40:18 +01:00
13 changed files with 304 additions and 160 deletions
+2 -2
View File
@@ -30,14 +30,14 @@ jobs:
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
path: go/src/github.com/${{ github.repository }}
fetch-depth: 0
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+1 -1
View File
@@ -99,7 +99,7 @@ clean_all_docker:
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.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-captcha/docker-compose.yml down --remove-orphans
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
+33 -6
View File
@@ -310,17 +310,16 @@ make run
### Note
> [!IMPORTANT]
> Some of the behaviours and configuration parameters are shared globally across *all* crowdsec middlewares even if you declare different middlewares with different settings.
> Some of the behaviours and configuration parameters are shared globally across _all_ crowdsec middlewares even if you declare different middlewares with different settings.
>
> **Cache is shared by all services**: This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP
>
> If you define different caches for different middlewares, only the first one to be instantiated will be bound to the crowdsec stream.
>
> Overall, this middleware is designed in such a way that **only one instance of the plugin is *possible*.** You can have multiple crowdsec middlewares in the same cluster, the key parameters must be aligned (MetricsUpdateIntervalSeconds, CrowdsecMode, CrowdsecAppsecEnabled, etc.)
> Overall, this middleware is designed in such a way that **only one instance of the plugin is _possible_.** You can have multiple crowdsec middlewares in the same cluster, the key parameters must be aligned (MetricsUpdateIntervalSeconds, CrowdsecMode, CrowdsecAppsecEnabled, etc.)
> [!WARNING]
> **Appsec maximum body limit is defaulted to 10MB**
> *Be careful when you upgrade to >1.4.x*
> **Appsec maximum body limit is defaulted to 10MB** > _Be careful when you upgrade to >1.4.x_
### Variables
@@ -351,7 +350,18 @@ make run
- CrowdsecAppsecHost
- string
- default: "crowdsec:7422"
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
- Crowdsec Appsec Server available on which host and port.
- CrowdsecAppsecTlsInsecureVerify
- bool
- default: false
- Disable verification of certificate presented by Appsec
- CrowdsecAppsecTlsCertificateAuthority
- string
- default: ""
- PEM-encoded Certificate Authority of Appsec
- CrowdsecAppsecScheme
- string
- default: value of `CrowdsecLapiScheme`, expected values are: `http`, `https`
- CrowdsecAppsecPath
- string
- default: "/"
@@ -368,6 +378,10 @@ make run
- int64
- default: 10485760 (= 10MB)
- Transmit only the first number of bytes to Crowdsec Appsec Server.
- CrowdsecAppsecKey
- string
- default: value of `CrowdsecLapiKey`
- Crowdsec AppSec key for the bouncer.
- CrowdsecLapiScheme
- string
- default: `http`, expected values are: `http`, `https`
@@ -497,6 +511,10 @@ make run
- string
- default: ""
- Path where the ban html file is stored (default empty ""=disabled)
- TraceHeadersCustomName
- string
- default: ""
- Request Header name whose value to inject in ban HTML response (default empty ""=disabled)
### Configuration
@@ -550,6 +568,7 @@ http:
httpTimeoutSeconds: 10
crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecScheme: ""
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecPath: "/"
crowdsecAppsecFailureBlock: true
@@ -603,12 +622,13 @@ http:
captchaGracePeriodSeconds: 1800
captchaHTMLFilePath: /captcha.html
banHTMLFilePath: /ban.html
traceHeadersCustomName: X-Request-ID
metricsUpdateIntervalSeconds: 600
```
#### Fill variable with value of file
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey`, `CaptchaSecretKey` and `RedisCachePassword` can be provided with the content as raw or through a file path that Traefik can read.
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecAppsecTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CrowdsecAppsecKey`, `CaptchaSiteKey`, `CaptchaSecretKey` and `RedisCachePassword` can be provided with the content as raw or through a file path that Traefik can read.
The file variable will be used as preference if both content and file are provided for the same variable.
Format is:
@@ -671,6 +691,13 @@ Set the `crowdsecLapiScheme` to https.
Crowdsec must be listening in HTTPS for this to work.
Please see the [tls-auth example](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/tls-auth/README.md) or the official documentation: [docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
#### Use HTTPS to communicate with the Appsec
To communicate with the Appsec in HTTPS you need to either accept any certificates by setting the `crowdsecAppsecTLSInsecureVerify` to true or add the CA used by the server certificate of Crowdsec using `crowdsecAppsecTLSCertificateAuthority` or `crowdsecAppsecTLSCertificateAuthorityFile`.
Set the `crowdsecAppsecScheme` to https.
Currently AppSec does not support mTLS authentication for the AppSec Component.
#### Manually add an IP to the blocklist (for testing purposes)
```bash
+61 -10
View File
@@ -85,8 +85,10 @@ type Bouncer struct {
enabled bool
appsecEnabled bool
appsecScheme string
appsecHost string
appsecPath string
appsecKey string
appsecFailureBlock bool
appsecUnreachableBlock bool
appsecBodyLimit int64
@@ -108,9 +110,11 @@ type Bouncer struct {
crowdsecHeader string
redisUnreachableBlock bool
banTemplate *htmltemplate.Template
traceCustomHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
httpAppsecClient *http.Client
cacheClient *cache.Client
captchaClient *captcha.Client
log *logger.Log
@@ -131,6 +135,23 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
var tlsAppsecConfig *tls.Config
if config.CrowdsecAppsecEnabled {
tlsAppsecConfig, err = configuration.GetTLSConfigCrowdsec(config, log, true)
if config.CrowdsecAppsecScheme == "" {
config.CrowdsecAppsecScheme = config.CrowdsecLapiScheme
}
if err != nil {
log.Error("New:getTLSConfigCrowdsec fail to get tlsAppsecConfig " + err.Error())
return nil, err
}
apiAppsecKey, errAppsecKey := configuration.GetVariable(config, "CrowdsecAppsecKey")
if errAppsecKey != nil && len(tlsAppsecConfig.Certificates) == 0 {
log.Info("New:crowdsecLapiKey fail to get CrowdsecAppsecKey and no client certificate setup " + errAppsecKey.Error())
}
config.CrowdsecAppsecKey = apiAppsecKey
}
var tlsConfig *tls.Config
crowdsecStreamRoute := ""
crowdsecHeader := ""
@@ -140,24 +161,26 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
config.CrowdsecLapiScheme = configuration.HTTPS
config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiPath = "/"
config.CrowdsecAppsecEnabled = false
config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
} else {
crowdsecStreamRoute = crowdsecLapiStreamRoute
crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log, false)
if err != nil {
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("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, errAPIKey
apiKey, errKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errKey.Error())
return nil, errKey
}
config.CrowdsecLapiKey = apiKey
if config.CrowdsecAppsecKey == "" {
config.CrowdsecAppsecKey = apiKey
}
}
var banTemplate *htmltemplate.Template
@@ -173,8 +196,10 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecScheme: config.CrowdsecAppsecScheme,
appsecHost: config.CrowdsecAppsecHost,
appsecPath: config.CrowdsecAppsecPath,
appsecKey: config.CrowdsecAppsecKey,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
@@ -193,6 +218,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
remediationStatusCode: config.RemediationStatusCode,
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
banTemplate: banTemplate,
traceCustomHeader: config.TraceHeadersCustomName,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
log: log,
@@ -210,6 +236,14 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
},
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
},
httpAppsecClient: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: tlsAppsecConfig,
},
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
},
cacheClient: &cache.Client{},
captchaClient: &captcha.Client{},
}
@@ -348,6 +382,9 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
} else {
value, err := handleNoStreamCache(bouncer, remoteIP)
if err != nil {
bouncer.log.Debug("handleNoStreamCache:crowdsecQuery " + err.Error())
}
if value == cache.NoBannedValue {
bouncer.handleNextServeHTTP(rw, req, remoteIP)
} else {
@@ -402,7 +439,21 @@ func (bouncer *Bouncer) handleBanServeHTTP(rw http.ResponseWriter, req *http.Req
if req.Method == http.MethodHead {
return
}
err := bouncer.banTemplate.Execute(rw, map[string]string{"RemediationReason": reason, "ClientIP": remoteIP})
templateData := map[string]string{
"RemediationReason": reason,
"ClientIP": remoteIP,
}
if bouncer.traceCustomHeader != "" {
headerVal := req.Header.Get(bouncer.traceCustomHeader)
if headerVal != "" {
templateData["TraceID"] = headerVal
}
}
err := bouncer.banTemplate.Execute(rw, templateData)
if err != nil {
bouncer.log.Error("handleBanServeHTTP banTemplateServe " + err.Error())
}
@@ -656,7 +707,7 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, data []byte) ([]byte, err
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Scheme: bouncer.appsecScheme,
Host: bouncer.appsecHost,
Path: bouncer.appsecPath,
}
@@ -681,14 +732,14 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
req.Header.Add(key, value)
}
}
req.Header.Set(crowdsecAppsecHeader, bouncer.crowdsecKey)
req.Header.Set(crowdsecAppsecHeader, bouncer.appsecKey)
req.Header.Set(crowdsecAppsecIPHeader, ip)
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
res, err := bouncer.httpClient.Do(req)
res, err := bouncer.httpAppsecClient.Do(req)
if err != nil {
bouncer.log.Error("appsecQuery:unreachable")
if bouncer.appsecUnreachableBlock {
+4 -3
View File
@@ -1,6 +1,6 @@
services:
traefik:
image: "traefik:v3.0.0"
image: "traefik:v3.5.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -16,8 +16,8 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-local:/var/log/traefik
- './ban.html:/ban.html:ro'
- './captcha.html:/captcha.html:ro'
- "./ban.html:/ban.html:ro"
- "./captcha.html:/captcha.html:ro"
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
@@ -52,6 +52,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=stream"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
- "traefik.http.middlewares.crowdsec.plugin.bouncer.ForwardedHeadersTrustedIPs=172.21.0.1/8"
bar2:
image: traefik/whoami
@@ -1,6 +1,6 @@
services:
traefik:
image: "traefik:v3.0.0"
image: "traefik:v3.5.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -13,7 +13,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
- "--experimental.plugins.bouncer.version=v1.5.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
+2
View File
@@ -50,8 +50,10 @@ make run_custom_ban_page
In the html of the ban page, you can use:
- {{ .ClientIP }} to display the IP used to ban the request.
- {{ .RemediationReason }} that convert on runtime into why the ban page is served. It's an enum with "APPSEC", "LAPI", "TECHNICAL_ISSUE" and it is useful to help user understand why the request is blocked.
- {{ .CustomHeader }} value of the specified Request Header (for example X-Request-ID)
```
<script>var remediation = "{{ .RemediationReason }}"</script>
<script>var clientIp = "{{ .ClientIP }}"</script>
<script>var traceID = "{{ .TraceID }}"</script>
```
With the above tweak and some other js, you can customize your ban page on runtime.
+23 -3
View File
@@ -1,7 +1,9 @@
# Example
## Using https communication and tls authentication with Crowdsec
##### Summary
This example demonstrates the use of https between the Traefik plugin and the Crowdsec LAPI.
It is possible to communicate with the LAPI in https and still authenticate with API key.
@@ -17,7 +19,9 @@ In that case the setting **crowdsecLapiTLSInsecureVerify** must be set to true.
It is recommended to validate the certificate presented by Crowdsec LAPI using the Certificate Authority which created it.
You can provide the Certificate Authority using:
* A file path readable by Traefik
- A file path readable by Traefik
```yaml
http:
middlewares:
@@ -26,9 +30,11 @@ http:
bouncer:
crowdsecLapiTlsCertificateAuthorityFile: /etc/traefik/certs/crowdsecCA.pem
```
* The PEM encoded certificate as a text variable
- The PEM encoded certificate as a text variable
In the static file configuration of Traefik
```yaml
http:
middlewares:
@@ -44,7 +50,9 @@ http:
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
In a dynamic configuration of a provider (ex docker) as a Label
```yaml
services:
whoami-foo:
@@ -71,22 +79,34 @@ The service `whoami-foo` will authenticate with an **API key** over HTTPS after
The service `whoami-bar` will authenticate with a **client certificate** signed by the CA.
Access to a route that communicate via https and authenticate with API-key:
```
curl http://localhost:8000/foo
```
Access to a route that communicate via https and authenticate with a client certificate:
```
curl http://localhost:8000/bar
```
Access to the traefik dashboard
```
curl http://localhost:8080/dashboard/#/
```
To play the demo environnement run:
```bash
make run_tlsauth
```
Note:
> Traefik need to be restart if certificates are regenerated after his launch
> Traefik need to be restarted if certificates are regenerated after his launch, crowdsec also
## Separate LAPI and Appsec HTTP/S config
To separate TLS config for LAPI and Appsec, you can use all the TLS LAPI variable beginning with `CrowdsecLapi...` into `CrowdsecAppsec...`.
Don't forget to set `CrowdsecAppsecScheme: HTTP` or `HTTPS` to trigger the separate setup.
+9
View File
@@ -2,3 +2,12 @@ filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
cert_file: /etc/crowdsec/certs/server.pem
key_file: /etc/crowdsec/certs/server-key.pem
+15 -9
View File
@@ -1,6 +1,6 @@
services:
traefik:
image: "traefik:v3.0.0"
image: "traefik:v3.5.0"
container_name: "traefik"
restart: unless-stopped
command:
@@ -13,15 +13,13 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
- "--experimental.plugins.bouncer.version=v1.5.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./LAPIKEY:/etc/traefik/LAPIKEY:ro
- logs-tls-auth:/var/log/traefik
- crowdsec-certs-tls-auth:/etc/traefik/crowdsec-certs
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
@@ -53,27 +51,34 @@ services:
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.rule=PathPrefix(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=none"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecscheme=https"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecAppsecTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapikey=40796d93c2958f9e58345514e67740e5="
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
# Enable AppSec
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
# Define AppSec host and port informations
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
image: crowdsecurity/crowdsec:latest
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
# whoami-foo is authenticating with api key over https
# whoami-bar is authenticating with tls cert over https
BOUNCER_KEY_TRAEFIK_FOO: 40796d93c2958f9e58345514e67740e5
BOUNCER_KEY_TRAEFIK_FOO: 40796d93c2958f9e58345514e67740e5=
LOCAL_API_URL: https://127.0.0.1:8080
USE_TLS: "true"
CERT_FILE: "/etc/crowdsec/certs/server.pem"
@@ -88,6 +93,7 @@ services:
# DISABLE_AGENT: "true"
# Disabled for the examples
DISABLE_ONLINE_API: "true"
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
volumes:
- ./config/acquis.yaml:/etc/crowdsec/acquis.yaml
# - ./config/config.yaml:/etc/crowdsec/config_local.yaml
+3 -1
View File
@@ -1,6 +1,8 @@
#!/bin/bash
stdout=/out/res.log
if [ -f "/out/inter-key.pem" ]; then
exit 0
fi
cfssl gencert --initca /in/ca.json 2>${stdout} | cfssljson --bare "/out/ca" && \
# Generate an intermediate certificate that will be used to sign the client certificates
cfssl gencert --initca /in/intermediate.json 2>${stdout} | cfssljson --bare "/out/inter" && \
+43 -17
View File
@@ -47,8 +47,18 @@ type Config struct {
LogFilePath string `json:"logFilePath,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecScheme string `json:"crowdsecAppsecScheme,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
CrowdsecAppsecKey string `json:"crowdsecAppsecKey,omitempty"`
CrowdsecAppsecKeyFile string `json:"crowdsecAppsecKeyFile,omitempty"`
CrowdsecAppsecTLSInsecureVerify bool `json:"crowdsecAppsecTlsInsecureVerify,omitempty"`
CrowdsecAppsecTLSCertificateAuthority string `json:"crowdsecAppsecTlsCertificateAuthority,omitempty"`
CrowdsecAppsecTLSCertificateAuthorityFile string `json:"crowdsecAppsecTlsCertificateAuthorityFile,omitempty"`
CrowdsecAppsecTLSCertificateBouncer string `json:"crowdsecAppsecTlsCertificateBouncer,omitempty"`
CrowdsecAppsecTLSCertificateBouncerFile string `json:"crowdsecAppsecTlsCertificateBouncerFile,omitempty"`
CrowdsecAppsecTLSCertificateBouncerKey string `json:"crowdsecAppsecTlsCertificateBouncerKey,omitempty"`
CrowdsecAppsecTLSCertificateBouncerKeyFile string `json:"crowdsecAppsecTlsCertificateBouncerKeyFile,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
@@ -75,6 +85,7 @@ type Config struct {
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
TraceHeadersCustomName string `json:"traceHeadersCustomName,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
@@ -116,11 +127,14 @@ func New() *Config {
LogFilePath: "",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecPath: "/",
CrowdsecAppsecFailureBlock: true,
CrowdsecAppsecUnreachableBlock: true,
CrowdsecAppsecBodyLimit: 10485760,
CrowdsecAppsecScheme: "",
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecPath: "/",
CrowdsecAppsecKey: "",
CrowdsecAppsecTLSInsecureVerify: false,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiPath: "/",
@@ -142,6 +156,7 @@ func New() *Config {
CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "",
TraceHeadersCustomName: "",
RemediationHeadersCustomName: "",
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
@@ -412,26 +427,27 @@ func validateParamsRequired(config *Config) error {
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
if !contains([]string{HTTP, HTTPS, ""}, config.CrowdsecAppsecScheme) {
return errors.New("CrowdsecAppsecScheme: must be one of 'http' or 'https'")
}
return nil
}
// GetTLSConfigCrowdsec get TLS config from Config.
//
//nolint:nestif
func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error) {
func getTLSConfig(config *Config, log *logger.Log, prefix, scheme string, insecureVerify bool) (*tls.Config, error) {
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
//nolint:gocritic
if config.CrowdsecLapiScheme != HTTPS {
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
if scheme != HTTPS {
log.Debug("getTLSConfigCrowdsec:" + prefix + "Scheme https:no")
return tlsConfig, nil
} else if config.CrowdsecLapiTLSInsecureVerify {
}
//nolint:nestif
if insecureVerify {
tlsConfig.InsecureSkipVerify = true
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
log.Debug("getTLSConfigCrowdsec:" + prefix + "TLSInsecureVerify tlsInsecure:true")
// If we return here and still want to use client auth this won't work
// return tlsConfig, nil
} else {
certAuthority, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
certAuthority, err := GetVariable(config, prefix+"TLSCertificateAuthority")
if err != nil {
return nil, err
}
@@ -439,17 +455,16 @@ 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, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
return nil, errors.New("getTLSConfigCrowdsec:" + prefix + "cannot load CA and verify cert is enabled")
}
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
log.Debug("getTLSConfigCrowdsec:" + prefix + "TLSCertificateAuthority CA added successfully")
}
}
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
certBouncer, err := GetVariable(config, prefix+"TLSCertificateBouncer")
if err != nil {
return nil, err
}
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
certBouncerKey, err := GetVariable(config, prefix+"TLSCertificateBouncerKey")
if err != nil {
return nil, err
}
@@ -464,3 +479,14 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
return tlsConfig, nil
}
// GetTLSConfigCrowdsec get TLS config from Config.
func GetTLSConfigCrowdsec(config *Config, log *logger.Log, isAppsec bool) (*tls.Config, error) {
var prefix string
if isAppsec && config.CrowdsecAppsecScheme != "" {
prefix = "CrowdsecAppsec"
return getTLSConfig(config, log, prefix, config.CrowdsecAppsecScheme, config.CrowdsecAppsecTLSInsecureVerify)
}
prefix = "CrowdsecLapi"
return getTLSConfig(config, log, prefix, config.CrowdsecLapiScheme, config.CrowdsecLapiTLSInsecureVerify)
}
+1 -1
View File
@@ -242,7 +242,7 @@ func Test_GetTLSConfigCrowdsec(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO", ""))
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO", ""), false)
if (err != nil) != tt.wantErr {
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
return