Skip to content

Threat Model

github-actions[bot] edited this page Oct 16, 2026 · 1 revision

This page answers three questions about sipnab:

  • what an attacker would want from it,
  • where an attacker can reach it,
  • what stops them.

It also lists what does not stop them.

  • The last section, residual risks, matters as much as the rest.
  • An assessment that lists only defenses tells an operator nothing about where to add their own.

SECURITY.md says how to report a vulnerability. It also says which classes of bug count as one.

  • This page is the analysis behind that list.
  • The assurance case builds on it.
    • It argues that sipnab applies secure design principles.
    • It walks the CWE Top 25 weaknesses one by one.

Terms used on this page

  • Asset: something worth protecting, such as a TLS private key or the calls in a capture.
  • Trust boundary: a place where data or commands cross into sipnab from something sipnab does not control. Every input an attacker can influence crosses one.
  • Threat: a specific thing an attacker could try at a boundary.
  • Mitigation: the code that makes a threat fail or limits its damage.
    • Every mitigation below names the file, and usually the function, that implements it.
    • So a reader can check the claim instead of trusting it.
  • Residual risk: a threat that no mitigation fully covers today.
  • Loopback: the 127.0.0.1 or ::1 address, reachable only from the same machine.
  • Bearer token: a secret a client sends in an HTTP Authorization: Bearer header. Whoever holds the token has the access it grants.
  • HMAC: a keyed checksum. Only a holder of the key can produce a value that verifies. So it proves who sent a message, and that nobody changed it.

Scope and method

The assessment covers the sipnab binary built with the full feature set on Linux. That is the build that has every listener and every hook.

  • It walks each trust boundary.
  • It names the likely threats there.
  • It cites the code that answers each one.
  • The authors read each cited function to confirm it does what this page says.
  • The code may do less than a reader might assume. Then the gap appears under residual risks.

sipnab is mostly passive: it reads packets and reports on them. Two features act on other systems:

  • scanner-kill (sends SIP responses),
  • the TFPS ban actions.

Both are off by default.

Assets

Asset Why an attacker wants it
Captured SIP and RTP Call metadata (who called whom, when), SIP digest credentials, and call audio.
Decryption secrets TLS private keys (--tls-key), TLS key logs (--keylog), SRTP keys, and secrets embedded in pcapng files. Any of these decrypts traffic beyond what sipnab shows.
Credentials sipnab checks REST API keys and signing keys, MCP tokens, metrics Basic-auth credentials, HEP shared secrets, archive passwords.
The capture host sipnab often starts as root to open a network device. Code execution in sipnab before the privilege drop is code execution as root.
Integrity of findings Security alerts, scanner-kill decisions and fail2ban lines drive action elsewhere. Forged findings can ban an innocent address.
Other networks The scanner-kill path transmits. Misused, it could aim traffic at a victim.
Evidence files A capture is often the only copy of an incident. Overwriting it destroys evidence.

Trust boundaries at a glance

Boundary Who is on the other side Exposure by default
Packet capture input Anyone who can send a packet the capture interface sees Whenever sipnab captures live
Capture and archive files Whoever produced the file Whenever -I reads a file
HEP senders Any host that can reach the HEP port Off. --hep-listen turns it on
REST API clients Any host that can reach the API port Off. --api turns it on
MCP clients An AI agent and whoever controls its prompts Off. --mcp turns it on
Exec hooks Contents of captured SIP, passed to an operator's command Off. --on-dialog-exec, --on-quality-exec and --alert-exec turn it on
WASM plugins The author of a plugin file Not in the default build. The plugins feature adds it
TLS and SRTP key material Other local users, crash dumps, swap Whenever a key option is present
Configuration files and the command line Anyone who can write a config file or read the process list Always

Packet capture input (live interfaces)

Every packet on a monitored interface is attacker-controlled.

  • A remote attacker needs no account and no connection.
  • The attacker needs only the ability to send one SIP or RTP packet that crosses the wire sipnab watches.
Threat Mitigation Where
A malformed packet crashes a parser or corrupts memory. The parsers are Rust. The project fuzzes every parser that reads untrusted bytes, and a smoke tier runs in ordinary CI. Fault model counts the unsafe blocks and names where they live. fuzz/fuzz_targets/, tests/smoke_fuzz_test.rs
A flood of unique Call-IDs or RTP SSRCs grows memory without limit. The dialog and stream stores cap their size and evict. A regression test floods 50,000 identities into a 1,000-entry store. tests/resource_bounds_test.rs
Oversized header lines, TCP streams or WebSocket frames exhaust memory. Fixed caps on header-line length, on the reassembly buffer and on WebSocket message size. src/sip/parser.rs::DEFAULT_MAX_HEADER_LINE_LEN, src/capture/reassembly.rs::DEFAULT_MAX_TCP_BUFFER, src/capture/websocket.rs::MAX_WS_MESSAGE_SIZE
A parser bug becomes root code execution. Once the capture thread reports the device open, sipnab drops to an unprivileged user (nobody unless --user names another) and checks that the drop took. See residual risk "libpcap is C, and it runs briefly as root" for the window before the drop. It sets PR_SET_NO_NEW_PRIVS at startup so a later exec cannot regain privilege. src/privilege.rs::drop_privileges, src/privilege.rs::verify_dropped, src/privilege.rs::block_privilege_escalation
A forged scanner packet turns scanner-kill into a reflector aimed at a victim. Responses pass a global per-second cap (default 10, zero refused) and a fixed cap of three per destination per minute that no option raises. Only a live source can create the send sockets: a capture file or a uprobe source cannot. The sockets live in a separate worker process that starts with an emptied environment and clears its Linux capabilities. src/cli.rs::DEFAULT_KILL_RATE_LIMIT, src/process_isolation.rs::MAX_PER_DST_PER_MINUTE, src/security/transmit_guard.rs::for_source, src/process_isolation/worker_process.rs::clear_capabilities

Capture and archive files

A capture file is as hostile as the network it came from. An archive adds decompression, and file names chosen by its author.

Threat Mitigation Where
A record header claims a huge length and triggers an out-of-bounds read. The reader uses checked arithmetic and a bounds check on every record length. src/capture/pcap_reader.rs
A decompression bomb fills memory or disk. Every byte that decompression produces, summed over every nested layer, counts against one ceiling (default 1 GiB, set with --max-gunzip-bytes). Nesting stops at four layers and member count at 10,000. src/capture/pcap_reader.rs::DEFAULT_MAX_GUNZIP_BYTES, src/capture/archive/mod.rs::MAX_DEPTH, src/capture/archive/mod.rs::MAX_ENTRIES
A member named ../../etc/cron.d/x writes outside the extraction directory. sipnab never uses a member's name as a path. It writes each member to a name it generates (m00003.pcap) inside a private temporary directory, creates the file exclusively with owner-only permissions, and never materializes links, device nodes or named pipes. src/capture/archive/mod.rs::ExtractDir
An output option points at an input and destroys the evidence. sipnab compares canonical paths, after resolving symlinks and .., and refuses before it opens anything for writing. src/capture/output_guard.rs::canonical_target
A pcapng file embeds TLS secrets that travel with a shared copy. sipnab reads Decryption Secrets Blocks so it can decrypt, and --strip-secrets writes a copy with every one removed, for use before anyone shares a capture. src/capture/pcapng_meta.rs, src/cli.rs (strip_secrets)
An archive password leaks through the process list, a URL or a readable file. --archive-password prints a warning naming the safer options on every use. sipnab refuses a password file you own that other users can read. The REST API refuses a password in a query string and limits wrong guesses to five per token and archive in 15 minutes. src/capture/archive/password.rs::INLINE_WARNING, src/privilege.rs::permission_refusal, src/output/api.rs::refuse_password_in_url_mw, src/output/api.rs::ARCHIVE_WRONG_LIMIT

HEP senders

HEP (Homer Encapsulation Protocol) lets a SIP server such as OpenSIPS or Kamailio mirror its traffic to sipnab over the network.

  • A HEP packet carries the SIP message and the addresses it claims the message used.
  • So a sender can assert any source address it likes.
Threat Mitigation Where
Anyone on the network injects fake calls. A listener on a non-loopback address refuses to start unless it has a shared secret or a source allowlist. It classifies loopback from the literal address only, without a DNS lookup, so a hostname counts as non-loopback. src/capture/hep.rs::enforce_hep_bind_policy
An attacker recovers the shared secret byte by byte from response timing. The shared secret check compares in constant time. src/capture/hep.rs::plain_auth_check, src/crypto.rs::constant_time_eq
An attacker replays or edits a captured HEP packet. The opt-in --hep-auth-mode hmac signs the whole datagram, rejects stale timestamps, and remembers the one-time number (nonce) in each accepted packet to refuse replays. It verifies the signature before touching the nonce cache, so a forged packet cannot poison it. src/capture/hep.rs::verify_hmac_datagram
A sender forges a source address so sipnab kills a call or bans an innocent host. Scanner-kill and fail2ban lines act on a HEP-reported address only with --hep-allow-kill, which defaults off. src/security/scanner_kill.rs::kill_response_eligible
A HEP-over-TLS key file readable by every user lets anyone impersonate the listener. sipnab refuses a world-readable key. src/tls_files.rs::pem_private_key
One sender floods the listener. A global packet ceiling (--hep-rate-limit, default 50,000 packets per second) and an optional per-sender cap (--hep-rate-limit-per-peer, off by default). src/capture/hep.rs::describe_hep_limiters

REST API and metrics clients

The REST API (--api) serves everything sipnab knows about the capture. The metrics endpoint (--metrics) serves Prometheus counters.

Threat Mitigation Where
The API listens on a public address with no credentials. A non-loopback bind with no API key or signing key refuses to start. The metrics server applies the same rule to its Basic-auth credential. src/output/api.rs::enforce_bind_auth_policy, src/output/prometheus_server.rs::start_metrics_server
A web page uses DNS rebinding to reach a loopback API with no key through a browser: it points its own name at 127.0.0.1, then reads /v1/dialogs or sends POST /v1/persistence as a same-origin request (CWE-352). Before any route, the rate limiter or authentication runs, the Host header must name localhost, 127.0.0.1, ::1, the bound address (any IP address on a wildcard bind) or an --api-allowed-host / [api] allowed_hosts entry. Anything else gets 403, and a request with no Host gets 400. * turns the check off. The MCP transport applies the same rule. src/host_allowlist.rs::HostAllowlist, src/output/api.rs::with_host_allowlist
An attacker forges or reuses a token. Tokens carry an HMAC-SHA256 signature, an expiry, an audience (api or mcp) and an id that a revocation list can name. Verification compares signatures in constant time. A value that starts like a signed token and fails any check gets a refusal, never a second chance as a static key. Authentication describes minting and rotation. src/auth.rs::verify_signed, src/auth.rs::verify_static
A token for one surface opens the other. The audience check is unconditional, so an API token fails on MCP and the reverse. src/auth.rs::verify_signed
An attacker guesses tokens at high speed. The per-client rate limit runs before authentication, so a wrong guess costs the same budget as a right one. src/output/api.rs::guard_scoped
A read-only credential triggers a ban on another system. TFPS ban and unban need a token with the actions scope and a server-side setting that enables them. src/output/api.rs::action_guard
A network observer reads bearer tokens or capture data in transit. With --api-tls-cert and --api-tls-key the API serves HTTPS only (TLS 1.2 and 1.3), refuses a world-readable key or a key that does not match the certificate at startup, and refuses to start on only one of the two flags rather than serving plain HTTP. HTTP MCP (--mcp-tls-cert / --mcp-tls-key) and the metrics endpoint (--metrics-tls-cert / --metrics-tls-key) apply the same checks through the same reader. src/output/api.rs::api_tls_config, src/tls_files.rs::server_config, src/cli.rs::tls_pair_problem
A client that connects and never finishes a TLS handshake holds the listener. On the API and HTTP MCP, each handshake runs in its own task with a 10-second timeout, at most 256 at once; sipnab closes connections past that. On the metrics endpoint each connection has its own thread with a 5-second read and write timeout, within the --metrics-max-conn limit. src/tls_listener.rs::accept_tls, src/output/prometheus_server.rs::start_metrics_server
A slow or huge request ties up the server. Every route has a 30-second timeout and a 1 MiB body limit. src/output/api.rs::build_router, src/output/api.rs::REQUEST_TIMEOUT, src/output/api.rs::MAX_REQUEST_BODY_BYTES

MCP clients

MCP (Model Context Protocol) lets an AI agent query sipnab. The agent is a client like any other, with one extra risk:

  • Text inside a capture can reach the agent's prompt.
  • So a hostile SIP header can try to talk the agent into calling a tool.

The defenses therefore limit what any caller can do. They do not trust the agent's judgment. MCP deployment covers running it safely.

Threat Mitigation Where
The HTTP transport listens on a public address with no credentials. A non-loopback bind without a token or signing key refuses to start. src/mcp/transport.rs::serve_http
A web page uses DNS rebinding to reach a loopback MCP server through a browser. Requests must carry a Host header on the allowlist (localhost, 127.0.0.1, ::1 and the bound address, plus --mcp-allowed-host entries), checked before the bearer guard by the same rule the REST API uses. src/host_allowlist.rs::HostAllowlist, src/mcp/transport.rs::host_layer
An agent replaces the capture, installs TLS probes, stops the server or writes findings. Each of these tools refuses unless the operator started sipnab with its own opt-in flag: --mcp-allow-open-capture, --mcp-allow-tls-capture, --mcp-allow-shutdown and --mcp-allow-save-findings. src/mcp/server.rs::open_capture and the allow_* fields beside it
An agent writes or reads outside a chosen directory. File tools accept a bare file name only, resolve symlinks, and refuse any result outside --mcp-file-root. With no root set, the file tools refuse. src/mcp/server.rs::resolve_in_root
One agent monopolizes the server. A per-caller cap on tool calls per second (--mcp-rate-limit-per-peer, default 100) and a 2 MiB request body cap. src/mcp/server.rs::with_rate_limit_per_peer, src/mcp/transport.rs::serve_http

Commands run by exec hooks

--on-dialog-exec, --on-quality-exec and --alert-exec run an operator's command when an event fires.

  • The event data comes from captured SIP.
  • So a caller can choose the From header that reaches the command.
Threat Mitigation Where
A crafted SIP header injects shell syntax into the command. sipnab passes event data in SIPNAB_* environment variables and never splices it into the command string. The legacy %from style placeholders become quoted "${SIPNAB_FROM}" references, not text substitution, so the shell neither splits the value into words nor expands * in it. src/output/event_exec.rs::spawn_command, src/security/exec_placeholders.rs::quote_placeholders, src/security/alerting.rs
A burst of events forks thousands of processes. A per-second rate limit and a cap on live child processes (default 100, [limits] exec_queue_depth). sipnab drops and counts events past either cap. src/output/event_exec.rs::DEFAULT_QUEUE_DEPTH, src/output/event_exec.rs::check_rate_limit

WASM plugins

A plugin is a WebAssembly module. It receives one dialog as JSON and returns findings.

  • The plugins feature adds it.
  • The default build leaves it out.
  • WASM plugins is the author's guide.
Threat Mitigation Where
A plugin reads files, opens sockets or runs commands. The host registers no imports at all, so a module that imports anything fails to load. The interpreter, wasmi, runs no JIT. src/plugin/mod.rs::instantiate
A plugin loops forever or allocates the host's memory. A fuel budget per dialog, a 16 MiB memory cap installed before the module's declared memory exists, a 4 MiB output cap and a 16 MiB file-size cap. A failing plugin fails that dialog only. src/plugin/mod.rs::FUEL_PER_DIALOG, src/plugin/mod.rs::MAX_MEMORY_PAGES, src/plugin/mod.rs::MAX_OUTPUT_BYTES, src/plugin/mod.rs::MAX_PLUGIN_BYTES

TLS and SRTP key material

sipnab decrypts traffic that other systems encrypted. So it holds keys that decrypt far more than one capture. Capture SIP over TLS covers the options.

Threat Mitigation Where
A crash writes keys into a core file. With any key option present, sipnab turns off core dumps for itself (PR_SET_DUMPABLE) and exits if that fails, unless --allow-coredump says otherwise. src/privilege.rs::disable_core_dumps, src/app/bootstrap.rs
The kernel writes keys to swap. sipnab locks its memory with mlockall. If the host's RLIMIT_MEMLOCK forbids that, it logs a warning and continues. src/privilege.rs::lock_key_memory
Keys linger in freed memory. Key-log entries, key-log read buffers and derived SRTP session keys wipe themselves on drop. src/capture/tls.rs::zeroize_material, src/capture/keylog_source.rs, src/rtp/srtp.rs::DerivedSessionKeys
A private key reaches a log line. The RSA key type prints as RsaKey(<redacted>), and a parse error never echoes key bytes. src/capture/rsa_key.rs
A libpcap defect reads another run's key log. The opt-in Landlock sandbox (--sandbox best-effort or required) limits which paths the process can open after startup. src/sandbox.rs::install

Configuration files and the command line

sipnab reads a TOML file. It uses the first it finds of these:

  • --config,
  • $SIPNAB_CONFIG,
  • $XDG_CONFIG_HOME/sipnab/sipnab.toml (by default ~/.config/sipnab/sipnab.toml),
  • ~/.sipnabrc,
  • /etc/sipnab/sipnab.toml.

The file can name commands to run (alert_exec). So write access to it amounts to running code as sipnab.

Threat Mitigation Where
A typo in a key silently disables a setting, such as a rate limit. sipnab warns about every unknown key, and validation refuses unsafe values such as a zero exec queue or a zero kill rate. src/config.rs::unknown_keys, src/config.rs (validate)
An environment variable in the config expands into more than the config named. Only the ${NAME} form expands, never recursively, and an unset variable is an error rather than an empty string. src/config.rs::expand_env_vars
A secret on the command line shows up in ps output. The listener secrets have file or environment-variable forms, such as --api-signing-key-file, --mcp-token-file, --hep-auth-file, --metrics-auth-file and SIPNAB_API_KEY. src/cli.rs

Hardening that spans every boundary

  • Privilege drop and chroot. These apply during startup (src/privilege.rs::do_chroot).
    • --user sets the account sipnab drops to.
    • --chroot narrows the filesystem it can see.
    • Architecture explains the privilege-drop design.
  • Landlock path sandbox, opt-in with --sandbox (src/sandbox.rs).
  • Seccomp system-call filter, opt-in with --seccomp (src/seccomp.rs, syscall sandbox design).
    • The log mode records calls and blocks nothing.
    • The enforce mode needs an allowlist the operator supplies.
  • Dependency checks.

Residual risks and known gaps

Each item below is a threat the code does not fully answer today.

  • Most are choices with a stated reason.

  • Some are open work.

  • An operator who needs protection against one of these has to supply it outside sipnab.

  • TLS is opt-in on every network listener.

    • The REST API, HTTP MCP and the metrics endpoint serve HTTPS only when given a certificate and key. These come from --api-tls-cert, --mcp-tls-cert, --metrics-tls-cert and their key flags, or the tls_cert / tls_key keys in sipnab.toml (src/cli.rs::tls_settings_problem).
    • Without them, each server logs a warning on a non-loopback bind but still starts.
    • Without TLS or a reverse proxy, these cross the network in clear text:
      • bearer tokens,
      • the metrics endpoint's Basic credential (encoding, not encryption),
      • capture data.
  • Loopback means no authentication.

    • With no key configured, the REST API and HTTP MCP accept every request on a loopback bind (src/output/api.rs::authenticate).
    • On a shared host, every local user can read the capture through them.
  • Authenticated clients see real data.

    • --redact replaces identities with pseudonyms in exported vCon containers only.
    • The REST API and MCP answer with the real addresses and identities. Neither calls the redactor (src/output/redact.rs).
  • Failed MCP logins face no throttle.

    • The MCP rate limit counts tool calls after authentication.
    • The HTTP bearer check has no limiter of its own (src/mcp/transport.rs::auth_layer).
    • Signed tokens resist guessing through their 256-bit signature. A short static --mcp-token does not.
  • Plain HEP authentication is weak.

    • In the default plain mode, each packet carries the shared secret in clear text.
      • So anyone on the path can read and reuse it.
      • Nothing stops replays.
    • The HMAC mode fixes both, but only another sipnab sends it.
    • A source allowlist alone trusts UDP source addresses. An attacker can forge those.
  • Exec hooks inherit the environment.

    • The child processes start with sipnab's full environment. That includes any SIPNAB_API_KEY or similar secret set there (src/output/event_exec.rs::spawn_command).
    • Passing data through variables also only helps while the hook script quotes them. A script that runs eval $SIPNAB_FROM reintroduces the injection.
  • No permission check on most secret files or the config file.

    • sipnab checks permissions on archive password files and the HEP TLS key only.
    • It reads these without checking who else can read or write them:
    • Whoever can write the config file can set alert_exec.
  • The sandbox and system-call filter are off by default.

  • Capabilities survive a non-root start.

    • After --setup-caps, a non-root sipnab holds CAP_NET_RAW and CAP_NET_ADMIN from the file itself.
    • The privilege drop does nothing for a process that did not start as root (src/privilege.rs::drop_privileges).
    • Only the scanner-kill worker clears its capability set.
    • On macOS the drop also needs an explicit --user.
  • libpcap is C, and it runs briefly as root.

    • It parses every live packet before sipnab's Rust code does. It does this inside the process that holds keys and tokens.
    • The capture thread starts reading as soon as it reports the device open. It does not wait for the privilege drop on the main thread (src/capture/live.rs::capture_live, src/app/bootstrap.rs).
    • So the first packets of a root-started run can reach libpcap before the drop.
    • The privilege drop and the opt-in sandbox limit the damage after that. Nothing removes the risk.
  • File-tool path check and file open are separate steps.

    • The MCP file tools resolve the path and check it against the root. Then they open it later.
    • A local user who can change links inside --mcp-file-root between those two steps could redirect a write.
    • Keep the root writable by sipnab alone.
  • Plugins are not signed.

    • sipnab loads any module the operator names.
    • A plugin can emit misleading findings, even though it cannot touch the host.
  • The metrics endpoint does not check Host.

    • The REST API and HTTP MCP refuse a Host that is not on their allowlist. That stops DNS rebinding.
    • The standalone --metrics server does not (src/output/prometheus_server.rs::start_metrics_server). So a web page can read the counters of a loopback metrics endpoint with no credentials, through a browser.
    • It serves read-only counters and no capture content.
    • A non-loopback bind already needs Basic auth.
    • --api-allowed-host '*' and --mcp-allowed-host '*' put the REST API and MCP back in the same position.

Keeping this page true

tests/openssf_badge_test.rs checks three things:

  • this page names every trust boundary above,
  • every path it cites exists,
  • every file::name citation names something defined in that file.

A renamed function turns that test red. So the page cannot quietly point at code that moved.

Home

Getting started

Using the TUI

CLI & automation

Configuration

Integrations (API & MCP)

Your SIP server

Add to your voice stack

Development & internals

Clone this wiki locally