Skip to content

Commit 5f40b2b

Browse files
Merge main into feature/add-spoa-duration-metrics
Resolved conflicts in pkg/spoa/root.go: - Adopted new dropmorepackets/haproxy-go SPOP library from main - Re-applied duration metrics instrumentation: - CaptchaValidationDuration: timing captcha validation calls - GeoLookupDuration: timing geo database lookups - IPCheckDuration: already instrumented in dataset.CheckIP()
2 parents fd60bc3 + dd56244 commit 5f40b2b

23 files changed

+1502
-539
lines changed

Dockerfile

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,53 @@ FROM golang:${GOVERSION}-alpine AS build
44

55
WORKDIR /go/src/cs-spoa-bouncer
66

7-
RUN apk add --update --no-cache make git
7+
RUN apk add --update --no-cache make git ca-certificates
88
COPY . .
99

1010
RUN make build DOCKER_BUILD=1
1111

12-
FROM alpine:latest
13-
COPY --from=build /go/src/cs-spoa-bouncer/crowdsec-spoa-bouncer /usr/local/bin/crowdsec-spoa-bouncer
14-
COPY --from=build /go/src/cs-spoa-bouncer/config/crowdsec-spoa-bouncer.yaml /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
15-
COPY --from=build /go/src/cs-spoa-bouncer/docker/docker_start.sh /docker_start.sh
12+
# Create directory structure for scratch image (with .keep files so COPY works)
13+
RUN mkdir -p /run/crowdsec-spoa /var/log/crowdsec-spoa && \
14+
touch /run/crowdsec-spoa/.keep /var/log/crowdsec-spoa/.keep
1615

17-
# Set permissions for config file and binary
18-
RUN chmod 644 /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml && \
19-
chmod 755 /usr/local/bin/crowdsec-spoa-bouncer
16+
# Final minimal image
17+
FROM scratch
2018

21-
## Add the same haproxy user as the official haproxy image
22-
RUN addgroup -g 99 -S haproxy && adduser -S -D -H -u 99 -h /var/lib/haproxy -s /sbin/nologin -G haproxy -g haproxy haproxy
23-
## Add worker user
24-
RUN addgroup -S crowdsec-spoa && adduser -S -D -H -s /sbin/nologin -g crowdsec-spoa crowdsec-spoa
19+
# Default environment variables (can be overridden at runtime)
20+
ENV LOG_MODE=stdout \
21+
LOG_LEVEL=info \
22+
CROWDSEC_URL=http://crowdsec:8080/ \
23+
UPDATE_FREQUENCY=10s \
24+
INSECURE_SKIP_VERIFY=false \
25+
LISTEN_TCP=0.0.0.0:9000 \
26+
PROMETHEUS_ENABLED=true \
27+
PROMETHEUS_ADDR=0.0.0.0 \
28+
PROMETHEUS_PORT=6060
2529

26-
## Create a socket for the spoa to inherit crowdsec-spoa:haproxy user from official haproxy image
27-
RUN mkdir -p /run/crowdsec-spoa/ && chown crowdsec-spoa:haproxy /run/crowdsec-spoa/ && chmod 770 /run/crowdsec-spoa/
30+
# Copy CA certificates for HTTPS connections to LAPI
31+
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
2832

29-
## Copy Lua files (matching Debian/RPM paths)
30-
RUN mkdir -p /usr/lib/crowdsec-haproxy-spoa-bouncer/lua
31-
COPY --from=build /go/src/cs-spoa-bouncer/lua/* /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
33+
# Copy the static binary
34+
COPY --from=build /go/src/cs-spoa-bouncer/crowdsec-spoa-bouncer /crowdsec-spoa-bouncer
3235

33-
## Copy templates (matching Debian/RPM paths)
34-
RUN mkdir -p /var/lib/crowdsec-haproxy-spoa-bouncer/html
35-
COPY --from=build /go/src/cs-spoa-bouncer/templates/* /var/lib/crowdsec-haproxy-spoa-bouncer/html/
36+
# Copy Docker-optimized config file
37+
COPY --from=build /go/src/cs-spoa-bouncer/config/crowdsec-spoa-bouncer.docker.yaml /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
3638

37-
RUN chown -R root:haproxy /usr/lib/crowdsec-haproxy-spoa-bouncer/lua /var/lib/crowdsec-haproxy-spoa-bouncer/html
39+
# Copy Lua files for HAProxy integration
40+
COPY --from=build /go/src/cs-spoa-bouncer/lua/ /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
3841

39-
VOLUME [ "/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/", "/var/lib/crowdsec-haproxy-spoa-bouncer/html/" ]
42+
# Copy HTML templates for ban/captcha pages
43+
COPY --from=build /go/src/cs-spoa-bouncer/templates/ /var/lib/crowdsec-haproxy-spoa-bouncer/html/
4044

41-
RUN chmod +x /docker_start.sh
45+
# Copy runtime directories (required for Unix socket and logs)
46+
COPY --from=build /run/crowdsec-spoa/ /run/crowdsec-spoa/
47+
COPY --from=build /var/log/crowdsec-spoa/ /var/log/crowdsec-spoa/
4248

43-
# Run as user
44-
USER crowdsec-spoa
49+
# Declare volumes for customizable content
50+
VOLUME /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
51+
VOLUME /var/lib/crowdsec-haproxy-spoa-bouncer/html/
4552

46-
ENTRYPOINT ["/docker_start.sh"]
53+
EXPOSE 9000 6060
54+
55+
ENTRYPOINT ["/crowdsec-spoa-bouncer"]
56+
CMD ["-c", "/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml"]

Vagrantfile

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# -*- mode: ruby -*-
2+
# vi: set ft=ruby :
3+
4+
Vagrant.configure("2") do |config|
5+
config.vm.define "dev" do |vm|
6+
vm.vm.box = "debian/bookworm64"
7+
vm.vm.hostname = "crowdsec-spoa-test"
8+
vm.vm.network "private_network", ip: "192.168.56.10"
9+
vm.vm.network "forwarded_port", guest: 9090, host: 9090
10+
11+
vm.vm.provider "libvirt" do |lv|
12+
lv.memory = "4096"
13+
lv.cpus = 2
14+
end
15+
16+
vm.vm.synced_folder ".", "/vagrant", type: "rsync", rsync__exclude: [".git/", "node_modules/", "*.log"]
17+
18+
vm.vm.provision "shell", inline: <<-SHELL
19+
set -e
20+
21+
# Update system and install base packages
22+
apt-get update && apt-get upgrade -y
23+
apt-get install -y tcpdump vim curl wget git build-essential ca-certificates \
24+
gnupg lsb-release apt-transport-https software-properties-common nginx unzip
25+
26+
# Install HAProxy 3.1
27+
curl -fsSL https://haproxy.debian.net/haproxy-archive-keyring.gpg \
28+
--create-dirs --output /etc/apt/keyrings/haproxy-archive-keyring.gpg
29+
echo "deb [signed-by=/etc/apt/keyrings/haproxy-archive-keyring.gpg]" \
30+
https://haproxy.debian.net bookworm-backports-3.1 main > /etc/apt/sources.list.d/haproxy.list
31+
apt-get update && apt-get install -y haproxy=3.1.*
32+
33+
# Install Go 1.25.2
34+
GO_VERSION="1.25.2"
35+
wget -qO- "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" | tar -xzC /usr/local
36+
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile
37+
echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/vagrant/.bashrc
38+
39+
# Install CrowdSec
40+
curl -s https://install.crowdsec.net | sh
41+
apt-get install -y crowdsec
42+
43+
# Install Nuclei for AppSec testing
44+
NUCLEI_VERSION="3.1.7"
45+
wget -qO /tmp/nuclei.zip "https://github.com/projectdiscovery/nuclei/releases/download/v${NUCLEI_VERSION}/nuclei_${NUCLEI_VERSION}_linux_amd64.zip"
46+
unzip -q /tmp/nuclei.zip -d /tmp && mv /tmp/nuclei /usr/local/bin/nuclei && chmod +x /usr/local/bin/nuclei
47+
rm -f /tmp/nuclei.zip
48+
nuclei -update-templates -silent 2>/dev/null || true
49+
50+
# Clone CrowdSec Hub
51+
git clone -q https://github.com/crowdsecurity/hub.git /opt/hub || true
52+
53+
# Create user and directories
54+
groupadd -r crowdsec-spoa 2>/dev/null || true
55+
useradd -r -g crowdsec-spoa -d /opt/crowdsec-spoa-bouncer -s /bin/false crowdsec-spoa 2>/dev/null || true
56+
mkdir -p /opt/crowdsec-spoa-bouncer /etc/crowdsec/bouncers /var/log/crowdsec-spoa-bouncer \
57+
/run/crowdsec-spoa /usr/lib/crowdsec-haproxy-spoa-bouncer/lua /var/lib/crowdsec-haproxy-spoa-bouncer/html
58+
chown -R crowdsec-spoa:crowdsec-spoa /opt/crowdsec-spoa-bouncer /var/log/crowdsec-spoa-bouncer /run/crowdsec-spoa
59+
60+
# Copy Lua scripts and templates
61+
mkdir -p /usr/lib/crowdsec-haproxy-spoa-bouncer/lua /var/lib/crowdsec-haproxy-spoa-bouncer/html
62+
cp /vagrant/lua/*.lua /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/ 2>/dev/null || true
63+
cp /vagrant/templates/*.html /var/lib/crowdsec-haproxy-spoa-bouncer/html/ 2>/dev/null || true
64+
chmod 644 /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/*.lua 2>/dev/null || true
65+
chmod 644 /var/lib/crowdsec-haproxy-spoa-bouncer/html/*.html 2>/dev/null || true
66+
67+
# Configure nginx
68+
cat > /etc/nginx/sites-available/default << 'EOF'
69+
server {
70+
listen 4444 default_server;
71+
listen [::]:4444 default_server;
72+
73+
root /var/www/html;
74+
index index.html index.htm index.nginx-debian.html;
75+
76+
server_name _;
77+
78+
location / {
79+
try_files $uri $uri/ =404;
80+
}
81+
}
82+
EOF
83+
84+
# Copy and configure HAProxy
85+
cp /vagrant/config/haproxy.cfg /etc/haproxy/haproxy.cfg 2>/dev/null || true
86+
cp /vagrant/config/crowdsec.cfg /etc/haproxy/crowdsec.cfg 2>/dev/null || true
87+
# Update server addresses and remove the second SPOA server (port 9001 doesn't exist)
88+
sed -i 's/whoami:2020/127.0.0.1:4444/g; s/spoa:9000/127.0.0.1:9000/g; /server s3 spoa:9001/d' \
89+
/etc/haproxy/haproxy.cfg 2>/dev/null || true
90+
# Increase SPOA processing timeout to accommodate AppSec calls (AppSec has 5s timeout)
91+
sed -i 's/timeout\s\+processing\s\+500ms/timeout processing 6s/' \
92+
/etc/haproxy/crowdsec.cfg 2>/dev/null || true
93+
94+
# Copy and configure bouncer
95+
cp /vagrant/config/crowdsec-spoa-bouncer.yaml.local /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml 2>/dev/null || true
96+
# Update URLs (match with or without trailing slash) and API key
97+
sed -i 's|http://crowdsec:8080|http://127.0.0.1:8080|g; s|http://crowdsec:7422|http://127.0.0.1:4241|g; s|api_key:.*|api_key: this_is_a_bad_password|g' \
98+
/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml 2>/dev/null || true
99+
100+
# Configure AppSec before starting CrowdSec
101+
# Install AppSec collections first
102+
cscli collections install crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules || true
103+
104+
# Configure AppSec acquisition
105+
mkdir -p /etc/crowdsec/acquis.d
106+
cat > /etc/crowdsec/acquis.d/appsec.yaml << 'EOF'
107+
appsec_config: crowdsecurity/appsec-default
108+
labels:
109+
type: appsec
110+
listen_addr: 0.0.0.0:4241
111+
source: appsec
112+
EOF
113+
114+
# Now start all services with CrowdSec properly configured
115+
systemctl enable --now nginx haproxy crowdsec
116+
sleep 5
117+
cscli bouncers add crowdsec-spoa-bouncer --key this_is_a_bad_password 2>/dev/null || true
118+
SHELL
119+
120+
vm.vm.provision "shell", run: "always", inline: <<-SHELL
121+
set -e
122+
export PATH=$PATH:/usr/local/go/bin
123+
124+
# Build SPOA bouncer
125+
if [ -f "/vagrant/main.go" ]; then
126+
cd /vagrant
127+
if go build -ldflags="-s -w" -o /opt/crowdsec-spoa-bouncer/crowdsec-spoa-bouncer .; then
128+
chown crowdsec-spoa:crowdsec-spoa /opt/crowdsec-spoa-bouncer/crowdsec-spoa-bouncer
129+
chmod +x /opt/crowdsec-spoa-bouncer/crowdsec-spoa-bouncer
130+
131+
# Install systemd service
132+
cp /vagrant/config/crowdsec-spoa-bouncer.service /etc/systemd/system/crowdsec-spoa-bouncer.service
133+
sed -i 's|${BIN}|/opt/crowdsec-spoa-bouncer/crowdsec-spoa-bouncer|g; s|${CFG}|/etc/crowdsec/bouncers|g' \
134+
/etc/systemd/system/crowdsec-spoa-bouncer.service
135+
sed -i 's|Type=notify|Type=simple|g; /ExecStartPre=/d' \
136+
/etc/systemd/system/crowdsec-spoa-bouncer.service
137+
138+
systemctl daemon-reload
139+
systemctl enable --now crowdsec-spoa-bouncer
140+
fi
141+
fi
142+
143+
# Restart services in order
144+
systemctl restart nginx
145+
sleep 2
146+
systemctl restart crowdsec-spoa-bouncer 2>/dev/null || true
147+
sleep 3
148+
systemctl restart haproxy
149+
150+
# Verify services
151+
for svc in nginx crowdsec-spoa-bouncer haproxy; do
152+
systemctl is-active --quiet $svc && echo "✅ $svc: running" || echo "❌ $svc: failed"
153+
done
154+
SHELL
155+
end
156+
end

cmd/root.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/spf13/pflag"
2020
"golang.org/x/sync/errgroup"
2121

22+
"github.com/crowdsecurity/crowdsec-spoa/internal/appsec"
2223
"github.com/crowdsecurity/crowdsec-spoa/internal/session"
2324
"github.com/crowdsecurity/crowdsec-spoa/pkg/cfg"
2425
"github.com/crowdsecurity/crowdsec-spoa/pkg/dataset"
@@ -132,16 +133,36 @@ func Execute() error {
132133
)
133134

134135
if config.PrometheusConfig.Enabled {
135-
go func() {
136-
http.Handle("/metrics", promhttp.Handler())
136+
promMux := http.NewServeMux()
137+
promMux.Handle("/metrics", promhttp.Handler())
137138

138-
listenOn := net.JoinHostPort(
139-
config.PrometheusConfig.ListenAddress,
140-
config.PrometheusConfig.ListenPort,
141-
)
139+
listenOn := net.JoinHostPort(
140+
config.PrometheusConfig.ListenAddress,
141+
config.PrometheusConfig.ListenPort,
142+
)
143+
144+
promServer := &http.Server{
145+
Addr: listenOn,
146+
Handler: promMux,
147+
}
148+
149+
g.Go(func() error {
142150
log.Infof("Serving metrics at %s", listenOn+"/metrics")
143-
log.Error(http.ListenAndServe(listenOn, nil))
144-
}()
151+
if err := promServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
152+
return fmt.Errorf("prometheus server error: %w", err)
153+
}
154+
return nil
155+
})
156+
157+
g.Go(func() error {
158+
<-ctx.Done()
159+
log.Info("Shutting down prometheus server...")
160+
// Use background context since parent ctx is already canceled
161+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
162+
defer cancel()
163+
//nolint:contextcheck // parent ctx is canceled, need fresh context for shutdown
164+
return promServer.Shutdown(shutdownCtx)
165+
})
145166
}
146167

147168
// pprof debug endpoint for runtime profiling (memory, CPU, goroutines)
@@ -211,6 +232,20 @@ func Execute() error {
211232
hostManagerLogger := log.WithField("component", "host_manager")
212233
HostManager := host.NewManager(hostManagerLogger)
213234

235+
// Create and initialize global AppSec (for use when no host is matched or as fallback)
236+
var globalAppSec *appsec.AppSec
237+
if config.AppSecURL != "" {
238+
globalAppSec = &appsec.AppSec{
239+
URL: config.AppSecURL,
240+
APIKey: config.APIKey,
241+
Timeout: config.AppSecTimeout,
242+
}
243+
appSecLogger := log.WithField("component", "global_appsec")
244+
if err := globalAppSec.Init(appSecLogger); err != nil {
245+
return fmt.Errorf("failed to initialize global AppSec: %w", err)
246+
}
247+
}
248+
214249
// Create and initialize global session manager (single GC goroutine for all hosts)
215250
globalSessions := &session.Sessions{
216251
SessionIdleTimeout: "1h", // Default values
@@ -242,6 +277,7 @@ func Execute() error {
242277
HostManager: HostManager,
243278
GeoDatabase: &config.Geo,
244279
GlobalSessions: globalSessions,
280+
GlobalAppSec: globalAppSec,
245281
Logger: spoaLogger,
246282
}
247283

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## Docker-optimized configuration for CrowdSec SPOA Bouncer
2+
## Environment variables can override settings: ${VAR_NAME}
3+
## If a variable is not set, the default value is used.
4+
5+
## Log configuration
6+
## stdout is recommended for Docker (use `docker logs` to view)
7+
log_mode: ${LOG_MODE}
8+
log_level: ${LOG_LEVEL}
9+
10+
## LAPI configuration
11+
api_url: ${CROWDSEC_URL}
12+
api_key: ${CROWDSEC_KEY}
13+
update_frequency: ${UPDATE_FREQUENCY}
14+
insecure_skip_verify: ${INSECURE_SKIP_VERIFY}
15+
16+
## SPOA listener configuration
17+
listen_tcp: ${LISTEN_TCP}
18+
#listen_unix: ${LISTEN_UNIX}
19+
20+
## GeoIP databases (optional, mount as volumes)
21+
#asn_database_path: /var/lib/crowdsec/data/GeoLite2-ASN.mmdb
22+
#city_database_path: /var/lib/crowdsec/data/GeoLite2-City.mmdb
23+
24+
## Global AppSec configuration (optional)
25+
#appsec_url: ${APPSEC_URL}
26+
#appsec_timeout: ${APPSEC_TIMEOUT}
27+
28+
## Prometheus metrics endpoint
29+
prometheus:
30+
enabled: ${PROMETHEUS_ENABLED}
31+
listen_addr: ${PROMETHEUS_ADDR}
32+
listen_port: ${PROMETHEUS_PORT}
33+
34+
## pprof debug endpoint (disabled by default)
35+
## WARNING: Only enable for debugging, exposes internal runtime data
36+
#pprof:
37+
# enabled: false
38+
# listen_addr: 0.0.0.0
39+
# listen_port: 6070

config/crowdsec-upstreamproxy.cfg

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)