Skip to content

About

Kernel-level abuse protection for container hosts (Pterodactyl/Docker/VPS): detects & stops miners, DDoS, port scans, zip bombs and exploits. One static Go binary.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

🛡️ Protection Plus

Kernel-level abuse protection & antivirus for hosting providers

One static Go binary that guards Pterodactyl/Wings nodes, Docker hosts and bare VPS boxes against miners, outbound DDoS, port scans, Tor exits, proxies, webshells, zip bombs and exploits — then alerts and shuts them down automatically.

release license go platform


Table of contents


What Protection Plus is (and is not)

If you rent out game servers, VPS slices or Docker containers, some of your customers will abuse them. They run cryptocurrency miners on your CPUs, launch DDoS attacks at other people's networks, scan the internet from your IPs, host Tor exits and open proxies, upload webshells and malware through your panel, drop zip bombs into upload directories and try to escape their containers. Every one of these costs you money, bandwidth, and eventually your IP reputation and your upstream's patience.

Protection Plus is a single-binary, CGO-free Linux daemon that watches a host from the outside and deals with exactly that class of abuse. It reads /proc directly (no agents inside containers, nothing tenants can see or tamper with), gathers connections from every container's network namespace, inspects archives without extracting them, scans uploads the moment they land on disk with a SHA-256 blocklist and YARA, and enforces through a simple rule set: kill the container, suspend the Pterodactyl server, quarantine the file, kill the process — and alert you on Discord, email or a webhook.

It is built for hosting providers: Pterodactyl/Wings nodes, generic Docker hosts, and plain bare-metal VPS boxes. One policy works on all three.

What it is not: Protection Plus is egress protection. It stops your customers from attacking others; it does not mitigate inbound DDoS attacks aimed at your nodes. Keep using your firewall/XDP/upstream layer for that.

Design highlights:

  • Single static binary, zero cgo. Drop it on any Linux amd64/arm64 node. No runtime, no SDK, no kernel modules.
  • Full container network visibility. Container sockets never appear in the host's connection table, so Protection Plus reads each container's network namespace via /proc/<pid>/net/tcp — a miner phoning home from inside a container is seen and attributed to that container.
  • Pterodactyl-native. Offending containers and volume files are mapped back to the owning server UUID and can be suspended through the Application API.
  • Event-driven where it matters. An active extraction (CPU + disk-write spike) triggers an immediate zip-bomb inspection; uploads are scanned by fsnotify the moment they are closed after writing.
  • Cheap to run. One shared /proc snapshot per scan tick feeds every detector. Measured on live nodes: ~13 MB RSS and 1–3% CPU at the default 5-second interval — and a synthetic 104K pps outbound flood was detected and attributed to the offending container.
  • Safe by default. Ships in dry_run: true: it detects and alerts but never enforces until you arm it.

Feature matrix

All ten detectors are independent modules — delete a block from the config (or set enabled: false) and that feature disappears completely.

Detector Category What it catches Default state
miner miner Known miner binaries (xmrig, t-rex, nbminer…), miner argument fingerprints (stratum+tcp, --donate-level…), sustained high CPU from unknown binaries, processes running from deleted executables, and connections to mining-pool ports enabled
portscan portscan Half-open (SYN_SENT) fan-out across many ports/hosts in a sliding window, plus known scanner binaries (nmap, masscan, zmap, rustscan…) enabled
ddos ddos Per-container egress floods (pps/bps via docker stats), stress tools (hping3, t50, mhddos, LOIC/HOIC…), Java flooders (java -jar *ddos.jar), and per-process connection floods enabled
zipbomb zipbomb Decompression bombs (zip, jar, tar, gzip, xz, bzip2, 7z, rar) detected from archive metadata without extracting — caught both mid-extraction (event-driven hot trigger) and on disk (periodic sweep) enabled
exploit exploit Exploit/privesc tools (pwnkit, dirtypipe, linpeas, nsenter…), network-bound reverse-shell patterns (/dev/tcp/, nc -e, socat exec…), privilege escalation (chmod +s, sudoers tampering, namespace escapes), execution from world-writable dirs, and setuid payloads dropped in them enabled
abuse abuse Hosting-abuse services: Tor exits/relays, proxy/VPN tunnels (OpenVPN, WireGuard, Shadowsocks, v2ray/xray, trojan, hysteria, sing-box, ngrok, frp…), customer mail servers (spam), IRC bots, listeners on Tor/SOCKS/proxy ports, and executables launched from user upload dirs enabled
onaccess malware The antivirus hot path: fsnotify watches upload dirs and scans every file the moment it is closed after writing — SHA-256 against the MalwareBazaar hash blocklist, then YARA against the curated rule bundle enabled
fim system File-integrity monitoring: alerts when the daemon's own binary, its config, or any listed path changes on disk enabled
yara abuse Periodic full-tree YARA sweep of scan paths with the yara CLI (installed separately; no-op if missing) disabled
trivy system Container-image vulnerability scanning via the trivy CLI — one alert per image with HIGH/CRITICAL vulnerability counts (installed separately; no-op if missing) disabled

Installation

One-line installer (recommended)

curl -fsSL https://raw.githubusercontent.com/AnAverageBeing/protection/main/install.sh | sudo bash

The installer detects your architecture (amd64 or arm64), downloads the prebuilt static binary from the latest release to /usr/local/bin/protection, installs the systemd unit, creates /var/lib/protection, and then asks a few quick questions on the terminal (it works even when piped through curl | bash):

? Name this installation [node-fra-01]:
? Protect what? (server / docker / both) [both]:
? Discord webhook URL for alerts (blank to skip):
? Using Pterodactyl? auto-suspend abusive servers (y/N): y
?   Panel URL (e.g. https://panel.example.com):
?   Application API key (ptla_...):
? Directories to scan for zip bombs (comma-separated, or ALL) [/var/lib/pterodactyl/volumes]:
? Arm enforcement now? 'no' keeps safe dry-run mode (y/N): N

Notes on the questions:

  • Scan directories accepts a single path (/var/lib/pterodactyl/volumes), a comma-separated list (/var/lib/pterodactyl/volumes,/home), or the literal ALL, which expands to the standard user-writable locations (/tmp, /var/tmp, /dev/shm, /home, plus /var/lib/pterodactyl/volumes, /var/lib/pterodactyl/mounts, /var/www, /srv and /opt when they exist). The answer fills zipbomb.scan_paths, yara.scan_paths and onaccess.watch_paths.
  • Arm enforcement controls dry_run. Answer N and the daemon detects and alerts but touches nothing — review alerts for a few days, then arm it.
  • If /etc/protection/config.yaml already exists it is left untouched.
  • The service is enabled and started automatically when systemd is available.

Manual install from source

Requires Go ≥ 1.22.

git clone https://github.com/AnAverageBeing/protection.git
cd protection
sudo make install            # builds bin/protection, installs binary + systemd unit
sudo protection config init  # writes /etc/protection/config.yaml (dry_run: true)
sudo systemctl daemon-reload && sudo systemctl enable --now protection

make install honours PREFIX (default /usr/local) and DESTDIR.

Quick start

After installing:

# 1. Pull the threat intel (YARA rule bundle + SHA-256 malware blocklist).
#    Required once for on-access hash scanning.
sudo protection rules update

# 2. See what the daemon sees, right now — no actions are ever taken here.
sudo protection scan

# 3. Verify your alert channels end-to-end.
sudo protection test-alert

# 4. Run dry for a few days and watch what it WOULD do.
journalctl -u protection -f

# 5. Happy with the signal-to-noise? Arm enforcement.
sudo nano /etc/protection/config.yaml   # set dry_run: false
sudo systemctl restart protection

CLI reference

Global flag for every command: --config <path> (default /etc/protection/config.yaml).

Command Description
protection run Start the daemon (aliases: start, daemon). Loads config, builds detectors/alerters/actions, starts the intel auto-updater and the scan loop. Warns if not running as root.
protection scan Run every detector once against a fresh system snapshot and print findings as a severity-sorted table (SEVERITY CATEGORY TARGET TITLE). Takes no action and ignores cooldowns — pure reconnaissance. Exits after at most 60 s.
protection status Show the loaded config: installation name, mode, scan interval, dry-run state, enabled detectors, enabled alert channels, and a live Docker connectivity check.
protection config init [path] Write a fully documented starter config (mode 0640) and validate it. Refuses to overwrite an existing file. Flags: --name, --mode server|docker|both, --dry-run true|false, --discord-webhook <url>, --pterodactyl-url <url>, --pterodactyl-key <ptla_…>, --scan-paths <a,b|ALL>.
protection config check [path] Parse, apply defaults and validate a config file; prints ✓ <path> is valid or the exact problem.
protection test-alert Fire a synthetic critical alert through every enabled channel and report per-channel delivery. The way to confirm Discord/SMTP/webhook wiring.
protection rules update Download the latest YARA rule bundle and the MalwareBazaar SHA-256 blocklist (writes are atomic). Prints where each artifact landed and how many hashes were loaded.
protection debug-conns [port] Dump every TCP/UDP connection Protection Plus can see — host plus every container network namespace — with owning PID, process name and container ID, as a table. Optional numeric argument filters by local or remote port. The quickest way to verify container visibility.
protection version Print the version (aliases: -v, --version).

Exit codes: 0 success, 1 runtime error, 2 usage error.

Configuration reference

Everything lives in one YAML file (default /etc/protection/config.yaml). Every section is modular, and anything you omit falls back to a documented default, so a near-empty config still works. The tables below list every supported key; "default" is the value applied when the key is absent. The shipped config.example.yaml carries the same information as inline comments.

One caveat: boolean enabled flags and dry_run have no implicit default — omitting them means false. The shipped config and the installer explicitly set dry_run: true; keep it that way until you have reviewed alerts.

general — daemon-wide settings

Key Default Description
name hostname → primary IP → "Protection" Human label for this installation, shown in every alert. Use something that tells you which node is paging you at 3am (node-fra-01, not vps).
mode both Scope: server = host/VPS processes only, docker = containerised threats only, both = everything (recommended for game hosts).
scan_interval 5s How often detectors run. Lower = faster detection, more CPU. 5 s is the sweet spot; 10 s is fine on small nodes; do not go below 2 s.
cooldown 5m Suppresses duplicate alerts/actions for the same threat on the same target. Raise if you get alert fatigue; lower for faster re-alerts while a threat persists.
log_level info debug (noisy, for troubleshooting), info, warn, error.
log_file (none — stderr/journald only) Log file path; the shipped config uses /var/log/protection.log. Alerts and enforcement always go to stdout/journald too. Built-in rotation is available under limits:; leave limits.log_max_size_mb at 0 if you prefer logrotate.
dry_run (none — false if omitted; shipped config sets true) Safety switch. true = detect and alert only, never kill/quarantine. Run dry on a new node, review the alerts, then arm it.

detectors.miner — cryptocurrency miners

Key Default Description
enabled true in shipped config Master switch for the detector.
cpu_threshold 85 Per-core CPU% a process must sustain to be treated as a possible unknown miner. Game servers legitimately sit at 90%+; that is what the whitelist below is for. Recommended: 80–90.
sustained_seconds 45 How long CPU must stay above the threshold before flagging. Miners are constant; game servers spike and dip. 45 s separates them well.
known_processes built-in list (xmrig, minerd, t-rex, nbminer, teamredminer, srbminer, …) Known miner binary names. Setting this replaces the built-in list — copy the defaults and extend if you want both.
pool_ports built-in list (3333, 4444, 5555, 7777, 8888, 9999, 14433, 14444, 45560, 45700, 20580) Mining-pool ports. Only public remote IPs are checked, so local services on the same ports are safe. Replaces the built-in list when set.
pool_domains built-in list (minexmr, nanopool, ethermine, nicehash, …) Known pool domains, parsed and defaulted for forward compatibility. Detection today is connection/port-based; harmless to extend.
whitelist_processes built-in game-server list (java, bedrock_server, srcds, RustDedicated, fivem, valheim, …) Processes skipped by the CPU heuristic (known miner signatures still apply). Replaces the built-in list when set.

detectors.portscan — outbound port scans

Key Default Description
enabled true in shipped config Master switch.
distinct_ports 100 Flags a process fanning out half-open (SYN_SENT) connections to this many distinct destination ports within window. Legit servers do not do this; scanners do. Lower = more sensitive, more false positives from peer-to-peer software.
distinct_hosts 50 Same, for distinct destination hosts.
window 15s Sliding accumulation window for the two counters above.
known_scanner_processes built-in list (nmap, masscan, zmap, unicornscan, hping3, naabu, rustscan) Scanner binaries that flag on sight. Replaces the built-in list when set.

detectors.ddos — outbound floods

Your customers attacking other people. This is the anti-DDoS-source side; it does not mitigate inbound attacks.

Key Default Description
enabled true in shipped config Master switch.
pps_threshold 60000 Packets/sec per container, measured via docker stats. 60000 ≈ a real flood; game servers rarely exceed 10–20k pps. Lower it on small hosts (e.g. 20000) for earlier warnings.
bps_threshold 125000000 Bytes/sec per container (~1 Gbit/s). Set this to ~80% of your node's uplink so a single container cannot saturate you.
conn_threshold 1500 Simultaneous outbound TCP connections (or active UDP sockets) per process. Tor exits and floods hold thousands; game servers hold dozens per player. 1500 is conservative; 500 is aggressive.
known_tools built-in list (hping3, t50, mhddos, slowloris, goldeneye, xerxes, loic, hoic, …) Stress-tool signatures, word-boundary matched so generic fragments never cause false positives. Replaces the built-in list when set.
whitelist_processes built-in game-server list Processes skipped by the connection-flood heuristic (docker-stats rates and tool signatures still apply).

detectors.zipbomb — decompression bombs

Key Default Description
enabled true in shipped config Master switch.
scan_paths [/var/lib/pterodactyl/volumes] Directories walked for archive bombs. Set to your panel volumes and/or anywhere users can upload. protection config init --scan-paths fills this in (use ALL for all user-writable dirs).
ratio_threshold 150 uncompressed:compressed ratio that marks a bomb. Zip bombs are >1000:1; media archives ~1:1; text logs ~10:1. 150 is safe.
max_uncompressed 53687091200 (50 GiB) Absolute uncompressed ceiling regardless of ratio. Lower on small disks (e.g. 10 GiB).
max_nesting 3 Accepted for forward compatibility; inspection is currently metadata-first and does not recurse into nested archives.
hot_trigger true Event-driven path: when a process spikes CPU + disk writes (the signature of an active extraction — measured per process group so tar+gzip pipelines are caught), the archives it has open are inspected immediately instead of waiting for the next sweep. Keep this on.
hot_cpu_percent 80 Per-core CPU% (combined across the process group) that counts as "extracting".
hot_write_mbps 25 Disk write rate in MB/s (combined across the process group) that counts as "extracting".
full_scan_interval 30m Slow backstop sweep of scan_paths. The hot trigger catches live extractions; this catches bombs uploaded but not yet extracted.
full_scan_max_duration 5m Stops a sweep after this long; already-cleared files are skipped next time so it resumes where it left off.
min_compressed_size 10240 (10 KiB) Archives smaller than this are skipped — they cannot hide a meaningful bomb.
inspect_timeout 5s Hard cap per archive inspection.
max_concurrent_inspects 4 Worker-pool size for sweeps.
probe_compressed_limit 1048576 (1 MiB) Bounded probe for stream formats (xz, bzip2, wrapped gzip): at most this many compressed bytes are read…
probe_uncompressed_limit 10485760 (10 MiB) …and at most this many decompressed bytes are produced before extrapolating the total.

detectors.exploit — privesc & container escape

Key Default Description
enabled true in shipped config Master switch.
flag_reverse_shell true in shipped config Match network-bound reverse-shell patterns: /dev/tcp/, nc -e, socat exec, bash -i >&… Bare interactive shells are deliberately ignored.
flag_privilege_escalation true in shipped config Match privesc/escape patterns: chmod +s, sudoers edits, setcap, nsenter --target 1 (a container escape ⇒ critical).
watch_paths [/tmp, /dev/shm, /var/tmp] World-writable dirs scanned for setuid/setgid payloads and flagged as suspicious execution origins. Add your custom tmpfs mounts.
suspicious_processes built-in list (dirtycow, dirtypipe, pwnkit, linpeas, nsenter, deepce, …) Known exploitation tools. Replaces the built-in list when set.

detectors.abuse — Tor, proxies, tunnels, spam

Key Default Description
enabled true in shipped config Master switch.
known_processes built-in list (tor, openvpn, wireguard, shadowsocks, v2ray, xray, trojan, hysteria, sing-box, cloudflared, frpc/frps, ngrok, sendmail, postfix, exim, znc, eggdrop, …) Abuse-service binaries. Replaces the built-in list when set.
known_cmd_patterns built-in list (--orport, --socksport, --protocol vmess, inbound:, listen=0.0.0.0, …) Command-line substrings that strongly indicate abuse services.
abusive_ports built-in list (Tor 9001/9030/9050/9051/9150/9151, SOCKS 1080/1081/1090, proxy 7890/7891/8000/8001/8008/8082, Shadowsocks 8388/8389, trojan/xray 4433/8443, hysteria/sing-box 10086/12345/54321) Listening ports that indicate abuse tunnels. Replaces the built-in list when set.
watch_upload_paths [/var/lib/pterodactyl/volumes, /var/lib/pterodactyl/mounts, /home/container, /tmp, /var/tmp, /dev/shm] Executables must never run from upload dirs.
whitelist_processes built-in game-server list Game servers etc. exempt from abuse heuristics.

detectors.yara — periodic YARA sweep

Full-tree scanning of scan_paths with the yara CLI (installed separately, e.g. apt install yara). A permanent no-op if the binary is missing — shelling out instead of linking go-yara keeps the daemon CGO-free. For instant per-file scanning prefer onaccess; this is the periodic backstop.

Key Default Description
enabled false Off by default; the on-access scanner covers the hot path.
rules_dir /etc/protection/yara Directory scanned recursively (yara -r -f). rules update drops protection.yar here.
scan_paths [/var/lib/pterodactyl/volumes] Trees to sweep.
interval 10m Minimum time between full passes (scanning whole trees is expensive).

detectors.fim — file-integrity monitoring

Key Default Description
enabled true in shipped config Master switch.
paths the daemon's own binary + its config file Files to hash-watch. Add anything an attacker tampering with Protection Plus itself — or your SSH config — would touch (e.g. /etc/ssh/sshd_config).
interval 5m Time between hashing passes. Each change alerts exactly once (the new hash becomes the baseline).

detectors.trivy — image vulnerability scanning

Container-image vulnerability scanning via the trivy CLI (installed separately). A permanent no-op if the binary is missing. One event per image with HIGH/CRITICAL vulnerability counts, including the top CVEs. Alert-only by design.

Key Default Description
enabled false Off by default (requires trivy installed).
binary trivy Binary name/path to invoke.
interval 1h Time between passes (image scans are expensive).
min_severity medium Gate for the events this detector feeds the engine: medium (any HIGH present) escalates to high when a CRITICAL is present.

detectors.onaccess — on-access antivirus

The antivirus hot path. Watches upload dirs with fsnotify and scans every file the moment it is closed after writing: SHA-256 against the hash blocklist, then YARA against the rule bundle. Matched files hit the malware rule (quarantine + alert by default).

Key Default Description
enabled true in shipped config Master switch.
watch_paths the six upload paths listed under abuse.watch_upload_paths Trees to watch (recursive; new subdirectories are picked up automatically).
hash_check true SHA-256 vs the blocklist. Needs one protection rules update first; until then the blocklist is simply empty. Files over 512 MiB are skipped.
yara_check true YARA scan of each settled file. Needs the yara CLI installed; silently self-disables when it is not. Files over 64 MiB are skipped; each scan is capped at 30 s.
settle_ms 500 Wait for writes to settle before scanning — editors and downloaders write in bursts, and scanning a half-written file wastes I/O and can misfire.

intel — threat-intel management

Where rules and the hash blocklist live and how they refresh. protection rules update runs this once manually; the daemon refreshes automatically every update_interval when enabled. All downloads are written atomically (temp file + rename), so readers never see a partial file.

Key Default Description
enabled true in shipped config Enables the daemon-side auto-refresh.
rules_dir /etc/protection/yara Where protection.yar is written.
rules_url the project's curated rules/protection.yar Curated rule bundle shipped with the project (webshells, miners, Tor, Mirai, IRC bots, Linux backdoors, privesc exploits, crypto stealers). Point at your own URL to self-host.
hashlist_url MalwareBazaar full SHA-256 export (…/txt/sha256/full/) Effectively all publicly known malware hashes. For a lighter list use …/txt/sha256/recent/ instead — the full export is 1M+ hashes and costs a few hundred MB of RAM in memory. Raw-text and zipped exports are both accepted.
hashlist_file /var/lib/protection/blocklist.sha256 Where the parsed blocklist is stored (one lowercase hash per line).
custom_hashlist "" Your own extra hashes, one SHA-256 per line. Merged into the blocklist, never overwritten by updates.
update_interval 24h Daemon-side refresh cadence. Failures are logged and retried at the next interval; each source (rules / hashes) is attempted independently.

alerts — notification channels

Each channel has its own min_severity gate (info < low < medium < high < critical; unknown values fall back to medium). See Alert channels for payload details.

Key Default Description
discord.enabled / webhook_url / username false / "" / Protection Rich, severity-coloured Discord embeds. Enabling without a URL is a config error.
discord.min_severity medium medium is a good default; use high on quiet channels.
smtp.enabled, host, port, username, password, from, to, tls false, port 587 in shipped config Plain-text email. 587 = STARTTLS, 465 = implicit TLS; authenticated or open relay (leave username empty). Enabling without host/to is a config error.
smtp.min_severity high in shipped config Email is the noisy channel — gate it higher.
webhook.enabled, url, method, headers false, POST, none POSTs the full event as JSON ({"installation": …, "event": {…}}) for SIEM/automation, with custom headers. Enabling without a URL is a config error.
webhook.min_severity medium Per-channel gate.
batch.enabled false Alert aggregation: when more than threshold alerts fire inside window, they collapse into one digest alert. Enable on busy nodes so a burst pages you once, not 50 times.
batch.threshold 10 Alerts inside the window that trigger digest mode.
batch.window 1m Aggregation window.

actions — enforcement backends

Key Default Description
docker.enabled true in shipped config Enables kill_container / stop_container. If the socket is unreachable the daemon logs a warning and container enforcement is disabled — everything host-side keeps working.
docker.socket /var/run/docker.sock Docker Engine API unix socket.
pterodactyl.enabled, url, api_key false Enables suspend_server via the Application API (needs a key with server read + suspend). Enabling without url/api_key is a config error.
file.enabled true in shipped config Enables quarantine_file / delete_file.
file.quarantine_dir /var/lib/protection/quarantine Quarantine destination (created 0700; files land there chmod 000).

whitelist — trusted targets

Whitelisted paths are never scanned or flagged, even if they also fall under a scan/watch path; whitelisted containers are never flagged, killed, or suspended. Use this for known-good build dirs, your own admin containers, or anything Protection Plus must never touch.

Key Default Description
paths [] Prefix-matched: a whitelisted directory exempts everything beneath it (e.g. /srv/trusted-builds).
containers [] Full ID, short (12-char) ID, or container name.

limits — optional resource safety

All disabled by default — turn these on if the daemon itself ever misbehaves on a huge or heavily loaded node. Every field is opt-in so upgrades never change behavior.

Key Default Description
detector_timeout 0s (disabled) Hard cap per detector per tick; a detector that exceeds it is aborted and logged.
max_alerts_per_minute 0 (disabled) Global alert rate limit; excess alerts are dropped (with a once-per-minute warning in the log).
max_setuid_walk_files 0 (disabled) Caps how many files the exploit detector walks per scan when hunting setuid binaries.
cache_directory_mtimes false Skip walking directories whose mtime has not changed since the last zipbomb/exploit scan. Best-effort: some filesystems do not update directory mtime on every file change.
log_max_size_mb 0 (disabled) Rotate general.log_file at this size. Leave at 0 if you use logrotate.
log_max_backups 0 Rotated log files to keep (rotation requires both this and log_max_size_mb > 0).

rules — the enforcement policy

See the next section. When rules: is omitted entirely, the built-in default policy below is used.

Rules engine & actions

Rules map category + minimum severity → actions. They are evaluated top-to-bottom and every matching rule contributes its actions (the union is executed), so the catch-all at the bottom layers alert under any rule above it. categories: ["*"] matches everything.

Available actions:

Action Effect
alert Notify all alert channels (subject to each channel's severity gate).
neutralize Smart: kills the container for containerised threats, else SIGKILLs the host process. Used by the default rules so one policy works on Docker nodes and bare VPS alike. Falls back to process-kill when Docker is unavailable.
kill_container SIGKILL the offending container immediately — the fastest way to stop an active miner, flood or exploit.
stop_container Graceful stop (10 s grace).
suspend_server Suspend the server via the Pterodactyl Application API (silently skipped for host threats with no associated server, so it can sit in a shared rule).
quarantine_file Move the file to quarantine_dir (timestamp-prefixed), chmod 000. Preserves evidence; handles cross-filesystem moves.
delete_file Permanently remove the file. Prefer quarantine until you trust your false-positive rate.
kill_process SIGKILL the offending PID (refuses PID ≤ 1).
log_only Record only — the event is logged, nothing else happens.

The default policy (identical to the shipped config):

rules:
  - {name: miners,    categories: [miner],    min_severity: high,   actions: [neutralize, suspend_server, alert]}
  - {name: ddos,      categories: [ddos],     min_severity: high,   actions: [neutralize, suspend_server, alert]}
  - {name: abuse,     categories: [abuse],    min_severity: high,   actions: [neutralize, suspend_server, alert]}
  - {name: malware,   categories: [malware],  min_severity: high,   actions: [quarantine_file, alert]}
  - {name: exploits,  categories: [exploit],  min_severity: high,   actions: [neutralize, alert]}
  - {name: zipbombs,  categories: [zipbomb],  min_severity: medium, actions: [quarantine_file, alert]}
  - {name: portscans, categories: [portscan], min_severity: medium, actions: [alert]}
  - {name: catch-all, categories: ["*"],      min_severity: low,    actions: [alert]}

Matching details:

  • Severity ladder: info < low < medium < high < critical. A rule fires when the event's severity is ≥ min_severity. An unrecognised severity string parses as medium, so a typo never silently disables a rule.
  • Cooldown: a general.cooldown (default 5 m) suppresses duplicate alerts/actions for the same threat on the same target. The dedup key is category|detector|target, where target is the container ID, the file path, or pid:<n> — two findings about the same miner in the same container collapse into one page.
  • Whitelist first: events whose path or container matches the whitelist: section are dropped before rules ever see them. Paths are prefix-matched; a whitelisted directory exempts everything beneath it. Containers match by full ID, short ID, or name and are never flagged, killed, or suspended.
  • Mode scoping: general.mode filters events by origin — container events (those with a container ID or Pterodactyl server) belong to docker scope, everything else to server scope.
  • Dry run: with dry_run: true every destructive action is logged as [dry-run] would run action … and skipped. Alerts still fire.

Alert channels

Every channel dispatches in its own goroutine with a 12 s timeout and honours its own min_severity gate. Test the whole pipeline with protection test-alert.

  • Discord — rich embeds: severity-coloured, category emoji, the human description, a bold target line, an inline metadata grid (installation, container, Docker ID, Pterodactyl server, process+PID, path) and the full evidence map as a YAML block. username overrides the webhook display name.
  • SMTP — plain-text email with the same metadata and evidence. STARTTLS (587) or implicit TLS (465), authenticated or open relay.
  • Webhook — POSTs {"installation": "<name>", "event": {…}} with the complete event as JSON (severity, category, target fields, evidence map) to any endpoint, with configurable method and headers — wire it into a SIEM, PagerDuty, n8n, or your own automation.

Alert batching. When alerts.batch is enabled, alerts flow individually until more than threshold fire inside window; then individual alerts pause and everything is collected until the window closes, after which one digest alert summarizing the burst (3× miner: Cryptocurrency miner running; 1× ddos: Outbound flood…, at the highest severity seen) is sent. Enable it on busy nodes so a burst pages you once. Independently, limits.max_alerts_per_minute can hard-cap total alert volume.

Technical design

One shared snapshot per tick. Each scan interval the engine calls system.Gather() once — a full /proc process walk (stat, cmdline, exe, uid, cgroup, I/O counters) plus the merged connection table — and hands that single coherent snapshot to every detector. This cuts per-tick syscall cost by ~4× compared with each detector walking /proc on its own, and guarantees all detectors reason about the same instant.

Per-container network namespace gathering. Docker gives each container its own network namespace, so container sockets never appear in the host's /proc/net/tcp. Protection Plus buckets processes by network-namespace inode and reads /proc/<pid>/net/tcp{,6} (and udp{,6}) through a representative process of each namespace. Two wins: connections are attributed to their container for free, and socket→PID resolution only has to scan that container's few processes — no host-wide fd walk. Containers in host-network mode are attributed via the PID→container map from /proc/<pid>/cgroup (a 64-hex container ID; works on cgroup v1/v2, docker/containerd/k8s).

Socket→PID resolution with a time budget. Mapping a socket inode to its owning PID means readlinking /proc/<pid>/fd/* — hundreds of thousands of syscalls on a busy host. The walk is fanned out across a worker pool (4×CPU, min 4, max 64 workers) with per-process fd tables chunked at 2048 entries so one socket-leaking process cannot pin a worker, and the whole resolution is capped by a 1.5 s budget. On a healthy node it finishes in milliseconds; on a pathological one the unresolved connections simply keep PID 0 and are skipped by PID-level checks — container-level checks are unaffected, and a noisy neighbour can never stall the detection loop.

Zip-bomb inspection: metadata first, never full extraction. Formats with real headers are read for their declared sizes, essentially for free: zip central directory (also .jar), gzip's 4-byte ISIZE trailer, tar headers, 7z and rar file headers. Stream formats without a directory (xz, bzip2) get a bounded probe — at most 1 MiB compressed in, 10 MiB decompressed out — and the total is extrapolated from the actual consumed/produced ratio. The gzip ISIZE field is the uncompressed size mod 2³², so when it reports a value smaller than the compressed file (a wrap — a real gzip stream never shrinks its input) the detector falls back to the bounded probe. Archives under 10 KiB are skipped, each inspection has a 5 s timeout, and sweeps use a 4-worker pool with a 5 m total budget plus a cleared-file mtime cache so an interrupted sweep resumes where it left off. An archive is flagged when its ratio ≥ ratio_threshold (150) or its declared uncompressed size ≥ max_uncompressed (50 GiB); severity is high when the ratio is 10× the threshold or the size ceiling is hit.

The zip-bomb hot trigger is process-group aware. Real extractions are pipelines: tar xzf burns CPU in the gzip child while the tar parent does the writes — neither alone trips a per-PID check. So CPU% and write rate are aggregated per process group; when a group exceeds 80% CPU and 25 MB/s writes, the archives its members have open (via /proc/<pid>/fd) plus the archives in their working directories are inspected immediately. Paths are translated out of container mount namespaces via /proc/<pid>/root. A bomb is caught mid-unzip in seconds, not after the 30-minute sweep.

On-access pipeline: hash, then YARA. A single fsnotify watcher covers the upload trees recursively (new directories are added as they appear). Because writers write in bursts, each path is debounced for settle_ms (500 ms) and only scanned once quiet, by a pool of 2 workers. The scan is: SHA-256 against the in-memory blocklist (files ≤ 512 MiB; a hit is critical) — then the yara CLI against protection.yar, falling back to a recursive scan of the rules directory (files ≤ 64 MiB, 30 s cap; a hit is high). The event queue is capped at 1000 entries (oldest dropped with a warning) so a write storm cannot grow memory, and events are drained into the engine on the next tick.

Intel updates are atomic and independent. rules update (and the 24 h auto-refresh) downloads the YARA bundle and the MalwareBazaar SHA-256 export — raw text or zipped, detected by magic bytes — parses, lowercases and deduplicates the hashes, and writes each artifact with temp-file + rename so readers never see a partial file. Rules and hashes are fetched independently; one failing does not block the other. Your custom_hashlist is merged in memory and never overwritten. Downloads carry a protection/<version> User-Agent and a 5-minute HTTP timeout.

FIM. Watched files are streamed through SHA-256 (multi-gigabyte files are never loaded into memory). First sighting records a baseline silently; any later content change raises one high-severity event and becomes the new baseline, so each change alerts exactly once.

Engine: dedup, cooldown, timeouts, rate limits. Every event is keyed (category|detector|target) and suppressed for cooldown; expired keys are garbage-collected at 2× cooldown. limits.detector_timeout wraps each detector run in a context that aborts and logs slow detectors instead of letting them stall the tick. limits.max_alerts_per_minute caps total alert dispatches. On-access caps its queue; the zipbomb sweep caps workers and duration; the exploit setuid walk can be capped with max_setuid_walk_files; cache_directory_mtimes skips unchanged trees.

systemd integration. The unit runs Type=simple with WatchdogSec=60s: the daemon sends READY=1 at startup and pets WATCHDOG=1 after every scan tick via a pure-Go sd_notify (no libsystemd), so a hung detection loop gets the service killed and restarted (Restart=on-failure). The daemon runs as root — it must read other users' processes — but the unit still applies ProtectSystem=strict with a restricted ReadWritePaths (/var/lib/protection, /var/log, Pterodactyl volumes when present), ProtectHome=read-only, ambient capabilities limited to CAP_SYS_PTRACE, CAP_KILL, CAP_DAC_READ_SEARCH, and hard resource caps MemoryMax=256M / CPUQuota=50% — the watchdog can never become the problem it watches for.

Measured footprint. On live nodes: ~13 MB RSS and 1–3% CPU at the default 5 s interval. A synthetic 104K pps outbound flood from inside a container was detected and attributed to that container.

Detection methods

How each detector actually decides — and what keeps it from crying wolf on a game host.

Miner

Three independent signals that escalate when combined:

  1. Signature — binary names are matched against comm and the exe basename only (never the full command line, so a shell that merely references a path is not flagged), plus miner argument fingerprints that no legitimate launcher carries (stratum+tcp, --donate-level, --randomx, --algo , --nicehash…). ⇒ high on sight.
  2. Sustained CPU — per-core CPU% from jiffy deltas across ticks must stay above cpu_threshold for sustained_seconds ⇒ medium (catches custom / unknown miners). A process burning CPU from a deleted on-disk binary is a classic masked miner ⇒ high.
  3. Pool connection — an established connection to a known mining-pool port ⇒ high.

Signature and sustained high CPU together ⇒ critical.

False-positive design: pool ports are only checked against public remote IPs — a local redis or HTTP-alt on port 3333 is ignored. Game-server processes (java, srcds, RustDedicated…) are exempt from the CPU heuristic but not from signature matching, so a miner actually named xmrig inside a game container is still caught.

DDoS (outbound)

  • Container egress rate — docker stats sampled per tick; pps or bps over threshold ⇒ high, attributed to the container and its Pterodactyl server.
  • Tool signatures — matched on word boundaries, so loic matches ./loic but byte-style fragments inside unrelated flags never trigger. The built-in list deliberately excludes short/generic tokens.
  • Java flooders — java processes whose command line contains ddos, booter, stresser, flood, doser — the classic game-node booter pattern.
  • Connection floods — one process holding more than conn_threshold simultaneous outbound connections (TCP established/half-open, or active UDP sockets for amplification floods). Game servers are exempt via the whitelist.

Port scan

Accumulates distinct destination ports and hosts per process from half-open (SYN_SENT) connections over the sliding window. Crossing either threshold ⇒ medium; a known scanner binary (nmap, masscan…) ⇒ high on sight.

Zip bomb

See Technical design: metadata-first ratio/size checks across eight archive formats, event-driven hot trigger plus periodic sweep. Pterodactyl volume paths (/var/lib/pterodactyl/volumes/<uuid>/…) are mapped back to the owning server UUID so suspend_server and quarantine target the right customer.

Exploit / escape

  • Known privesc/exploit tools (pwnkit, dirtypipe, linpeas, nsenter…) ⇒ high.
  • Reverse shells — network-bound patterns only: /dev/tcp/, nc -e, socat exec, bash -i >& /dev/tcp… Bare interactive shells are intentionally excluded to avoid false positives. ⇒ high.
  • Privesc / escape — chmod +s, sudoers tampering, setcap, nsenter --target 1; nsenter into PID 1's namespaces from inside a container is a textbook escape ⇒ critical.
  • Droppers — any process executing from a world-writable scratch dir ⇒ medium; setuid/setgid binaries found in those dirs ⇒ high.

Abuse

Four signals: known abuse-service binaries or cmdline patterns (Tor flags, --protocol vmess, ss-server…) ⇒ high; listening sockets on Tor/SOCKS/ proxy ports ⇒ high; and any executable launched from a user upload directory (miners/proxies uploaded through a panel and run) ⇒ medium. Game servers are whitelisted from all of it.

On-access (malware)

SHA-256 blocklist hit ⇒ critical ("Known malware (hash blocklist)"); YARA rule hit ⇒ high ("Malware detected by YARA rule", rule name in evidence). Both map to the malware category and the default quarantine_file + alert rule.

YARA sweep / Trivy / FIM

  • yara: yara -r -f <rules_dir> <path> per scan path every interval; each match is a high event with the rule name; if the matched file is currently executing, the event is annotated with the process and its container so actions can target it directly.
  • trivy: trivy image --severity HIGH,CRITICAL --format json per image (images of running containers via the API, else docker image ls); one event per affected image — high when a CRITICAL is present, else medium — with counts and the top 5 CVEs.
  • fim: content change on a watched file ⇒ high, once per change.

Security & privacy

  • Runs as root. Reading other users' /proc entries, mapping sockets to PIDs, entering container namespaces and killing processes require it (or equivalently CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, CAP_KILL plus Docker socket access). The systemd unit pins exactly those capabilities and sandboxes the rest of the filesystem read-only.
  • What it reads: /proc process metadata and connection tables (host + container namespaces), files under your configured scan/watch paths (archive headers and bounded probes for zip bombs; full contents only for on-access hashing/YARA and FIM hashing), and the Docker unix socket. It writes only to its state dir (/var/lib/protection), its log file, the quarantine dir, and the intel files.
  • What it sends out: nothing, except (a) threat-intel downloads (intel.rules_url, intel.hashlist_url), (b) alerts you configured (Discord webhook, SMTP, your webhook URL), (c) Pterodactyl API calls when suspend_server is enabled, and (d) the local Docker socket. There is no telemetry, no phone-home, no license server. Alerts contain host-local details (paths, command lines, IPs) — send them only where you want that data.
  • Secrets in config: your Discord webhook, SMTP password and Pterodactyl API key live in /etc/protection/config.yaml; config init writes it mode 0640 (root-readable). Keep it that way.
  • Quarantine is evidence-preserving: files are moved, not deleted, and chmod 000'd so they cannot execute; the quarantine dir itself is 0700.

Troubleshooting

docker unreachable at /var/run/docker.sock … container detection/enforcement disabled Logged at startup when the socket is missing or denied. Host-side detection keeps working; container rates, attribution and kill_container / stop_container do not. Check that Docker is running and the daemon runs as root (or can access the socket). protection status shows the same check live.

YARA checks silently do nothing. The yara detector and on-access yara_check shell out to the yara CLI; if it is not installed they are a permanent no-op by design (the daemon stays CGO-free). Install it (apt install yara) and run protection rules update so /etc/protection/yara/protection.yar exists. The periodic yara detector is also enabled: false by default — turn it on if you want the full-tree sweep.

Trivy does nothing. Same story: install the trivy CLI and set detectors.trivy.enabled: true. The daemon logs at startup when the binary is not found.

onaccess: watch path … is not accessible / zipbomb: scan path … is not accessible / exploit: watch path … is not accessible A configured path does not exist on this node (common on non-Pterodactyl hosts). The detector skips it and keeps working; remove the path from the config to silence the warning.

could not load hash blocklist / blocklist has 0 hashes. Run sudo protection rules update once. Until then, on-access hash checking simply has an empty blocklist (YARA scanning still works).

Nothing is being killed or quarantined. You are in dry_run: true — the log line says [dry-run] would run action …. Set dry_run: false and systemctl restart protection to arm.

Too many / too few alerts. Tune per-channel min_severity, raise general.cooldown, enable alerts.batch, or set limits.max_alerts_per_minute. To see everything the detectors produce without cooldowns, run sudo protection scan.

Config errors at startup. Run protection config check — it prints the exact problem (e.g. discord alert enabled but webhook_url not set).

Verifying network visibility. protection debug-conns lists every connection the daemon sees, host and container, with owning PID and container ID — confirm a suspicious connection appears there before wondering why it was not flagged. Add a port number to filter.

Debugging detection decisions. Set general.log_level: debug and watch journalctl -u protection -f.

Building from source & development

Requires Go ≥ 1.22 (Linux; CGO is disabled — the binary is fully static).

make build       # -> bin/protection (static, trimmed, version-stamped)
make test        # unit tests across config, engine, detectors, actions, intel, system, core
make vet         # go vet
make fmt         # gofmt the tree
make run         # build + one-off scan against the default config
sudo make install    # binary + systemd unit (PREFIX, DESTDIR supported)
make uninstall   # remove installed files
make clean       # remove bin/

Version is injected at build time from git (git describe --tags --always --dirty, falling back to 1.0.0) into both the CLI and the intel User-Agent.

Cross-compile for ARM nodes:

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o protection-linux-arm64 ./cmd/protection

CI. Every push/PR to main runs gofmt verification, go vet, go test ./... and go build ./... (.github/workflows/ci.yml). Pushing a v* tag builds static linux/amd64 and linux/arm64 binaries and attaches them to a GitHub Release (.github/workflows/release.yml) — the artifacts the installer downloads.

Layout:

cmd/protection        CLI entrypoint + embedded starter config
internal/core         Event, Severity, Category (shared leaf package)
internal/config       YAML config + defaults + validation
internal/system       /proc + network introspection, sd_notify (pure Go)
internal/docker       tiny Docker Engine API client over the unix socket
internal/detectors    miner, portscan, ddos, zipbomb, exploit, abuse, yara, fim, trivy, onaccess
internal/intel        rules + hash blocklist download/refresh (atomic)
internal/alerts       discord, smtp, webhook
internal/actions      docker, file, pterodactyl, process, neutralize
internal/engine       scheduler, snapshot fan-out, dedup, rule matching, batching
rules/protection.yar  curated YARA bundle served by `rules update`
packaging/            systemd unit

Adding a detector: implement detectors.Detector (Name() string + Run(ctx, *system.Snapshot) ([]core.Event, error)), register it in buildDetectors in cmd/protection/main.go, and add its config block to internal/config. Detectors receive the shared snapshot, must return within roughly one scan interval, and should treat every error as non-fatal (log and skip) so one broken detector never blinds the rest.


Protection Plus v1.0.0 • Built for hosting operators who'd rather sleep at night • MIT licensed

About

Kernel-level abuse protection for container hosts (Pterodactyl/Docker/VPS): detects & stops miners, DDoS, port scans, zip bombs and exploits. One static Go binary.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages