v1.3.20 -- Country geo-blocking emit fix (INCOMPLETE)¶
Fixed in v1.3.21
v1.3.21 ships the architecturally correct fix: panel-side expansion of country bans into the equivalent list of scope=Range LAPI decisions, which the plugin handles natively. See v1.3.21 release notes for the upgrade path. The smoke script scripts/smoke/country-block.sh now PASSes on v1.3.21 stacks after the operator converts a country via POST /api/security/countries/expand.
The "incomplete fix" banner below remains for historical accuracy.
Incomplete fix -- country blocking still does NOT work
Post-merge investigation (Apr 25 2026, same day) showed this release does not actually solve the country-blocking bug. Setting enable_streaming: false does land in the runtime Caddy config, but the upstream hslatman/caddy-crowdsec-bouncer plugin does not handle scope=Country in either stream OR live mode:
- Stream mode rejects non-IP scopes outright (store.go L43-L58):
default: return fmt.Errorf("got unhandled scope: %s", scope). - Live mode (which
enable_streaming: falseselects) queries LAPI withIPEquals: &valueonly -- noScopeEquals. LAPI's IP-equals filter does not match Country-scope decisions. - Zero references to
country,geoip,maxmindin the plugin OR in the underlyinggithub.com/crowdsecurity/go-cs-bouncerlibrary. - No open issue upstream -- this gap has never been filed.
Last verified Apr 25 2026 against plugin commit f1e77b2. Real BR-resolving request still returned HTTP 200 with enable_streaming: false confirmed in runtime config and Country=BR active in cscli decisions list.
Status of country blocking documented since v1.3.17: NOT functional. Apologies for the incorrect documentation.
v1.3.21 will resolve this by expanding Country bans into equivalent Range decisions panel-side, which the plugin handles natively. See docs/planning/v1.3.21-country-expansion.md for the design.
A focused single-bug release. Was intended to close a silent-failure regression that has shipped in argos since at least v1.3.17: the panel emits a Caddy bouncer config that lets the plugin default to stream mode, which only indexes scope=Ip / scope=Range. The flag is correct; the assumption that flipping it solved the problem was wrong.
This was verified Apr 25 2026 with a real request from 149.102.251.103 (BR, AS212238 Datacamp) and Country=BR active in CrowdSec: HTTP 304, no block.
What this release DOES ship: the emit change + tests + smoke script. What it does NOT ship: actual country blocking. That needs v1.3.21.
What ships¶
Panel emits enable_streaming: false¶
backend/internal/caddycfg/caddycfg.go adds the flag to the emitted apps.crowdsec block:
csApp := map[string]any{
"api_url": crowdsec.LAPIURL,
"api_key": CrowdSecBouncerKeyPlaceholder,
"ticker_interval": interval,
"enable_streaming": false,
}
Stream mode (the plugin default) batches decision pulls every ticker_interval, then resolves each request against an in-memory IP/CIDR index. Non-IP scopes are not indexable that way -- the bouncer's optimization assumed all decisions are IP-shape. Setting enable_streaming: false switches the plugin to per-request LAPI lookups with the resolved client IP, which is the only path that honours Country / AS / other scopes.
Trade-off: per-request LAPI roundtrip replaces in-memory index lookup. The bouncer ships an in-process cache that absorbs the steady-state cost; for homelab traffic shapes the latency delta is noise. Hardcoded false for v1.3.20 -- homelab has no real use case for stream mode. v1.3.21 may surface it as a Settings toggle if a workload genuinely needs streamMode performance and is willing to give up non-IP scopes.
Verification: scripts/smoke/country-block.sh¶
Adds a Country decision via cscli, probes the live stack with X-Forwarded-For spoofing an IP that GeoLite2 resolves to that country, asserts a 403, cleans up the test decision on exit. Refuses to run with placeholder defaults so a bare invocation cannot silently pretend to verify nothing.
TEST_COUNTRY=<ISO-2> \
TEST_IP=<ip-resolving-to-iso> \
TEST_HOST=https://<your-host> \
./scripts/smoke/country-block.sh
Exit codes:
0-- 403 received, country blocking enforced.1-- non-403 received, regression. Likely cause:enable_streamingdefaulted to true (pre-v1.3.20 bug) or the panel did not reload the Caddy config after the decision was added.2-- prerequisite missing (container not running, defaults still in place).
The script lives at scripts/smoke/country-block.sh and is intended to run after every change to the caddycfg crowdsec emit path. Keeping it committed means a future refactor that re-introduces the bug will trip the smoke immediately.
Tests¶
Three new tests in backend/internal/caddycfg/crowdsec_test.go:
TestCrowdSecEmitsEnableStreamingFalse-- asserts the flag is emitted with valuefalsein the minimum-viable config. Failed againstmainbefore the fix landed; passes after.TestCrowdSecEmitsEnableStreamingFalseWithAppSec-- same assertion when AppSec wiring is also present, because the country-block bug is independent of AppSec mode.TestCrowdSecBouncerEmitMaintainsTickerInterval-- no-regression assertion that the v1.3.20 emit change did not drop or renameticker_interval. Covers both the operator-set value and the empty-default fallback.
What the unit tests cover (and what they don't)¶
Lesson from this release: unit tests asserting "the panel emits X" are necessary but not sufficient. They prove the emit path is correct; they say nothing about whether X actually solves the upstream problem. v1.3.20 shipped because the unit tests passed; the live-stack smoke script (which DOES test upstream behavior) was not run before tagging.
scripts/smoke/country-block.sh IS the oracle of success for the country-blocking work, and it currently FAILS on v1.3.20. That is the correct signal -- v1.3.21 must make it PASS, and the working agreement going forward is smoke-must-pass before any release that claims to fix country blocking.
The unit tests fail on main before the fix is applied:
=== RUN TestCrowdSecEmitsEnableStreamingFalse
crowdsec_test.go:114: enable_streaming MUST be emitted
(default true silently drops Country decisions); got
block map[api_key:... api_url:... ticker_interval:15s]
--- FAIL: TestCrowdSecEmitsEnableStreamingFalse (0.00s)
=== RUN TestCrowdSecEmitsEnableStreamingFalseWithAppSec
crowdsec_test.go:133: enable_streaming must be false even
when AppSec configured, got <nil>
--- FAIL: TestCrowdSecEmitsEnableStreamingFalseWithAppSec (0.00s)
After the fix:
=== RUN TestCrowdSecEmitsEnableStreamingFalse
--- PASS: TestCrowdSecEmitsEnableStreamingFalse (0.00s)
=== RUN TestCrowdSecBouncerEmitMaintainsTickerInterval
--- PASS: TestCrowdSecBouncerEmitMaintainsTickerInterval (0.00s)
=== RUN TestCrowdSecEmitsEnableStreamingFalseWithAppSec
--- PASS: TestCrowdSecEmitsEnableStreamingFalseWithAppSec (0.00s)
End-to-end verification is on the operator: run scripts/smoke/country-block.sh against the live stack with real TEST_COUNTRY / TEST_IP / TEST_HOST exports.
Files changed¶
backend/internal/caddycfg/caddycfg.go-- emitenable_streaming: falsein the apps.crowdsec blockbackend/internal/caddycfg/crowdsec_test.go-- three new tests (two assertions on the new flag + one no-regression onticker_interval)scripts/smoke/country-block.sh(new) -- end-to-end verification script with placeholder gatedocs/operations/access-control.md-- pre-v1.3.20 silent- failure callout under Country-based blocking + reference to the smoke scriptdocs/release-notes/v1.3.19.md-- known-limitation entry marking this release as the fixCHANGELOG.md,mkdocs.yml, version bump
Upgrade¶
No migrations. No env vars. No compose surface change. The panel reconciler emits the new Caddy config on the next reconcile pass; enable_streaming: false lands in /config/apps/crowdsec/enable_streaming of the running Caddy admin API. Verify with:
docker exec argos-prod-caddy wget -qO- localhost:2019/config/apps/crowdsec | \
grep -o '"enable_streaming":[^,}]*'
Should print "enable_streaming":false. Then run scripts/smoke/country-block.sh for the end-to-end check.
Not changed¶
- v1.3.19's AppSec sane defaults, self-block banner, whitelist lifecycle, migration 028 schema -- all untouched.
- Bouncer in-memory cache TTL, LAPI auth path, Caddy admin API surface -- untouched.
- The dormant
hosts.true_detect_modecolumn from v1.3.19 remains dormant; per-host enforcement still queued for v1.3.21 alongside the SelfBlockBanner v2 + audit log work.