# Enodia Sentinel configuration (TOML). # Read at startup from /etc/enodia-sentinel.toml (or $ENODIA_CONFIG). # Restart after editing: sudo systemctl restart enodia-sentinel.service # # Every key is optional; omitted keys keep their built-in default. # --- timing -------------------------------------------------------------- sample_interval = 4.0 # seconds between detector sweeps cooldown = 60 # min seconds before re-alerting the same signature baseline_grace = 10 # seconds after start before new-listener/suid arm suid_refresh = 3600 # seconds between SUID baseline refreshes suid_scan_interval = 60 # seconds between (backgrounded) SUID filesystem scans # --- detectors (omit one to disable it) ---------------------------------- detectors = [ "reverse_shell", "ld_preload", "deleted_exe", "new_listener", "new_suid", "persistence", "egress", ] # --- tuning -------------------------------------------------------------- # Process names treated as "interpreters" for reverse-shell / egress checks. interpreters = [ "bash", "sh", "dash", "zsh", "ksh", "ash", "nc", "ncat", "netcat", "socat", "telnet", "python", "python2", "python3", "perl", "ruby", "php", "lua", "awk", ] # Trusted public CIDRs that should NOT trip the egress detector # (your VPS, VPN exit, monitoring endpoints, …). egress_allow_cidrs = [] # Listener ports that never alert even if they appear after baseline. listener_allow_ports = [] # Listeners owned by these process names never alert (e.g. P2P clients that # churn ports: qbittorrent, transmission, nicotine, kdeconnectd, syncthing…). listener_allow_comms = [] # Suppress new_listener when the listening binary ships with an installed # package (strong provenance signal). The single best knob for a desktop / # seedbox that runs lots of legitimate networked apps. Off by default so the # detector stays honest; turn on if new_listener is noisy. suppress_package_owned_listeners = false # Dirs where any SUID binary is treated as CRITICAL (attacker-writable). suid_hot_dirs = ["/tmp", "/dev/shm", "/var/tmp", "/home", "/run/user"] # Writable dirs always scanned for SUID even if on a separate filesystem # (a tmpfs /tmp would otherwise be skipped by the on-device walk). suid_scan_extra_dirs = ["/tmp", "/dev/shm", "/var/tmp", "/run/user"] # Persistence files/dirs watched for modification. watch_persistence = [ "/etc/cron.d", "/etc/crontab", "/etc/cron.daily", "/etc/cron.hourly", "/etc/cron.weekly", "/var/spool/cron", "/etc/systemd/system", "/etc/ld.so.preload", "/etc/passwd", "/etc/sudoers", "/etc/sudoers.d", "/root/.ssh/authorized_keys", "/root/.bashrc", "/root/.profile", ] # --- file integrity monitoring (FIM) ------------------------------------ # SHA-256 baseline of security-critical files the package manager doesn't track # (configs, /usr/local, systemd units, keys). A pacman PostTransaction hook runs # `enodia-sentinel fim-update` after every upgrade, so package changes never # alert. Package-owned binaries are instead verified against the distro's own # checksums via `pacman -Qkk` (fim_pkg_verify). fim_enabled = true fim_scan_interval = 300 # seconds between hash scans fim_paths = [] # [] = built-in critical set; or your own list fim_pkg_verify = false # run `pacman -Qkk` (thorough but slow) fim_pkg_verify_interval = 21600 # seconds between package verifications # --- tamper-evidence ----------------------------------------------------- # Detect out-of-band edits to the pacman DB itself (an attacker rewriting a # stored checksum to mask a modified binary). The DB anchor is refreshed only # by the pacman hook, so a DB change with no logged transaction = tampering. pkgdb_verify = true pkgdb_interval = 600 # seconds between DB integrity checks heartbeat_max_age = 120 # daemon heartbeat older than this = stale/silent # Layer 2: compare on-disk files to hashes in cached signed packages. This # survives a rewritten local package DB, but needs a populated package cache and # costs untar+hash work, so it samples packages on a slow cadence. pkgdb_pkgverify = false pkgdb_pkgverify_interval = 21600 # seconds between signed-package passes pkgdb_pkgverify_sample = 40 # packages verified per pass (rotates) # Sentinel's own binaries/config/units/hook are always FIM-watched (self- # integrity). For real teeth, make them immutable: chattr +i # and run an external `enodia-sentinel watchdog` against this host's dashboard. # --- anti-rootkit cross-view -------------------------------------------- # Ask the same question two ways and diff the answers: kill(0) vs /proc for # hidden processes, /sys/module vs /proc/modules for hidden LKMs, /proc/net/tcp # vs ss for hidden listeners, and interface flags for promiscuous sniffing. rootcheck_enabled = true rootcheck_interval = 300 # seconds between cross-view sweeps rootcheck_pid_cap = 65536 # upper PID to brute-force via kill(0) # --- host posture -------------------------------------------------------- # Config-hygiene audit run on demand (`enodia-sentinel posture check`), not in # the daemon loop: root SSH login, password auth, passwordless sudo, world- # writable PATH dirs, loose perms on sensitive files, and package-signature # policy. Paths are overridable mostly for testing on non-standard hosts. posture_sshd_config = "/etc/ssh/sshd_config" posture_sudoers = "/etc/sudoers" posture_sudoers_dir = "/etc/sudoers.d" # posture_path = [] # PATH dirs to audit; empty = safe default set # --- eBPF event layer ---------------------------------------------------- # Event-driven execve monitor: catches short-lived processes the poll loop # misses, matched against the Snort-style rule engine. Requires python-bpfcc # and root; degrades gracefully to polling otherwise. ebpf_exec_monitor = true # Optional path to a TOML file of extra [[exec_rules]] (sid/msg/severity/ # classtype + path_prefixes/exec_comm/parent_comm/argv_regex). exec_rules_file = "" # Add a 3s bpftrace execve trace to each snapshot (requires the bpftrace pkg). capture_execve_bpftrace = false # --- retention ----------------------------------------------------------- max_snapshots = 300 # auto-delete oldest beyond this count (0 = no cap) max_snapshot_age_days = 60 # auto-delete older than this (0 = no age cap) # --- notifications: desktop --------------------------------------------- notify_users = [] # e.g. ["luna"] — desktop notify-send on alert notify_urgency = "critical" # low / normal / critical # --- notifications: phone push ------------------------------------------ # A backend turns on when its required keys are set. Only alerts at or above # notify_min_severity are pushed. notify_min_severity = "HIGH" # MEDIUM / HIGH / CRITICAL # ntfy (open-source, self-hostable). Install the ntfy app and subscribe to the # topic; keep the topic name secret — it is the only access control on ntfy.sh. notify_ntfy_url = "" # e.g. "https://ntfy.sh" or your own server notify_ntfy_topic = "" # e.g. "enodia-" notify_ntfy_token = "" # optional Bearer token (protected topics) # Pushover notify_pushover_token = "" # application token notify_pushover_user = "" # user/group key # Generic webhook — receives the alert JSON via POST (Discord/Slack/your own). notify_webhook_url = "" # Optional dashboard base URL embedded in pushes (e.g. your Tailscale URL). dashboard_url = "" # --- web dashboard (read-only) ------------------------------------------ web_bind = "" # "" = auto-detect Tailscale IP; or an explicit address web_port = 8787 web_token = "" # bearer token; auto-generated + saved if empty on a # non-loopback bind. Set explicitly to keep the unit # fully read-only.