Documentation
Configuration and policies
Almost everything that changes behaviour is a policy with three layers — global, account, domain — and the more specific layer wins.
The policy model
Every setting that steers behaviour is a policy on three levels:
- global — the whole host
- account — one CyberPanel user
- domain — one website or child domain
The more specific level wins, and each level may inherit or override individual keys. Inside a domain there can also be path rules using globs relative to the document root.
Assignment works through a document-root index the broker keeps from the CyberPanel database
(website, child domain, document root, owner), refreshed on a timer and on panel changes.
fanotify events are mapped to an account and a domain by the longest matching document-root
prefix. The PHP adapter passes DOCUMENT_ROOT and SERVER_NAME; the document root decides and
the server name is only used as a plausibility check. Files outside every document root — mail,
backups, the home root — fall under the account policy.
Policies are versioned. A change invalidates only the path-dependent policy verdicts of the affected domain, not content verdicts.
The keys
| Key | What it does | Default |
|---|---|---|
upload.enabled | whether the synchronous upload gate is consulted | on |
upload.on_error | open or closed when the gate cannot answer | open |
upload.timeout_ms | gate budget per request, capped at 60000 | 2000 |
upload.max_size | largest upload the gate inspects | per install |
heuristics.action | report, quarantine or off | report |
heuristics.threshold | score above which a heuristic hit counts | per install |
signature.action | action on a signature hit | quarantine |
hash.action | action on a hash hit | quarantine |
notify.targets | who is told, and how | per install |
scan.window | when scheduled scans may run | per install |
scan.exclude | path globs excluded from scheduled scans | empty |
realtime.enabled | whether the real-time watcher acts for this scope | on |
quarantine.retention_days | how long quarantined content is kept | 30 |
findings.retention_days | how long findings are kept | 180 |
Reading and writing from the shell:
shelltrap policy get global
shelltrap policy set global signature.action=report hash.action=report
shelltrap policy set domain 42 heuristics.action=quarantine
shelltrap policy effective site 42
policy effective is the one to use in an argument: every key comes back with its value and
its origin — domain, account, global or default — so inheritance is visible rather than
inferred.
Ignore rules are not policy
Ignore rules live in their own API and are an administrator action. Four kinds:
path_glob, signature, hash and user.
shelltrap ignore add --scope domain --scope-id 42 --kind path_glob --value 'cache/**' \
--note 'generated cache'
shelltrap ignore list
shelltrap ignore remove <id>
An ignore rule suppresses the action, never the scan or the record of the finding. That distinction is what makes an ignore list auditable instead of a blindfold.
The local API
The API is HTTP/1.1 over the Unix socket /run/shelltrap/api.sock, mode 0600, owned by root.
It is not reachable over TCP and it uses no cookies. The server reads SO_PEERCRED on every
accepted connection and, in production, accepts only peer UID 0; anything else gets HTTP 403
before a backend call happens.
- JSON media type
application/json, request bodies capped at 1 MiB, page size capped at 500. X-Shelltrap-Actor: <panel-user>makes the broker resolve the name and role itself. A role claimed by the client is never accepted, and the backend additionally restricts results to the actor’s own sites.- Every response carries
X-Request-ID. On writes, a client-supplied ID also acts as replay protection: reusing it inside the memory window returns HTTP 409replayed_request. - Rate limiting is per peer UID, 600 requests per minute by default; exceeding it returns 429.
- Metrics come back in Prometheus text format, not JSON.
curl --silent --unix-socket /run/shelltrap/api.sock http://localhost/v1/health
curl --silent --unix-socket /run/shelltrap/api.sock \
-H 'X-Shelltrap-Actor: reseller1' \
'http://localhost/v1/findings?verdict=malicious&page=1&page_size=50'
Free-form path scanning (POST /v1/scan with a path) is available only to the system
actor on a connection with peer UID 0. A panel or UI actor can never trigger root file access by
supplying a path.
The CLI
shelltrap status
shelltrap health
shelltrap scan SITE | --account ACCOUNT | --path PATH
shelltrap findings list|show|resolve
shelltrap quarantine list|restore|purge
shelltrap policy get|set|effective
shelltrap ignore add|list|remove
shelltrap feeds list|update|rollback
shelltrap audit tail
shelltrap jobs list
shelltrap metrics
Global options are --json, --socket PATH (default /run/shelltrap/api.sock) and
--actor NAME, and they come before the command. --json prints the API response unchanged;
without it you get a deterministic human-readable table. Exit codes: 0 success, 1 API or
response error, 2 usage error, 3 API unreachable.
$ shelltrap license activate STL-7QK3M-P2VDA-XN94T-B6HRE-Z8SWF
plan server
key prefix STL-7QK3M
valid until 2027-09-04
token until 2026-10-04
fingerprint e3b0c44298fc1c14…
servers 1 of 1
token written to /etc/shelltrap/license.token (0600, root)
$ shelltrap --json health
{"status":"ok","watcher_tier":"A","scanner_workers":2,"clamd":"ok",
"ruleset_gen":"20260903T120000Z-0001","policy_gen":17,"queue_age_s":0,
"overflows":0,"feeds":"ok"}
Activation and a health check with –json, which returns the API response unchanged.
Health and what to alert on
GET /v1/health returns the operating state:
{
"status": "ok",
"watcher_tier": "A",
"scanner_workers": 2,
"clamd": "ok",
"ruleset_gen": "20260903T120000Z-0001",
"policy_gen": 17,
"queue_age_s": 0,
"overflows": 0,
"feeds": "ok"
}
status is ok or degraded. On degraded, degraded_reasons names every cause — live checks
such as ruleset_missing, worker_dead, worker_landlock_missing, clamd_unavailable,
namespace_fallback, policy_generation_unavailable, notify_target_unavailable and
policy_error, plus internal error counters since process start.
A not_configured sub-status on an optional notify target does not by itself make the whole
status degraded. An impaired scanner or a clamd outage must never be read as a clean scan.
Worth an alert rule, from our own observation checklist:
| Signal | Source | What normal looks like |
|---|---|---|
| watcher events and overflows | shelltrap_watcher_events_total, shelltrap_watcher_overflows_total | overflows near zero; bursts visible at deploy time |
| queue age | shelltrap_queue_age_seconds | under 60 s in steady state |
| scans by verdict | shelltrap_scans_total{verdict} | clean dominates; check the reason spread on unscanned |
| worker restarts | shelltrap_worker_restarts_total | zero |
| worker memory | ps, systemctl status | stable RSS over days, no growth |
| clamd state and feed age | shelltrap health | stable |
| audit chain | shelltrap audit tail | unbroken, verification without error |
Stop and investigate if overflows persist, queue age exceeds ten minutes, workers restart in a loop, worker RSS grows without bound, or customers complain about I/O load.
Retention and data protection
Defaults: findings 180 days, quarantine 30 days with advance warning, audit one year. All configurable. A purge is never silent. By default nothing leaves the host: outbound targets are allowlisted, and sample and telemetry code is not loaded without an opt-in. Logs contain no secrets and no file contents.
Next
- The CyberPanel plugin — the same operations with roles, in the panel
- Signature feeds — generations, gating and rollback
Abridged from the Shelltrap architecture and API manuals, revision 2026-09-04.