Releases: crowdsecurity/cs-haproxy-spoa-bouncer
v0.3.0
Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
1) SPOE execution now uses spoe-groups (explicit send-spoe-group)
What changed
SPOE execution is now driven by HAProxy spoe-groups and explicit calls to:
http-request send-spoe-group crowdsec <group-name>
This replaces relying on SPOE “event hooks” in the SPOE configuration.
Why this changed
This gives operators full control over when SPOE messages are sent, and allows you to set/override the source IP before the request is evaluated by the SPOA.
Who is impacted
All users upgrading to v0.3.0.
Required migration
-
Update your HAProxy config to explicitly call the relevant SPOE group(s) in your
frontend(s). -
Standardize on the upstream
crowdsec.cfgand stop relying on custom SPOE config logic (for example forcingreq.hdr_ip()usage).- Use the upstream config:
https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/blob/main/config/crowdsec.cfg
- Use the upstream config:
Example
frontend test
mode http
bind *:9090
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
# Ensure HAProxy sees the correct client IP before calling the SPOE group
http-request set-src hdr_ip(X-Real-IP) if { req.hdr(X-Real-IP) -m found }
# Explicitly trigger the SPOE group at the right point in the request lifecycle
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }
Operational note
You now control SPOE evaluation placement explicitly. If you previously relied on event hooks, ensure you add send-spoe-group to every relevant frontend and at the appropriate stage(s).
2) Captcha now requires a persistent signing_key (minimum 32 bytes)
What changed
Captcha moved away from purely server-side in-memory session state and now relies on signed tokens (JWT). As a result, a signing_key is now required.
Why this changed
Previously, restarting the SPOA invalidated in-memory captcha state, forcing users to re-complete captcha regardless of TTLs. Signed tokens allow captcha state to remain verifiable across restarts.
Who is impacted
All users with captcha enabled.
Required migration
Add signing_key to your captcha configuration (minimum 32 bytes):
hosts:
- host: "*.example.com"
captcha:
site_key: "123"
secret_key: "456"
provider: "hcaptcha"
timeout: 10 # HTTP client timeout in seconds (default: 5)
pending_ttl: "30m" # TTL for pending captcha tokens (default: 30m)
passed_ttl: "24h" # TTL for passed captcha tokens (default: 24h)
signing_key: "your-32-byte-minimum-secret-key-here" # REQUIRED in 0.3.0
- host: "*"
captcha:
fallback_remediation: allow
Change redirection logic to use %[url] instead of %[var(txn.crowdsec.redirect)]
http-request redirect code 302 location %[url] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
note we found an issue for some users where without defining the following settings, the validation requests were being rejected
defaults
option http-buffer-request
Multi-SPOA / HA setups
If you run multiple SPOA instances serving the same domains, use the same signing_key across all deployments so tokens validate consistently.
Key generation
openssl rand -hex 32
New Features
AppSec (WAF evaluation via SPOA)
The SPOA bouncer can now forward requests to CrowdSec AppSec for WAF evaluation, enabling HAProxy to provide:
- IP-based remediation (ban/captcha/allow)
- Request filtering via AppSec rules (WAF)
Docs:
- Intro: https://docs.crowdsec.net/docs/next/appsec/intro
- Quickstart: https://docs.crowdsec.net/docs/next/appsec/quickstart/general_setup
Enable AppSec forwarding in the SPOA bouncer
After following the quickstart, configure the SPOA bouncer with a global AppSec endpoint and optional per-host overrides:
# Global AppSec URL (optional)
api_key: 12345
appsec_url: http://127.0.0.1:7422
hosts:
- host: "*"
appsec:
always_send: false # Only validate if not already banned/captcha'd
# url: http://custom-appsec:7422 # Optional per-host override
# api_key: custom-key # Optional per-host override
Behavior note
With always_send: false, AppSec evaluation is skipped when a request already has a higher-priority remediation outcome (for example, ban or captcha). This reduces unnecessary WAF calls and latency.
AppSec limitations and required HAProxy settings
Because HAProxy is optimized for high-throughput proxying, request body inspection through SPOE/SPOP has hard constraints.
Body access is limited by tune.bufsize
You have limited access to request bodies via tune.bufsize. The value can only go up to 65536 (64KB), due to an underlying library limitation:
global
log stdout format raw local0
tune.bufsize 65536 # 64KB - increased for WAF body inspection
Request buffering must be enabled
To allow body inspection/forwarding, enable request buffering in defaults or the relevant frontend:
defaults
option http-buffer-request
Choosing the correct SPOE group
Two SPOE groups are provided to make performance vs inspection explicit:
-
crowdsec-http-body- Sends request body (when available/within limits)
- Required if you use captcha (captcha validation requires body-capable handling)
-
crowdsec-http-no-body- Avoids body forwarding for lower overhead
- Suitable for “phase 1” WAF checks and IP remediation when you do not need body inspection
Recommended layered approach
If you want full-depth inspection:
- Use HAProxy SPOA for IP remediation and phase 1 WAF signals (often
crowdsec-http-no-body) - Use a downstream component (for example, the Nginx remediation component) for full body inspection and deeper WAF enforcement
This keeps HAProxy fast while enabling deeper inspection where it is most effective.
What’s Changed
- feat: refactor captcha to stateless JWT-based design (#146) @LaurenceJJones
- CI: docker build (#132) @mmetc
- feat(prometheus): Add duration metrics for SPOA message processing (#135) @LaurenceJJones
- feat(spoe): Migrate implementation to dropmorepackets/haproxy-go (#138) @LaurenceJJones
- fix(prometheus): add graceful shutdown for server (#137) @LaurenceJJones
- feat(dockerfile): migrate to scratch-based image (#143) @LaurenceJJones
- feat(appsec): implementation and tests (#63) @LaurenceJJones
v0.3.0-rc1
Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
1) SPOE execution now uses spoe-groups (explicit send-spoe-group)
What changed
SPOE execution is now driven by HAProxy spoe-groups and explicit calls to:
http-request send-spoe-group crowdsec <group-name>
This replaces relying on SPOE “event hooks” in the SPOE configuration.
Why this changed
This gives operators full control over when SPOE messages are sent, and allows you to set/override the source IP before the request is evaluated by the SPOA.
Who is impacted
All users upgrading to v0.3.0.
Required migration
-
Update your HAProxy config to explicitly call the relevant SPOE group(s) in your
frontend(s). -
Standardize on the upstream
crowdsec.cfgand stop relying on custom SPOE config logic (for example forcingreq.hdr_ip()usage).- Use the upstream config:
https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/blob/main/config/crowdsec.cfg
- Use the upstream config:
Example
frontend test
mode http
bind *:9090
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
# Ensure HAProxy sees the correct client IP before calling the SPOE group
http-request set-src hdr_ip(X-Real-IP) if { req.hdr(X-Real-IP) -m found }
# Explicitly trigger the SPOE group at the right point in the request lifecycle
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }
Operational note
You now control SPOE evaluation placement explicitly. If you previously relied on event hooks, ensure you add send-spoe-group to every relevant frontend and at the appropriate stage(s).
2) Captcha now requires a persistent signing_key (minimum 32 bytes)
What changed
Captcha moved away from purely server-side in-memory session state and now relies on signed tokens (JWT). As a result, a signing_key is now required.
Why this changed
Previously, restarting the SPOA invalidated in-memory captcha state, forcing users to re-complete captcha regardless of TTLs. Signed tokens allow captcha state to remain verifiable across restarts.
Who is impacted
All users with captcha enabled.
Required migration
Add signing_key to your captcha configuration (minimum 32 bytes):
hosts:
- host: "*.example.com"
captcha:
site_key: "123"
secret_key: "456"
provider: "hcaptcha"
timeout: 10 # HTTP client timeout in seconds (default: 5)
pending_ttl: "30m" # TTL for pending captcha tokens (default: 30m)
passed_ttl: "24h" # TTL for passed captcha tokens (default: 24h)
signing_key: "your-32-byte-minimum-secret-key-here" # REQUIRED in 0.3.0
- host: "*"
captcha:
fallback_remediation: allow
Change redirection logic to use %[url] instead of %[var(txn.crowdsec.redirect)]
http-request redirect code 302 location %[url] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
note we found an issue for some users where without defining the following settings, the validation requests were being rejected
defaults
option http-buffer-request
Multi-SPOA / HA setups
If you run multiple SPOA instances serving the same domains, use the same signing_key across all deployments so tokens validate consistently.
Key generation
openssl rand -hex 32
New Features
AppSec (WAF evaluation via SPOA)
The SPOA bouncer can now forward requests to CrowdSec AppSec for WAF evaluation, enabling HAProxy to provide:
- IP-based remediation (ban/captcha/allow)
- Request filtering via AppSec rules (WAF)
Docs:
- Intro: https://docs.crowdsec.net/docs/next/appsec/intro
- Quickstart: https://docs.crowdsec.net/docs/next/appsec/quickstart/general_setup
Enable AppSec forwarding in the SPOA bouncer
After following the quickstart, configure the SPOA bouncer with a global AppSec endpoint and optional per-host overrides:
# Global AppSec URL (optional)
api_key: 12345
appsec_url: http://127.0.0.1:7422
hosts:
- host: "*"
appsec:
always_send: false # Only validate if not already banned/captcha'd
# url: http://custom-appsec:7422 # Optional per-host override
# api_key: custom-key # Optional per-host override
Behavior note
With always_send: false, AppSec evaluation is skipped when a request already has a higher-priority remediation outcome (for example, ban or captcha). This reduces unnecessary WAF calls and latency.
AppSec limitations and required HAProxy settings
Because HAProxy is optimized for high-throughput proxying, request body inspection through SPOE/SPOP has hard constraints.
Body access is limited by tune.bufsize
You have limited access to request bodies via tune.bufsize. The value can only go up to 65536 (64KB), due to an underlying library limitation:
global
log stdout format raw local0
tune.bufsize 65536 # 64KB - increased for WAF body inspection
Request buffering must be enabled
To allow body inspection/forwarding, enable request buffering in defaults or the relevant frontend:
defaults
option http-buffer-request
Choosing the correct SPOE group
Two SPOE groups are provided to make performance vs inspection explicit:
-
crowdsec-http-body- Sends request body (when available/within limits)
- Required if you use captcha (captcha validation requires body-capable handling)
-
crowdsec-http-no-body- Avoids body forwarding for lower overhead
- Suitable for “phase 1” WAF checks and IP remediation when you do not need body inspection
Recommended layered approach
If you want full-depth inspection:
- Use HAProxy SPOA for IP remediation and phase 1 WAF signals (often
crowdsec-http-no-body) - Use a downstream component (for example, the Nginx remediation component) for full body inspection and deeper WAF enforcement
This keeps HAProxy fast while enabling deeper inspection where it is most effective.
What’s Changed
- feat: refactor captcha to stateless JWT-based design (#146) @LaurenceJJones
- CI: docker build (#132) @mmetc
- feat(prometheus): Add duration metrics for SPOA message processing (#135) @LaurenceJJones
- feat(spoe): Migrate implementation to dropmorepackets/haproxy-go (#138) @LaurenceJJones
- fix(prometheus): add graceful shutdown for server (#137) @LaurenceJJones
- feat(dockerfile): migrate to scratch-based image (#143) @LaurenceJJones
- feat(appsec): implementation and tests (#63) @LaurenceJJones
v0.2.1
What's Changed
No breaking changes as minor patch version
Warning
We plan to release this around a CrowdSec release make sure to update either CrowdSec OR this remediation firstly, do not update both at the same time as the LAPI will be unavailable when the remediation comes up and will fail to connect unless retry_initial_connect configuration is set.
🚀 New Features
- BART-Based Trie for Ranges – Replaced O(n) CIDR lookups with a hardware-accelerated path-compressed trie for faster decision matching when dealing with IP ranges. The trie now correctly implements longest prefix match (LPM) so more specific ranges override general ones. In the current hybrid design, BART is only used for range-based remediations, while single IP decisions stay on a simpler, fast path. (#66)
Explanation for BART
Previously, we stored decision ranges as a slice and checked them one by one:
// sudo code example of old approach: linear scan over a slice of CIDR ranges.
for _, cidr := range rangeSet {
if cidr.Contains(ip) {
return remediationFor(cidr)
}
}
This was simple, but had two important drawbacks:
-
Order-dependent (“first match wins”)
The first matching CIDR in the slice was used, even if a more specific range existed later. That makes decision order matter and can give surprising results. -
O(n) lookup time
In the worst case, every lookup walks the entire slice. As the number of rangesngrows, misses and broad scans become slower.
With BART we now use a path-compressed trie for range storage and lookups. Instead of scanning all ranges, lookups follow the bits of the IP address through the trie:
- Lookup complexity is O(k) where
kis the number of bits in the address (at most 32 for IPv4, 128 for IPv6). - The number of steps depends on the address length, not on how many prefixes you have.
- The trie naturally performs longest-prefix match, so more specific ranges always win over broader ones, regardless of insertion order. EG:
192.168.0.0/16vs192.168.1.0/24the latter wins for192.168.1.160.
In practice: even with hundreds or thousands of ranges, lookups stay fast and deterministic.
- pprof Debug Endpoint – Added an optional HTTP endpoint exposing Go pprof handlers, making it easier to capture CPU/heap profiles from live instances during load tests and production debugging. (#127)
⚡ Performance & Architecture Improvements
-
Global Session Management – Consolidated per-host session managers into a single global manager, reducing goroutine overhead and preparing the ground for future AppSec integration. (#113)
-
Simplified Signal Handling – Improved shutdown reliability using Go’s native signal.NotifyContext, ensuring graceful termination on SIGTERM/SIGINT. (#103)
-
HAProxy SPOE Library Upgrade – Upgraded to v1.0.7 with automatic workgroup handling, simplifying code and improving reliability. (#119)
-
Host Runtime Cleanup – Removed unused runtime operations in the host management logic, simplifying the code and shaving off minor overhead. (#124)
-
Hybrid IP Storage & Parallel Batch Processing – Reworked the dataset backend to split storage between single IPs and ranges. BART is now dedicated to CIDR ranges for true LPM, while single IP decisions are handled via a straightforward map-based fast path. Batch updates are processed in parallel to reduce lock contention and speed up large decision syncs. (#128)
-
Prometheus Metrics Allocation – Switched to WithLabelValues for metric labelling to avoid per-call map allocations in hot paths, reducing overhead in high-traffic environments. (#130)
🔧 Bug Fixes
-
Geo Database Initialization – Fixed database loading issues and improved error handling for missing or invalid GeoIP databases. (#121)
-
Dataset Stability – Fixed a potential nil map panic in dataset handling and removed redundant Clone calls, improving safety and reducing unnecessary allocations in hot paths. (#125)
-
Decision Stream GC Improvements – Broke long-lived string references to DecisionsStreamResponse payloads so large updates can be garbage-collected promptly, reducing memory pressure during intensive syncs. (#126)
📦 Dependencies & Maintenance
-
GeoIP2 Library Upgrade – Updated to v2.0.0 with modern netip.Addr support, improving performance and type safety. (#118)
-
Development Tools – Added .vagrant/ to .gitignore for cleaner development workflows. (#117)
Full Changelog: v0.2.0...v0.2.1
v0.2.1-rc3
What's Changed
🚀 New Features
-
BART-Based Trie for Ranges – Replaced O(n) CIDR lookups with a hardware-accelerated path-compressed trie for faster decision matching when dealing with IP ranges. The trie now correctly implements longest prefix match (LPM) so more specific ranges override general ones. In the current hybrid design, BART is only used for range-based remediations, while single IP decisions stay on a simpler, fast path. (#66)
-
pprof Debug Endpoint – Added an optional HTTP endpoint exposing Go
pprofhandlers, making it easier to capture CPU/heap profiles from live instances during load tests and production debugging. (#127)
⚡ Performance & Architecture Improvements
-
Global Session Management – Consolidated per-host session managers into a single global manager, reducing goroutine overhead and preparing the ground for future AppSec integration. (#113)
-
Simplified Signal Handling – Improved shutdown reliability using Go’s native
signal.NotifyContext, ensuring graceful termination onSIGTERM/SIGINT. (#103) -
HAProxy SPOE Library Upgrade – Upgraded to v1.0.7 with automatic workgroup handling, simplifying code and improving reliability. (#119)
-
Host Runtime Cleanup – Removed unused runtime operations in the host management logic, simplifying the code and shaving off minor overhead. (#124)
-
Hybrid IP Storage & Parallel Batch Processing – Reworked the dataset backend to split storage between single IPs and ranges. BART is now dedicated to CIDR ranges for true LPM, while single IP decisions are handled via a straightforward map-based fast path. Batch updates are processed in parallel to reduce lock contention and speed up large decision syncs. (#128)
-
Prometheus Metrics Allocation – Switched to
WithLabelValuesfor metric labelling to avoid per-call map allocations in hot paths, reducing overhead in high-traffic environments. (#130)
🔧 Bug Fixes
-
Geo Database Initialization – Fixed database loading issues and improved error handling for missing or invalid GeoIP databases. (#121)
-
Dataset Stability – Fixed a potential nil map panic in dataset handling and removed redundant
Clonecalls, improving safety and reducing unnecessary allocations in hot paths. (#125) -
Decision Stream GC Improvements – Broke long-lived string references to
DecisionsStreamResponsepayloads so large updates can be garbage-collected promptly, reducing memory pressure during intensive syncs. (#126)
📦 Dependencies & Maintenance
-
GeoIP2 Library Upgrade – Updated to v2.0.0 with modern
netip.Addrsupport, improving performance and type safety. (#118) -
Development Tools – Added
.vagrant/to.gitignorefor cleaner development workflows. (#117)
Full Changelog: v0.2.0...v0.2.1-rc3
v0.2.1-rc2
What's Changed
🚀 New Features
- BART-Based Trie Implementation – Replaced O(n) CIDR lookups with a hardware-accelerated path-compressed trie for faster decision matching, especially with large numbers of decisions. Now correctly implements longest prefix match (LPM) so more specific rules override general ones. (#66)
⚡ Performance & Architecture Improvements
-
Global Session Management – Consolidated per-host session managers into a single global manager, reducing goroutine overhead and preparing for future AppSec integration. (#113)
-
Simplified Signal Handling – Improved shutdown reliability using Go's native
signal.NotifyContext, ensuring graceful termination on SIGTERM/SIGINT. (#103) -
HAProxy SPOE Library Upgrade – Upgraded to v1.0.7 with automatic workgroup handling, simplifying code and improving reliability. (#119)
-
Host Runtime Cleanup – Removed unused runtime operations in the host management logic, simplifying the code and shaving off minor overhead. (#124)
🔧 Bug Fixes
-
Geo Database Initialization – Fixed database loading issues and improved error handling for missing or invalid GeoIP databases. (#121)
-
Dataset Stability – Fixed a potential nil map panic in dataset handling and removed redundant
Clonecalls, improving safety and reducing unnecessary allocations in hot paths. (#125)
📦 Dependencies & Maintenance
-
GeoIP2 Library Upgrade – Updated to v2.0.0 with modern
netip.Addrsupport, improving performance and type safety. (#118) -
Development Tools – Added
.vagrant/to.gitignorefor cleaner development workflows. (#117)
Full Changelog: v0.2.0...v0.2.1-rc2
v0.2.1-rc1
What's Changed
🚀 New Features
- BART-Based Trie Implementation - Replaced O(n) CIDR lookups with a hardware-accelerated path-compressed trie for faster decision matching, especially with large numbers of decisions. Now correctly implements longest prefix match (LPM) so more specific rules override general ones. (#66)
⚡ Performance & Architecture Improvements
-
Global Session Management - Consolidated per-host session managers into a single global manager, reducing goroutine overhead and preparing for future AppSec integration. (#113)
-
Simplified Signal Handling - Improved shutdown reliability using Go's native
signal.NotifyContext, ensuring graceful termination on SIGTERM/SIGINT. (#103) -
HAProxy SPOE Library Upgrade - Upgraded to v1.0.7 with automatic workgroup handling, simplifying code and improving reliability. (#119)
🔧 Bug Fixes
- Geo Database Initialization - Fixed database loading issues and improved error handling for missing or invalid GeoIP databases. (#121)
📦 Dependencies & Maintenance
-
GeoIP2 Library Upgrade - Updated to v2.0.0 with modern
netip.Addrsupport, improving performance and type safety. (#118) -
Development Tools - Added
.vagrant/to.gitignorefor cleaner development workflows. (#117)
Full Changelog: v0.2.0...v0.2.1-rc1
v0.2.0
Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
TL;DR (Key Changes)
- Parent/worker model removed – the SPOA now runs as a single process.
workers,worker_user,worker_groupoptions removed – replaced bylisten_tcp/listen_unix.admin_socketremoved – the setting is now ignored and can be deleted.- Service now runs fully as
crowdsec-spoauser – config/log file permissions must allow this. - Docker image now runs as
crowdsec-spoa. - Default log directory moved to
/var/log/crowdsec-spoa/(config change required).
Inner Architecture
In short:
We removed the parent/worker architecture and now run a simpler, single-process model.
Background
Previously:
-
Multiple workers handled incoming SPOE messages from HAProxy.
-
Each worker communicated with a parent process over a Unix socket to fetch:
- State
- Decisions
- Host configuration (e.g. ban / captcha)
This design existed to support multiple SPOA listeners from a single process.
In practice, we found that users almost never configured more than one SPOA listener.
The added complexity:
- Slowed down feature development,
- Increased code debt,
- Made onboarding new contributors harder.
As a result, we have simplified the architecture to a single process that directly handles SPOE messages.
Configuration Changes
We have removed the following YAML options:
workersworker_userworker_group
These are replaced with:
listen_tcp: 127.0.0.1:9090
listen_unix: /path/to/unix.sock
Recommendation:
Add these keys to your configuration before upgrading to avoid startup failures.
Admin Socket
Because we no longer support multiple SPOA listeners, the admin socket no longer provides value.
- The following option has been removed and is now ignored:
admin_socket: /path/to/admin.sock
You can safely remove this key from your configuration.
We know users still need a way to reload the SPOA without dropping in-flight messages from HAProxy.
In a future release, we plan to add a proper systemctl reload hook to handle this cleanly.
Process Owner (systemd Unit)
With the removal of the parent/worker model and the admin socket, the systemd unit now runs entirely as the crowdsec-spoa user.
This improves separation between service accounts and root, but it also means:
- All files needed by the SPOA must be readable by the
crowdsec-spoagroup.
For example:
chown root:crowdsec-spoa /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
chmod 640 /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
If you have created .local variants of these configuration files, please run equivalent commands for those files as well.
Note:
The DEB/RPM packages are patched to adjust these permissions on upgrade.
If you encounter issues, manually validate permissions/ownership and open an issue on this repository if problems persist.
Dockerfile
The Docker image has also been updated to run as the crowdsec-spoa user.
If you are building the container locally and mounting configuration files, ensure that:
- The mounted configuration is readable by
crowdsec-spoa.
Log Files
Since the systemd unit now runs as crowdsec-spoa, it can no longer write to the root-owned default /var/log directly.
The unit is configured to create a dedicated directory:
/var/log/crowdsec-spoa
Please update your YAML configuration accordingly:
log_dir: /var/log/crowdsec-spoa/
Note:
The DEB/RPM packages may not clean up old log files in previous locations.
After the upgrade, you may want to manually remove any obsolete log files or directories.
v0.2.0-rc5
Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
TL;DR (Key Changes)
- Parent/worker model removed – the SPOA now runs as a single process.
workers,worker_user,worker_groupoptions removed – replaced bylisten_tcp/listen_unix.admin_socketremoved – the setting is now ignored and can be deleted.- Service now runs fully as
crowdsec-spoauser – config/log file permissions must allow this. - Docker image now runs as
crowdsec-spoa. - Default log directory moved to
/var/log/crowdsec-spoa/(config change required).
Inner Architecture
In short:
We removed the parent/worker architecture and now run a simpler, single-process model.
Background
Previously:
-
Multiple workers handled incoming SPOE messages from HAProxy.
-
Each worker communicated with a parent process over a Unix socket to fetch:
- State
- Decisions
- Host configuration (e.g. ban / captcha)
This design existed to support multiple SPOA listeners from a single process.
In practice, we found that users almost never configured more than one SPOA listener.
The added complexity:
- Slowed down feature development,
- Increased code debt,
- Made onboarding new contributors harder.
As a result, we have simplified the architecture to a single process that directly handles SPOE messages.
Configuration Changes
We have removed the following YAML options:
workersworker_userworker_group
These are replaced with:
listen_tcp: 127.0.0.1:9090
listen_unix: /path/to/unix.sock
Recommendation:
Add these keys to your configuration before upgrading to avoid startup failures.
Admin Socket
Because we no longer support multiple SPOA listeners, the admin socket no longer provides value.
- The following option has been removed and is now ignored:
admin_socket: /path/to/admin.sock
You can safely remove this key from your configuration.
We know users still need a way to reload the SPOA without dropping in-flight messages from HAProxy.
In a future release, we plan to add a proper systemctl reload hook to handle this cleanly.
Process Owner (systemd Unit)
With the removal of the parent/worker model and the admin socket, the systemd unit now runs entirely as the crowdsec-spoa user.
This improves separation between service accounts and root, but it also means:
- All files needed by the SPOA must be readable by the
crowdsec-spoagroup.
For example:
chown root:crowdsec-spoa /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
chmod 640 /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
If you have created .local variants of these configuration files, please run equivalent commands for those files as well.
Note:
The DEB/RPM packages are patched to adjust these permissions on upgrade.
If you encounter issues, manually validate permissions/ownership and open an issue on this repository if problems persist.
Dockerfile
The Docker image has also been updated to run as the crowdsec-spoa user.
If you are building the container locally and mounting configuration files, ensure that:
- The mounted configuration is readable by
crowdsec-spoa.
Log Files
Since the systemd unit now runs as crowdsec-spoa, it can no longer write to the root-owned default /var/log directly.
The unit is configured to create a dedicated directory:
/var/log/crowdsec-spoa
Please update your YAML configuration accordingly:
log_dir: /var/log/crowdsec-spoa/
Note:
The DEB/RPM packages may not clean up old log files in previous locations.
After the upgrade, you may want to manually remove any obsolete log files or directories.
v0.2.0-rc4
Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
TL;DR (Key Changes)
- Parent/worker model removed – the SPOA now runs as a single process.
workers,worker_user,worker_groupoptions removed – replaced bylisten_tcp/listen_unix.admin_socketremoved – the setting is now ignored and can be deleted.- Service now runs fully as
crowdsec-spoauser – config/log file permissions must allow this. - Docker image now runs as
crowdsec-spoa. - Default log directory moved to
/var/log/crowdsec-spoa/(config change required).
Inner Architecture
In short:
We removed the parent/worker architecture and now run a simpler, single-process model.
Background
Previously:
-
Multiple workers handled incoming SPOE messages from HAProxy.
-
Each worker communicated with a parent process over a Unix socket to fetch:
- State
- Decisions
- Host configuration (e.g. ban / captcha)
This design existed to support multiple SPOA listeners from a single process.
In practice, we found that users almost never configured more than one SPOA listener.
The added complexity:
- Slowed down feature development,
- Increased code debt,
- Made onboarding new contributors harder.
As a result, we have simplified the architecture to a single process that directly handles SPOE messages.
Configuration Changes
We have removed the following YAML options:
workersworker_userworker_group
These are replaced with:
listen_tcp: 127.0.0.1:9090
listen_unix: /path/to/unix.sock
Recommendation:
Add these keys to your configuration before upgrading to avoid startup failures.
Admin Socket
Because we no longer support multiple SPOA listeners, the admin socket no longer provides value.
- The following option has been removed and is now ignored:
admin_socket: /path/to/admin.sock
You can safely remove this key from your configuration.
We know users still need a way to reload the SPOA without dropping in-flight messages from HAProxy.
In a future release, we plan to add a proper systemctl reload hook to handle this cleanly.
Process Owner (systemd Unit)
With the removal of the parent/worker model and the admin socket, the systemd unit now runs entirely as the crowdsec-spoa user.
This improves separation between service accounts and root, but it also means:
- All files needed by the SPOA must be readable by the
crowdsec-spoagroup.
For example:
chown root:crowdsec-spoa /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
chmod 640 /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
If you have created .local variants of these configuration files, please run equivalent commands for those files as well.
Note:
The DEB/RPM packages are patched to adjust these permissions on upgrade.
If you encounter issues, manually validate permissions/ownership and open an issue on this repository if problems persist.
Dockerfile
The Docker image has also been updated to run as the crowdsec-spoa user.
If you are building the container locally and mounting configuration files, ensure that:
- The mounted configuration is readable by
crowdsec-spoa.
Log Files
Since the systemd unit now runs as crowdsec-spoa, it can no longer write to the root-owned default /var/log directly.
The unit is configured to create a dedicated directory:
/var/log/crowdsec-spoa
Please update your YAML configuration accordingly:
log_dir: /var/log/crowdsec-spoa/
Note:
The DEB/RPM packages may not clean up old log files in previous locations.
After the upgrade, you may want to manually remove any obsolete log files or directories.
v0.2.0-rc3
Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
TL;DR (Key Changes)
- Parent/worker model removed – the SPOA now runs as a single process.
workers,worker_user,worker_groupoptions removed – replaced bylisten_tcp/listen_unix.admin_socketremoved – the setting is now ignored and can be deleted.- Service now runs fully as
crowdsec-spoauser – config/log file permissions must allow this. - Docker image now runs as
crowdsec-spoa. - Default log directory moved to
/var/log/crowdsec-spoa/(config change required).
Inner Architecture
In short:
We removed the parent/worker architecture and now run a simpler, single-process model.
Background
Previously:
-
Multiple workers handled incoming SPOE messages from HAProxy.
-
Each worker communicated with a parent process over a Unix socket to fetch:
- State
- Decisions
- Host configuration (e.g. ban / captcha)
This design existed to support multiple SPOA listeners from a single process.
In practice, we found that users almost never configured more than one SPOA listener.
The added complexity:
- Slowed down feature development,
- Increased code debt,
- Made onboarding new contributors harder.
As a result, we have simplified the architecture to a single process that directly handles SPOE messages.
Configuration Changes
We have removed the following YAML options:
workersworker_userworker_group
These are replaced with:
listen_tcp: 127.0.0.1:9090
listen_unix: /path/to/unix.sock
Recommendation:
Add these keys to your configuration before upgrading to avoid startup failures.
Admin Socket
Because we no longer support multiple SPOA listeners, the admin socket no longer provides value.
- The following option has been removed and is now ignored:
admin_socket: /path/to/admin.sock
You can safely remove this key from your configuration.
We know users still need a way to reload the SPOA without dropping in-flight messages from HAProxy.
In a future release, we plan to add a proper systemctl reload hook to handle this cleanly.
Process Owner (systemd Unit)
With the removal of the parent/worker model and the admin socket, the systemd unit now runs entirely as the crowdsec-spoa user.
This improves separation between service accounts and root, but it also means:
- All files needed by the SPOA must be readable by the
crowdsec-spoagroup.
For example:
chown root:crowdsec-spoa /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
chmod 640 /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
If you have created .local variants of these configuration files, please run equivalent commands for those files as well.
Note:
The DEB/RPM packages are patched to adjust these permissions on upgrade.
If you encounter issues, manually validate permissions/ownership and open an issue on this repository if problems persist.
Dockerfile
The Docker image has also been updated to run as the crowdsec-spoa user.
If you are building the container locally and mounting configuration files, ensure that:
- The mounted configuration is readable by
crowdsec-spoa.
Log Files
Since the systemd unit now runs as crowdsec-spoa, it can no longer write to the root-owned default /var/log directly.
The unit is configured to create a dedicated directory:
/var/log/crowdsec-spoa
Please update your YAML configuration accordingly:
log_dir: /var/log/crowdsec-spoa/
Note:
The DEB/RPM packages may not clean up old log files in previous locations.
After the upgrade, you may want to manually remove any obsolete log files or directories.