Firewall discussions end at the front door: which ports accept incoming connections, which addresses are trusted, and the whole internet agrees that inbound filtering is basic hygiene. The traffic leaving a server gets a fraction of the attention, which is a strange inversion of the risk, because a compromised server's first move is outbound, contacting its control channel, downloading a second-stage payload, exfiltrating whatever it was sent to take. An OUTPUT chain with a default-deny policy turns that first move into a failure, and the discipline of whitelisting exactly what a server legitimately talks to makes anything else stand out in the logs. This article builds the complete outbound policy: the mechanics of the OUTPUT chain, a working whitelist for a real application server, the operational exceptions for DNS and package updates, logging the drops, persistence across reboots, and the staged rollout that never takes a server dark by accident.
Why the outbound half is the forgotten, decisive half
An inbound policy defines who may knock. An outbound policy defines what the machine may do after the worst has happened, and the asymmetry explains the value: inbound rules face a wall of constant scanning noise, while outbound rules face the rare event, the successful compromise, and at that moment they are the only control still standing between the attacker and the rest of the world. A server that can reach only its database, its monitoring host and its package mirrors is a server whose compromised web application can reach nothing else, and the beacon that never connects is an attack that never proceeds.
The honest threat model for outbound filtering has three residents. The first is the reverse shell and command channel, an outbound connection to an address the attacker controls. The second is the data pull, the second-stage download from a payload host. The third is exfiltration, the connection that carries the stolen data home, which on a properly filtered server cannot even open. All three die in the same place, the OUTPUT chain, and the filter that stops them costs less configuration effort than the inbound rules most administrators already maintain by reflex.
The chain, its policy and the state shortcut
The OUTPUT chain sees every packet the local machine generates, and the default-deny stance is one command that changes the server's entire outbound posture:
iptables -P OUTPUT DROP
After which every outbound packet needs an explicit rule, and the working order of the whitelist builds upward from there. The first rule in every functioning outbound policy is the established-connection shortcut:
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
which lets replies to inbound connections, the response half of every accepted web request, flow freely, and without which even a fully permitted inbound service appears broken to the outside. The loopback is the second universal:
iptables -A OUTPUT -o lo -j ACCEPT
because local inter-process traffic rides the loopback interface, and a surprising share of modern software, local databases, caches, management sockets, talks to itself constantly.
The state match deserves its own sentence in the picture, because it is what keeps the whitelist short: the ESTABLISHED rule covers every reply to every inbound permitted connection with one line, and the RELATED family covers the protocol helpers, the FTP data channels of legacy setups, so the explicit rules that remain are only for connections the server initiates itself, the exact category the whitelist exists to police.
Building the whitelist for a real application server
The whitelist is a statement of what the server is for, and a typical three-tier application server's statement reads as four rules. The database:
iptables -A OUTPUT -p tcp -d 10.0.2.31 --dport 5432 -j ACCEPT
the monitoring agent's report path:
iptables -A OUTPUT -p tcp -d 10.0.9.5 --dport 443 -j ACCEPT
the internal package mirror, covered with the update exceptions in the next section, and the site's own logging or API endpoints by exact address and port. The shape of each rule is deliberate: protocol, destination, port, and the absence of any wildcard, because a rule permitting all destinations on a port is a hole wearing whitelist clothing. The destination-specific form is what makes the policy a list of the server's actual business relationships, and the list has a property worth saying out loud: it is short. A server with a dozen outbound rules is a normal, healthy server, and a server that needs fifty is usually a server whose architecture nobody has audited.
The owner match extends the pattern for multi-role hosts, matching outbound packets by the uid that generated them:
iptables -A OUTPUT -m owner --uid-owner backup -p tcp -d 10.0.4.12 --dport 22 -j ACCEPT
which restricts a specific service account to its one permitted destination, the pattern for backup agents, monitoring users and the one application that genuinely needs a wider reach than its peers. Owner-scoped rules keep each service inside its own envelope and prevent the accidental discovery that the compromised web application was able to use the backup agent's permissions.
The operational exceptions, DNS, time and updates
Three categories of outbound traffic are not destinations in the application sense, and every working policy handles them explicitly rather than by accident. DNS comes first, and the correct production shape is the internal resolver by address:
iptables -A OUTPUT -p udp -d 10.0.0.2 --dport 53 -j ACCEPT
with the TCP fallback on the same destination permitted alongside, because oversized answers switch protocols. The public-resolver version of this rule, a wildcard on port 53 to any address, is the classic leak in a well-meaning policy, since DNS itself is a perfectly usable exfiltration channel, and the resolver-specific form closes it while keeping name resolution functional.
Time synchronization follows the same pattern by address, the internal NTP host on port 123. Package updates are the exception with a calendar attached: the mirror by address and port, enabled for the patch window and either left enabled deliberately as a documented standing exception, or toggled by the patch script through a rule that the runbook names. The modern mirror problem, the pool of rotating addresses behind a hostname, has two clean answers, a pin to the specific mirror addresses in use with the pin reviewed when the mirror list changes, or the local internal mirror that most data centers run precisely for this reason, a host whose address is stable and whose job is to be the one machine allowed to reach the outside world.
Logging the drops and reading the story
A default-deny chain without logging is a policy that fails silently, and the logging rule before the final drop is what converts the filter from a wall into an instrument:
iptables -A OUTPUT -j LOG --log-prefix "OUTDROP: "
Every dropped packet arrives in the kernel log with the prefix, the destination, the port and the generating interface, readable through journalctl -k or the standard syslog destinations. The first week of logs is the tuning phase in disguise: each recurring drop is either a missing legitimate rule, discovered now instead of during an outage, or exactly the noise the policy was designed to catch, an application trying somewhere it has no business going. The prefix makes grep trivial, and a weekly glance at the drop log becomes the maintenance rhythm that keeps the whitelist aligned with the software's actual behavior after updates.
The counters complete the observational loop:
iptables -L OUTPUT -v -n
showing per-rule packet and byte counts, which confirm the active rules doing their work and turn the logging rule's counter into a single-number health metric of how much the server is trying to reach the world beyond its list.
Persistence and the modern backend reality
Rules typed at the command line vanish at reboot, and the persistence mechanism differs by distribution family in a way worth naming precisely. Debian-family uses netfilter-persistent with the rules saved through netfilter-persistent save into files under /etc/iptables/, applied at boot by the corresponding service. RHEL-family stores the rules through iptables-save into /etc/sysconfig/iptables, applied by the iptables service, with firewalld as the higher-level alternative whose direct and rich-rule interfaces can express the same outbound policy. Underneath both, the modern kernel speaks nftables, and the iptables command on current releases is the compatibility layer over the nft backend, which changes nothing about the rule semantics and everything about the persistence plumbing, since the saved artifact may be iptables-format rules or nft rules depending on which tooling the site standardizes on. The choice matters less than the discipline: the saved configuration is versioned, the applied state matches it, and a reboot is the final test rather than an assumption.
The staged rollout and the working checklist
The rollout of a default-deny outbound policy is a change with a specific failure mode, the silent timeout of something nobody remembered, and the staging exists to surface every such something before the policy hardens. The first stage runs the policy in log-only form: every would-be drop logged, every packet still delivered, one week of normal operation. The log review at the week's end converts each recurring drop into a rule or a fix, and the second stage applies the policy for real, with the rollback, a policy reset to ACCEPT and a flush, documented in the change ticket and tested once on a staging host. The monitoring integration from the logging section turns the production rollout into an observable event, and the first week of counters is the evidence that the policy is doing what the design said it would.
The checklist compresses the article into the working order. State the server's outbound relationships in a short list. Write the policy: loopback, established, the application destinations by address and port, DNS and NTP by internal address, the update exception with its calendar. Add the logging rule before the drop. Stage in log-only form for a week, read every drop, promote to enforcement with a tested rollback, persist through the distribution's mechanism, and keep the drop log in the weekly reading rotation. The server that emerges is the same server to its users, and to the next attacker it is a machine that cannot call home, cannot fetch its second stage and cannot exfiltrate a single byte, which is the entire outbound story told in three things it cannot do.
A worked policy for a web and database host
The complete policy for a concrete example reads better than fragments, so a two-role server, web application on the front, database queries to the back, carries the following OUTPUT chain as its saved configuration, readable top to bottom as the server's biography:
Default stance # iptables -P OUTPUT DROP
Loopback and established # iptables -A OUTPUT -o lo -j ACCEPT # iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
Database tier # iptables -A OUTPUT -p tcp -d 10.0.2.31 --dport 5432 -j ACCEPT
Monitoring and logs # iptables -A OUTPUT -p tcp -d 10.0.9.5 --dport 443 -j ACCEPT
Infrastructure services # iptables -A OUTPUT -p udp -d 10.0.0.2 --dport 53 -j ACCEPT # iptables -A OUTPUT -p tcp -d 10.0.0.2 --dport 53 -j ACCEPT # iptables -A OUTPUT -p udp -d 10.0.0.3 --dport 123 -j ACCEPT
Package mirror # iptables -A OUTPUT -p tcp -d 10.0.20.10 --dport 443 -j ACCEPT
The observer # iptables -A OUTPUT -j LOG --log-prefix "OUTDROP: "
Twelve lines, one server's entire relationship with the world outside its own rack, and the property that makes the format worth keeping in the runbook: each line names its purpose in the comment above it, the whole policy fits on one screen, and any future administrator reads in thirty seconds what this machine is allowed to do, which is the outbound half of security that inbound rules have enjoyed all along.
The policy also composes with the earlier articles of this series naturally: the iptables OUTPUT list is what the fail2ban actions plug into on the inbound side, the auditd rules watch the files that define this very policy, and AIDE would report any change to the saved configuration, one security stack where each layer watches the next.