Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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