CSM - Continuous Security Monitor
Security monitoring and response for Linux web servers. Single Go binary that detects compromise, phishing, mail abuse, and suspicious activity - then auto-responds and alerts within seconds.
Originally designed as a full Imunify360 replacement for cPanel/WHM on CloudLinux/AlmaLinux. Also runs on plain Ubuntu/Debian + Nginx/Apache and on plain AlmaLinux/Rocky/RHEL + Apache/Nginx: the daemon auto-detects the OS, control panel, and web server at startup and picks the correct log paths, config candidates, and check set.
Includes nftables firewall (replaces LFD/fail2ban), ModSecurity management, email security, threat intelligence, hardening audit, performance monitoring, and a web dashboard.
See installation.md for supported platforms and how the check set differs between cPanel and non-cPanel hosts.
What CSM Does
csm daemon
+-- fanotify file monitor < 1s detection on /home, /tmp, /dev/shm
+-- inotify log watchers ~2s detection on auth, access, exim, FTP logs
+-- PAM brute-force listener Real-time login failure tracking
+-- PHP runtime shield auto_prepend_file protection
+-- critical scanner (10 min) 34 checks: processes, network, tokens, logins, firewall
+-- deep scanner (60 min) 28 checks: WP integrity, RPM, DB injection, phishing
+-- nftables firewall engine Kernel netlink API, IP sets, rate limiting
+-- threat intelligence IP reputation, attack scoring, GeoIP
+-- ModSecurity manager Rule deployment, overrides, escalation
+-- email security AV scanning, quarantine, password/forwarder audit
+-- challenge server Proof-of-work pages for suspicious IPs
+-- alert dispatcher Email, Slack, Discord, webhooks
+-- web UI HTTPS dashboard with 14 authenticated pages
+-- hardening audit On-demand server hardening checks + scoring
+-- performance monitor PHP, MySQL, Redis, WordPress metrics
Performance
Benchmarked on production (168 accounts, 275 WordPress sites, 28M files):
| Component | Speed | Memory |
|---|---|---|
| fanotify monitor | < 1 second | ~5 MB |
| Log watchers | ~2 seconds | ~1 MB |
| Critical checks (34) | < 1 sec | ~35 MB peak |
| Deep checks (28) | ~40 sec | ~100 MB peak |
| Daemon idle | - | 45 MB resident |
| Binary | - | ~8 MB static |
Built From Real Incidents
CSM was built after real attacks where GSocket reverse shells, LEVIATHAN webshell toolkits, credential-stuffed cPanel accounts, and phishing kits were found across production servers.
Installation
Supported Platforms
| Platform | Web server | Package | Notes |
|---|---|---|---|
| cPanel/WHM on CloudLinux / AlmaLinux / Rocky | Apache (EA4) or LiteSpeed | .rpm | Primary target. All 62 checks run. |
| Plain AlmaLinux / Rocky / RHEL 8+ / CentOS Stream 8+ | Apache (httpd) or Nginx | .rpm | Generic Linux + web server checks. cPanel-specific checks are skipped cleanly. |
| Plain Ubuntu 20.04+ / Debian 11+ | Apache (apache2) or Nginx | .deb | Same as above, with debsums/dpkg --verify in place of rpm -V. |
The daemon auto-detects the OS, control panel (cPanel/Plesk/DirectAdmin/none), and web server (Apache/Nginx/LiteSpeed) at startup. The detected platform is logged at startup as:
[2026-04-10 08:13:37] platform: os=ubuntu/24.04 panel=none webserver=nginx
Check it with journalctl -u csm.service | grep platform: after starting the daemon.
APT repository (Debian / Ubuntu) – recommended
The package repository at mirrors.pidginhost.com/csm/ is the preferred install method for Debian and Ubuntu. Future updates are picked up automatically via apt upgrade, and package metadata is GPG-signed so the trust chain is enforced by dpkg.
# 1. Install the signing key
curl -fsSL https://mirrors.pidginhost.com/csm/csm-signing.gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/csm.gpg
# 2. Add the repository
echo "deb [signed-by=/etc/apt/keyrings/csm.gpg] https://mirrors.pidginhost.com/csm/deb stable main" | \
sudo tee /etc/apt/sources.list.d/csm.list
# 3. Install
sudo apt update
sudo apt install csm
Works on Ubuntu 20.04+, Debian 11+, and any derivative. The single stable suite serves all Debian/Ubuntu releases – the Go binary is statically linked and has no per-release glibc dependency.
To upgrade later: sudo apt update && sudo apt upgrade csm.
DNF repository (AlmaLinux / Rocky / RHEL / CloudLinux / cPanel) – recommended
# 1. Import the signing key into the RPM keyring
sudo rpm --import https://mirrors.pidginhost.com/csm/csm-signing.gpg
# 2. Add the repository
sudo tee /etc/yum.repos.d/csm.repo >/dev/null <<'EOF'
[csm]
name=CSM - Continuous Security Monitor
baseurl=https://mirrors.pidginhost.com/csm/rpm/el$releasever/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.pidginhost.com/csm/csm-signing.gpg
EOF
# 3. Install
sudo dnf install csm
The explicit rpm --import is important: without it, the first dnf install csm prompts “Is this ok [y/N]:” to trust the repo key, and dnf install -y answers package install prompts but not the key-trust prompt. If the prompt goes unanswered on a non-interactive install, dnf fails with repomd.xml GPG signature verification error: Signing key not found.
The $releasever variable auto-selects the matching EL major (8, 9, or 10). Both x86_64 and aarch64 are published. Works on AlmaLinux 8+, Rocky 8+, RHEL 8+, CloudLinux 8+, and cPanel-managed hosts.
To upgrade later: sudo dnf upgrade csm.
Quick Install (all platforms, one-shot)
For situations where you can’t add a package repository (disconnected hosts, air-gapped mirrors, Docker base images):
curl -sSL https://raw.githubusercontent.com/pidginhost/csm/main/scripts/install.sh | bash
Auto-detects hostname, email, and generates a WebUI auth token. Prompts for confirmation before applying. Works on Debian/Ubuntu and RHEL-family distros. Non-interactive mode:
curl -sSL https://raw.githubusercontent.com/pidginhost/csm/main/scripts/install.sh | bash -s -- --email admin@example.com --non-interactive
Manual .rpm / .deb download
If you need a specific version or want to install without adding the repository:
# RHEL family
curl -LO https://github.com/pidginhost/csm/releases/latest/download/csm-VERSION-1.x86_64.rpm
sudo dnf install -y ./csm-VERSION-1.x86_64.rpm
# Debian/Ubuntu
curl -LO https://github.com/pidginhost/csm/releases/latest/download/csm_VERSION_amd64.deb
sudo apt install -y ./csm_VERSION_amd64.deb
Replace VERSION with a real version (e.g. 2.2.2). Both files are also available at https://mirrors.pidginhost.com/csm/deb/pool/main/c/csm/ and https://mirrors.pidginhost.com/csm/rpm/elN/ARCH/ if you prefer to pin versions from the mirror.
Post-install (all methods)
vi /opt/csm/csm.yaml # Set hostname, alert email, infra IPs
csm validate # Check config syntax
csm baseline # Record current state as known-good
systemctl enable --now csm.service # Start the daemon
Rollback to an older version
Both the APT and DNF repositories retain the last 5 tagged releases at any time. To downgrade:
# Debian/Ubuntu
sudo apt-cache policy csm # Show available versions
sudo apt install csm=2.2.0-1
# RHEL family
sudo dnf --showduplicates list csm # Show available versions
sudo dnf downgrade csm
Verifying platform auto-detection
After systemctl start csm.service, the first line after “CSM daemon starting” reports what CSM detected:
[2026-04-10 08:13:37] CSM daemon starting
[2026-04-10 08:13:37] platform: os=almalinux/10.0 panel=none webserver=apache
[2026-04-10 08:13:37] Watching: /var/log/secure
[2026-04-10 08:13:37] Watching: /var/log/httpd/error_log
[2026-04-10 08:13:37] Watching: /var/log/httpd/access_log
If any field shows none or unknown when you expect something, the auto-detect missed it. File a bug with the output of cat /etc/os-release, systemctl is-active nginx apache2 httpd, and which nginx apache2 httpd.
Optional system dependencies
CSM runs as a single static Go binary and has no hard dependencies beyond systemd, but a few host packages enable additional checks:
| Package | Platforms | Enables |
|---|---|---|
auditd | All | Shadow file / SSH key tamper detection via auditd |
debsums | Debian/Ubuntu | Cleaner system binary integrity output vs. dpkg --verify fallback |
logrotate | All | Rotation of /var/log/csm/monitor.log |
wp-cli | Optional | WordPress core integrity check |
| ModSecurity | All | WAF enforcement checks (see platform-specific install below) |
Installing ModSecurity
CSM detects ModSecurity but doesn’t install it for you. Platform-specific commands:
# Ubuntu/Debian + Nginx
sudo apt install libnginx-mod-http-modsecurity modsecurity-crs
# Ubuntu/Debian + Apache
sudo apt install libapache2-mod-security2 modsecurity-crs && sudo a2enmod security2
# AlmaLinux/Rocky/RHEL + Apache (requires EPEL)
sudo dnf install -y epel-release
sudo dnf install -y mod_security
sudo systemctl restart httpd
# AlmaLinux/Rocky/RHEL + Nginx (requires EPEL)
sudo dnf install -y epel-release
sudo dnf install -y nginx-mod-http-modsecurity
sudo systemctl restart nginx
After installing ModSecurity, run csm check and the waf_status finding should disappear.
Manual (deploy.sh)
/opt/csm/deploy.sh install
vi /opt/csm/csm.yaml # set hostname, alert email, infra IPs
csm validate
csm baseline
systemctl enable --now csm.service
Post-Install
- Edit
/opt/csm/csm.yaml– set hostname, alert email, infrastructure IPs - Run
csm validateto check config syntax (add--deepfor connectivity probes) - Run
csm baselineto record current state as known-good (see below) - Start the daemon:
systemctl enable --now csm.service - Open the Web UI:
https://<server>:9443/login
All installation methods produce the same installed state. RPM/DEB packages auto-detect hostname and email, and generate the auth token.
Baseline Scan
The csm baseline command scans the entire server and records the current state as known-good. This is required on first install so CSM knows what’s “normal” for your server.
What it does:
- Scans all cPanel accounts for malware, permissions, and configuration issues
- Records file hashes, email forwarder hashes, and plugin versions
- Stores everything in the bbolt database (
/opt/csm/state/csm.db)
How long it takes: Depends on server size. A server with 100+ cPanel accounts and thousands of WordPress sites can take 5-10 minutes. During this time, the daemon cannot start (bbolt lock).
When to re-run:
- After a fresh install
- After restoring from backup
- If the database is lost or corrupted (delete
csm.dband re-run) - You do NOT need to re-run for normal deploys/upgrades – the daemon handles incremental state
Important: The baseline scan holds the database lock. Do not start the daemon (systemctl start csm) until the baseline completes. The daemon will fail with “store: opening bbolt: timeout” if the baseline is still running.
Configuration
CSM is configured via a single YAML file at /opt/csm/csm.yaml.
Platform & Web Server
CSM auto-detects the host OS (Ubuntu, Debian, AlmaLinux, Rocky, RHEL, CloudLinux), control panel (cPanel, Plesk, DirectAdmin, or none), and web server (Apache, Nginx, LiteSpeed, or none) at daemon startup. The detected platform is logged as:
[2026-04-10 08:13:37] platform: os=ubuntu/24.04 panel=none webserver=nginx
The daemon then chooses the correct log paths, config candidates, and check set without any configuration from you. Verify with:
journalctl -u csm.service | grep platform:
Web server overrides
For hosts with a custom layout (reverse proxy, non-standard package locations, chroot), add a web_server: section to csm.yaml. Every field is optional – anything left blank falls back to auto-detection.
web_server:
type: "nginx" # apache | nginx | litespeed -- overrides auto-detect
config_dir: "/etc/nginx" # for info/diagnostics only
access_logs: # tried in order until one exists
- "/var/log/nginx/access.log"
- "/srv/logs/nginx/access.log"
error_logs: # used by ModSecurity deny watcher
- "/var/log/nginx/error.log"
modsec_audit_logs:
- "/var/log/nginx/modsec_audit.log"
modsec_error_log (legacy single-path override) is still honored and takes precedence over web_server.error_logs for the ModSecurity watcher only:
modsec_error_log: "/opt/myapp/logs/modsec_audit.log"
Account roots (plain Linux web-scan coverage)
By default, the account-scan based checks (perf_error_logs, perf_wp_config, perf_wp_transients, and related) iterate /home/*/public_html which is the cPanel layout. On plain Ubuntu / AlmaLinux with Nginx or Apache, point CSM at your actual web roots:
account_roots:
- "/var/www/*/public" # e.g. Laravel/Symfony sites
- "/srv/http/*" # Arch / generic layouts
- "/home/*/public_html" # add if you also have cPanel-style accounts
Each entry is a glob pattern expanded at scan time. Non-existent matches are silently dropped. If account_roots is empty and CSM is not on a cPanel host, the account-scan checks return no findings (they run but find nothing, which is the correct behavior for a plain-Linux host with no configured web roots).
Today, three checks consume this: perf_error_logs, perf_wp_config, perf_wp_transients. The remaining account-scan checks (WordPress core integrity, phishing kit detection, htaccess tampering, fileindex, etc.) still assume the cPanel /home/*/public_html layout and will be migrated in a follow-up release.
Minimal Config
hostname: "cluster6.example.com"
alerts:
email:
enabled: true
to: ["admin@example.com"]
disabled_checks: [] # optional: suppress these checks from email only
smtp: "localhost:25"
webui:
enabled: true
listen: "0.0.0.0:9443"
auth_token: "your-secret-token"
infra_ips: ["10.0.0.0/8"]
Full Reference
hostname: "cluster6.example.com"
# --- Alerts ---
alerts:
email:
enabled: true
to: ["admin@example.com"]
from: "csm@cluster6.example.com"
smtp: "localhost:25"
disabled_checks: [] # check names to keep in web/history but exclude from email
webhook:
enabled: false
url: ""
type: "slack" # slack, discord, generic
heartbeat:
enabled: false
url: "" # healthchecks.io, cronitor, dead man's switch
max_per_hour: 10 # default: 10
# --- Integrity ---
integrity:
binary_hash: "" # auto-populated by install/rehash
config_hash: "" # auto-populated by install/rehash
immutable: false # prevent config changes at runtime
# --- Thresholds ---
thresholds:
mail_queue_warn: 500 # default: 500
mail_queue_crit: 2000 # default: 2000
state_expiry_hours: 24 # default: 24
deep_scan_interval_min: 60 # minutes between deep scans (default: 60)
wp_core_check_interval_min: 60 # WordPress core checksum interval (default: 60)
webshell_scan_interval_min: 30 # webshell scan interval (default: 30)
filesystem_scan_interval_min: 30 # filesystem scan interval (default: 30)
multi_ip_login_threshold: 3 # IPs per account before alert (default: 3)
multi_ip_login_window_min: 60 # time window for multi-IP check (default: 60)
plugin_check_interval_min: 1440 # WordPress plugin check interval (default: 1440)
brute_force_window: 5000 # failed auth attempts window (default: 5000)
# SMTP brute-force tracker (Exim mainlog, dovecot SASL on submission ports)
smtp_bruteforce_threshold: 5 # per-IP failed auths before block (default: 5)
smtp_bruteforce_window_min: 10 # sliding window in minutes (default: 10)
smtp_bruteforce_suppress_min: 60 # cooldown between repeat findings (default: 60)
smtp_bruteforce_subnet_threshold: 8 # unique IPs per /24 before subnet block (default: 8)
smtp_account_spray_threshold: 12 # unique IPs targeting one mailbox before visibility finding (default: 12)
smtp_bruteforce_max_tracked: 20000 # soft cap on tracked entries; oldest evicted (default: 20000)
# Mail brute-force tracker (Dovecot direct: IMAP/POP3/ManageSieve via /var/log/maillog)
mail_bruteforce_threshold: 5 # per-IP failed auths before block (default: 5)
mail_bruteforce_window_min: 10 # sliding window in minutes (default: 10)
mail_bruteforce_suppress_min: 60 # cooldown between repeat findings (default: 60)
mail_bruteforce_subnet_threshold: 8 # unique IPs per /24 before subnet block (default: 8)
mail_account_spray_threshold: 12 # unique IPs targeting one mailbox before visibility finding (default: 12)
mail_bruteforce_max_tracked: 20000 # soft cap on tracked entries; oldest evicted (default: 20000)
# --- Infrastructure ---
infra_ips: [] # management/monitoring CIDRs - never blocked
# --- State ---
state_path: "/opt/csm/state" # bbolt DB and state files
# --- Suppressions ---
suppressions:
upcp_window_start: "00:30" # cPanel nightly update window start
upcp_window_end: "02:00" # cPanel nightly update window end
known_api_tokens: [] # API tokens to ignore in auth logs (e.g. ["phclient"])
ignore_paths: # glob patterns to skip in filesystem scans
- "*/cache/*"
- "*/vendor/*"
suppress_webmail_alerts: true # don't alert on webmail logins
suppress_cpanel_login_alerts: false # don't alert on cPanel direct logins
suppress_blocked_alerts: true # don't alert on IPs that were auto-blocked
trusted_countries: ["RO"] # ISO 3166-1 alpha-2 - suppress cPanel login alerts from these
# --- Auto-Response ---
auto_response:
enabled: false
kill_processes: false # kill malicious processes
quarantine_files: false # move malware to quarantine
block_ips: false # block attacker IPs via firewall
block_expiry: "24h" # duration for temp blocks (e.g. "24h", "12h")
enforce_permissions: false # auto-chmod 644 world/group-writable PHP files
block_cpanel_logins: false # block IPs on cPanel/webmail login alerts
netblock: false # auto-block /24 subnets
netblock_threshold: 3 # IPs from same /24 before subnet block
permblock: false # promote temp blocks to permanent
permblock_count: 4 # temp blocks before promotion
permblock_interval: "24h" # window for counting temp blocks
# --- Challenge Pages ---
challenge:
enabled: false # enable PoW challenge pages instead of hard block
listen_port: 8439 # port for challenge server (default: 8439)
secret: "" # HMAC secret for tokens (auto-generated if empty)
difficulty: 2 # SHA-256 proof-of-work difficulty 0-5 (default: 2)
# --- PHP Shield ---
php_shield:
enabled: false # watch php_events.log for PHP Shield alerts
# --- Reputation ---
reputation:
abuseipdb_key: "" # AbuseIPDB API key for IP reputation lookups
whitelist: [] # IPs to never flag as malicious
# --- Signatures ---
signatures:
rules_dir: "/opt/csm/rules" # YAML signature rules directory
update_url: "" # remote URL to fetch rule updates
auto_update: false # auto-download rules on schedule
update_interval: "" # how often to check (e.g. "24h")
signing_key: "" # required for any remote rule update path; 64-char hex Ed25519 public key
yara_forge:
enabled: false # auto-fetch YARA Forge community rules
tier: "core" # "core", "extended", "full" (default: "core")
update_interval: "168h" # how often to check for updates (default: weekly)
disabled_rules: [] # YARA rule names to exclude from Forge downloads
`signatures.signing_key` is mandatory whenever either `signatures.update_url` is set or `signatures.yara_forge.enabled` is `true`.
The value must be the hex-encoded Ed25519 public key used to verify detached `.sig` files for downloaded rule bundles.
It is not a PEM block and not a filesystem path.
If you are not operating a signed remote rule feed yet, leave `update_url` empty and keep `yara_forge.enabled: false`.
# --- Web UI ---
webui:
enabled: true
listen: "0.0.0.0:9443" # address:port for HTTPS server
auth_token: "" # Bearer/cookie auth token (auto-generated on install)
tls_cert: "" # path to TLS certificate PEM file
tls_key: "" # path to TLS private key PEM file
ui_dir: "" # path to UI files on disk (default: /opt/csm/ui)
# --- Email AV ---
email_av:
enabled: false
clamd_socket: "/var/run/clamd.scan/clamd.sock" # path to ClamAV daemon socket
scan_timeout: "30s" # per-attachment scan timeout
max_attachment_size: 26214400 # max single attachment size in bytes (25MB)
max_archive_depth: 1 # max nested archive extraction depth
max_archive_files: 50 # max files extracted from a single archive
max_extraction_size: 104857600 # max total extraction size in bytes (100MB)
quarantine_infected: true # quarantine emails with infected attachments
scan_concurrency: 4 # parallel scan workers
# --- Email Protection ---
email_protection:
password_check_interval_min: 1440 # how often to audit email passwords (default: 1440)
high_volume_senders: [] # accounts expected to send high volume (skip rate alerts)
rate_warn_threshold: 50 # emails per window before warning (default: 50)
rate_crit_threshold: 100 # emails per window before critical (default: 100)
rate_window_min: 10 # rate check window in minutes (default: 10)
known_forwarders: [] # accounts that forward mail (skip rate alerts)
# --- Firewall ---
firewall:
enabled: false
# Open ports (IPv4)
tcp_in: [20,21,25,26,53,80,110,143,443,465,587,993,995,2077,2078,2079,2080,2082,2083,2091,2095,2096]
tcp_out: [20,21,25,26,37,43,53,80,110,113,443,465,587,873,993,995,2082,2083,2086,2087,2089,2195,2325,2703]
udp_in: [53,443]
udp_out: [53,113,123,443,873]
# IPv6
ipv6: false
tcp6_in: [] # if empty, uses tcp_in
tcp6_out: [] # if empty, uses tcp_out
udp6_in: [] # if empty, uses udp_in
udp6_out: [] # if empty, uses udp_out
# Restricted ports (infra IPs only)
restricted_tcp: [2086,2087,2325] # WHM ports
# Passive FTP range
passive_ftp_start: 49152
passive_ftp_end: 65534
# Infra IPs for firewall rules
infra_ips: []
# Rate limiting
conn_rate_limit: 30 # new connections/min per IP
syn_flood_protection: true
conn_limit: 50 # max concurrent connections per IP (0 = disabled)
# Per-port flood protection
port_flood:
- port: 25
proto: tcp
hits: 40
seconds: 300
- port: 465
proto: tcp
hits: 40
seconds: 300
- port: 587
proto: tcp
hits: 40
seconds: 300
# UDP flood protection
udp_flood: true
udp_flood_rate: 100 # packets per second
udp_flood_burst: 500 # burst allowance
# Country blocking
country_block: [] # ISO country codes to block
country_db_path: "" # path to MaxMind DB (uses geoip config if empty)
# Silent drop (no logging)
drop_nolog: [23,67,68,111,113,135,136,137,138,139,445,500,513,520]
# IP limits
deny_ip_limit: 30000 # max permanent blocked IPs
deny_temp_ip_limit: 5000 # max temporary blocked IPs
# Outbound SMTP restriction
smtp_block: false # block outgoing mail except allowed users
smtp_allow_users: [] # usernames allowed to send
smtp_ports: [25,465,587]
# Dynamic DNS
dyndns_hosts: [] # hostnames to resolve and whitelist periodically
# Logging
log_dropped: true # log dropped packets
log_rate: 5 # log entries per minute
# --- GeoIP ---
geoip:
account_id: "" # MaxMind account ID
license_key: "" # MaxMind license key
editions: # MaxMind database editions
- GeoLite2-City
- GeoLite2-ASN
auto_update: true # auto-update GeoIP databases (default: true when credentials set)
update_interval: "24h" # update check interval
# --- ModSecurity ---
modsec_error_log: "" # path to Apache/LiteSpeed error log for ModSec parsing
modsec:
rules_file: "" # path to modsec2.user.conf
overrides_file: "" # path to csm-overrides.conf
reload_command: "" # command to reload web server (e.g. "/usr/sbin/apachectl graceful")
# --- Performance ---
performance:
enabled: true
load_high_multiplier: 1.0 # load average / CPU cores multiplier for warning (default: 1.0)
load_critical_multiplier: 2.0 # load average / CPU cores multiplier for critical (default: 2.0)
php_process_warn_per_user: 20 # per-user PHP process count warning (default: 20)
php_process_critical_total_multiplier: 5 # total PHP processes / CPU cores for critical (default: 5)
error_log_warn_size_mb: 50 # error log size warning threshold (default: 50)
mysql_join_buffer_max_mb: 64 # MySQL join_buffer_size warning threshold (default: 64)
mysql_wait_timeout_max: 3600 # MySQL wait_timeout warning threshold (default: 3600)
mysql_max_connections_per_user: 10 # per-user MySQL connections warning (default: 10)
redis_bgsave_min_interval: 900 # minimum seconds between Redis BGSAVE (default: 900)
redis_large_dataset_gb: 4 # Redis dataset size warning threshold in GB (default: 4)
wp_memory_limit_max_mb: 512 # WordPress memory_limit warning threshold (default: 512)
wp_transient_warn_mb: 1 # WordPress transient data warning in MB (default: 1)
wp_transient_critical_mb: 10 # WordPress transient data critical in MB (default: 10)
# --- Cloudflare ---
cloudflare:
enabled: false # auto-whitelist Cloudflare IP ranges
refresh_hours: 6 # how often to refresh Cloudflare IPs (default: 6)
# --- Threat Intel ---
c2_blocklist: [] # known C2 server IPs to block permanently
backdoor_ports: [4444,5555,55553,55555,31337] # ports indicating backdoor activity
TLS Certificates
The Web UI serves over HTTPS. Configure TLS certificates under webui:
webui:
tls_cert: "/var/cpanel/ssl/cpanel/mycpanel.pem" # certificate PEM file
tls_key: "/var/cpanel/ssl/cpanel/mycpanel.pem" # private key PEM file
On cPanel servers, you can reuse the cPanel self-signed certificate (both cert and key are in the same PEM file). For production, use a proper certificate from Let’s Encrypt or your CA.
If tls_cert and tls_key are empty, the Web UI will not start.
Validation
csm validate # syntax check
csm validate --deep # syntax + connectivity probes (SMTP, webhooks)
csm config show # display config with secrets redacted
Upgrading
deploy.sh (recommended)
/opt/csm/deploy.sh upgrade
This will:
- Stop the daemon
- Back up the current binary
- Download the new version
- Verify SHA256 checksum
- Extract UI assets and rules
- Rehash config
- Restart the daemon
Rolls back automatically on failure.
Troubleshooting
“store: opening bbolt: timeout” – Commands that need live daemon state now route through the control socket at /var/run/csm/control.sock and no longer open the database directly, so this error should only appear from csm baseline, csm firewall ..., or the check-* dry-run commands (the remaining in-process paths; see the roadmap for the phase-2 migration). If you hit it from one of those:
- A
csm baseline,csm scan, orcsm check-deepcommand is still running - The daemon was killed uncleanly (SIGKILL, OOM) and the lock file is stale
Fix: check if a CSM process is running (pgrep csm). If not, remove the stale lock:
rm -f /opt/csm/state/csm.lock
systemctl start csm
“csm: daemon not running” – CLI commands that talk to the daemon (csm run-critical, csm run-deep, csm status) exit 2 with this message when the control socket is missing. Start the daemon with systemctl start csm. Bootstrap commands that run before the daemon exists (csm install, csm validate, csm verify, csm rehash) do not require it.
Never delete csm.db – it contains all historical findings, firewall state, email forwarder baselines, and per-account data. If you delete it, the web UI will show empty data until the next full scan cycle (up to 60 minutes for deep scan findings). If you must reset, use csm baseline instead.
Config changes require rehash – After editing csm.yaml, run csm rehash twice (the config hash is stored inside the config file, creating a circular dependency – the second run stabilizes it). Or just restart via systemctl restart csm.
RPM/DEB
yum update csm # RPM
dpkg -i csm_NEW.deb # DEB
Package managers handle stop/start automatically.
CLI Commands
Daemon
| Command | Description |
|---|---|
csm daemon | Run as persistent daemon (fanotify + inotify + PAM + periodic checks) |
Checks
| Command | Description |
|---|---|
csm run | Run all checks once, send alerts |
csm run-critical | Critical checks only (used by systemd timer) |
csm run-deep | Deep checks only (used by systemd timer) |
csm check | Run all checks, print to stdout (no alerts) |
csm check-critical | Test critical checks only |
csm check-deep | Test deep checks only |
csm scan <user> | Scan single cPanel account |
Management
| Command | Description |
|---|---|
csm install | Deploy config, systemd, auditd rules, logrotate, WHM plugin |
csm uninstall | Clean removal |
csm baseline | Full server scan, records current state as known-good. Takes 5-10 min on large servers. Required on first install. |
csm rehash | Update binary/config hashes without scanning. Use after config edits. Run twice (circular hash). |
csm status | Show current state, last run, active findings |
csm validate | Validate config (--deep for connectivity probes) |
csm config show | Display config with secrets redacted |
csm verify | Verify binary and config integrity |
csm version | Version and build info |
Remediation
| Command | Description |
|---|---|
csm clean <path> | Clean infected PHP file (backs up original) |
csm enable --php-shield | Enable PHP runtime protection |
csm disable --php-shield | Disable PHP runtime protection |
Updates
| Command | Description |
|---|---|
csm update-rules | Download latest signature rules |
csm update-geoip | Update MaxMind GeoLite2 databases |
Firewall
23 subcommands. See Firewall for the full reference.
csm firewall status
csm firewall deny <ip> [reason]
csm firewall allow <ip> [reason]
csm firewall tempban <ip> <dur> [reason]
csm firewall deny-subnet <cidr> [reason]
csm firewall grep <pattern>
csm firewall flush
# ...
Real-Time Detection
CSM detects threats in under 2 seconds using three kernel-level watchers running inside the daemon.
fanotify File Monitor (< 1 second)
Monitors /home, /tmp, /dev/shm for filesystem events.
Detects:
- Webshell creation (PHP files in web directories)
- PHP in uploads, languages, upgrade directories
- PHP in
.ssh,.cpanel, mail directories (critical escalation) - Executable drops in
.config .htaccessinjection (auto_prepend, eval, base64 handlers).user.initampering- Obfuscated PHP (encoded, packed, concatenated)
- Fragmented base64 evasion (
$a="base"; $b="64_decode"– function name split across variables) - Concatenation payloads (hundreds of
$z .= "xxxx"lines with eval at end) - Tail scanning: payloads appended to the end of large legitimate PHP files (beyond the 32KB head window)
- CGI backdoors: Perl, Python, Bash, Ruby scripts in web directories (e.g., LEVIATHAN toolkit)
- SEO spam: gambling/togel dofollow link injection in PHP/HTML files
- Phishing pages and credential harvest logs
- Phishing kit ZIP archives
- YAML signature matches (PHP, HTML, .htaccess, .user.ini)
- YARA-X rule matches (if built with
-tags yara)
Features:
- Per-path alert deduplication (30s cooldown)
- Process info enrichment (PID, command, UID)
- Auto-quarantine on high-confidence matches (category + entropy validation)
inotify Log Watchers (~2 seconds)
Tails auth, access, and mail logs in real-time. The exact file paths are chosen per platform at daemon startup – see the platform: ... line in the daemon log.
| Log | Platforms | What it detects |
|---|---|---|
cPanel session log (/usr/local/cpanel/logs/session_log) | cPanel only | Logins from non-infra IPs, password changes, File Manager uploads |
cPanel access log (/usr/local/cpanel/logs/access_log) | cPanel only | cPanel-API auth patterns |
| Auth log | All | SSH logins and failures. /var/log/auth.log on Debian/Ubuntu, /var/log/secure on RHEL family and cPanel |
Exim mainlog (/var/log/exim_mainlog) | cPanel only | Mail anomalies, queue issues |
| Apache/LiteSpeed/Nginx access log | All | WordPress brute force (wp-login.php, xmlrpc.php), real-time. Paths: /var/log/apache2/access.log (Debian), /var/log/httpd/access_log (RHEL), /var/log/nginx/access.log (Nginx), /usr/local/apache/logs/access_log (cPanel) |
Dovecot log (/var/log/maillog) | cPanel only | IMAP/POP3 account compromise |
FTP log (/var/log/messages) | cPanel only | FTP logins and failures |
| ModSecurity error log | All (if ModSec installed) | WAF blocks and attacks. Auto-discovered from the detected web server |
Nginx error log (/var/log/nginx/error.log) | Nginx hosts | General web errors, ModSecurity denies |
Cpanel-only log watchers are not registered on non-cPanel hosts, so you will not see “not found, retrying every 60s” warnings for them on plain Ubuntu or AlmaLinux.
SMTP / Dovecot Brute-Force Tracker
Detects credential stuffing and password spray against mail authentication. Runs as part of the Exim mainlog watcher on cPanel hosts.
Three attack patterns:
| Signal | What triggers it | Auto-response |
|---|---|---|
smtp_bruteforce | A single attacker IP exceeds the per-IP failed-auth threshold within the configured window | IP blocked via nftables |
smtp_subnet_spray | Multiple distinct attacker IPs from the same /24 subnet exceed the subnet threshold | Entire /24 subnet blocked via nftables |
smtp_account_spray | Many distinct attacker IPs targeting the same mailbox exceed the account threshold | Visibility finding only. No auto-block, because attackers span many subnets and no single-IP action helps |
Tunable via the thresholds.smtp_bruteforce_* keys in csm.yaml. Infrastructure IPs (from infra_ips) are never counted or blocked.
Mail Auth Brute-Force Tracker
Detects credential stuffing and password spray against IMAP, POP3, and ManageSieve. Runs as part of the Dovecot log watcher on cPanel hosts. The wrapper composes with the existing geo-based login monitor, so email_suspicious_geo keeps firing for successful logins from novel countries.
Four attack patterns:
| Signal | What triggers it | Auto-response |
|---|---|---|
mail_bruteforce | A single attacker IP exceeds the per-IP failed-auth threshold within the configured window | IP blocked via nftables |
mail_subnet_spray | Multiple distinct attacker IPs from the same /24 subnet exceed the subnet threshold | Entire /24 subnet blocked via nftables |
mail_account_spray | Many distinct attacker IPs targeting the same mailbox exceed the account threshold | Visibility finding only. No auto-block, because attackers span many subnets and no single-IP action helps |
mail_account_compromised | A successful login comes from an IP that just failed auth against the same account | IP blocked immediately. Rotate the password and revoke sessions |
Tunable via the thresholds.mail_bruteforce_* keys in csm.yaml. Independent from the SMTP tracker so the Dovecot noise floor can be tuned separately. Infrastructure IPs are never counted or blocked.
Admin-Panel Brute-Force Tracker
Counts repeated POST requests to high-value non-WordPress admin login endpoints. Runs as part of the web access-log watcher.
Covered endpoints (tight set to avoid false positives on shared hosting):
- phpMyAdmin:
/phpmyadmin/index.php,/pma/index.php,/phpMyAdmin/index.php - Joomla:
/administrator/index.php
When an IP crosses the POST-rate threshold, admin_panel_bruteforce fires and the attacker IP is auto-blocked.
Drupal /user/login and Tomcat Manager /manager/html are intentionally out of scope here. Drupal’s path is too generic on shared hosting, and Tomcat Manager uses HTTP Basic auth (repeated GET requests with 401 responses), not POST form submissions. Both need different detectors and are tracked as follow-up work.
PAM Brute-Force Listener
Real-time authentication monitoring across all PAM-enabled services.
- SSH login tracking with geolocation
- cPanel, FTP, and webmail authentication
- Blocks IPs within seconds of threshold breach
- Integrates with the nftables firewall for instant blocking
Critical Checks
34 checks, run every 10 minutes. Complete in under 1 second.
Process & System
| Check | Description |
|---|---|
fake_kernel_threads | Non-root processes masquerading as kernel threads (rootkit indicator) |
suspicious_processes | Reverse shells, interactive shells, GSocket, suspicious executables |
php_processes | PHP process execution, working dirs, environment variables |
shadow_changes | /etc/shadow modification outside maintenance windows |
uid0_accounts | Unauthorized root (UID 0) accounts |
kernel_modules | Kernel module loading (post-baseline) |
SSH & Access
| Check | Description |
|---|---|
ssh_keys | Unauthorized entries in /root/.ssh/authorized_keys |
sshd_config | SSH hardening (PermitRootLogin, PasswordAuthentication, etc.) |
ssh_logins | SSH access anomalies with geolocation |
api_tokens | cPanel/WHM API token usage |
whm_access | WHM/root login patterns, multi-IP access |
cpanel_logins | cPanel login anomalies, multi-IP correlation |
cpanel_filemanager | File Manager usage for unauthorized access |
Network
| Check | Description |
|---|---|
outbound_connections | Root-level outbound to non-infra IPs (C2, backdoor ports) |
user_outbound | Per-user outbound connections (non-standard ports) |
dns_connections | DNS exfiltration and suspicious queries |
firewall | Firewall status and rule integrity |
Brute Force & Auth
| Check | Description |
|---|---|
wp_bruteforce | WordPress login brute force (wp-login.php, xmlrpc.php) |
ftp_logins | FTP access patterns and failed auth |
webmail_logins | Roundcube/Horde access anomalies |
api_auth_failures | API authentication failure patterns |
| Check | Description |
|---|---|
mail_queue | Mail queue buildup (spam outbreak indicator) |
mail_per_account | Per-account email volume spikes |
Data & Integrity
| Check | Description |
|---|---|
crontabs | Suspicious cron jobs and scheduled commands |
mysql_users | MySQL user accounts and privileges |
database_dumps | Database exfiltration attempts |
exfiltration_paste | Connections to pastebin/code-sharing sites |
Threat Intelligence
| Check | Description |
|---|---|
ip_reputation | IPs against external threat databases (AbuseIPDB) |
local_threat_score | Aggregated score from internal attack database |
modsec_audit | ModSecurity audit log parsing |
Performance
| Check | Description |
|---|---|
perf_load | CPU load average thresholds |
perf_php_processes | PHP process count and memory |
perf_memory | Swap usage and OOM killer activity |
Health
| Check | Description |
|---|---|
health | Daemon health, binary integrity, required services |
Platform Support
Runs on every supported platform unless noted below. The daemon auto-detects OS and panel at startup and silently skips cPanel-specific checks on plain Linux hosts (no “not found” spam).
cPanel-only (skipped on plain Ubuntu/AlmaLinux):
api_tokens,whm_access,cpanel_logins,cpanel_filemanager– read WHM API and cPanel session logswp_bruteforce– iterates/home/*/public_html/*/wp-login.phpand per-domain access logswebmail_logins– parses cPanel Roundcube/Horde logsmail_queue,mail_per_account– read Exim queue and/var/log/exim_mainlog
Plain Linux equivalents that still provide coverage:
- Access log brute-force detection (
wp_login_bruteforce,xmlrpc_abuse) runs against the detected web server’s access log (/var/log/nginx/access.logor/var/log/httpd/access_log), so WordPress brute-force alerts still fire on non-cPanel hosts – they just rely on the live log watcher rather than per-domain domlog scanning. modsec_auditruns on any host with ModSecurity installed.ssh_logins, SSH brute force, PAM listener, firewall, kernel modules, RPM/DEB integrity, and threat intelligence all run on every supported platform.
Deep Checks
28 checks, run every 60 minutes. Thorough filesystem and database scans.
Filesystem
| Check | Description |
|---|---|
filesystem | Backdoors, hidden executables, suspicious SUID binaries |
webshells | Known webshell patterns (c99, r57, b374k, etc.) |
htaccess | .htaccess injection (auto_prepend_file, eval, base64 handlers) |
file_index | Indexed file listing to detect new/unauthorized files |
php_content | Suspicious PHP functions (exec, eval, system, passthru) |
group_writable_php | World/group-writable PHP files (privilege escalation) |
symlink_attacks | Symlink-based privilege escalation attempts |
WordPress
| Check | Description |
|---|---|
wp_core | Core file integrity via official WordPress.org checksums |
nulled_plugins | Cracked/nulled plugin detection |
outdated_plugins | Plugins with known CVEs |
db_content | Database injection, siteurl hijacking, rogue admins, spam |
Phishing & Malware
| Check | Description |
|---|---|
phishing | 8-layer phishing detection (kit directories, credential harvesting) |
email_content | Outbound email body scanning for credentials and suspicious URLs |
System Integrity
| Check | Description |
|---|---|
rpm_integrity | System binary verification via rpm -V |
open_basedir | open_basedir restriction validation |
php_config_changes | php.ini modifications |
DNS & SSL
| Check | Description |
|---|---|
dns_zones | DNS zone file changes (MX record hijacking) |
ssl_certs | SSL certificate issuance (subdomain takeover) |
waf_status | WAF mode, staleness, bypass detection |
Email Security
| Check | Description |
|---|---|
email_weak_password | Email accounts with weak passwords |
email_forwarder_audit | Forwarders redirecting to external addresses |
Performance
| Check | Description |
|---|---|
perf_php_handler | PHP handler configuration (DSO vs CGI vs FPM) |
perf_mysql_config | MySQL my.cnf optimization |
perf_redis_config | Redis configuration |
perf_error_logs | Error log file growth (bloat) |
perf_wp_config | WordPress wp-config.php settings |
perf_wp_transients | WordPress database transient bloat |
perf_wp_cron | WordPress cron scheduling (missed crons) |
Platform Support
The deep checks are the most cPanel-biased part of CSM because they iterate account home directories and per-user public_html trees. On plain Ubuntu/AlmaLinux the account-scan based checks do not run today:
cPanel-only (skipped on plain Linux):
htaccess,file_index,php_content,group_writable_php,symlink_attacks– iterate/home/*/public_html/**wp_core,nulled_plugins,outdated_plugins,db_content– find WordPress installs under/home/*/public_htmlphishing,email_content– scan user home directories and Exim spooldns_zones,ssl_certs– read cPanel’s DNS zone store and SSL installation recordsemail_weak_password,email_forwarder_audit– read/etc/valiases, Dovecot/Courier auth databasesopen_basedir,php_config_changes– read EA-PHPphp.iniunder/opt/cpanel/ea-php*/perf_wp_config,perf_wp_transients,perf_wp_cron,perf_php_handler– WordPress and PHP handler introspection via cPanel’s EA-PHP layout
Runs on every platform:
filesystem,webshells– fanotify and file-tree scans over/home,/tmp,/dev/shmrpm_integrity– dispatches torpm -Von RHEL family ordebsums/dpkg --verifyon Debian familywaf_status– detects ModSecurity on Apache, Nginx, and LiteSpeed across all supported distrosperf_mysql_config,perf_redis_config,perf_error_logs– rely on standard service locations
A future release may add a config-driven account_roots option so the account-scan checks can iterate generic Linux webroots (/var/www/*, /srv/http/*, etc.). See the project roadmap.
Auto-Response
When enabled, CSM automatically responds to detected threats. All actions are logged in the audit trail.
Actions
| Action | Description |
|---|---|
| Kill processes | Fake kernel threads, reverse shells, GSocket. Never kills root or system processes. |
| Quarantine files | Moves webshells, backdoors, phishing to /opt/csm/quarantine/ with full metadata (owner, permissions, mtime). Restoreable from the web UI. |
| Block IPs | Adds attacker IPs to the nftables firewall with configurable expiry. Rate-limited to 50 blocks/hour. |
| Clean malware | 7 strategies: @include removal, prepend/append stripping, inline eval removal, base64 chain decoding, chr/pack cleanup, hex injection removal, DB spam cleanup. |
| PHP shield | Blocks PHP execution from uploads/tmp directories, detects webshell parameters. |
| PAM blocking | Instant IP block on brute force threshold breach. |
| Subnet blocking | Auto-blocks /24 when 3+ IPs from the same range attack. |
| Permblock escalation | Promotes temporary blocks to permanent after N repeated offenses. |
Configuration
auto_response:
enabled: true
kill_processes: true
quarantine_files: true
block_ips: true
block_expiry: "24h" # default temp block duration
netblock: true # enable subnet blocking
netblock_threshold: 3 # IPs from same /24 before subnet block
permblock: true # promote temp blocks to permanent
permblock_count: 4 # temp blocks before promotion
Safety Guards
- Never kills root processes, system daemons, or cPanel services
- Infrastructure IPs (
infra_ipsin config) are never blocked - Quarantined files preserve full metadata for restoration
- Auto-quarantine requires high confidence: category match (webshell/backdoor/dropper) + entropy >= 4.8 or hex density > 20%. This prevents legitimate WordPress plugins from being quarantined.
- IP block rate limited to 50/hour to prevent runaway blocking
- CRITICAL alerts always bypass the email rate limit (default 30/hour)
- Trusted countries (
trusted_countries) suppress login alerts from expected geolocations
What CSM Detects in Real-Time
Beyond standard malware patterns, CSM detects advanced evasion techniques:
- Fragmented function names: attackers split
base64_decodeacross variables ($a="base"; $b="64_decode") to evade simple string matching - Appended payloads: malicious code added to the end of large legitimate files, beyond typical scan windows. CSM scans both the first and last 32KB of every PHP file.
- Non-PHP backdoors: Perl, Python, Bash CGI scripts in web directories (detects toolkits like LEVIATHAN)
- SEO spam injection: gambling/togel dofollow link injection into theme files
- WordPress brute force: real-time access log monitoring for wp-login.php and xmlrpc.php floods (blocks within seconds, not the 10-minute periodic scan)
- Admin-panel brute force: same access-log path, tracks POSTs to
/phpmyadmin/index.php,/pma/index.php,/phpMyAdmin/index.php, and Joomla/administrator/index.php. Emitsadmin_panel_bruteforceand auto-blocks the IP. Path matcher is intentionally tight to avoid false positives on shared hosting; Drupal and Tomcat Manager use different attack shapes and need separate detectors. - SMTP brute force: tails
/var/log/exim_mainlogfor dovecot SASL auth failures on submission ports. Emitssmtp_bruteforce(per-IP, auto-blocks),smtp_subnet_spray(per-/24, auto-blocks the whole subnet), andsmtp_account_spray(per-mailbox, visibility only). - Mail brute force: tails
/var/log/maillogfor direct IMAP, POP3, and ManageSieve auth failures. Composes with the existing geo-login monitor soemail_suspicious_geokeeps working. Emitsmail_bruteforce,mail_subnet_spray,mail_account_spray, andmail_account_compromised(the last one fires when a successful login arrives from an IP that just failed auth against the same mailbox; auto-blocks with no false positives by construction).
Firewall (nftables)
CSM includes a native nftables firewall engine that replaces LFD and fail2ban. It uses the kernel netlink API directly via google/nftables - no iptables, no Perl, no shell commands.
Features
- Atomic ruleset - single netlink transaction, no partial application
- Named IP sets with per-element timeouts (blocked, allowed, infra, country)
- Rate limiting - SYN flood, UDP flood, per-IP connection rate, per-port flood
- Country blocking via MaxMind GeoIP CIDR ranges
- Outbound SMTP restriction by UID (prevent spam from compromised accounts)
- Subnet/CIDR blocking with auto-escalation from individual IPs
- Permanent block escalation after repeated temp blocks
- Dynamic DNS hostname resolution (updated every 5 min)
- IPv6 dual-stack with separate sets
- Commit-confirmed safety - Juniper-style auto-rollback timer
- Infra IP protection - refuses to block infrastructure IPs
- cphulk integration - unblock flushes cphulk too
- Audit trail - JSONL log with 10MB rotation
- State persistence with atomic writes
CLI Commands
# Status
csm firewall status # Show status and statistics
csm firewall ports # Show configured port rules
# Block / Allow
csm firewall deny <ip> [reason] # Block IP permanently
csm firewall allow <ip> [reason] # Allow IP (all ports)
csm firewall allow-port <ip> <port> [reason] # Allow IP on specific port
csm firewall remove <ip> # Remove from blocked and allowed
csm firewall remove-port <ip> <port> # Remove port-specific allow
# Temporary
csm firewall tempban <ip> <dur> [reason] # Temporary block
csm firewall tempallow <ip> <dur> [reason] # Temporary allow
# Subnets
csm firewall deny-subnet <cidr> [reason] # Block subnet
csm firewall remove-subnet <cidr> # Remove subnet block
# Search
csm firewall grep <pattern> # Search blocked/allowed IPs
csm firewall lookup <ip> # GeoIP + block status lookup
# Bulk operations
csm firewall deny-file <path> # Bulk block from file
csm firewall allow-file <path> # Bulk allow from file
csm firewall flush # Clear all dynamic blocks
# Safety
csm firewall apply-confirmed <minutes> # Apply with auto-rollback timer
csm firewall confirm # Confirm applied changes
csm firewall restart # Reapply full ruleset
# Profiles
csm firewall profile save|list|restore <name> # Profile management
# Audit
csm firewall audit [limit] # View audit log
# GeoIP
csm firewall update-geoip # Download country IP blocks
Configuration
firewall:
enabled: true
ipv6: false
conn_rate_limit: 30 # new connections per minute per IP
syn_flood_protection: true
conn_limit: 50 # max concurrent connections per IP
smtp_block: false # restrict outbound SMTP
log_dropped: true
ModSecurity Integration
CSM detects and manages ModSecurity (WAF) on Apache, Nginx, and LiteSpeed across cPanel, plain Debian/Ubuntu, and plain AlmaLinux/Rocky/RHEL hosts. It deploys custom rules (cPanel only) and provides a web UI for rule overrides and escalation.
Supported Web Servers
| Web server | Config candidates | Status check | Custom rule deployment |
|---|---|---|---|
| Apache on cPanel EA4 | /usr/local/apache/conf/*, /etc/apache2/conf.d/modsec*, whmapi1 modsec_is_installed | Yes | Yes (via cPanel modsec user conf) |
| Apache on Debian/Ubuntu | /etc/apache2/mods-enabled/security2.conf, /etc/apache2/conf-enabled/*, /etc/apache2/conf.d/modsec2.conf | Yes | Not yet (plain Linux) |
| Apache on RHEL/Alma/Rocky | /etc/httpd/conf.d/mod_security.conf, /etc/httpd/conf.modules.d/* | Yes | Not yet (plain Linux) |
| Nginx on any distro | /etc/nginx/nginx.conf, /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf, /etc/nginx/modsec/main.conf | Yes | Not yet (plain Linux) |
| LiteSpeed | /usr/local/lsws/conf/httpd_config.xml, /usr/local/lsws/conf/modsec2.conf | Yes | Not yet |
When ModSecurity is not installed, the waf_status check emits a platform-specific install hint:
# On Ubuntu + Nginx:
Install: apt install libnginx-mod-http-modsecurity modsecurity-crs
# On Ubuntu + Apache:
Install: apt install libapache2-mod-security2 modsecurity-crs && a2enmod security2
# On AlmaLinux + Apache:
Install (requires EPEL): dnf install -y epel-release && dnf install -y mod_security
# On AlmaLinux + Nginx:
Install (requires EPEL): dnf install -y epel-release && dnf install -y nginx-mod-http-modsecurity
# On cPanel:
Install: WHM > Security Center > ModSecurity
Rule-staleness alerts scan both the flat CRS layout (/usr/share/modsecurity-crs/rules/*.conf) used by distro packages and the per-vendor subdirectory layout used by cPanel (/usr/local/apache/conf/modsec_vendor_configs/VENDOR/*.conf). Update instructions are also platform-specific (apt update && apt upgrade modsecurity-crs, dnf upgrade modsecurity-crs, or WHM on cPanel).
Features
- Custom CSM rules - IDs 900000-900999 in
configs/csm_modsec_custom.conf(cPanel only today) - Rule override management -
SecRuleRemoveByIddirectives for false positive suppression - Escalation control - change rule severity or action per-rule
- WAF event log parsing - correlates events by IP, URI, and rule ID
- Hot-reload - apply changes without Apache restart (cPanel only)
Web UI Pages
ModSecurity (/modsec) - WAF status overview, event log, active block list
ModSec Rules (/modsec/rules) - per-rule management:
- View loaded rules with descriptions
- Enable/disable individual rules
- Override rule severity or action
- Deploy custom rules
API Endpoints
GET /api/v1/modsec/stats WAF statistics
GET /api/v1/modsec/blocks Blocked request log
GET /api/v1/modsec/events WAF event details
GET /api/v1/modsec/rules Loaded rules list
POST /api/v1/modsec/rules/apply Apply custom rules
POST /api/v1/modsec/rules/escalation Change rule severity/action
Signature Rules
CSM uses YAML and YARA-X rules for malware detection. Rules are stored in /opt/csm/rules/ and scanned both in real-time (fanotify) and during deep scans.
YAML Rules
rules:
- name: webshell_c99
severity: critical
category: webshell
file_types: [".php"]
patterns: ["c99shell", "c99_buff_prepare"]
min_match: 1
- name: phishing_login
severity: high
category: phishing
file_types: [".html", ".php"]
patterns: ["password.*submit", "credit.*card.*number"]
exclude: ["legitimate_form_handler"]
min_match: 2
Fields:
name- unique rule identifierseverity- critical, high, or warningcategory- webshell, backdoor, phishing, dropper, exploitfile_types- file extensions to match (or["*"]for all)patterns- literal strings or regex patternsexclude- patterns that prevent a match (false positive reduction)min_match- minimum patterns that must match
YARA-X Rules (Optional)
Build CSM with YARA-X support:
CGO_LDFLAGS="$(pkg-config --libs --static yara_x_capi)" go build -tags yara ./cmd/csm/
Place .yar or .yara files alongside YAML rules in /opt/csm/rules/. CSM compiles them at startup and uses them for:
- Real-time fanotify file scanning
- Deep scan filesystem sweeps
- Email attachment scanning
Without the yara build tag, YARA rules are silently ignored.
Updating Rules
csm update-rules # download latest rules and reload the running daemon
csm update-rules now asks the daemon to reload through the control socket once the download completes. If the daemon is not running, the next start picks the files up automatically. kill -HUP $(pidof csm) still works.
Or from the web UI: Rules page > Reload Rules button.
Remote rule updates are now signature-verified. Any configuration that enables signatures.update_url or signatures.yara_forge.enabled must also set signatures.signing_key to the 64-character hex-encoded Ed25519 public key that verifies the downloaded .sig files.
YARA Forge Integration
CSM can automatically fetch curated YARA rules from YARA Forge, which aggregates and quality-tests rules from 40+ public sources including signature-base, Elastic, Malpedia, and ESET.
Configuration
signatures:
signing_key: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
yara_forge:
enabled: true
tier: "core" # core (5K rules, low FP), extended (10K), full (12K)
update_interval: "168h" # weekly
disabled_rules: # rule names to exclude from Forge downloads
- SUSP_Example_Rule
signing_key must be a hex string for the Ed25519 public key that matches the private key used to sign the remote Forge artifact. It is not a PEM block and not a file path.
If you do not have a signed update source yet, disable remote updates instead:
signatures:
signing_key: ""
update_url: ""
yara_forge:
enabled: false
Tiers
| Tier | Rules | Size | False Positive Risk |
|---|---|---|---|
| core | ~5,000 | 1.6 MB | Low (quality >= 70, score >= 65) |
| extended | ~10,500 | 3.3 MB | Medium |
| full | ~11,600 | 3.7 MB | Higher (includes score >= 40) |
Update Flow
- CSM checks the latest YARA Forge release tag on GitHub
- If newer than the installed version, downloads the ZIP for the configured tier and its detached signature
- Verifies the download against
signatures.signing_key - Filters out any rules listed in
disabled_rules - Compile-tests the rules with YARA-X before installing
- Atomically replaces the previous Forge rules file
- Reloads the YARA scanner
Custom rules in malware.yar are never overwritten by the Forge fetcher.
Disabling Rules
If a Forge rule produces false positives, add its name to disabled_rules in the config and reload:
signatures:
disabled_rules:
- SUSP_XOR_Encoded_URL
- HKTL_Mimikatz_Strings
After editing, send SIGHUP or restart the daemon to apply.
How Rules Avoid False Positives
Signature rules require structural nesting, not co-presence of strings. Two dangerous function calls appearing in the same file but in unrelated code paths won’t trigger a rule. The call must directly wrap or chain with the other for a match.
Auto-quarantine adds a safety gate: files need Shannon entropy >= 4.8 or hex density > 20% before automatic quarantine. Legitimate plugin code (~4.2 entropy) passes through; obfuscated malware (~5.5+) is caught.
Alert Rate Limiting
Default: 30 emails/hour (configurable via max_per_hour). CRITICAL findings always get through regardless of rate limit. Only lower-severity alerts are rate-limited.
Suppressions
Create suppression rules to silence known false positives:
- From the Findings page: click the suppress button on any finding
- From the Rules page: manage suppression rules directly
- Via API:
POST /api/v1/suppressions
To suppress email alerts for specific checks while keeping them visible in the web UI, use disabled_checks in your config:
alerts:
email:
disabled_checks:
- "email_spam_outbreak"
- "perf_memory"
Email AV
CSM scans email attachments in real-time using ClamAV and YARA-X on the Exim mail spool.
How It Works
- fanotify watches the Exim spool directory for new messages
- Attachments are extracted and scanned by ClamAV (socket) and YARA-X (if available)
- Infected messages are quarantined with full metadata
- Sender, recipient, and message ID are logged
Web UI
The Email page (/email) shows:
- AV watcher status (active, engine health)
- Scan statistics (scanned, infected, quarantined)
- Quarantined email list with release/delete actions
API Endpoints
GET /api/v1/email/stats Scan statistics
GET /api/v1/email/quarantine Quarantined email list
GET /api/v1/email/av/status AV watcher status
POST /api/v1/email/quarantine/ Release or delete quarantined email
Related Checks
email_content- scans outbound email body for credentials and suspicious URLsemail_weak_password- detects email accounts with weak passwordsemail_forwarder_audit- audits forwarders for exfiltration redirectsmail_queue- alerts on queue buildup (spam outbreak indicator)mail_per_account- per-account sending volume spikes
Threat Intelligence
CSM tracks, scores, and correlates attacks using a local attack database enriched with external feeds and GeoIP data.
Attack Database
- Per-IP event tracking (brute force, webshell upload, phishing, C2, WAF block)
- Threat score calculation with temporal decay (older attacks weighted less)
- Auto-block on reputation threshold
- Top attackers leaderboard
IP Intelligence
Combines multiple sources into a unified verdict:
| Source | Data |
|---|---|
| Local attack DB | Event count, types, score |
| AbuseIPDB | External reputation (if API key configured) |
| Permanent blocklist | Operator-managed persistent blocks |
| Firewall state | Currently blocked/allowed status |
| GeoIP | Country, city, ASN, ISP |
| RDAP | Network name, organization (cached 24h) |
Verdicts: clean, suspicious, malicious, blocked
Web UI
The Threat Intel page (/threat) provides:
- IP lookup with composite scoring
- Top attackers with GeoIP enrichment
- Attack type breakdown chart
- Hourly trend chart
- Whitelist management (permanent and temporary)
API Endpoints
GET /api/v1/threat/stats Attack stats and type breakdown
GET /api/v1/threat/top-attackers Top attacking IPs with GeoIP
GET /api/v1/threat/ip IP threat lookup
GET /api/v1/threat/events IP event history
GET /api/v1/threat/whitelist Whitelisted IPs
GET /api/v1/threat/db-stats Attack database statistics
POST /api/v1/threat/block-ip Block IP permanently
POST /api/v1/threat/whitelist-ip Permanent whitelist
POST /api/v1/threat/temp-whitelist-ip Temporary whitelist
POST /api/v1/threat/clear-ip Clear from attack DB
POST /api/v1/threat/unwhitelist-ip Remove from whitelist
GeoIP
MaxMind GeoLite2 integration for IP geolocation and ASN enrichment.
Features
- City database - country, city, latitude/longitude
- ASN database - ISP, organization, autonomous system number
- Auto-download on first use
- Auto-update every 24 hours (configurable)
- RDAP fallback for detailed ISP/org info (cached 24h)
Where It’s Used
- Threat intel page (top attackers, IP lookup)
- Firewall audit log (country flags)
- Login alerts (geographic context)
- Country-based login suppression (
trusted_countries) - Country blocking (firewall CIDR ranges)
Configuration
geoip:
account_id: "YOUR_MAXMIND_ACCOUNT_ID"
license_key: "YOUR_MAXMIND_LICENSE_KEY"
editions:
- GeoLite2-City
- GeoLite2-ASN
auto_update: true
update_interval: 24h
Free account: maxmind.com/en/geolite2/signup
CLI
csm update-geoip # Manual database update
csm firewall update-geoip # Download country CIDR blocks
csm firewall lookup <ip> # GeoIP + block status lookup
API
GET /api/v1/geoip IP geolocation (?ip=&detail=1)
POST /api/v1/geoip/batch Batch lookup (array of IPs)
Challenge Pages
JavaScript proof-of-work challenge pages - a CAPTCHA alternative for suspicious IPs.
How It Works
- Suspicious IP hits a protected resource
- CSM serves a challenge page requiring client-side SHA-256 proof-of-work
- Browser computes the proof (shows progress bar)
- On valid solution, CSM issues an HMAC-verified token
- Subsequent requests pass through automatically
Features
- SHA-256 based difficulty - configurable 0-5 levels
- Client-side computation - no server load
- HMAC token verification - prevents replay attacks
- Nonce-based anti-replay
- User-friendly - progress bar, instant feedback
- Bot filtering - headless browsers and scripts fail the challenge
Use Cases
- Gray-listing alternative to hard IP blocks
- Protecting WordPress login pages
- Rate limiting without blocking legitimate users
- DDoS mitigation layer
Routing Behavior
When challenge.enabled: true, CSM routes eligible IPs to the challenge page instead of hard-blocking them. This works independently of auto_response settings.
Challenge-Eligible Checks
Login brute force (wp_login_bruteforce, cpanel_login_*), WAF triggers (modsec_*), XML-RPC abuse, FTP/SSH brute force, IP reputation, and other suspicious-but-not-confirmed-malicious activity.
Always Hard-Blocked
Confirmed malware (webshells, YARA/signature matches), C2 connections, backdoor ports, phishing pages, database injections, and spam outbreaks are always hard-blocked immediately, even when challenge is enabled.
Timeout Escalation
If an IP doesn’t solve the PoW challenge within 30 minutes, it is automatically escalated to a hard firewall block.
Trusted Proxies
By default, the challenge server uses RemoteAddr to identify clients. If deployed behind a reverse proxy (e.g. Apache with mod_rewrite), configure trusted_proxies so X-Forwarded-For is trusted only from those IPs:
challenge:
enabled: true
trusted_proxies:
- "127.0.0.1"
- "::1"
Without trusted_proxies, X-Forwarded-For is ignored to prevent IP spoofing.
Successful Verification
When a client passes the challenge:
- The IP is temporarily allowed through the firewall for 4 hours
- A verification cookie is set
- The IP is removed from the challenge list (Apache stops redirecting)
Performance Monitor
CSM monitors server performance metrics and generates findings when thresholds are exceeded.
Critical Checks (every 10 min)
| Check | What it monitors |
|---|---|
perf_load | CPU load average vs core count (critical/high/warning thresholds) |
perf_php_processes | PHP process count and total memory usage |
perf_memory | Swap usage percentage and OOM killer activity |
Deep Checks (every 60 min)
| Check | What it monitors |
|---|---|
perf_php_handler | PHP handler type (DSO vs CGI vs FPM) and configuration |
perf_mysql_config | MySQL my.cnf settings (buffer pool, connections, query cache) |
perf_redis_config | Redis memory limits, persistence, eviction policy |
perf_error_logs | Error log file sizes (bloat detection) |
perf_wp_config | WordPress wp-config.php hardening and debug settings |
perf_wp_transients | WordPress database transient bloat |
perf_wp_cron | WordPress cron scheduling (missed crons, excessive events) |
Web UI
The Performance page (/performance) shows real-time metrics:
- Server load and CPU usage
- PHP process and memory charts
- MySQL and Redis health
- WordPress performance indicators
API
GET /api/v1/performance Current performance metrics snapshot
Web UI
HTTPS dashboard with polling-based live updates (10s feed, 30s stats). Dark/light theme toggle.
Pages
| Page | URL | Purpose |
|---|---|---|
| Dashboard | /dashboard | 24h stats, timeline chart, live feed, accounts at risk, auto-response summary, top attacked accounts |
| Findings | /findings | Active findings with search, filter by check/account, grouping, fix/dismiss/suppress actions, bulk operations, on-demand account scan |
| Findings > History | /findings?tab=history | Paginated archive of all findings with date range and severity filters, CSV export |
| Quarantine | /quarantine | Quarantined files with content preview, restore capability |
| Firewall | /firewall | Blocked IPs/subnets with GeoIP, whitelist management, search, audit log |
| ModSecurity | /modsec | WAF status, event log, active blocks |
| ModSec Rules | /modsec/rules | Per-rule management, overrides, escalation control |
/email | Email AV status, quarantined attachments, scan statistics | |
| Threat Intel | /threat | IP lookup with scoring/GeoIP/ASN, top attackers, attack type charts, trends |
| Hardening | /hardening | On-demand hardening audit, stored report, score, and remediation guidance |
| Incidents | /incident | Forensic timeline correlating events by IP or account |
| Rules | /rules | YAML/YARA rule management, suppressions, state export/import, test alerts |
| Account | /account | Per-account analysis: findings, quarantine, history, on-demand scan |
| Audit | /audit | System-wide action log (block, fix, dismiss, whitelist, restore) |
| Performance | /performance | Server load, PHP processes, MySQL, Redis, WordPress metrics |
Security
- Authentication - Bearer token (header or HttpOnly/Secure/SameSite=Strict cookie)
- CSRF - HMAC-derived token on all POST mutations
- Headers - X-Frame-Options DENY, Content-Security-Policy, HSTS, nosniff
- TLS - Auto-generated self-signed certificate
- Rate limiting - 5 login attempts/min, 600 API requests/min per IP
- Bearer auth skips CSRF (for API-to-API calls)
Keyboard Shortcuts
| Key | Action |
|---|---|
? | Show shortcut help |
/ | Focus search input |
g d | Go to Dashboard |
g f | Go to Findings |
g h | Go to Findings > History tab |
g t | Go to Threat Intel |
g r | Go to Rules |
g b | Go to Firewall |
j / k | Move selection down/up (Findings) |
d | Dismiss selected finding |
f | Fix selected finding |
WHM Plugin
CSM installs a WHM plugin (addon_csm.cgi) that proxies the dashboard through WHM’s interface. All API URLs are rewritten via the CSM.apiUrl() helper to support this proxy mode.
API Reference
65+ REST endpoints. All require token authentication. POST mutations require CSRF token.
Authentication
# Bearer token (header)
curl -H "Authorization: Bearer YOUR_TOKEN" https://server:9443/api/v1/status
# Cookie-based (after login)
curl -b "csm_auth=YOUR_TOKEN" https://server:9443/api/v1/status
POST requests require the X-CSRF-Token header (obtained from the login response or page meta tag).
Status & Data
GET /api/v1/status Daemon status, uptime, scan state
GET /api/v1/health Daemon health (fanotify, watchers, engines)
GET /api/v1/findings Current active findings
GET /api/v1/findings/enriched Enriched findings with GeoIP, accounts, fix info
GET /api/v1/finding-detail Finding detail with action history (?check=&message=)
GET /api/v1/history Paginated history (?limit=&offset=&from=&to=&severity=&search=)
GET /api/v1/history/csv CSV export (up to 5,000 entries)
GET /api/v1/stats 24h severity counts, accounts at risk, auto-response summary
GET /api/v1/stats/trend 30-day daily severity counts
GET /api/v1/stats/timeline Event timeline
GET /api/v1/quarantine Quarantined files with metadata
GET /api/v1/quarantine-preview Preview quarantined file content (?id=)
GET /api/v1/blocked-ips Blocked IPs with reason and expiry
GET /api/v1/accounts cPanel account list
GET /api/v1/account Per-account findings, quarantine, history (?name=)
GET /api/v1/audit UI audit log
GET /api/v1/export Export state (suppressions, whitelist)
GET /api/v1/incident Incident timeline (?ip=&account=&hours=)
GET /api/v1/performance Performance metrics snapshot
GET /api/v1/hardening Last stored hardening audit report
GeoIP
GET /api/v1/geoip IP geolocation (?ip=&detail=1)
POST /api/v1/geoip/batch Batch GeoIP lookup (JSON array of IPs)
Threat Intelligence
GET /api/v1/threat/stats Attack stats, type breakdown, hourly trend
GET /api/v1/threat/top-attackers Top attacking IPs with GeoIP (?limit=)
GET /api/v1/threat/ip IP threat lookup (?ip=)
GET /api/v1/threat/events IP event history (?ip=&limit=)
GET /api/v1/threat/whitelist Whitelisted IPs
GET /api/v1/threat/db-stats Attack database statistics
POST /api/v1/threat/block-ip Block IP permanently
POST /api/v1/threat/whitelist-ip Permanent whitelist
POST /api/v1/threat/temp-whitelist-ip Temporary whitelist (with expiry)
POST /api/v1/threat/clear-ip Clear IP from attack database
POST /api/v1/threat/unwhitelist-ip Remove from whitelist
Firewall
GET /api/v1/firewall/status Config, blocked/allowed counts
GET /api/v1/firewall/subnets Blocked subnets
GET /api/v1/firewall/audit Firewall audit log
GET /api/v1/firewall/check Check if IP is blocked (?ip=)
POST /api/v1/block-ip Block an IP
POST /api/v1/unblock-ip Unblock an IP
POST /api/v1/unblock-bulk Bulk unblock IPs
POST /api/v1/firewall/deny-subnet Block subnet
POST /api/v1/firewall/remove-subnet Remove subnet block
POST /api/v1/firewall/flush Clear all blocks
POST /api/v1/firewall/unban Unblock IP + flush cphulk
ModSecurity
GET /api/v1/modsec/stats WAF statistics
GET /api/v1/modsec/blocks Blocked requests log
GET /api/v1/modsec/events WAF event details
GET /api/v1/modsec/rules Loaded rules list
POST /api/v1/modsec/rules/apply Apply custom rules
POST /api/v1/modsec/rules/escalation Change rule severity/action
Rules & Suppressions
GET /api/v1/rules/status YAML/YARA rule counts, version
GET /api/v1/rules/list Rule files
GET /api/v1/suppressions Suppression rules
POST /api/v1/rules/reload Reload signature rules from disk
POST /api/v1/suppressions Add or delete suppression rule
POST /api/v1/rules/modsec-escalation ModSec escalation override
GET /api/v1/email/stats Email scanning statistics
GET /api/v1/email/quarantine Quarantined email list
GET /api/v1/email/av/status Email AV watcher status
POST /api/v1/email/quarantine/ Release or delete quarantined email
Hardening
GET /api/v1/hardening Load last hardening audit report
POST /api/v1/hardening/run Run hardening audit and save report
Actions
POST /api/v1/fix Apply fix for a finding
POST /api/v1/fix-bulk Bulk fix multiple findings
POST /api/v1/dismiss Dismiss a finding
POST /api/v1/scan-account On-demand account scan
POST /api/v1/quarantine-restore Restore quarantined file
POST /api/v1/test-alert Send test alert through all channels
POST /api/v1/import Import state bundle (suppressions, whitelist)
Building & Testing
Build
# Standard build (no YARA-X)
go build ./cmd/csm/
# Build with YARA-X support (requires libyara_x_capi)
CGO_LDFLAGS="$(pkg-config --libs --static yara_x_capi)" go build -tags yara ./cmd/csm/
Test
go test ./... -count=1 # all tests
go test -race -short ./... # CI mode (race detector, skip slow tests)
Fuzz
CSM has a dozen parsers that read attacker-controlled input: Exim mainlog lines, Dovecot maillog lines, Apache Combined Log Format, /proc/net/tcp rows, wp-config.php bodies, /etc/shadow, auditd comm fields, and finding messages coming back from the WebUI.
Each parser has a Go fuzz target (files named fuzz_parsers_test.go under internal/checks/ and internal/daemon/). Fuzz targets do two things:
- Their seed corpus runs as part of the normal test suite.
go test ./...executes every seed, so a known-bad input stays a regression test forever. - The actual fuzzer runs with
-fuzz=FuzzFoo.
Run a target for a fixed time while investigating:
go test ./internal/checks/... -run=^$ -fuzz=^FuzzExtractPHPDefine$ -fuzztime=30s
Run only the seeds:
go test -run=Fuzz ./internal/checks/... ./internal/daemon/...
If the fuzzer finds a crasher it writes the failing input to testdata/fuzz/FuzzFoo/<hash>. Commit that file alongside the fix and the input becomes a permanent seed.
Adding a fuzz target:
func FuzzMyParser(f *testing.F) {
// Seeds: real-world valid shape, empty, malformed.
f.Add("valid input")
f.Add("")
f.Add("corrupt/truncated")
f.Fuzz(func(t *testing.T, s string) {
_ = myParser(s) // must not panic on any input
})
}
Keep the target tight: call one function, assert it returns. Output verification belongs in a regular test.
Lint
make lint # must pass before push
gofmt -l . # must produce no output
make lint uses repo-local cache directories under .cache/ so the command behaves consistently in local shells, sandboxes, and CI runners.
Linter config in .golangci.yml: errcheck, govet, staticcheck, unused, ineffassign, gocritic, misspell, bodyclose, nilerr.
CI/CD
GitLab CI (.gitlab-ci.yml) is the internal build pipeline. It runs lint/test/package jobs, publishes internal packages, mirrors to GitHub, and creates the public GitHub release artifacts.
| Stage | What it does |
|---|---|
| lint | golangci-lint, gofmt, gosec (blocking), govulncheck |
| test | go test -v -race -timeout=300s -covermode=atomic -coverprofile -coverpkg=./internal/... ./... |
| build-image | Build CSM builder Docker image with YARA-X (manual trigger) |
| build | Two architectures: amd64 with YARA-X CGO, arm64 pure Go |
| integration | Spin up AlmaLinux + Ubuntu cloud servers via phctl, install CSM from the public mirror, run the integration test binary on both hosts, collect coverage. Only runs on main |
| package | RPM + DEB via nFPM |
| sign | Detached signatures on release artifacts |
| publish | Internal GitLab Generic Package Registry (versioned + latest) |
| repo | Publish RPM/DEB to the public mirrors.pidginhost.com apt/dnf repos |
| pages | Docs + coverage HTML (GitLab Pages preview) |
| cleanup | Remove old package versions |
| release | GitLab release on tags matching v* |
| github | Mirror to GitHub + upload release artifacts (auto on tag push) |
Public Releases
To cut a release:
- Move the
[Unreleased]heading inCHANGELOG.mdto the new version (e.g.[2.4.2] - YYYY-MM-DD), commit asrelease: cut X.Y.Z. - Tag and push:
git tag vX.Y.Z git push origin main vX.Y.Z - Wait. The tag pipeline runs integration, publishes packages to the mirror, creates the GitHub release, and uploads every artifact including the fresh
merged-coverage.out. No manual pipeline clicks needed.
The coverage badge rebuilds automatically once the GitHub release exists, because the Pages workflow fetches merged-coverage.out from the latest release that carries one (it walks back through releases if the newest is missing the asset).
Installs and upgrades on end-user servers come from the GitHub release artifacts or the apt/dnf mirror. The internal GitLab package registry is operational tooling only.
Code Conventions
- Imports: stdlib, blank line, third-party, blank line, internal. Use
goimports -local github.com/pidginhost/csm - Errors: Return up the call stack. Wrap with
fmt.Errorf("context: %w", err) - Store:
store.Global()singleton bbolt DB. Always nil-check. - State:
state.Storehandles finding dedup, alert throttling, baseline tracking, latest findings persistence. Passed to subsystems at init - Web UI: Vanilla JS, no framework, no build step. Tabler CSS framework. All API calls via
CSM.apiUrl()/CSM.post(). Escape withCSM.esc(). - Logging: New code should use
internal/log(wrapslog/slog). Legacyfmt.Fprintf(os.Stderr, "[%s] ...", ts())call sites remain valid until migrated.
Structured Logging (slog)
CSM’s daemon emits ~190 log lines via fmt.Fprintf(os.Stderr, "[%s] ...", ts()). The internal/log package provides a drop-in slog wrapper so operators can opt into JSON output for log-shipping pipelines (Loki, ELK, Datadog) without a big bang migration.
Operator controls
Two environment variables, read once at daemon startup:
| Variable | Values | Default | Effect |
|---|---|---|---|
CSM_LOG_FORMAT | text, json | text | Output handler |
CSM_LOG_LEVEL | debug, info, warn, error | info | Minimum log level |
Set via systemd drop-in:
# /etc/systemd/system/csm.service.d/logging.conf
[Service]
Environment="CSM_LOG_FORMAT=json"
Environment="CSM_LOG_LEVEL=info"
Then systemctl daemon-reload && systemctl restart csm.
Writing new logging code
import csmlog "github.com/pidginhost/csm/internal/log"
csmlog.Info("scan complete", "findings", len(f), "duration_ms", d.Milliseconds())
csmlog.Warn("log not found, will retry", "path", path, "retry_in", "60s")
csmlog.Error("alert dispatch failed", "err", err, "channel", "email")
Keys should be snake_case. Values should be machine-parseable (numbers, strings, booleans) – avoid formatted strings when you can pass the raw value.
Migrating legacy call sites
Migration is incremental and optional. The legacy format stays valid. Start with the hottest subsystems (alert dispatch, firewall operations, WAF handlers) where structured fields provide the most value, then work outward. Do not batch-convert – each subsystem should get a dedicated commit with before/after log samples in the PR description.
Keep the [TIMESTAMP] prefix of journalctl lines readable by humans: slog’s text handler uses time=... level=... msg=... which is also human-parseable, so journalctl viewers still work.
Building the Documentation
cd docs
mdbook build # generates docs/book/
mdbook serve # local preview at http://localhost:3000
Release Signing
CSM signs every release binary, tarball, and package with an ed25519 key. Installers verify signatures before touching disk, which lets operators trust the curl | bash install path and catches tampered mirror content.
Status
| Release | Signed |
|---|---|
| v2.1.1 and older | No (pre-signing era) |
| Next release | Yes (once CSM_SIGNING_KEY is set in CI) |
Until the key is provisioned, releases ship unsigned and install scripts skip verification with a warning. This is the current state of the pipeline: all signing infrastructure is in place, but no key is configured.
One-Time Operator Setup
On a trusted workstation (NOT a CI runner):
# Generate the key pair
openssl genpkey -algorithm ed25519 -out csm-signing.key
openssl pkey -in csm-signing.key -pubout -out csm-signing.pub
# Store the private key
cat csm-signing.key
# Copy the entire output to GitLab > Settings > CI/CD > Variables:
# Key: CSM_SIGNING_KEY
# Type: Variable
# Flags: Protected, Masked (if length allows; ed25519 PEMs are long
# enough to need "masked and hidden")
# Expose to protected branches/tags only
# Commit the public key into the repo
cat csm-signing.pub
# Paste the "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"
# block into scripts/install.sh and scripts/deploy.sh at the
# EMBEDDED_SIGNING_KEY variable (currently empty).
Store the private key in an offline password manager and a second hardware location. Do not commit the private key to the repo. Do not email it to yourself. Do not paste it into Slack.
What Gets Signed
The sign:artifacts CI job runs after build and package. It signs:
csm-linux-amd64,csm-linux-arm64(raw binaries)csm-*-linux-*.rpm(RPM packages)csm_*_amd64.deb,csm_*_arm64.deb(Debian packages)
The publish job signs csm-assets.tar.gz (because that tarball is created in the publish job, not the build stage). The release:github job re-signs csm-assets.tar.gz for the same reason – it rebuilds the tarball for the GitHub release.
Each signed artifact gets a .sig sibling uploaded to the same location. For example:
csm-2.2.0-linux-amd64
csm-2.2.0-linux-amd64.sha256
csm-2.2.0-linux-amd64.sig <-- new
Signature Algorithm
Ed25519 “raw” signatures, meaning the signature covers the raw bytes of the artifact with no prior hashing wrapper. Verification uses:
openssl pkeyutl -verify -pubin -inkey csm-signing.pub -rawin \
-sigfile csm-linux-amd64.sig -in csm-linux-amd64
This matches what scripts/install.sh and scripts/deploy.sh do internally.
Installer Behavior
Both install scripts read the public key from two sources in priority order:
CSM_SIGNING_KEY_PEMenvironment variable (operator override at install time)EMBEDDED_SIGNING_KEYvariable inside the script (set once when committing the public key)
If neither is set, the installer warns and proceeds – this lets pre-signing releases install. To enforce strict verification (fail rather than warn), export CSM_REQUIRE_SIGNATURES=1 before running the installer:
CSM_REQUIRE_SIGNATURES=1 curl -sSL https://raw.githubusercontent.com/pidginhost/csm/main/scripts/install.sh | bash
When a .sig file is published but verification fails, the installer always aborts (regardless of CSM_REQUIRE_SIGNATURES). A failed signature is always fatal – the installer never falls back to “trust on install”.
Key Rotation
To rotate the signing key:
- Generate a new key pair as described above.
- Update
CSM_SIGNING_KEYin GitLab CI variables. - Update
EMBEDDED_SIGNING_KEYinscripts/install.shandscripts/deploy.sh. - Tag a new release. The
sign:artifactsjob will use the new key automatically. - Existing deployed instances that use
/opt/csm/deploy.sh upgradewill continue to work as long as the new release uses the new key (they read the embedded public key fresh each time from the updated deploy.sh).
Old releases remain verifiable with the old public key because the signatures are immutable. Archive the old public key alongside the new one so historical releases can still be validated.
Verifying a Release Manually
# Download artifact + sig + public key
curl -LO https://github.com/pidginhost/csm/releases/download/v2.2.0/csm-2.2.0-linux-amd64
curl -LO https://github.com/pidginhost/csm/releases/download/v2.2.0/csm-2.2.0-linux-amd64.sig
curl -LO https://raw.githubusercontent.com/pidginhost/csm/main/scripts/csm-signing.pub
# Verify
openssl pkeyutl -verify -pubin -inkey csm-signing.pub -rawin \
-sigfile csm-2.2.0-linux-amd64.sig -in csm-2.2.0-linux-amd64
# Signature Verified Successfully
If that command fails, treat the binary as untrusted. Do not install it.