Using net-snmp
Net-SNMP is the standard, near-universal SNMP toolkit on Linux, made up of three distinct pieces that get used very differently: a set of command-line query tools (snmpget, snmpwalk, and friends) for asking a device questions, snmpd for turning your own Linux machine into something other tools can query, and snmptrapd for receiving the notifications devices send out unprompted. This guide covers all three, plus something that trips people up across all of them in slightly different ways: getting MIB names to actually resolve instead of staring at raw numeric OIDs.
Installing net-snmp
# Debian / Ubuntu
sudo apt update
sudo apt install snmp snmp-mibs-downloader snmpd snmptrapd
# RHEL / CentOS / Fedora
sudo dnf install net-snmp net-snmp-utils
# (older systems: sudo yum install net-snmp net-snmp-utils)
On Debian-family systems, the client query tools, the agent, and the trap receiver come from separate packages (snmp, snmpd, snmptrapd); on RHEL-family systems, net-snmp and net-snmp-utils pull in all three together. You don't need all three on every machine - a monitoring server typically only needs the client tools, while a host you want to be monitored needs snmpd running instead.
The client tools
These are what you reach for to ask a device something. They share the same connection flags: -v for SNMP version, -c for the community string (v1/v2c), and the target host/OID at the end.
# Get a single value
snmpget -v2c -c public 192.168.1.1 sysDescr.0
# Walk an entire subtree
snmpwalk -v2c -c public 192.168.1.1 system
# Faster walk for large tables (uses GETBULK instead of repeated GETNEXT)
snmpbulkwalk -v2c -c public 192.168.1.1 ifTable
# Set a writable value (needs a community with write access)
snmpset -v2c -c private 192.168.1.1 sysContact.0 s "[email protected]"
# Render a table as an actual table instead of one row per line
snmptable -v2c -c public 192.168.1.1 ifTable
For SNMPv3, the version flag changes and you authenticate with a username and security parameters instead of a community string:
snmpget -v3 -u myuser -l authPriv -a SHA -A 'authpassword' -x AES -X 'privpassword' 192.168.1.1 sysDescr.0
Loading MIBs: the shared mechanism
Without MIBs loaded, every result prints as a raw numeric OID. All three net-snmp components - the client tools, snmpd, and snmptrapd - share the exact same underlying MIB-loading library, so the mechanism itself is identical everywhere. What differs, and what actually catches people out, is how each one is normally invoked - and that changes where you actually need to set things. The shared pieces:
- Default directories - typically
/usr/share/snmp/mibs(system-wide) and~/.snmp/mibs(per-user). Drop a.txt/.mibfile in either and it's picked up automatically. - The
MIBSenvironment variable - controls which loaded MIBs are actually used for translating OIDs.export MIBS=+ALLloads everything found in the search path (verbose, but reliable);export MIBS=+IF-MIB:CISCO-PROCESS-MIBloads specific ones. - The
MIBDIRSenvironment variable - adds directories to the search path without touching which specific MIBs get loaded from them. -m- load specific MIBs for a single invocation, overridingMIBSfor that run only:snmpwalk -m +CISCO-PROCESS-MIB -v2c -c public 192.168.1.1 cpmCPUTotal5minRev.-M- add a directory to the search path for a single invocation, overridingMIBDIRS:snmpwalk -M +/opt/custom-mibs -m +MY-CUSTOM-MIB ..../etc/snmp/snmp.conf(system-wide) or~/.snmp/snmp.conf(per-user) - makes a setting permanent instead of exporting it or passing flags every time:
mibdirs +/opt/custom-mibs
mibs +ALL
That part is genuinely identical across all three tools. Where it gets specific is in how each one is actually started day to day - an interactive shell session behaves very differently from a service managed by systemd.
Loading MIBs for the client tools
This is the straightforward case, because you're the one typing the command. Any of the mechanisms above work directly: export MIBS/MIBDIRS in your shell (or, better, in ~/.bashrc or similar so it persists across sessions), pass -m/-M on individual commands, or set it once in ~/.snmp/snmp.conf and forget about it. Walking a Cisco device's CPU utilization, with the vendor MIB loaded so the result comes back as a readable name instead of a bare OID:
snmpwalk -m +CISCO-PROCESS-MIB -v2c -c public 192.168.1.1 cpmCPUTotal5minRev
CISCO-PROCESS-MIB::cpmCPUTotal5minRev.1 = INTEGER: 4
Without -m +CISCO-PROCESS-MIB loaded (and no MIBS=+ALL set anywhere), the same walk instead prints the raw OID (.1.3.6.1.4.1.9.9.109.1.1.1.1.8.1) with no name attached - the same underlying data, just harder to read at a glance. If you want to confirm a specific MIB is actually loadable before relying on it in a real command, snmptranslate is the quickest sanity check:
snmptranslate -m +CISCO-PROCESS-MIB -On cpmCPUTotal5minRev
.1.3.6.1.4.1.9.9.109.1.1.1.1.8
If that resolves to a numeric OID instead of erroring, the MIB loaded correctly and the name-to-OID translation is working.
Loading MIBs for snmpd
This one's genuinely different from the other two, and worth being explicit about: snmpd generally doesn't need textual MIB files loaded to do its normal job. The standard MIBs it serves - system, IF-MIB, HOST-RESOURCES-MIB, and so on - are compiled directly into the agent as native code, not parsed from .mib files at startup. Setting MIBS=+ALL in snmpd's environment doesn't make it serve anything it couldn't already serve; that setting only affects how a client interprets responses, not what an agent has available to respond with in the first place.
Where MIB loading does become relevant for snmpd is when you're extending it with your own custom data via extend or pass/pass_persist (covered below) and you've written a MIB file documenting those custom OIDs. In that case, "loading" isn't something snmpd itself needs to do - it's about making that MIB file available to whoever queries your agent afterward, typically by dropping it in a shared location or distributing it alongside whatever monitoring setup will be reading those values. The practical way to verify your own custom OIDs resolve as expected is to query your own agent locally using the client tools' MIB loading, not snmpd's:
snmpwalk -m +MY-CUSTOM-MIB -v2c -c public localhost 1.3.6.1.4.1.99999.1
Loading MIBs for snmptrapd
This is the one that actually catches people out in practice, because snmptrapd is almost always run as a background service rather than an interactive command - and a systemd service does not inherit your personal shell's environment variables. Exporting MIBS=+ALL in your own ~/.bashrc and then wondering why systemctl status snmptrapd still shows unresolved OIDs in the log is an extremely common version of this exact problem.
The fix is to set the MIB configuration in the service's own environment, not yours. With systemd, a drop-in override file is the cleanest way to do this without editing the shipped unit file directly (which a package upgrade could overwrite):
sudo mkdir -p /etc/systemd/system/snmptrapd.service.d/
sudo tee /etc/systemd/system/snmptrapd.service.d/mibs.conf <<'EOF'
[Service]
Environment=MIBDIRS=+/opt/custom-mibs:/opt/custom-mibs/cisco
Environment=MIBS=+ALL
EOF
sudo systemctl daemon-reload
sudo systemctl restart snmptrapd
Alternatively, skip the environment variables entirely and bake the MIBs directly into the command line by editing the service's ExecStart (via systemctl edit snmptrapd, which opens an override file the same way):
[Service]
ExecStart=
ExecStart=/usr/sbin/snmptrapd -f -m ALL -M /opt/custom-mibs /etc/snmp/snmptrapd.conf
(The empty ExecStart= line first is required - systemd needs it to clear the original ExecStart before your override line replaces it, rather than trying to run both.)
On older systems still using SysV-style init scripts rather than systemd, the equivalent lives in an options file the init script reads on startup - typically /etc/default/snmptrapd on Debian-family systems or /etc/sysconfig/snmptrapd on RHEL-family ones:
# /etc/default/snmptrapd
TRAPDOPTS="-Lsd -M /opt/custom-mibs -m ALL"
Whichever mechanism your system uses, the underlying point is the same: snmptrapd's MIB configuration has to live wherever it actually starts from, not wherever you happen to be typing commands.
Running snmpd (the agent)
snmpd answers SNMP queries about the Linux machine it runs on - CPU, memory, disk, network interfaces, and so on - via HOST-RESOURCES-MIB and the standard system MIBs. If you want your own servers to show up when something polls them over SNMP, this is the daemon that makes that happen.
Basic configuration
Configuration lives in /etc/snmp/snmpd.conf. A minimal working config just needs a community string and what it's allowed to see:
# /etc/snmp/snmpd.conf
rocommunity public 10.0.0.0/24
syslocation "Rack 4, DC1"
syscontact [email protected]
agentaddress udp:161
sudo systemctl restart snmpd
sudo systemctl enable snmpd
snmpwalk -v2c -c public localhost system # test locally first
SNMPv3 instead of a plaintext community string
sudo net-snmp-create-v3-user -a authpassword -A SHA -x privpassword -X AES myuser
snmpget -v3 -u myuser -l authPriv -a SHA -A authpassword -x AES -X privpassword localhost sysDescr.0
Restricting what a community/user can actually see (views)
view systemview included .1.3.6.1.2.1.1 # sysDescr, sysName, etc.
view systemview included .1.3.6.1.2.1.2 # interfaces
rocommunity public 10.0.0.0/24 -V systemview
Exposing a custom value with extend
To surface something snmpd doesn't natively know about - a local script's output, a custom health check:
extend disk-check /usr/local/bin/check_disk_usage.sh
snmpwalk -v2c -c public localhost NET-SNMP-EXTEND-MIB::nsExtendOutputFull
Fully custom OIDs with pass / pass_persist
When extend isn't flexible enough - you want the data under your own private enterprise OID rather than the generic extend tree - pass hands off an entire subtree to an external script implementing GET/GETNEXT itself:
pass .1.3.6.1.4.1.99999.1 /usr/local/bin/my_custom_oid_handler.sh
pass re-launches the script on every request (simple, but slower under load); pass_persist keeps one long-running instance and talks to it over stdin/stdout - the better choice for anything queried frequently.
Running snmptrapd (the trap receiver)
snmptrapd listens for incoming traps and informs, and can log them, forward them, or hand each one to a script for custom handling.
Basic configuration
Configuration lives in /etc/snmp/snmptrapd.conf. By default it drops everything as unauthorized, so at minimum you need to allow specific community strings (v1/v2c) or users (v3):
# /etc/snmp/snmptrapd.conf
authCommunity log,execute,net public
createUser myuser SHA "authpassword" AES "privpassword"
authUser log,execute,net myuser priv
The log,execute,net flags control what happens with a matching trap: log writes it to the log, execute allows traphandle scripts to run, net allows forwarding. Run it in the foreground first to confirm it's actually receiving traps before setting it up as a background service:
sudo snmptrapd -f -Lo -m ALL -c /etc/snmp/snmptrapd.conf
-f keeps it in the foreground, -Lo logs to stdout instead of syslog so you see traps arrive in real time, and -m ALL here is exactly the client-side style MIB loading described above - fine for this interactive foreground test, but remember it won't carry over once this runs as an unattended service (see loading MIBs for snmptrapd above for making it stick).
Pointing snmptrapd at a handler script
The traphandle directive runs an external script for every matching trap, passing the trap's variable bindings on stdin:
# /etc/snmp/snmptrapd.conf
traphandle default /usr/local/bin/traphandler.py
default matches every trap OID; you can also target one specifically, e.g. traphandle 1.3.6.1.6.3.1.1.5.3 /usr/local/bin/linkdown_handler.py for only linkDown traps.
Example traphandle script (Python)
snmptrapd writes the trap to the script's stdin as plain text: the source host on the first line, the source IP/port on the second, then one OID value pair per line for every variable binding.
#!/usr/bin/env python3
"""
Minimal snmptrapd traphandle script - reads a trap from stdin and
logs it to a file. Wire this up with:
traphandle default /usr/local/bin/traphandler.py
"""
import sys
import datetime
LOG_FILE = "/var/log/snmp-traps.log"
def main():
lines = sys.stdin.read().splitlines()
if len(lines) < 2:
return # malformed/empty input, nothing to do
hostname = lines[0]
source = lines[1]
varbinds = lines[2:]
timestamp = datetime.datetime.now().isoformat()
with open(LOG_FILE, "a") as f:
f.write(f"[{timestamp}] Trap from {hostname} ({source})\n")
for line in varbinds:
f.write(f" {line}\n")
if __name__ == "__main__":
main()
Make it executable (chmod +x /usr/local/bin/traphandler.py) and confirm the shebang line points at a Python 3 actually on the system's PATH. From here, this is where you'd add real logic - parsing a specific varbind by OID, filtering on which trap it is, calling out to a notification API. If you'd rather resolve OID names by calling out to an API instead of relying on locally-loaded MIBs at all (useful for vendor MIBs you haven't tracked down yet), see the traphandle example in the Using the API guide.