#4651·crowdsec

postinst/rpm scriptlet never detects api.server.enable: false (cscli prints &false), skips service start on agent-only hosts

Author: M0riousCreated Sep 2, 2026Updated Sep 3, 2026
Labelsneeds/triageos/linuxkind/packaging

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
false

So in build/debian/postinst (lines 69-78):

bash
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 disabled

On 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)?

  1. On an existing Debian/Ubuntu install, set api.server.enable: false in /etc/crowdsec/config.yaml and point api.client.credentials_path at a remote LAPI. Leave api.server.listen_uri at its default 127.0.0.1:8080 — if it is removed, the [ -z "$PORT" ] clause masks the bug.
  2. Confirm the direct cause: cscli config show --key "Config.API.Server.Enable" prints &false, not false.
  3. Occupy that port with anything else, e.g. python3 -m http.server 8080 --bind 127.0.0.1.
  4. apt-get install --reinstall crowdsec
  5. systemctl is-active crowdsecinactive, while postinst printed "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:

  1. cscli config show --key "Config.API.Server.Enable" -o json already prints a plain false (json.MarshalIndent dereferences the pointer), so adding -o json to that call is a one-line fix. If the Config.API.Server read above is switched to -o json as well, its comparison has to change from = "nil" to = "null".
  2. 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 Enable is 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 abort postinst under set -e and half-configure the package (#771).
  3. 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:

yaml
# /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.0

Package 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-amd64

Also 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
      - ::1