Product
Shelltrap for CyberPanel
A webshell scanner is only as good as the two things nobody puts on a feature list: what it does when it is not sure, and what it does when it is broken. Here is all of it.
What it watches
Shelltrap watches the document roots of the accounts on your CyberPanel host — /home by
default — and reacts to file events as they happen. The event mask deliberately includes more
than “a file was written”:
CLOSE_WRITE— a write finished, the usual moment to scanMOVED_TO/MOVED_FROM— a rename into or out of a watched pathCREATE/DELETE— appearance and disappearanceATTRIB— attribute changes, so that a laterchmod, a setuid bit or an ownership change is an event of its own and not something the scanner learns about next Sunday
Hot non-customer paths are marked as ignored on purpose: a database data directory, log trees and panel caches. A filesystem-level mark on the root filesystem otherwise delivers every open on the host, which is a performance problem, not a security feature.
Queue overflow is not swallowed. FAN_Q_OVERFLOW is counted, alarmed and answered with a
time-boxed reconciliation scan. The same reconciliation runs after a reboot, a crash, an
upgrade and after any new mount appears under a customer root. “Not scanned” and “scanner
impaired” are their own states and are never displayed as clean.
Four moves between a write and a verdict
The split in that picture is the whole security argument. A scanner is a program that opens files chosen by an attacker and runs them through archive, image, document and executable parsers. That is the most dangerous code in the system, so it does not run as root:
The root broker (shelltrapd) is a statically linked Go binary without cgo. It holds the
fanotify groups, normalises events, opens files safely (openat2 with
RESOLVE_NO_SYMLINKS, or a component-wise openat walk with O_NOFOLLOW on older kernels),
records minimal metadata, hands scan jobs to workers, runs quarantine transactions and serves
the local API. It contains no YARA, no ClamAV code and no archive, Office or ELF parsing. It
accepts no free-form path from the panel or the UI, only opaque object IDs. The single
exception is the root CLI call scan <path>, verified as uid 0 through SO_PEERCRED.
The scanner worker is a separate process under its own system user, in its own mount and
PID namespace, with seccomp, rlimits and cgroup limits, a defined timeout, crash-retry — and no
network. It receives a read-only descriptor from the broker over SCM_RIGHTS; it never
resolves a path itself.
The engines
Inside the worker, in order:
- Type detection by magic bytes. It steers priority only. Size filters, media, backups and
caches are never silently counted as clean — they are reported
unscannedand queued for a bounded deep scan or a scheduled special scan. Image headers followed by PHP,.phar,.phtml,.user.iniand nested archives always go through the full pipeline. - ClamAV over the clamd socket, in the full profile only.
- YARA 4.5, with rules precompiled per feed generation against exactly that engine version.
- Hash sets — SHA-256 sets plus a known-good allowlist covering WordPress core and widely used plugins and themes, Laravel and Joomla. The allowlist may not blindly override a path-context hit: a known-good hash in an uploads directory is still a finding.
- Heuristics, with explainable, per-signal scoring: PHP obfuscation markers (
eval,base64_decode,gzinflate,str_rot13, chr chains, long hex strings, variable function calls, entropy), PHP inside upload directories,.htaccessand.user.initricks (AddHandler,AddType,auto_prepend_file), symlinks pointing out of the home directory, world-writable and setuid files. A hit needs several independent signals. Entropy or one long line, on their own, never produce asuspiciousverdict — that lesson cost us a handful of false positives on ordinary JPEGs and Wordfence’s own transient cache, and it is fixed.
Per-tenant and global limits apply to file size, scan time, archive recursion, file count,
compression ratio, expanded bytes and temporary storage. Exceeding one kills the worker process
and marks the object unscanned with a reason — a bounded failure instead of an unbounded one.
Want to see the reasoning rather than read about it? The Webshell Signal Explorer walks through six real shapes, including the false positive that must not happen.
Five verdicts
| Verdict | Meaning | Default action |
|---|---|---|
clean | Scanned by every engine the profile provides, nothing found | none |
suspicious | Several independent signals, below the action threshold | report |
malicious | Signature or hash match, or a heuristic score above the threshold | quarantine for signature and hash hits |
unscanned | A limit was hit; the reason is recorded | report, retry as scheduled |
degraded | The scanner itself was impaired | report, alarm |
The verdict cache key contains the mount ID, inode, high-resolution mtime and ctime, size,
content hash and the rule set, engine and policy generations. A signature update invalidates
old clean verdicts automatically. Path-dependent policy verdicts (such as “PHP in uploads”)
live in a separate cache, because the same inode can change context through a hard link or a
rename.
Response, and why quarantine is a transaction
Restore never overwrites an existing target, and puts back owner, mode, times, ACLs, extended
attributes and the SELinux context. There is no chmod 000 action: breaking a file in place is
not containment, it is a support ticket. EXDEV — quarantine and original on different
filesystems — is the normal case, not an error.
Notification is admin mail immediately on malicious, a digest for suspicious, optional
customer mail through the CyberPanel contact, and webhooks restricted to an allowlist of
targets. Ignore rules cover path globs per site, signature IDs, hash allowlists and users.
An ignore rule suppresses the action, never the scan or the record of the finding.
The upload gate
A single PHP file, loaded through auto_prepend_file in a global lsphp ini per PHP version,
does nothing at all unless $_FILES is non-empty. When it is, it passes the temporary
descriptors to the broker over a local socket and waits for at most upload.timeout_ms
(default 2000 ms, capped at 60000 ms). A malicious answer means HTTP 403 and a deleted
temporary file. Fail-open is the default on a gate error; fail-closed is available per domain.
This is the mechanism that works identically on OpenLiteSpeed and LiteSpeed Enterprise without
ModSecurity, and it is the honest reason it was chosen. We also state its limit plainly:
auto_prepend_file can be overridden by a .user.ini inside the site. That exact pattern is
itself a heuristic hit, and the real-time watcher stays behind the gate as the catch-all.
FTP uploads are enqueued through pure-uploadscript and scanned right after the upload; that
post-upload character is stated rather than glossed over.
More detail: how the upload gate works .
Platform tiers
shelltrapd --check returns JSON with tier, diagnostic.tier and, per mount, a reason and
error. When a backend is configured explicitly, the top-level tier reflects that choice —
so for a real capability statement, always read diagnostic.tier as well.
Debian is only third-party supported by CyberPanel itself, so we do not promise it. Global inotify sysctls are never raised behind your back: the installer counts watches and asks.
Full and Lite profiles
The profile is chosen after measurement, not from a version table. Full requires a running
or installable clamd, a socket actually reachable by the shelltrap-scan user, and at least
1.5 GiB of MemAvailable remaining after the measured reload peak. Otherwise the install
chooses Lite, sets scanner.enable_clamd = false, and says so. Lite has visibly reduced
coverage: the software support promise is the same, the detection promise is not.
A clamd that starts later does not silently upgrade you to Full. That is deliberate: a profile change is a security decision and it gets a measurement, not a guess.
Signature feeds
Third-party rules are never activated straight from a foreign mirror. Every rule carries its
provenance — source, commit, licence, author, whether it was modified — and the build only
admits DRL-1.1, BSD-3-Clause, Apache-2.0 and MIT licensed rules (plus GPL-2.0-only
for ClamAV databases). Where an upstream licence obliges us to keep author, URI and licence
visible, that attribution travels all the way into the finding display and the notification
mail.
ClamAV’s own daily, main and bytecode databases are fetched per server by freshclam and
not bundled.
The CyberPanel plugin
The plugin is a user interface, not a second scanner. The broker and its workers keep running when the panel is down, being upgraded or broken.
- Pages: dashboard and health, findings with filters, paging, detail and signals, quarantine with restore (purge is admin only), policies per domain with visible global → account → domain inheritance, ignore lists, feed generations, audit (admin only), jobs, and a help and status page.
- Roles: administrators see everything; resellers and users see their own sites, can request
a restore inside their scope, and may change only the policy keys an administrator has
released.
upload.on_errorandheuristics.actionstay locked for both roles, and the broker enforces that boundary independently of the UI. - Integrity: the CyberPanel installer patches core files line by line, so the plugin takes atomic snapshots before it touches anything, verifies them by checksum, and rolls back if the install or a repair fails. After a panel upgrade the repair path is idempotent and runs again.
Details: the plugin documentation .
Licensing
One licence per server. Activation binds the licence to a server fingerprint derived from
/etc/machine-id; deactivate releases the slot again. The daemon verifies an Ed25519-signed
token offline — the licence service does not have to be reachable for the scanner to work —
and a systemd timer renews it daily with an hour of jitter.
Without a valid licence the daemon does not crash and does not loop: the watcher keeps counting
events, the scheduler plans nothing, the upload gate answers allow with reason unlicensed,
and health reports unlicensed with a precise reason. Feeds return 401. That is the commercial
boundary, and it is designed to fail visibly rather than dangerously.
More: licensing and activation .
What Shelltrap is not
Honesty is cheaper than a refund:
- It is not a WAF and not a firewall. It looks at files, not requests.
- It does not prevent execution at the kernel level. Blocking a file open before the fact is a 1.x prototype, deliberately not promised today. What exists now is a synchronous gate on the web-upload path and fast asynchronous detection everywhere else.
- It does not claim to catch everything. No scanner does. It claims to tell you honestly what it scanned, what it could not scan, and why it decided what it decided.
- It cannot defend against an attacker who already has root on the machine. Nothing that runs on that machine can.
Coming from CXS?
The migration page maps concepts, quarantine and configuration, and the calculator works out how many licences a fleet needs.
CXS migration
A practical migration path from ConfigServer eXploit Scanner to Shelltrap on CyberPanel: capability mapping, a cutover checklist that starts in report-only, and a licence calculator.