Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ 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/

## 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

## 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/
Expand All @@ -34,8 +37,8 @@ COPY --from=build /go/src/cs-spoa-bouncer/lua/* /usr/lib/crowdsec-haproxy-spoa-b
RUN mkdir -p /var/lib/crowdsec-haproxy-spoa-bouncer/html
COPY --from=build /go/src/cs-spoa-bouncer/templates/* /var/lib/crowdsec-haproxy-spoa-bouncer/html/

RUN chown -R root:haproxy /usr/lib/crowdsec-haproxy-spoa-bouncer/lua /var/lib/crowdsec-haproxy-spoa-bouncer/html

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
30 changes: 30 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 @@ -178,12 +179,33 @@ func Execute() error {
}
}

// Create HTTP template server if enabled (after HostManager is created, but before starting)
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)
}
}

if config.HostsDir != "" {
if err := HostManager.LoadFromDirectory(config.HostsDir); err != nil {
return fmt.Errorf("failed to load hosts from directory: %w", err)
}
}

// Start HTTP template server after hosts are loaded to ensure host configurations are available
if httpTemplateServer != nil {
g.Go(func() error {
if err := httpTemplateServer.Serve(ctx); err != nil {
return fmt.Errorf("HTTP template server failed: %w", err)
}
return nil
})
}

// Create single SPOA listener - ultra-simplified architecture
spoaLogger := log.WithField("component", "spoa")

Expand Down Expand Up @@ -228,5 +250,13 @@ 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 err
}
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

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

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
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"

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 "", ""
}
48 changes: 41 additions & 7 deletions pkg/cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,30 @@ type PrometheusConfig struct {
ListenPort string `yaml:"listen_port"`
}

type TLSConfig struct {
Enabled bool `yaml:"enabled"`
CertFile string `yaml:"cert_file"`
KeyFile string `yaml:"key_file"`
}

type HTTPTemplateServerConfig struct {
Enabled bool `yaml:"enabled"`
ListenAddress string `yaml:"listen_addr"`
ListenPort string `yaml:"listen_port"`
TLS TLSConfig `yaml:"tls"`
BanTemplate string `yaml:"ban_template_path"`
CaptchaTemplate string `yaml:"captcha_template_path"`
}

type BouncerConfig struct {
Logging cslogging.LoggingConfig `yaml:",inline"`
Hosts []*host.Host `yaml:"hosts"`
HostsDir string `yaml:"hosts_dir"`
Geo geo.GeoDatabase `yaml:",inline"`
ListenTCP string `yaml:"listen_tcp"`
ListenUnix string `yaml:"listen_unix"`
PrometheusConfig PrometheusConfig `yaml:"prometheus"`
Logging cslogging.LoggingConfig `yaml:",inline"`
Hosts []*host.Host `yaml:"hosts"`
HostsDir string `yaml:"hosts_dir"`
Geo geo.GeoDatabase `yaml:",inline"`
ListenTCP string `yaml:"listen_tcp"`
ListenUnix string `yaml:"listen_unix"`
PrometheusConfig PrometheusConfig `yaml:"prometheus"`
HTTPTemplateServer HTTPTemplateServerConfig `yaml:"http_template_server"`
}

// MergedConfig() returns the byte content of the patched configuration file (with .yaml.local).
Expand Down Expand Up @@ -73,5 +89,23 @@ func (c *BouncerConfig) Validate() error {
return fmt.Errorf("configuration requires at least one listener: set listen_tcp or listen_unix")
}

// Validate HTTP template server configuration if enabled
if c.HTTPTemplateServer.Enabled {
if c.HTTPTemplateServer.ListenAddress == "" {
return fmt.Errorf("http_template_server.listen_addr is required when http_template_server.enabled is true")
}
if c.HTTPTemplateServer.ListenPort == "" {
return fmt.Errorf("http_template_server.listen_port is required when http_template_server.enabled is true")
}
if c.HTTPTemplateServer.TLS.Enabled {
if c.HTTPTemplateServer.TLS.CertFile == "" {
return fmt.Errorf("http_template_server.tls.cert_file is required when http_template_server.tls.enabled is true")
}
if c.HTTPTemplateServer.TLS.KeyFile == "" {
return fmt.Errorf("http_template_server.tls.key_file is required when http_template_server.tls.enabled is true")
}
}
}

return nil
}
Loading
Loading