Skip to content

✨ add custom selfhosted captcha #154 #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ run_tlsauth:
docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans

run_appsec:
docker compose -f examples/appsec-enabled/docker-compose.yml up -d
docker compose -f examples/appsec-enabled/docker-compose.yml up -d --remove-orphans

run_custom_captcha:
docker compose -f examples/custom-captcha/docker-compose.yml up -d --remove-orphans

run_captcha:
docker compose -f examples/captcha/docker-compose.yml up -d
docker compose -f examples/captcha/docker-compose.yml up -d --remove-orphans

run_custom_ban_page:
docker compose -f examples/custom-ban-page/docker-compose.yml up -d
docker compose -f examples/custom-ban-page/docker-compose.yml up -d --remove-orphans

run:
docker compose -f docker-compose.yml up -d --remove-orphans
Expand Down Expand Up @@ -96,8 +99,9 @@ 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.yml down --remove-orphans
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml down --remove-orphans
docker compose -f examples/captcha/docker-compose.yml down --remove-orphans
docker compose -f examples/custom-captcha/docker-compose.yml down --remove-orphans
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
docker compose -f docker-compose.local.yml down --remove-orphans
docker compose -f docker-compose.yml down --remove-orphans
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The following captcha providers are supported now:
- [hcaptcha](https://www.hcaptcha.com/)
- [recaptcha](https://www.google.com/recaptcha/about/)
- [turnstile](https://www.cloudflare.com/products/turnstile/)
- [custom/wicketkeeper](https://github.com/a-ve/wicketkeeper)

There are 5 operating modes (CrowdsecMode) for this plugin:

Expand Down Expand Up @@ -465,7 +466,19 @@ make run
- Used only in `alone` mode, scenarios for Crowdsec CAPI
- CaptchaProvider
- string
- Provider to validate the captcha, expected values are: `hcaptcha`, `recaptcha`, `turnstile`
- Provider to validate the captcha, expected values are: `hcaptcha`, `recaptcha`, `turnstile` or `custom`
- CaptchaCustomJsURL
- string
- If CaptchaProvider is `custom`, URL used to load the challenge in the HTML (in case of hcaptcha: `https://hcaptcha.com/1/api.js`)
Copy link
Collaborator

@mathieuHa mathieuHa Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use an exemple with whicketkeeper maybe

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm waiting for @a-ve to release a new version to use the "official" image instead of the image from my fork of wicketkeeper....

- CaptchaCustomValidateURL
- string
- If CaptchaProvider is `custom`, URL used to validate the challenge (in case of hcaptcha: `https://api.hcaptcha.com/siteverify`)
- CaptchaCustomKey
- string
- If CaptchaProvider is `custom`, used to set class name of the div used by captcha provider (in case of hcaptcha: `h-captcha`)
- CaptchaCustomResponse
- string
- If CaptchaProvider is `custom`, used to set the field in the POST body from the captcha.html to Traefik (in case of hcaptcha: `h-captcha-response`)
- CaptchaSiteKey
- string
- Site key for the captcha provider
Expand Down Expand Up @@ -690,6 +703,8 @@ docker exec crowdsec cscli decisions remove --ip 10.0.0.10 -t captcha

#### 10. Using Traefik with Custom Ban HTML Page [examples/custom-ban-page/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/custom-ban-page/README.md)

#### 11. Using Traefik with Custom Captcha Whiketkeeper[examples/custom-captcha/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/custom-captcha/README.md)

### Local Mode

Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub.
Expand Down
6 changes: 6 additions & 0 deletions bouncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
)
config.CaptchaSiteKey, _ = configuration.GetVariable(config, "CaptchaSiteKey")
config.CaptchaSecretKey, _ = configuration.GetVariable(config, "CaptchaSecretKey")

err = bouncer.captchaClient.New(
log,
bouncer.cacheClient,
Expand All @@ -240,13 +241,18 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
},
config.CaptchaProvider,
config.CaptchaCustomJsURL,
config.CaptchaCustomKey,
config.CaptchaCustomResponse,
config.CaptchaCustomValidateURL,
config.CaptchaSiteKey,
config.CaptchaSecretKey,
config.RemediationHeadersCustomName,
config.CaptchaHTMLFilePath,
config.CaptchaGracePeriodSeconds,
)
if err != nil {
log.Error("CaptchaClient not valid " + err.Error())
return nil, err
}

Expand Down
8 changes: 2 additions & 6 deletions examples/appsec-enabled/docker-compose.appsec-enabled.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.8"

services:
traefik:
image: "traefik:v3.0.0"
Expand Down Expand Up @@ -36,7 +34,7 @@ services:
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
Expand All @@ -48,8 +46,6 @@ services:
# Define AppSec host and port informations
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"



crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
Expand All @@ -65,7 +61,7 @@ services:
- crowdsec-config-appsec-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"

volumes:
logs-appsec-enabled:
crowdsec-db-appsec-enabled:
Expand Down
2 changes: 1 addition & 1 deletion examples/captcha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ For now 3 captcha providers are supported:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
# Define captcha secret key
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
# Define captcha grade period seconds
# Define captcha grace period seconds
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800"
# Define captcha HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
Expand Down
2 changes: 1 addition & 1 deletion examples/captcha/captcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
</div>
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
</div>
</form>
Expand Down
76 changes: 76 additions & 0 deletions examples/custom-captcha/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Example

Read the example captcha before this, to better understand what is done here.

### Traefik configuration

The minimal configuration is defined below to implement custom captcha.
This documentation use https://github.com/a-ve/wicketpeeker, a self-hosted captcha provider that have a similar API than big providers.

Minimal API requirement:

- the JS file URL to load the captcha on the served `captcha.html`
- the HTML className to tell to the JS where to display the challenge
- the verify URL endpoint to send the field `response` from the captcha with `content-type: application/x-www-form-urlencoded`
- the name of the field when you POST the resolved captcha to Traefik

- the JS file need to respect the `data-callback` on the div that contains the captcha if you use our template, but you can customize it by your side

```yaml
traefik:
...
labels:
# Choose captcha provider
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=custom"
# Define captcha grace period seconds
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaCustomJsURL=http://captcha.localhost:8000/fast.js"
# Inside Traefik container the plugin must be able to reach wicketkeeper service so we can go through a Traefik localhost
# domain which would resolve traefik itself and the port for the dashboard
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomValidateURL=http://wicketkeeper:8080/v0/siteverify"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomKey=wicketkeeper"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CaptchaCustomResponse=wicketkeeper_solution"
# Define captcha HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
```

```yaml
wicketkeeper:
image: ghcr.io/a-ve/wicketkeeper:latest
user: root
ports:
- "8080:8080"
environment:
- ROOT_URL=http://localhost:8080
- LISTEN_PORT=8080
- REDIS_ADDR=redis:6379
- DIFFICULTY=4
- ALLOWED_ORIGINS=*
- PRIVATE_KEY_PATH=/data/wicketkeeper.key
volumes:
- ./data:/data
depends_on:
- redis
redis:
image: redis/redis-stack-server:latest
```

## Exemple navigation

We can try to query normally the whoami server:

```bash
curl http://localhost:8000/foo
```

We can try to ban ourself and retry.

```bash
docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 10m --type captcha
```

To play the demo environment run:

```bash
make run_custom_captcha
```
4 changes: 4 additions & 0 deletions examples/custom-captcha/acquis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
Loading
Loading