Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
21 changes: 14 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,23 @@ RUN addgroup -S crowdsec-spoa && adduser -S -D -H -s /sbin/nologin -g crowdsec-s
## Create a socket for the spoa to inherit crowdsec-spoa:haproxy user from official haproxy image
RUN mkdir -p /run/crowdsec-spoa/ && chown crowdsec-spoa:haproxy /run/crowdsec-spoa/ && chmod 770 /run/crowdsec-spoa/

## Copy templates
RUN mkdir -p /var/lib/crowdsec/lua/haproxy/templates/
COPY --from=build /go/src/cs-spoa-bouncer/templates/* /var/lib/crowdsec/lua/haproxy/templates/
## Create log directory with proper permissions
RUN mkdir -p /var/log/crowdsec-spoa && chown crowdsec-spoa:crowdsec-spoa /var/log/crowdsec-spoa && chmod 755 /var/log/crowdsec-spoa

RUN mkdir -p /usr/local/crowdsec/lua/haproxy/
COPY --from=build /go/src/cs-spoa-bouncer/lua/* /usr/local/crowdsec/lua/haproxy/
## Copy Lua files (matching Debian/RPM paths)
RUN mkdir -p /usr/lib/crowdsec-haproxy-spoa-bouncer/lua
COPY --from=build /go/src/cs-spoa-bouncer/lua/* /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/

RUN chown -R root:haproxy /var/lib/crowdsec/lua/haproxy /usr/local/crowdsec/lua/haproxy
## Copy templates (matching Debian/RPM paths)
## Copy .tmpl files explicitly to ensure they're included
RUN mkdir -p /var/lib/crowdsec-haproxy-spoa-bouncer/html
COPY --from=build /go/src/cs-spoa-bouncer/templates/ban.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.tmpl
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

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

The Dockerfile now only copies .tmpl template files explicitly, but the old Dockerfile copied all templates with templates/* which would have included ban.html and captcha.html. These HTML templates are still needed for Lua-based rendering (as evidenced by the HAProxy configs that set CROWDSEC_BAN_TEMPLATE_PATH to .html files). Either add explicit COPY commands for the .html templates, or revert to using templates/* to copy all template files.

Suggested change
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.tmpl /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
COPY --from=build /go/src/cs-spoa-bouncer/templates/ban.html /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html
COPY --from=build /go/src/cs-spoa-bouncer/templates/captcha.html /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.html

Copilot uses AI. Check for mistakes.

VOLUME [ "/usr/local/crowdsec/lua/haproxy/", "/var/lib/crowdsec/lua/haproxy/templates/" ]
RUN chown -R root:haproxy /usr/lib/crowdsec-haproxy-spoa-bouncer/lua /var/lib/crowdsec-haproxy-spoa-bouncer/html && \
chmod -R 755 /usr/lib/crowdsec-haproxy-spoa-bouncer/lua /var/lib/crowdsec-haproxy-spoa-bouncer/html

VOLUME [ "/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/", "/var/lib/crowdsec-haproxy-spoa-bouncer/html/" ]

RUN chmod +x /docker_start.sh

Expand Down
27 changes: 27 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/crowdsecurity/crowdsec-spoa/pkg/cfg"
"github.com/crowdsecurity/crowdsec-spoa/pkg/dataset"
"github.com/crowdsecurity/crowdsec-spoa/pkg/host"
"github.com/crowdsecurity/crowdsec-spoa/pkg/httptemplate"
"github.com/crowdsecurity/crowdsec-spoa/pkg/metrics"
"github.com/crowdsecurity/crowdsec-spoa/pkg/spoa"
csbouncer "github.com/crowdsecurity/go-cs-bouncer"
Expand Down Expand Up @@ -194,6 +195,24 @@ func Execute() error {
}
}

// Create and start HTTP template server if enabled (after HostManager is created)
var httpTemplateServer *httptemplate.Server
if config.HTTPTemplateServer.Enabled {
httpTemplateLogger := log.WithField("component", "http_template_server")
var err error
httpTemplateServer, err = httptemplate.NewServer(&config.HTTPTemplateServer, HostManager, httpTemplateLogger)
if err != nil {
return fmt.Errorf("failed to create HTTP template server: %w", err)
}

g.Go(func() error {
if err := httpTemplateServer.Serve(ctx); err != nil {
return fmt.Errorf("HTTP template server failed: %w", err)
}
return nil
})
}
Comment on lines +198 to +214
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

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

The HTTP template server is started (line 208) before the hosts are loaded from directory (line 217). This could result in the server starting with no host configurations loaded. Consider moving the HTTP template server startup after the hosts are loaded to ensure host configurations are available when the server starts handling requests.

Copilot uses AI. Check for mistakes.

if config.HostsDir != "" {
if err := HostManager.LoadFromDirectory(config.HostsDir); err != nil {
return fmt.Errorf("failed to load hosts from directory: %w", err)
Expand Down Expand Up @@ -252,6 +271,14 @@ func Execute() error {
log.Errorf("Failed to shutdown SPOA: %v", shutdownErr)
}

// Shutdown HTTP template server if it was started
if httpTemplateServer != nil {
log.Info("Shutting down HTTP template server")
if shutdownErr := httpTemplateServer.Shutdown(shutdownCtx); shutdownErr != nil {
log.Errorf("Failed to shutdown HTTP template server: %v", shutdownErr)
}
}

// Return error only if it was unexpected
if err != nil && !isExpectedShutdown {
return err
Expand Down
16 changes: 16 additions & 0 deletions config/crowdsec-spoa-bouncer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,19 @@ prometheus:
enabled: false
listen_addr: 127.0.0.1
listen_port: 60601

## HTTP Template Server (Experimental)
# This feature allows HAProxy to use an HTTP backend instead of Lua scripts for template rendering
# When enabled, HAProxy can be configured to proxy requests to this server instead of using Lua
# Uses Go native templates (.tmpl files) instead of Lua-style templates
http_template_server:
enabled: false
listen_addr: 127.0.0.1
listen_port: 8081
# Optional: Custom template paths (defaults to .tmpl files in standard locations if not specified)
# ban_template_path: /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.tmpl
# captcha_template_path: /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.tmpl
tls:
enabled: false
# cert_file: /path/to/cert.pem
# key_file: /path/to/key.pem
74 changes: 74 additions & 0 deletions config/haproxy-httptemplate.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# https://www.haproxy.com/documentation/hapee/latest/onepage/#home
# HAProxy configuration example using HTTP Template Server instead of Lua
# This demonstrates the experimental HTTP template server feature

global
log stdout format raw local0

defaults
log global
option httplog
timeout client 1m
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
Comment on lines +12 to +16
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

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

Inconsistent use of tabs and spaces for indentation. Lines 12-16 use tabs, but line 11 and others in the file appear to use spaces. The file should use consistent indentation throughout (either all tabs or all spaces).

Suggested change
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket

Copilot uses AI. Check for mistakes.

frontend test
mode http
bind *:9090

unique-id-format %[uuid()]
unique-id-header X-Unique-ID
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

## Define ACLs for remediation types
acl is_ban var(txn.crowdsec.remediation) -m str "ban"
acl is_captcha var(txn.crowdsec.remediation) -m str "captcha"
acl is_allow var(txn.crowdsec.remediation) -m str "allow"

## Set headers for HTTP template server
## IMPORTANT: Remove any user-provided headers first for security, then set from transaction variables
## The server will look up host configuration using the Host header (automatically forwarded by HAProxy)
## Only the remediation type needs to be sent - all other data comes from host configuration
http-request del-header X-Crowdsec-Remediation
http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }

## Set a custom header on the request for upstream services to use
http-request set-header X-Crowdsec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if is_allow { var(txn.crowdsec.redirect) -m found }

## Route to HTTP template server for ban and captcha remediations
## Instead of using Lua, we proxy to the HTTP template server
## The server uses a catch-all route, so no path rewriting is needed
## Use OR operator (||) to combine ACLs for cleaner configuration
use_backend http_template_server if is_ban || is_captcha

## Handle captcha cookie management via HAProxy
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }

## Default backend for allowed requests
use_backend test_backend

backend test_backend
mode http
server s1 whoami:2020

backend crowdsec-spoa
mode tcp
balance roundrobin
server s2 spoa:9000
server s3 spoa:9001

backend http_template_server
mode http
# Proxy to the HTTP template server
# The server will read X-Crowdsec-Remediation header to determine which template to render
server template_server spoa:8081

8 changes: 4 additions & 4 deletions config/haproxy-upstreamproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ global
log stdout format raw local0
lua-prepend-path /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/?.lua
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua
setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/captcha.html
setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.html

defaults
log global
Expand Down Expand Up @@ -41,9 +41,9 @@ frontend test
# tcp-request content reject if { var(txn.crowdsec.remediation) -m str "ban" }

## Set a custom header on the request for upstream services to use
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
## Set a custom header on the request for upstream services to use
http-request set-header X-CrowdSec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }
http-request set-header X-Crowdsec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
Expand Down
8 changes: 4 additions & 4 deletions config/haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ global
log stdout format raw local0
lua-prepend-path /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/?.lua
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua
setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/captcha.html
setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.html

defaults
log global
Expand All @@ -31,9 +31,9 @@ frontend test
# tcp-request content reject if { var(txn.crowdsec.remediation) -m str "ban" }

## Set a custom header on the request for upstream services to use
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
## Set a custom header on the request for upstream services to use
http-request set-header X-CrowdSec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }
http-request set-header X-Crowdsec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
Expand Down
4 changes: 3 additions & 1 deletion debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ override_dh_auto_install:
install -m 644 -D "lua/template.lua" "debian/$$PKG/usr/lib/$$PKG/lua/template.lua"; \
mkdir -p "debian/$$PKG/var/lib/$$PKG/html"; \
install -m 644 -D "templates/ban.html" "debian/$$PKG/var/lib/$$PKG/html/ban.html"; \
install -m 644 -D "templates/captcha.html" "debian/$$PKG/var/lib/$$PKG/html/captcha.html"
install -m 644 -D "templates/captcha.html" "debian/$$PKG/var/lib/$$PKG/html/captcha.html"; \
install -m 644 -D "templates/ban.tmpl" "debian/$$PKG/var/lib/$$PKG/html/ban.tmpl"; \
install -m 644 -D "templates/captcha.tmpl" "debian/$$PKG/var/lib/$$PKG/html/captcha.tmpl"

execute_after_dh_fixperms:
@BOUNCER=crowdsec-spoa-bouncer; \
Expand Down
68 changes: 68 additions & 0 deletions docker-compose.httptemplate-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
services:
spoa:
image: crowdsecurity/crowdsec-spoa:latest
build:
context: .
dockerfile: Dockerfile
depends_on:
- crowdsec
volumes:
- sockets:/run/
- geodb:/var/lib/crowdsec/data/
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

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

The spoa service doesn't mount template volumes (unlike the other docker-compose files which mount templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/). The HTTP template server needs access to .tmpl template files. Either add the volume mount or ensure the templates are baked into the Docker image.

Suggested change
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/

Copilot uses AI. Check for mistakes.
networks:
crowdsec:
ipv4_address: 10.5.5.254
deploy:
resources:
limits:
cpus: "4.0"
memory: 250M

whoami:
image: traefik/whoami:latest
networks:
- crowdsec
command:
- --port=2020

haproxy:
image: haproxy:2.9.7-alpine
volumes:
- ./config/haproxy-httptemplate.cfg:/usr/local/etc/haproxy/haproxy.cfg
- ./config/crowdsec.cfg:/etc/haproxy/crowdsec.cfg
- sockets:/run/
ports:
- "9090:9090"
depends_on:
- crowdsec
- spoa
- whoami
networks:
- crowdsec

crowdsec:
image: crowdsecurity/crowdsec:latest
environment:
- BOUNCER_KEY_SPOA=+4iYgItcalc9+0tWrvrj9R6Wded/W1IRwRtNmcWR9Ws
- DISABLE_ONLINE_API=true
- CROWDSEC_BYPASS_DB_VOLUME_CHECK=true
volumes:
- geodb:/staging/var/lib/crowdsec/data/
networks:
- crowdsec

volumes:
sockets:
driver: local
geodb:
driver: local

networks:
crowdsec:
driver: bridge
ipam:
driver: default
config:
- subnet: "10.5.5.0/24"

6 changes: 3 additions & 3 deletions docker-compose.proxy-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ services:
- crowdsec
volumes:
- sockets:/run/
- templates:/var/lib/crowdsec/lua/haproxy/templates/
- lua:/usr/local/crowdsec/lua/haproxy/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
- geodb:/var/lib/crowdsec/data/
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
networks:
Expand All @@ -35,7 +35,7 @@ services:
- ./config/haproxy-upstreamproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
- ./config/crowdsec-upstreamproxy.cfg:/etc/haproxy/crowdsec.cfg
- sockets:/run/
- templates:/var/lib/crowdsec/lua/haproxy/templates/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
# HAProxy is now only accessible via nginx (not exposed directly)
depends_on:
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ services:
- crowdsec
volumes:
- sockets:/run/
- templates:/var/lib/crowdsec/lua/haproxy/templates/
- lua:/usr/local/crowdsec/lua/haproxy/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
- geodb:/var/lib/crowdsec/data/
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
networks:
Expand All @@ -37,7 +37,7 @@ services:
- ./config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
- ./config/crowdsec.cfg:/etc/haproxy/crowdsec.cfg
- sockets:/run/
- templates:/var/lib/crowdsec/lua/haproxy/templates/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
ports:
- "9090:9090"
Expand Down
9 changes: 9 additions & 0 deletions internal/remediation/captcha/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@ func ValidProvider(provider string) bool {
_, ok := providers[provider]
return ok
}

// GetProviderInfo returns the frontend key and JS URL for a given provider
// Returns empty strings if the provider is not found
func GetProviderInfo(provider string) (frontendKey, frontendJS string) {
if info, ok := providers[provider]; ok {
return info.key, info.js
}
return "", ""
}
Loading
Loading