postinst/rpm scriptlet never detects api.server.enable: false (cscli prints &false), skips service start on agent-only hosts
What happened?
The Enable guard added in #3802 (to fix #3691) never matches, so on an agent-only host the package scriptlet skips the service start and leaves crowdsec stopped after every upgrade.
Config.API.Server.Enable is a *bool (pkg/csconfig/api.go), and cscli config show --key dumps non-string values with litter.Dump, which renders a pointer with a leading &:
$ cscli config show --key "Config.API.Server.Enable"
&false
$ cscli config show --key "Config.API.Server.Enable" -o raw
&false
$ cscli config show --key "Config.API.Server.Enable" -o json
falseSo in build/debian/postinst (lines 69-78):
LAPI=true
API=$(cscli config show --key "Config.API.Server")
if [ "$API" = "nil" ] ; then
LAPI=false
else
# If API server is explicitly disabled, don't attempt port checks
ENABLED=$(cscli config show --key "Config.API.Server.Enable" 2>/dev/null || true)
if [ "$ENABLED" = "false" ]; then
LAPI=false
fi[ "$ENABLED" = "false" ] compares against &false and can never be true. LAPI stays true, so the port check at line 84 runs even though the local API is disabled. LoadAPIServer only sets DisableAPI = true and leaves the struct pointer non-nil, so the "$API" = "nil" branch above does not catch this case either.
If anything else holds the port from listen_uri, the start is skipped:
Not attempting to start crowdsec, port 8080 is already used or lapi was disabledOn my host a Docker container publishes 127.0.0.1:8080. The agent has no LAPI listener at all — only the Prometheus port:
LISTEN 0 4096 127.0.0.1:6060 users:(("crowdsec",pid=...,fd=18))prerm stops the service on upgrade, postinst then skips the start, and the exit code is 0, so apt reports success. Restart=always does not help, because an explicit systemctl stop is not a failure.
The same block is in build/rpm/SPECS/crowdsec.spec (lines 199-207), so RPM installs are affected identically.
What did you expect to happen?
With api.server.enable: false, the scriptlet should recognise the agent-only configuration, skip the port check entirely, and start the service.
How can we reproduce it (as minimally and precisely as possible)?
- On an existing Debian/Ubuntu install, set
api.server.enable: falsein/etc/crowdsec/config.yamland pointapi.client.credentials_pathat a remote LAPI. Leaveapi.server.listen_uriat its default127.0.0.1:8080— if it is removed, the[ -z "$PORT" ]clause masks the bug. - Confirm the direct cause:
cscli config show --key "Config.API.Server.Enable"prints&false, notfalse. - Occupy that port with anything else, e.g.
python3 -m http.server 8080 --bind 127.0.0.1. apt-get install --reinstall crowdsecsystemctl is-active crowdsec→inactive, whilepostinstprinted "Not attempting to start crowdsec".
I reproduced this deliberately on a second, unaffected host (port 8080 free there) by adding the listener from step 3 — with the listener the agent stayed down, without it the start went through.
Anything else we need to know?
Why it is easy to miss: the comparison cannot fail loudly and apt exits 0. Once the crowdsec repo is included in unattended-upgrades, this runs nightly with nobody watching and a security agent stays down with no trace in the apt output.
Possible fixes — the postinst is parsing litter's Go-value output, which is not a stable contract:
cscli config show --key "Config.API.Server.Enable" -o jsonalready prints a plainfalse(json.MarshalIndentdereferences the pointer), so adding-o jsonto that call is a one-line fix. If theConfig.API.Serverread above is switched to-o jsonas well, its comparison has to change from= "nil"to= "null".- Prefer starting when the LAPI state cannot be determined. The port check only protects the case where crowdsec would actually bind that port; if the read of
Enableis inconclusive, skipping the start is the worse failure. If that branch is widened, line 85 could use the same guard the RPM scriptlet already has (systemctl start crowdsec || echo "crowdsec is not started"), so a genuine bind failure does not abortpostinstunderset -eand half-configure the package (#771). - Emit the "Not attempting to start" message on stderr and/or via
logger, so an unattended run leaves a trace where stdout is discarded.
Workaround, same one @LaurenceJJones suggested in #3691, but via config.yaml.local so config.yaml stays an untouched conffile:
# /etc/crowdsec/config.yaml.local
api:
server:This makes Config.API.Server evaluate to nil, the first branch is taken, and the service starts. api.client is unaffected and the agent keeps reporting to the remote LAPI.
Crowdsec version
$ cscli version
version: v1.8.0-debian-pragmatic-amd64-cc76dbbc
Codename: alphaga
BuildDate: 2026-08-31_13:36:10
GoVersion: 1.26.3
Platform: linux
libre2: C++
User-Agent: crowdsec/v1.8.0-debian-pragmatic-amd64-cc76dbbc-linux
Constraint_parser: >= 1.0, <= 3.0
Constraint_scenario: >= 1.0, <= 3.0
Constraint_api: v1
Constraint_acquis: >= 1.0, < 2.0Package version 1.8.0. build/debian/postinst is identical at tag v1.8.0 and on master, so this affects current master too.
OS version
Debian GNU/Linux 13 (trixie)
6.12.107+deb13-amd64Also reproduced on an Ubuntu container with the same package.
Config show
api:
client:
insecure_skip_verify: false
credentials_path: /etc/crowdsec/local_api_credentials.yaml
server:
enable: false
log_level: info
listen_uri: 127.0.0.1:8080
profiles_path: /etc/crowdsec/profiles.yaml
console_path: /etc/crowdsec/console.yaml
online_client:
credentials_path: /etc/crowdsec/online_api_credentials.yaml
trusted_ips:
- 127.0.0.1
- ::1Source: crowdsecurity/crowdsec