Skip to content
Altis-TechnologyPublic

About

Defensive abuse and malware detection for game server hosting panels (Pterodactyl/Pelican). Finds miners, tunnels, floods, and container escapes on servers you operate. Detects; never acts.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

Rookery

Meet Rookery — abuse and malware detection for Pterodactyl/Pelican. Read-only collection, human-first enforcement.

https://github.com/Altis-Technology/rookery

Rookery is a defensive abuse- and malware-detection system for operators of game server hosting panels (Pterodactyl/Pelican). It finds crypto miners, proxy and tunnel abuse, botnet nodes, phishing kits, decompression bombs, container-escape attempts, and panel-feature abuse running on servers the operator hosts — and it can, under strict and deliberate constraints, suspend the servers it proves out.

It is built for free-tier and budget hosts, where abuse arrives within days and upstream abuse complaints arrive after the damage is done.

Before Rookery: late complaints and chaotic triage. With Rookery: corroborated findings and human decision. Unknown is never clean; no single signal escalates.

The sixty-second mental model

The pipeline: collect, detect, score, decide — an incident goes to a human, or the gate.

Everything Rookery does follows one pipeline:

collect → detect → score → incident → human, or the gate
  1. Collect. A daemon on each node gathers facts, read-only: files (paths, hashes, sizes — never contents), startup commands, process lists, socket listeners, console transcripts. Collection never modifies, moves, or deletes a customer file; CI proves it by hashing a fixture tree before and after a full scan.
  2. Detect. Detectors turn facts into findings. Each finding is one observation tied to one signal — a rule declared in YAML, not code, with a weight, a false-positive risk, a plain-language explanation, and positive/negative fixtures enforced in CI.
  3. Score. Findings for one server combine into an incident score: repeated sightings deduplicate, high-false-positive signals are capped, and distinct signal families multiply the score. Corroboration is the whole design — one weak signal means little; three independent ones mean a lot.
  4. Decide. An incident goes to a human (Discord, CLI, logs). If — and only if — the operator has deliberately enabled it, an incident that passes every check of the enforcement gate can suspend the server, after a public countdown with a veto.

Two rules govern everything:

  • Unknown is never clean. A detector that cannot evaluate something says so, with the reason. Silence is never reported as safety.
  • No single signal escalates — with one documented exception: a high-confidence container-escape signal escalates alone, because a missed escape costs the whole node (ADR-020).

What it does not do

  • It does not act without being armed, twice. Automated suspension requires two separate flags (enforcement.enabled in both the daemon and module configs), a completed shadow-mode period, and a passing gate evaluation. Out of the box, Rookery finds and reports; it touches nothing.
  • It never deletes, throttles, reinstalls, or modifies a server. The only enforcement verb is suspend, and it is reversible in one call.
  • It does not scan third parties. It scans only infrastructure the operator owns, using the operator's own credentials, under a terms-of-service disclosure the operator makes to their users.
  • It does not collect file contents. Findings carry hashes, paths, sizes, metric values, and redacted excerpts. File contents never leave the node without an explicit, logged, per-incident evidence request.
  • It does not inspect traffic payloads. Network detection works from flow metadata: who talked to whom, on what port, how much, how often.
  • It contains no live malware, no exploit code, and no working attack tooling. Detection rules and synthetic fixtures only.

What it catches

The catch grid: miners and compute theft, proxies and tunnels, malware and exploits, resource and panel abuse.

Signals are grouped into families; each family is owned by a detection module. rookery rules list is the authoritative, current list — what follows is the map.

Module Family Catches
MinerGuard static, config, runtime Miners and compute theft: known miner hashes, startup-command drift, runtime-acquired binaries, idle CPU burn, download-and-execute startup commands
ArchiveGuard static Decompression bombs and quota abuse, detected before extraction — never extract-then-measure
TunnelGuard network, static Proxies and tunnels: proxy protocol configs (vless/vmess/…), tunnel tokens in env files, proxy cores (xray/sing-box/v2ray), public proxy and Tor listings
EgressGuard network Outbound abuse: floods, scanning, spam, C2 — flow metadata only
EscapeGuard panel, audit Container escapes and the node configuration that would allow them: runtime sockets, cgroup writes, privileged containers, missing seccomp/userns, EOL kernels, vulnerable panel/wings versions
PanelGuard panel Abuse of the panel's own features, from its activity log — needs no agent
ServiceGuard network Listening services: unallocated ports, exposed datastores, mail/services, excess listeners versus the game's profile
ResourceGuard runtime Resource attribution: fork bombs, PID/thread exhaustion, restart cycling, excess processes versus the game's profile
ConsoleGuard console Console transcripts: runtime downloads, shell spawns, log self-deletion, C2 pushes — redacted, chat excluded
WorkloadGuard workload "Is this actually a game server?" — positive identity scoring against the declared egg; broken installs surface as support findings, not accusations
EggGuard egg Malicious or drifted egg definitions: installer scripts, images, variables, upstream diffs

The field-tested reality these encode: abusers control binary names, file locations, and variable names — they cannot control the arguments their tools require, the network behaviour, or the process tree. Every durable signal keys on the second set.

A worked example

A customer uploads a "plugin pack". Inside plugins/EssentialsX/MyWorlds/usr/bin/ sits a renamed cloudflared. The pieces land like this:

  1. static.tunnel-binary fires on the binary's class; static.hidden-executable fires on the camouflaged dot-path nesting.
  2. plugins/.env holds CLOUDFLARED_TOKEN=… → static.tunnel-token fires. The token value never leaves the node; the finding carries the key name, the path, a digest, and a redacted excerpt (INV-011).
  3. A bare UUID in the same file fires static.proxy-uuid-context only because proxy indicators co-occur — a UUID alone is a config file, and the scanner encodes that.
  4. Three independent families corroborate → the scorer multiplies → the incident lands in Discord as high severity, in plain language an operator can paste into a ticket.

If enforcement is armed and the gate passes, the server is queued for suspension with a printed veto command; the countdown expires; the suspension is verified by reading state back from the panel. Every step is audited.

The safety model (enforcement)

The raw version of abuse response is a curl that suspends a server. Rookery's version is the same API call reached only through the gate. Same endpoint; completely different safety. All of this is in force whenever enforcement is enabled:

  • Off by default, always (INV-009). Two flags, in two files, both required. SIGHUP can never enable it.
  • Shadow mode first. The daemon records what it would have suspended until a configured sample of operator-reviewed decisions stays under a false-positive ceiling. No reviewed sample, no live action.
  • The gate. Every incident must pass every condition: signals on the eligible list, family diversity or a definitional signal, score and confidence floors, and exclusions — young servers, old accounts, paying customers (via the never-suspend list), recent incident history, allowlisted servers.
  • Grace with a veto. An eligible suspension waits out a countdown (default 10m, 2m for critical). rookery enforce veto <incident-id> cancels it.
  • Circuit breaker. Ceilings per hour, per day, and per account. A trip halts all automation until a human runs rookery enforce resume. A rejected panel token trips it too.
  • The operator overrules. If a human unsuspends a server, the machine stands down for the configured cooldown. It does not argue.
  • Kill switch. One file (/etc/rookery/DISABLE_ENFORCEMENT), statted before every action. Works without a restart, without the panel.
  • Audit everything. Every queue, execution, veto, trip, and resume lands in a durable audit log with actor, reason, and the gate's per-condition evaluation (INV-006).

Check it any time: rookery enforce status shows the breaker, the grace queue, and shadow progress.

Privacy boundaries

Zero files modified during collection: read-only, CI verified.

Surface Rule Invariant
Files contents never collected; hashes/paths/sizes only INV-003
Console findings never carry raw log lines; bounded redacted excerpts INV-010
Config files tokens, UUIDs, endpoints redacted at the scanner; fuzzed INV-011
Owner email audit trail and operator notification only, never finding evidence INV-011
Discord no customer file contents or raw console lines, ever INV-003

Install

Requires Go 1.26 to build; a prebuilt binary drops anywhere.

go build -o rookery ./cmd/rookery

The systemd layout (see deploy/rookery.service for the full unit):

/usr/local/bin/rookery            the binary
/etc/rookery/                     configuration root (daemon.yaml, rules/, rookery.env)
/var/lib/rookery/                 durable state (store, suppressions, grace queue, audit log)

Environment (in /etc/rookery/rookery.env, never in flags or files):

ROOKERY_PANEL_URL=https://panel.example.com
ROOKERY_PANEL_TOKEN=ptla_...            # application API, read scope
ROOKERY_PANEL_ADMIN_TOKEN=ptla_...      # application API, read+write — only for enforcement
ROOKERY_DISCORD_BOT_TOKEN=...           # only for the Discord bot
ROOKERY_DISCORD_PUBLIC_KEY=...          # only for the Discord bot
ROOKERY_DISCORD_APPLICATION_ID=...      # only for the Discord bot

The read token needs read-only scopes; the admin token is required only when enforcement is enabled, and the suspension client uses exactly two verbs — suspend and unsuspend — with read-back verification.

Run

The daemon is the steady state: it watches the panel for new servers, schedules scans, posts findings, and (when armed) runs the enforcement loop.

rookery start --role all --node 4 --config /etc/rookery/daemon.yaml \
  --audit-config /etc/rookery/escapeguard.yaml --notify-config /etc/rookery/notify.yaml \
  --collector-config /etc/rookery/collector.yaml --archive-config /etc/rookery/archiveguard.yaml \
  --workload-config /etc/rookery/workloadguard.yaml --config-scan-config /etc/rookery/configscan.yaml \
  --score-config /etc/rookery/score.yaml

Startup prints a first-run status block: enforcement state, audit outcome, loop cadences, and — by name — every detector family that is not yet wired, with the reason. What the daemon cannot do, it says out loud.

Every command

The Rookery CLI: audit, scan, rules list, enforce status. Exit codes — 0 clean, 1 findings, 2 usage, 3 collection failure.

Exit codes are the charter's: 0 clean, 1 findings, 2 usage error, 3 collection failure.

Command What it does
rookery audit [--snapshot <path>] [--json] Node configuration audit: is this node one command away from a container escape? Specific remediation per finding. The --snapshot fixtures demo it in sixty seconds, no panel required.
rookery scan --node <id> [--server <uuid>] [--json] [--notify] Scan one node's servers through the panel API: startup drift, static passes, config indicators, workload identity, binary names. --notify posts to Discord. Budgets via --scan-max-files/-bytes/-duration.
rookery rules list [--json] Every registered signal: id, module, family, weight, FP risk.
rookery rules test Validates every rule has positive and negative fixtures. CI runs this; run it after editing rules.
rookery explain <signal-id> The plain-language explanation of one signal, written to be pasted into a support ticket.
rookery suppress add --signal <id> [--path-prefix <p>] --reason <text> [--expires <d>] Narrow a rule that is wrong for your fleet — scoped to a signal, a server, an egg, and optionally a path prefix, with a mandatory reason and author. Suppressed findings are reported as suppressed, never dropped silently.
rookery suppress list / rookery suppress rm <id> Inspect and lift suppressions.
rookery egg audit <file.json> Audit an egg definition: installer privilege, image references, variables, startup.
rookery egg diff <file.json> Diff an egg against its verified upstream.
rookery dashboard [--ascii] Live TUI: egress, gauges, findings, by-module bars. Staleness is always visible.
rookery bot serve / rookery bot register The Discord triage bot and slash-command registration (below).
rookery enforce status Breaker state, pending grace queue, shadow progress.
rookery enforce veto <incident-id> Cancel a queued suspension.
rookery enforce resume Re-arm a tripped breaker — a deliberate human act, audited.
rookery version Version.

The Discord bot

Most small hosts live in Discord; the bot is the triage surface. It is a client of the core, never an enforcement path — it recommends and records.

export ROOKERY_DISCORD_BOT_TOKEN=... ROOKERY_DISCORD_PUBLIC_KEY=... ROOKERY_DISCORD_APPLICATION_ID=...
rookery bot register --notify-config /etc/rookery/notify.yaml   # once per guild
rookery bot serve   --notify-config /etc/rookery/notify.yaml    # the interactions endpoint

Point the Discord application's Interactions Endpoint URL at the bot's public HTTPS URL (/interactions). Slash commands: /incident view|list, /queue, /server info, /stats (dismiss and exception rates per signal — the misconfiguration early-warning), /explain, /audit run. Alerts are idempotent across restarts, batched under storm conditions, and ephemeral wherever they carry customer identifiers.

Tuning

Rookery expects to be calibrated against your fleet. The tools, in order of preference:

  1. Suppress with a reason. rookery suppress add --signal … --path-prefix … --reason …. A suppression with no reason is a lie you tell yourself; the CLI requires one. /stats in Discord shows dismiss and exception rates per signal — a signal dismissed 90% of the time is misconfigured.
  2. Adjust thresholds in config/*.yaml. Every value is documented as a starting point requiring calibration.
  3. Allowlist agents (config/allowed-agents.txt) and never-suspend servers (config/never-suspend.txt).
  4. Write a rule. Signals are YAML in rules/signals/ with fixtures in rules/fixtures/signals/<id>/{positive,negative}/; the fixture gate fails the build without both. See docs/writing-rules.md.

Configuration files

File Owns
config/daemon.yaml Roles, state dir, health endpoint, watcher/scheduler cadences, scrutiny window, queue fairness, enforcement enable flag
config/enforcement.yaml The gate: signal lists, thresholds, exclusions, shadow, grace, breaker, kill switch, cooldown
config/collector.yaml Collection bounds: content dirs, static scan sizes
config/archiveguard.yaml Archive scan limits
config/configscan.yaml Config-indicator scan: indicator data paths, depth, size, egg and agent exclusions
config/workloadguard.yaml Identity thresholds, minimum markers, egg-family exclusions
config/score.yaml Score bands, diversity multipliers, decay
config/notify.yaml Discord routing by severity and module
config/resourceguard.yaml, serviceguard.yaml, service-policy.yaml, console.yaml, eggguard.yaml, escapeguard.yaml, minerguard.yaml Per-module detection tuning

Repository layout

cmd/rookery            the CLI (every command above)
internal/domain      core types, signal registry, egress validation
internal/detect      detector implementations by signal family
internal/scan        the scan engine shared by CLI and daemon
internal/daemon      the long-running process: loop, queue, scheduler, health
internal/enforce     the gate, the executor, the lists
internal/collect     node-side read-only gathering
internal/panel       the Pterodactyl API client (read + suspend/unsuspend only)
internal/bot         the Discord bot
internal/store       sqlite: incidents, suppressions, grace queue, breaker, audit
internal/score       scoring, decay, suppressions
internal/rules       YAML rule loading and the fixture gate
pkg/guard            standalone streaming decompression guard
pkg/tunnelcheck      standalone proxy/Tor listing checker
rules/               shipped detection data: signals, indicators, name lists, fixtures
testdata/            synthetic corpora (traces, archives, listings, snapshots, fixtures)
config/              module configuration files
deploy/              the systemd unit
docs/                hardening, operating, and rule-writing guides

Development

The pre-commit gate, also run in CI on every push and pull request:

gofmt -l .
go vet ./...
golangci-lint run
go test ./...
gitleaks detect --no-banner

Commits are atomic and conventional (feat, fix, refactor, test, docs, chore, ci, perf). Every detection rule change includes its fixtures. No secrets, real customer data, or live malware — ever. Design decisions are ADRs; operational history is the context tree.

Security

Rookery is defensive tooling for infrastructure the operator controls. The repository contains no live malware samples and no exploit code, only detection rules and synthetic fixtures.

Report vulnerabilities through GitHub private vulnerability reporting. Please do not open public issues for security reports. A code path that mutates customer data or bypasses the enforcement gate is a security bug in this project by definition — report it as one.

Support

For support, join our discord: https://discord.gg/q3bgy7AY2k

License

MIT. See LICENSE.

About

Defensive abuse and malware detection for game server hosting panels (Pterodactyl/Pelican). Finds miners, tunnels, floods, and container escapes on servers you operate. Detects; never acts.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages