Skip to content

Hardening & security

A SIP port on the public internet gets scanned within minutes. This recipe collects the layers SIPhon gives you — most are config, a few are one-liners in a script.

1. Drop abuse before it costs you (config)

The security: block runs before any SIP parsing or scripting, so banned/garbage traffic never reaches your handlers:

security:
  rate_limit:
    window_secs: 10
    max_requests: 30            # per source IP per window
    ban_duration_secs: 3600

  scanner_block:
    user_agents: ["sipvicious", "friendly-scanner", "VaxSip", "sipcli"]

  trusted_cidrs: ["10.0.0.0/8"] # own infra: never rate-limited, never banned

  failed_auth_ban:              # auto-ban at accept (UDP/TCP/TLS/WS/SCTP)
    threshold: 10               # weighted failures in window_secs → ban
    window_secs: 600
    ban_duration_secs: 3600
    strong_signal_weight: 3     # weight of a high-confidence abuse signal

  apiban:                       # optional: APIBAN community blocklist
    api_key: "your-api-key"
    interval_secs: 300
    ban_ttl_secs: 604800        # 7 days, matching the feed's own release
                                # policy. 0 = never expire.

trusted_cidrs covers the feed too: an address listed by APIBAN that matches a trusted CIDR is dropped as the feed is ingested, so it reaches neither the userspace ACL nor the kernel set. Put your own trunks, monitoring and management addresses there — a community blocklist has no way to know they're yours, and the kernel drop is port-agnostic, so a listed management address would cost you ssh along with the trunk.

How the scoring works

failed_auth_ban is a confidence-weighted counter, not a flat fail2ban tally. Every abuse signal from a source IP adds to a per-IP score within window_secs; crossing threshold bans the IP for ban_duration_secs. Signals are weighted by how hard they are to fake:

Signal Score
401/407 challenge with no follow-up success 1
INVITE server-transaction timeout (never ACKed) 1
Wrong password, or a forged/stale/replayed digest nonce strong_signal_weight (default 3)
Non-SIP bytes on a TCP/TLS stream strong_signal_weight
Failed TLS/WSS/WS handshake strong_signal_weight
Scanner User-Agent (scanner_block) strong_signal_weight

Signals arriving over TCP (handshake, malformed bytes) score high because the source IP is validated by the three-way handshake — it can't be spoofed, and a legitimate client never trips them. A successful authentication resets the score to zero, so a subscriber who mistypes a password twice then logs in is never banned, while an IP spraying garbage is banned ~3× faster than one just rattling doorknobs.

Bans are enforced at recv()/accept() — before any SIP parsing — and expire on their own. trusted_cidrs are exempt from scoring entirely, so put your load balancers and health checks there.

Drop bans in the kernel

With security.firewall, every ban is also pushed to a kernel nf_tables set, so abusive sources are dropped before they reach SIPhon — real defense against volume, not just userspace politeness.

In a script, you can also rate-limit a specific flow:

if not proxy.rate_limit(request, window_secs=1, max_requests=5):
    return    # silently drop — don't fingerprint the server

2. Drop malformed traffic (script)

proxy.sanity_check() runs the RFC 4475 semantic checks (mandatory headers, CSeq, Content-Length). Drop failures silently so scanners learn nothing:

@proxy.on_request
def route(request):
    if not request.in_dialog and not proxy.sanity_check(request):
        return                  # silent drop
    ...

Silent drop is intentional

Returning from a handler without reply()/relay()/reject() sends no response. For rate-limit and scanner blocking that's the point — a 403 would confirm the server exists. Don't "helpfully" reply.

3. Encrypt the signalling (config)

listen:
  tls: ["0.0.0.0:5061"]
tls:
  certificate: "/etc/siphon/tls/cert.pem"
  private_key:  "/etc/siphon/tls/key.pem"
  method: "TLSv1_3"
  # mTLS — require and verify client certs (SIP trunks with mutual auth):
  verify_client: true
  client_ca: "/etc/siphon/tls/client-ca.pem"

method is the minimum TLS version. TLSv1_3 here is a real 1.3-only floor — it refuses TLS 1.2 peers on the listeners and on outbound connections siphon dials, so check both sides can do 1.3 before hardening. TLSv1_2 (the default) negotiates 1.2 or 1.3.

verify_client: true requires a client cert chaining to client_ca (fails closed at startup if client_ca is missing). It applies to listen.tls and listen.wss.

4. Authenticate subscribers (script + config)

if not auth.require_digest(request, realm="example.com"):
    return                      # 401/407 challenge already sent
user = request.auth_user        # the authenticated username afterwards

The auth.backend can be static, http (REST credential lookup), database, or diameter_cx (IMS HSS). For REGISTER-time account-takeover protection, set registrar.enforce_auth_aor_match: true so a subscriber can't bind a Contact under someone else's AoR.

5. Verify caller ID — STIR/SHAKEN (script)

Sign on egress, verify on ingress at a trunk edge:

from siphon import proxy, stir, log

@proxy.on_request("INVITE")
def on_invite(request):
    if request.source_ip_in(["203.0.113.0/24"]):           # inbound from a peer
        result = stir.verify(request)
        if result.verstat == "TN-Validation-Failed":
            request.reply(438, "Invalid Identity Header")  # RFC 8224 §6.2.2
            return
        stir.apply_verstat(request, result)                 # convey downstream
    else:                                                    # outbound
        origid = stir.sign(request, attestation="A")
    request.record_route()
    request.relay()

Needs a stir: block with signing + verification configured.

The source_ip_in([...]) above hardcodes the peer's CIDR. If that peer is already a gateway group (a trunk you health-probe), test membership by group name instead so you never maintain two copies of the address list — see the next section.

5.5. Direction & trust — from_gateway

request.from_gateway("group") (and call.from_gateway("group") in a B2BUA) returns True when the message's source IP is one of the resolved addresses of the named gateway group. It's SIPhon's equivalent of Kamailio ds_is_from_list() / OpenSIPS ds_is_in_list() — a routing-direction predicate that replaces hardcoded source CIDRs with the trunk list you already maintain under gateway.groups.

from siphon import proxy, gateway

@proxy.on_request("INVITE")
def route(request):
    if request.from_gateway("teams"):
        # Inbound leg from Microsoft Teams — trust it, forward to the PBX.
        request.relay("sip:pbx.internal:5060")
    else:
        # Outbound leg from the PBX — send to Teams.
        request.relay(gateway.select("teams").uri)

It matches on IP only (source port ignored) against every resolved address in the group, so a hostname that round-robins across many IPs — Teams' sip/sip2/sip3.pstnhub.microsoft.com, a carrier's rotating trunk — matches on any of them. The member set is cached and refreshed on the health-probe cycle, so the predicate never resolves DNS on the request path.

Trustworthy on TCP/TLS/WS/WSS, a hint on UDP

On connection-oriented transports the source IP is verified by the handshake, so from_gateway is a sound authorization signal. On UDP the source IP is spoofable — treat from_gateway there as a best-effort direction hint, and gate real trust decisions on TLS/mTLS or digest/AKA auth.

6. IMS access security — IPsec (Gm)

For a P-CSCF, SIPhon does full 3GPP TS 33.203 sec-agree: parse Security-Client, run AKA, install kernel IPsec SAs, and route MT requests back over the flow. It's a substantial flow — see examples/ims_pcscf.py and the ipsec: config block. The SA lifetime tracks the registration lifetime automatically.

Checklist

  • [ ] security.failed_auth_ban + scanner_block on, infra in trusted_cidrs
  • [ ] proxy.sanity_check() on out-of-dialog requests, silent-drop failures
  • [ ] TLS (and mTLS for trunks); subscriber-facing access over TLS/WSS
  • [ ] Digest auth on REGISTER (+ enforce_auth_aor_match)
  • [ ] STIR/SHAKEN at PSTN edges; IPsec at IMS Gm
  • [ ] Alert on the security metrics (see Monitoring)

See also