Auditing Outbound Egress Rules with nftables and ufw to Catch Data Exfiltration

Most firewall reviews focus on inbound ports. That is backwards for a VPS threat model. An attacker who has already executed code does not need inbound access; they need outbound reachability to move your data somewhere else. Auditing egress rules is the highest-value security exercise most small deployments never do. This article covers how to inspect what your nftables or ufw rules really allow outbound, and how to build a baseline that surfaces unexpected traffic.

Default Policies Decide Everything

The single most important setting is the OUTPUT chain policy. A default of accept means every process on the box, including a compromised one, can open any outbound connection it likes. Start by reading the live ruleset rather than assuming what an installer created.

# nftables: show the full ruleset with handles
sudo nft list ruleset
sudo nft -a list chain inet filter output

# ufw: show the raw backing rules, not the friendly summary
sudo ufw status verbose
sudo iptables -S OUTPUT
sudo iptables -L OUTPUT -n -v

ufw’s status verbose hides a lot. Always drop to iptables -S to see the actual ACCEPT and DROP rules, including the ones ufw inserts above your own. If you see -P OUTPUT ACCEPT with no restrictive chain, you have no egress control at all.

What Legitimate Egress Looks Like

Before you can block, you must enumerate. A typical web or API VPS legitimately talks to: DNS resolvers (53/853), package repositories (80/443), NTP (123), your database replica, your object storage endpoint, and your monitoring/APM collector. Everything else deserves a reason.

DirectionPurposeTypical destDefault action
UDP 53/853DNS resolutionConfigured resolversAllow, pin to resolver IPs
TCP 80/443Updates, APIs, object storageKnown CIDRsAllow to allowlist
UDP 123NTP time syncNTP poolAllow
TCP 25/465/587SMTPRelay onlyBlock direct, allow relay
TCP 1-1024(no common need)anyLog and block
TCP 4444/1337/31337common C2 portsanyBlock

Building a Logging-First Egress Policy

Do not jump straight to DROP. First log outbound attempts for a week to build a baseline, then restrict. With nftables, create a dedicated chain and log before the decision.

table inet audit {
  chain egress {
    type filter hook output priority 0; policy accept;

    ct state established,related accept
    oifname "lo" accept

    # log and count new outbound connections
    ct state new log prefix "EGRESS-NEW: " level info
    ct state new counter
  }
}
# load it
nft -f /etc/nftables.d/audit-egress.nft
# watch the log
journalctl -k -f | grep EGRESS-NEW

With ufw you can approximate this using a LOG rule placed before your allow rules, but nftables gives far better control over chain order and counters. Whichever you use, the point is the same: establish what normal looks like before you break something in production.

Red Flags in the Baseline

Reviewing the baseline log, look for these patterns, which are classic exfiltration and command-and-control indicators:

  • Outbound connections from a web server process to a bare IP, not a hostname.
  • Traffic on ports with no business use: 4444, 8080 to an unknown host, 53/TCP tunneling.
  • Periodic beaconing at regular intervals with small payloads.
  • Large sustained uploads to a destination outside your provider’s ASN.
  • DNS queries to domains not in your resolver’s expected set, especially unusually long subdomains (DNS tunneling).

Confirming a Suspected Exfiltration Channel

Once a rule fires, attribute the traffic to a process. ss with process info and lsof on the socket will tell you who owns it.

# live outbound connections with owning process
ss -tunp state established
ss -tnp state established '( dport = :443 or dport = :4444 )'

# which binary opened a socket
lsof -i -n -P | grep ESTABLISHED

A sudden outbound connection from /tmp, a deleted binary (/proc/PID/exe (deleted)), or a process whose parent is cron and should not be making network calls are all signals worth investigating immediately. Correlate with file integrity monitoring and authentication logs. The managed versus unmanaged server comparison explains how much of this monitoring is typically handled for you, and where self-managed deployments must build it themselves. For getting the instance hardened before you layer egress rules on top, see the cron auditing walkthrough, since scheduled tasks are a common exfiltration trigger point.

Keep the audit as an ongoing control, not a one-time review. Rotate the baseline monthly, alert on new egress destinations, and re-run ss -tunp whenever the box has been exposed to a newly disclosed network-facing CVE.

Leave a Reply