Skip to content

Version 5.2.0

Latest

Choose a tag to compare

@andreapollastri andreapollastri released this 08 Sep 11:08

[5.2.0] — 2026-09-08

Opt-in CrowdSec (engine and a registered firewall bouncer) and a nightly integrity check. Neither is installed by setup.sh or cipi self-update. There is no WAF.

Added

  • cipi crowdsec enable. Installs the engine and crowdsec-firewall-bouncer-nftables (or -iptables) registered with cscli bouncers add — decisions without a bouncer ban nothing. Acquisition reads /home/*/logs/nginx-{access,error}.log, which is where every Cipi vhost actually logs; /var/log/nginx alone holds only the catch-all server block, so the nginx scenarios would never see application traffic. On Ubuntu 24.04 iptables is nft, so the bouncer matches fail2ban's iptables-multiport. enable refuses below 512MB MemAvailable, refuses if nginx looks like a reverse proxy without set_real_ip_from + real_ip_header (would ban Cloudflare, not the attacker), and allowlists this SSH session. disable flushes CrowdSec chains/tables, then purges, so DROP rules cannot outlive cscli.
  • Rescue TLS listener. enable starts a TLS server on a high port. The TLS handshake runs in the connection's own thread with a deadline, never in accept(): wrapping the listening socket lets one peer that opens a connection and sends nothing hold the break-glass path shut for as long as it likes. Concurrent connections are capped below the unit's TasksMax. One GET with a one-shot token allowlists the TCP peer (never X-Forwarded-For), unbans that IP in CrowdSec and fail2ban, mails a new token, and dies. Not a login, not an SSH key. UFW opens the port; an ACCEPT is punched at the head of the CrowdSec chain (and again on every bouncer restart) so a DROP-all ban cannot close it. cipi crowdsec rescue token|rotate. status shows the port and cert fingerprint, not the token.
  • Allowlists. Localhost, RFC1918, Let's Encrypt HTTP-01. GitHub webhook CIDRs from api.github.com/meta .hooks (fail-open: a dead fetch keeps the previous file). GitLab.com egress CIDRs are hardcoded, dated 2026-09-07, and documented as rotting.
  • cipi ban list|unban includes CrowdSec when the engine is up.
  • cipi scan. Nightly (04:40): integrity of current/ (or htdocs/) against a sha256 manifest written at every successful deploy and rollback, then one ClamAV process on upload dirs only with rfxn PHP-webshell signatures. Manifests live in /var/lib/cipi/manifests as root:root 0600, never in the app home: open_basedir gives PHP all of /home/<app>/, so a manifest kept there is editable by the very webshell the check exists to catch. The webhook deploy runs as the app user and reaches the writer through one sudo entry pinned to its own app name, and every re-baseline is written to events.log — a manifest rewrite with no deploy beside it is the thing to look for. Comparison slices sha256sum records by offset instead of splitting on whitespace: names with spaces were truncated in the report, and GNU coreutils escapes records holding a backslash or a newline, which a hash-anchored filter dropped entirely (a webshell called sh\ell.php was invisible). Timeouts, failed signature updates and releases that could not be hashed in full mail scan_incomplete — not "clean". Extra/changed files mail scan_integrity. FPM open_basedir / pool-user drift is included in that mail. Isolation itself (per-app Unix user + open_basedir) was already Cipi; this only checks it is still there. enable refuses below 2GB MemAvailable or 3GB free on /var (--force to override): clamscan loads the whole signature set per run, and an OOM kill at 04:40 may take MariaDB with it.
  • Shell tab-completion, installed automatically. setup.sh and every cipi self-update write /etc/bash_completion.d/cipi, /usr/share/zsh/vendor-completions/_cipi (when that directory exists) and an /etc/profile.d/cipi-completion.sh loader that fires for every interactive bash and zsh shell — with or without the bash-completion package, which setup.sh now also installs. Nobody edits a dotfile; a new login shell has it. cipi completion bash|zsh is there only for a non-login shell or a custom rc file (--print emits the raw script and changes nothing). It completes the top-level verbs, each verb's sub-commands and flags, and cipi help <topic>; app names are filled in from /etc/cipi/apps-public.json when the shell's user can read it (a root session, or the cipi-api group), and everything else is a static word list that works for any user. sudo cipi <TAB> resolves through the stock sudo completion. Hand-written, not framework-generated: tests/verify-5.2.0.sh fails if the verb list drifts from the dispatch in cipi.
  • Migration 5.2.0. Installs cipi-scan-manifest and the two rescue helpers, creates the manifest store, grants each existing app its manifest sudo entry, and adds SSH_CLIENT / SSH_CONNECTION to env_keep in /etc/sudoers.d/cipi-sudo. The helper install is not optional housekeeping: self-update never re-execs, so the copy loop running during the upgrade is the pre-5.2.0 one and knows nothing about the new binaries — without this, crowdsec enable refuses to start the rescue listener and no deploy ever refreshes a manifest. crowdsec enable allowlists the session it is typed from, and setup.sh sets PermitRootLogin no — so the normal path is sudo cipi, and sudo's env_reset was dropping the only variables that carry the operator's IP. The candidate file is validated with visudo -c before it replaces the live one. Nothing else is migrated: CrowdSec and the scan stay off.

Notes

  • CrowdSec is 24/7 after enable. The scan is the nightly job.
  • cipi status lists CrowdSec, its bouncer and the rescue listener only when those units exist. A rescue listener that has stopped is the one thing you want to find before you need it, not after.
  • The rescue listener runs as root (redeeming needs it) but with NoNewPrivileges, ProtectHome=read-only, PrivateTmp and a MemoryHigh cap. ProtectSystem and SystemCallFilter are deliberately left off — hardening that can break break-glass is worse than the surface it removes.
  • The rescue throttle applies to wrong tokens only. A correct token is always honoured, whatever the peer typed before it.
  • cipi ban list reads .decisions[] out of cscli decisions list -o json, which returns alerts. Reading .value at the top level printed a decision count and then no addresses at all. cipi ban unban asks before it claims success, because cscli decisions delete exits 0 whether it removed something or nothing.
  • The integrity check tells you a tree changed between deploys. An attacker who owns the app user can still re-baseline through the sudo entry, so read events.log alongside the alert.
  • Pre-symlink checkpoint and inotify on uploads are not this release.