1. Summary
SNMP Trap Receiver receives SNMP notifications (traps and informs in SNMP v1, v2c and v3) from network equipment and servers. It translates the numeric object IDs into names using MIB files, stores every notification in log files and shows them continuously in a web GUI in English or Norwegian.
Version 2.0 replaces the earlier Docker container. It runs as a hardened systemd service on Rocky Linux 9 (including the minimal installation) and is installed from a single self-contained file without internet access.
2. Purpose and scope
Receives |
SNMPv1 Trap-PDU, SNMPv2c Trap and InformRequest, SNMPv3 Trap and InformRequest (USM: noAuthNoPriv, authNoPriv, authPriv) over UDP/IPv4 and UDP/IPv6 |
Translates |
Trap OID and variable names using uploaded vendor MIBs and the built-in IETF MIBs |
Stores |
Human-readable text log and JSON Lines log, both rotated |
Shows |
Live list with filters, details per trap, CSV export, receiver status, MIB management, log download and license information |
Does not |
Forward traps, send e-mail, poll devices or store traps in a database |
3. Architecture
network devices Rocky Linux 9 server
┌───────────────┐ UDP 162 ┌────────────────────────────────────────────────┐
│ switch/router ├──────────► │ snmp-trap-receiver.service (user snmptrap) │
│ ESXi, UPS ... │ v1/v2c/v3 │ │
└───────────────┘ │ ┌────────────────────┐ ┌─────────────────┐ │
│ │ TrapReceiver │ │ MibManager │ │
│ │ thread + asyncio ├──►│ MIB compiler │ │
│ │ pysnmp 7 engine │ │ OID→name lookup │ │
│ └─────────┬──────────┘ └─────────────────┘ │
│ │ record │
│ ring buffer (2000) + snmp_traps.log/.jsonl │
│ ▲ │
│ ┌─────────┴──────────┐ │
┌──────────┐ HTTP 8080 │ │ Flask app on │ │
│ browser ├───────────────► │ │ Waitress, 8 threads│ │
└──────────┘ GUI + JSON API │ └────────────────────┘ │
└────────────────────────────────────────────────┘
Everything runs in a single process. The SNMP engine has its own thread with its own asyncio event loop. The web server runs in the Waitress worker threads and reads from the receiver’s in-memory ring buffer. The two meet only in a lock-protected queue, so a slow browser never delays the reception of traps.
3.1. Modules
| Module | Responsibility |
|---|---|
|
Reads |
|
|
|
|
|
The Flask application: login, CSRF, security headers, pages and JSON API. |
|
All GUI texts in English and Norwegian (121 keys). Both languages must have the same keys, otherwise the module will not load. |
|
Shows the user manual ( |
|
End-to-end test on a temporary port and in temporary directories. |
|
Command line: |
|
25 standard IETF MIB sources plus a stub for |
|
HTML templates, Bootstrap 5.3.7 and the application’s own JavaScript and CSS. |
4. Trap processing
4.1. Reception
For each listen entry the receiver opens a UDP transport (IPv4 or
IPv6) in pysnmp. pysnmp binds sockets asynchronously. The receiver therefore waits
for each binding and reports every error, for example Address already in
use or Address family not supported. If no address can be bound, the
receiver remains stopped and shows the error. If only some can be bound, it runs on those
and shows a warning.
Communities are registered as security entries for SNMPv1/v2c.
SNMPv3 users are registered in the USM with their authentication and
privacy protocols. A v3 trap is accepted only if the user is registered for
the sender’s snmpEngineID (engine_id in the configuration). A v3 inform
uses the receiver’s own engine ID and does not need engine_id.
4.2. Community check
An observer on the pysnmp execution point rfc2576.processIncomingMsg:writable sees
every v1/v2c message before it is authorised:
-
The community actually used is remembered for the log.
-
If it is unknown and
accept_any_community = false, pysnmp discards the message. The receiver counts it as rejected and writes aREJECTEDline with the sender address to the text log. -
If
accept_any_community = true, the community is mapped to the first configured one so that the message is accepted. The log still shows the community the device actually sent.
4.3. Notification processing
NotificationReceiver in pysnmp calls the receiver for every accepted trap
or inform and acknowledges informs automatically. The receiver then does
the following:
-
gets the source address and port from the transport information;
-
gets the SNMP version and PDU type (trap or inform) from the message context;
-
looks up each variable binding in the MIB view (name and value formatting, for example enumerations and
DisplayString); -
extracts
snmpTrapOID.0andsysUpTime.0. For SNMPv1 traps pysnmp has already converted the Trap-PDU into the SNMPv2 form, sosnmpTrapAddress.0,snmpTrapCommunity.0andsnmpTrapEnterprise.0are included as variables; -
builds a record, adds it to the ring buffer and writes it to both log files.
4.4. Record format
Each line in snmp_traps.jsonl is one JSON object:
{"id": 17, "time": "2026-10-02 10:45:20", "ts": 1790930720.642,
"source": "192.0.2.31", "source_port": 49052, "version": "v2c",
"kind": "trap", "security": "public",
"trap_oid": "1.3.6.1.6.3.1.1.5.3", "trap_name": "IF-MIB::linkDown",
"uptime": "123456",
"varbinds": [
{"oid": "1.3.6.1.2.1.2.2.1.1.12", "name": "IF-MIB::ifIndex.12",
"value": "12", "type": "Integer"},
{"oid": "1.3.6.1.2.1.2.2.1.8.12", "name": "IF-MIB::ifOperStatus.12",
"value": "down", "type": "Integer"}]}
|
Sequence number. Continues after a restart, because the buffer is reloaded from the JSON log |
|
Community (v1/v2c) or USM user name (v3) |
|
|
|
Empty if no loaded MIB defines the OID |
The text log has the same content in human-readable form:
[2026-10-02 10:45:20] TRAP v2c from 192.0.2.31:49052 (community: public) snmpTrapOID = IF-MIB::linkDown (1.3.6.1.6.3.1.1.5.3) sysUpTime = 123456 IF-MIB::ifIndex.12 = 12 IF-MIB::ifOperStatus.12 = down
5. MIB management
5.1. Upload
-
The file extension must be
.mib,.txt,.my,.smior none. -
The file must not be empty, must not be larger than
mib_max_kband must not contain NUL bytes in the first 4 KB. -
The module name is read from the content (
NAVN DEFINITIONS ::= BEGIN) and must match^[A-Za-z][A-Za-z0-9-]{0,127}$. -
The file is written atomically as
<data_dir>/mibs/source/<NAVN>. The name sent by the browser is only shown in the list and is never used as a path. Path traversal is therefore impossible.
5.2. Compilation
All uploaded modules are compiled with pysmi into pysnmp Python modules in
<data_dir>/mibs/compiled/. Each module is compiled separately. A single broken file
therefore does not stop the others, and each module gets its own status:
|
Compiled and loaded |
|
Message from the parser, or |
|
Not compiled yet |
Sources for IMPORTS are searched in this order: the upload directory,
the directories in mib_search_dirs (for example /usr/share/snmp/mibs if
net-snmp-libs is installed) and the built-in IETF MIBs. The HTTP reader in
pysmi is never used. The requests dependency is replaced by a stub that
refuses downloads, so compilation only ever happens offline.
5.3. Standard MIBs
The first time a version starts, the built-in standard MIBs
that define common notifications and objects are compiled once into
<data_dir>/mibs/base/ (about 1.5 seconds): IF-MIB, IANAifType-MIB,
HOST-RESOURCES-MIB, IP-MIB, TCP-MIB, UDP-MIB, DISMAN-EVENT-MIB,
NOTIFICATION-LOG-MIB and whatever they import. Together with the modules that ship
with pysnmp (SNMPv2-MIB, RFC1213-MIB, …) this means that, for example,
IF-MIB::linkDown and SNMPv2-MIB::coldStart are shown by name without any
upload. Uploaded modules take precedence over these.
6. Configuration
File: /etc/snmp-trap-receiver/config.ini (owner root:snmptrap, mode
0640). Restart the service after making a change.
| Section | Key | Default | Meaning |
|---|---|---|---|
|
|
|
Address of the web GUI |
|
|
TCP port |
|
|
|
Require a password |
|
|
(set by the installer) |
scrypt hash (Werkzeug). Set with |
|
|
(random) |
Signs the session cookie |
|
|
|
|
|
|
|
Session lifetime |
|
|
|
|
|
|
|
Start reception when the service starts |
|
|
|
Accepted communities |
|
|
|
Accept all communities |
|
|
|
Number of traps kept in memory for the GUI |
|
|
|
|
MIB sources, compiled modules and status |
|
|
Trap logs |
|
|
|
Size per file before rotation |
|
|
|
Number of rotated files kept per log |
|
|
|
Maximum MIB upload size |
|
|
|
Additional directories with MIB sources |
|
|
|
|
|
|
Authentication passphrase |
||
|
|
|
|
|
Privacy (encryption) passphrase |
||
|
The sender’s engine ID in hex (v3 traps) |
7. Web GUI and API
7.1. Pages
Traps |
Live list with the newest at the top, refreshed every 2 seconds. Free-text filter (all words must occur in the source, OID, name or value), version filter, pause, clearing the view and CSV export. Click a row to see all variables with OID, name, value and type. |
MIB files |
Upload one or more files. Status per module with error text, source download, deletion and recompilation. |
Receiver |
Running/stopped, listening addresses, addresses that failed, counters for received, rejected and errors, configured communities and v3 users, and the last error. Start, stop and restart. |
Logs |
All log files, including rotated ones, with size, time and download. |
Footer |
Version and links to Licenses (component list from |
The selected tab is remembered in the browser. The layout works down to mobile width. On narrow screens two of the columns in the trap list are hidden.
7.2. Endpoints
| Path | Method | Purpose |
|---|---|---|
|
GET/POST, POST |
Session |
|
GET |
Sets the language cookie |
|
GET |
Main page |
|
GET |
License view |
|
GET |
The user manual in the selected language, with table of contents and search. The User manual button links to |
|
GET |
|
|
GET |
Receiver status and counters |
|
GET |
Traps with |
|
POST |
Clears the in-memory list (the logs are not changed) |
|
POST |
Controls the receiver |
|
GET/POST |
List / upload ( |
|
POST |
Recompiles all |
|
POST |
Deletes a module |
|
GET |
Downloads the source |
|
GET |
Lists the log files |
|
GET |
Downloads a log file (only the trap logs and their rotations) |
|
GET |
CSV of the traps in memory |
|
GET |
License data in the selected language |
7.3. Security
-
Authentication: a single password, stored as an scrypt hash. After five failed attempts from the same address within 15 minutes, logins from that address are refused for 30 seconds. The time doubles with each further attempt, up to 15 minutes.
-
Session: signed cookie with
HttpOnlyandSameSite=Strict, valid for 12 hours. -
CSRF: every POST requires the session token (the
X-CSRF-Tokenheader or a form field). -
Headers:
Content-Security-Policy: default-src 'self'with no inline script or style,X-Frame-Options: DENY,nosniff,Referrer-Policy: no-referrerandCache-Control: no-storeon the API. -
Output: all content from traps is escaped before it is displayed. In the CSV export, cells starting with
=,+,-or@are prefixed with', so that spreadsheets cannot execute them as formulas. -
Upload: see Upload.
8. Language
English is the default. The default language can be changed in the configuration, and each user can switch with EN/NO in the top bar (stored in a cookie for one year). The server renders the page in the selected language, and the same dictionary is passed to JavaScript, so dynamic texts follow the language. The license report is shown in the selected language. The third-party notices are in English because they are the original license texts.
9. Installation and operation
9.1. Files and users
| Path | Owner, mode | Contents |
|---|---|---|
|
root, 0755 |
The application |
|
root, 0755 |
Python environment built from the bundled wheel files |
|
root |
This documentation and the license files |
|
root |
Previous version after an upgrade |
|
root:snmptrap, 0640 |
Configuration (contains the secret key and v3 keys) |
|
snmptrap, 0750 |
MIB sources, compiled modules and |
|
snmptrap, 0750 |
|
|
root, 0755 |
Command line |
9.2. The systemd unit
The service runs as the system user snmptrap. It is given exactly one
capability, CAP_NET_BIND_SERVICE, so that it can bind UDP 162. The unit
also sets NoNewPrivileges, ProtectSystem=strict (everything read-only
except the data and log directories), ProtectHome, PrivateTmp,
PrivateDevices, kernel and control group protection,
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX, RestrictNamespaces,
LockPersonality and UMask=0027. It restarts automatically after a
failure (Restart=on-failure).
SELinux needs no changes. The service runs in the
unconfined_service_t domain, like other programs under /opt.
9.3. Command line
|
Run the receiver and web GUI (used by systemd) |
|
Set the web password (root) |
|
Import and compile MIB files |
|
Compile all uploaded MIBs. Exit code 2 if any fail |
|
End-to-end test (see Verification) |
|
Version |
When run as root, all commands except set-password run as the
snmptrap user, so that files get the correct owner.
9.4. The offline installer
snmp-trap-receiver-offline-installer.run is a bash script followed by
a gzip-compressed tar archive. The script uses python3 to verify
the archive’s SHA-256 and extracts it with the data filter in tarfile (no
absolute paths, no ..). It then runs install.sh. Neither tar
nor pip needs to be installed on the server.
install.sh:
-
checks for root, the operating system, Python 3.9, the architecture against the package, glibc 2.34 or newer (required by the cryptography wheel) and the checksum of every file (
SHA256SUMS); -
stops a running service when upgrading;
-
creates the user and the directories;
-
builds a new virtual environment with
python3 -m venv --without-pipand installs the pinned wheel files using the bundled pip wheel (--no-index --no-deps --only-binary=:all:). pip itself is not installed in the environment; -
checks that the packages can be imported, swaps in the new version and moves the old one to
.previous/; -
creates the configuration on first installation (random secret key and generated password) or keeps the existing one, and applies the options from the command line;
-
imports MIBs, if requested;
-
installs the systemd unit, opens ports in firewalld, restarts the service and waits for
/healthz; -
runs the self-test.
9.5. Building the installer
On any machine with internet access:
bash build-offline-installer.sh # x86_64 + aarch64
The script downloads the wheel files listed in Application/requirements.lock for
CPython 3.9 on both architectures (manylinux 2014/2_17/2_28/2_34) and
checks that every package is available for each architecture. It generates
THIRD_PARTY_NOTICES.txt and third_party_inventory.json from the wheel files
(including the GPL scan) and copies the application and documentation.
If the build machine has Python 3.9, an offline installation and the self-test are run.
pip-audit is run if it is installed. Finally the installer,
a plain .tar.gz and the checksums are written to Offline-Installation/.
To update dependencies, edit Application/requirements.in,
resolve the dependencies with Python 3.9 and write the result to
requirements.lock.
10. Verification
snmp-trap-receiver selftest uses a free high UDP port on 127.0.0.1 and
temporary directories, so it never interferes with the running service. It
checks the following:
-
that
cryptographyis available (SNMPv3 encryption); -
that a test MIB uploaded with the file name
../../evil/selftest.mibis stored only under its module name; -
that the MIB compiles offline against the built-in IETF MIBs, and that a file that is not a MIB is rejected;
-
that the receiver binds. A v1 trap, a v2c trap, a v2c inform and a v3 trap with authPriv (SHA/AES) are sent, and all four are received with the correct version and type;
-
that the trap OID and the variables are resolved via the uploaded MIB, that the source address is recorded, that a wrong community is rejected and counted, and that both logs are written;
-
web: login is required, a wrong password is rejected and login works. The English page is the default, and a POST without a CSRF token is rejected. It also tests the trap API, MIB upload via the API, log download, rejection of path traversal in log download, CSV export, the user manual in English and Norwegian, the Norwegian page, security headers, the license inventory and both license documents in both languages, and stop and start via the API.
The installer runs the self-test after every installation.
11. Limitations
-
HTTP only. For access over untrusted networks, bind the GUI to
127.0.0.1and use an SSH tunnel, or put a reverse proxy with TLS in front. -
A single shared password. No user accounts or audit trail for actions in the GUI (start, stop, upload and delete are written to the journal with the client’s address).
-
The in-memory list holds
buffer_sizetraps. Older traps exist only in the log files. -
Measured capacity: around 700 traps per second on a single CPU core. Bursts of 2000 traps were received without loss. The receiver requests a 4 MB receive buffer, but the kernel caps it at
net.core.rmem_max(Rocky 9 default: 208 KB, enough for a few hundred traps in one burst). Where large trap storms can occur, the limit should be raised:echo net.core.rmem_max=4194304 > /etc/sysctl.d/90-snmp-trap-receiver.conf && sysctl --system.
12. Licenses
All third-party components have permissive licenses. See
Lisensrapport.md, THIRD_PARTY_NOTICES.txt and
third_party_inventory.json.
Appendix A: Ports
Port |
Protocol |
Direction |
Purpose |
162 |
UDP |
in |
SNMP traps and informs (configurable) |
8080 |
TCP |
in |
Web GUI (configurable) |
Appendix B: Documents
|
Installation, configuration, operation and upgrading |
|
Audit of the Docker version and what has been fixed |
|
Licenses and GPL scan |
|
Full license texts |
|
Machine-readable component list |
|
Changes |