security

How to Bootstrap a Secure Linux Setup Using iptables and ufw

FastSox Team2026-03-2714 min read

A fresh Linux server is wide open. Every port a service binds is reachable from the internet the moment the machine comes online. For an attacker running automated scanners, an unprotected machine is visible in seconds and exploitable in minutes. The firewall is the first control you should put in place — before you install anything else, before you configure your application, before you generate TLS certificates.

This guide walks you through getting a fresh Ubuntu or Debian machine to a defensible firewall baseline. It covers two paths: ufw for the fast, human-friendly route, and iptables for the precise, rule-by-rule route. It also covers common attack mitigations, the modern nftables successor, WireGuard port rules, and how to verify that your ruleset is actually doing what you think it is.


1. Why Firewall-First Matters

Default-Open vs Default-Deny

Most Linux distributions install with a default-open posture: no firewall rules, all inbound connections accepted. This is pragmatic for a desktop but dangerous for any internet-facing server.

A default-deny posture inverts the logic: everything is blocked unless you explicitly allow it. The firewall starts by rejecting all inbound traffic, and you punch holes only for services you intend to run. The security properties are fundamentally different:

| Posture | New service installed | Forgotten port | Attack surface | |---------|----------------------|----------------|----------------| | Default-open | Immediately exposed | Permanently exposed | Grows with every install | | Default-deny | Blocked until rule added | Stays blocked | Controlled and auditable |

Default-deny means that a misconfigured service, a dependency that opens a debug port, or a package that installs a daemon automatically cannot be reached from outside until you make a deliberate decision to allow it.

The firewall is not a substitute for patching, access control, or application-level security — but it is the most leverage you get per minute of effort. A single ufw default deny incoming command converts your attack surface from "everything" to "only what you explicitly enumerate."


2. Understanding iptables Architecture

Before writing rules, it helps to understand what iptables is actually doing.

Tables

iptables organises rules into tables based on their purpose:

  • filter — the default table. Controls whether packets are accepted, dropped, or rejected. This is where firewall rules live.
  • nat — handles Network Address Translation. Used for masquerading, port forwarding, and routing traffic through a WireGuard or VPN interface.
  • mangle — modifies packet headers (TTL, TOS, mark bits). Rarely needed for basic firewall hardening.
  • raw — processes packets before connection tracking. Used to exempt specific traffic from conntrack.

Unless you specify -t <table>, iptables commands operate on the filter table.

Chains

Within each table, rules are organised into chains. The three built-in chains in the filter table are:

  • INPUT — packets destined for the local machine (inbound connections, replies to your own requests).
  • OUTPUT — packets originating from the local machine (outbound connections).
  • FORWARD — packets routed through the machine (relevant if the server acts as a router, VPN gateway, or NAT device).

Each chain has a default policy: ACCEPT or DROP. When a packet reaches the end of a chain without matching any rule, the default policy applies. The goal of a default-deny setup is to set INPUT and FORWARD to DROP, then add explicit rules for the traffic you want.


3. UFW — The Fast Path for Most Users

ufw (Uncomplicated Firewall) is a front-end for iptables that ships by default on Ubuntu. It translates human-readable commands into iptables rules. For the vast majority of servers — web servers, API backends, SSH jump hosts — ufw provides everything you need with far less syntax overhead.

Install ufw

On Ubuntu it is pre-installed. On Debian:

sudo apt update && sudo apt install ufw -y

Apply a Default-Deny Baseline

# Block all inbound by default
sudo ufw default deny incoming

# Allow all outbound (your server can initiate connections freely)
sudo ufw default allow outgoing

Allow the Services You Run

# SSH — always allow this first, before enabling ufw
sudo ufw allow ssh           # resolves to port 22/tcp via /etc/services
# Or explicitly:
sudo ufw allow 22/tcp

# Web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Critical: allow SSH before enabling ufw. If you enable the firewall without an SSH rule, your current session will be the last one.

Enable and Verify

sudo ufw enable
sudo ufw status verbose

Example output:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere

4. Rate Limiting with UFW

A default-deny firewall still leaves SSH exposed to brute-force attacks. ufw has a built-in rate limiter: it uses iptables recent module to track connection frequency and drops packets from source IPs that exceed 6 connection attempts within 30 seconds.

sudo ufw limit ssh

After running this, ufw status verbose will show LIMIT IN instead of ALLOW IN for port 22. The rule blocks the most common automated SSH credential-stuffing tools with a single command.

For custom ports you can specify the port directly:

sudo ufw limit 2222/tcp

To allow SSH only from a specific IP (the strongest option if you have a static address):

sudo ufw delete allow ssh
sudo ufw allow from 203.0.113.42 to any port 22

5. iptables — The Precise Path for Advanced Users

When you need fine-grained control — complex multi-port rules, per-interface policies, logging specific traffic classes, or integration with custom chains — you work with iptables directly.

5.1 Flush Existing Rules Safely

Before building a new ruleset, clear the current state. Do not do this on a live production machine without console access, as it briefly leaves the machine open before the new rules are applied.

# Flush all rules in the filter table
sudo iptables -F

# Flush all chains in the nat table
sudo iptables -t nat -F

# Delete user-defined chains
sudo iptables -X

# Reset packet counters
sudo iptables -Z

5.2 Set Default DROP Policies

sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

At this point the machine drops all inbound traffic. Proceed immediately to allow established connections and SSH.

This rule is essential. Without it, your server can initiate outbound connections but can never receive the replies:

sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

ESTABLISHED matches reply packets for connections your server initiated. RELATED matches auxiliary connections opened by protocols like FTP's data channel.

5.4 Allow Loopback

sudo iptables -A INPUT -i lo -j ACCEPT

Many services (databases, caches, IPC) communicate over the loopback interface. Dropping loopback traffic breaks them.

5.5 Allow SSH, HTTP, HTTPS

sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

5.6 Log Dropped Packets

Before the final DROP, add a logging rule. This writes dropped-packet metadata to the kernel log (visible via dmesg or journalctl -k):

sudo iptables -A INPUT -m limit --limit 5/min -j LOG \
  --log-prefix "iptables-drop: " --log-level 4

The --limit 5/min prevents log flooding during a scan or flood attack.

5.7 Save the Ruleset

iptables rules are ephemeral — they disappear on reboot without persistence. Install iptables-persistent:

sudo apt install iptables-persistent -y
# Accept "yes" when prompted to save current IPv4 and IPv6 rules

# Save manually at any time:
sudo iptables-save > /etc/iptables/rules.v4
sudo ip6tables-save > /etc/iptables/rules.v6

Rules are loaded automatically at boot via a systemd unit installed by iptables-persistent.


6. Protecting Against Common Attacks

A default-deny policy blocks the majority of opportunistic attacks. For more targeted mitigations, add the following rules (these work with either ufw's underlying iptables or directly):

Port Scanning: Log and Drop Invalid Packets

Packets with an invalid connection-tracking state are commonly used in stealth scans (Nmap's -sX, -sF, -sN modes):

sudo iptables -A INPUT -m conntrack --ctstate INVALID -j LOG \
  --log-prefix "iptables-invalid: " --log-level 4
sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

SYN Flood Protection

SYN floods exhaust the server's half-open connection table. Use the limit module to cap the rate at which new TCP connections can arrive:

sudo iptables -A INPUT -p tcp --syn -m limit \
  --limit 25/second --limit-burst 50 -j ACCEPT
sudo iptables -A INPUT -p tcp --syn -j DROP

The --limit-burst 50 allows short bursts of legitimate connections (page loads, API calls) while dropping sustained floods.

ICMP Flood Limiting

Allow ICMP (ping) but rate-limit it to prevent ICMP flood amplification:

sudo iptables -A INPUT -p icmp --icmp-type echo-request \
  -m limit --limit 1/second --limit-burst 5 -j ACCEPT
sudo iptables -A INPUT -p icmp -j DROP

Block Private IP Spoofing on Public Interfaces

If your public interface is eth0, block packets claiming to come from RFC 1918 private addresses — a common spoofing technique:

IFACE="eth0"
sudo iptables -A INPUT -i $IFACE -s 10.0.0.0/8     -j DROP
sudo iptables -A INPUT -i $IFACE -s 172.16.0.0/12  -j DROP
sudo iptables -A INPUT -i $IFACE -s 192.168.0.0/16 -j DROP
sudo iptables -A INPUT -i $IFACE -s 127.0.0.0/8    -j DROP
sudo iptables -A INPUT -i $IFACE -s 169.254.0.0/16 -j DROP

Replace eth0 with your actual public interface name (check with ip link show).


7. nftables — The Modern Replacement

nftables is the kernel-level replacement for iptables, available in the Linux kernel since 4.x and the default on Debian 10+ and Ubuntu 20.10+. It uses a single tool (nft) and a cleaner rule syntax that avoids the separate iptables/ip6tables/arptables split.

A minimal nftables equivalent of the iptables baseline above:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Loopback
        iifname "lo" accept

        # Established and related
        ct state established,related accept

        # Invalid — drop
        ct state invalid drop

        # SSH (rate-limited)
        tcp dport 22 ct state new limit rate 6/minute accept

        # HTTP, HTTPS
        tcp dport { 80, 443 } accept

        # ICMP (rate-limited)
        icmp type echo-request limit rate 1/second accept

        # Log and drop everything else
        limit rate 5/minute log prefix "nft-drop: " level warn
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Save this to /etc/nftables.conf and enable it:

sudo nft -f /etc/nftables.conf
sudo systemctl enable nftables
sudo systemctl start nftables

nftables and iptables cannot run simultaneously on the same machine in conflicting modes. If you migrate to nftables, disable iptables-persistent.


8. WireGuard VPN Firewall Rules

If you are running a WireGuard server or using FastSox's WireGuard-based gateways, you need to open the WireGuard UDP port and allow IP forwarding for tunnel traffic.

Open the WireGuard Port

With ufw:

sudo ufw allow 51820/udp

With iptables:

sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT

Allow Forwarding for VPN Clients

If the machine acts as a WireGuard server and routes client traffic to the internet:

# Enable IP forwarding
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

# Allow forwarding from the WireGuard interface
sudo iptables -A FORWARD -i wg0 -j ACCEPT
sudo iptables -A FORWARD -o wg0 -j ACCEPT

# NAT outbound VPN traffic to the public interface
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

With ufw, forwarding requires one additional step: edit /etc/ufw/before.rules to add the POSTROUTING rule in the *nat section, and set DEFAULT_FORWARD_POLICY="ACCEPT" in /etc/default/ufw.


9. Verifying Your Firewall

Writing rules and enabling the firewall is not enough — you must verify that the ruleset behaves as intended. A misconfigured rule that looks correct in ufw status may still leave ports open due to rule ordering or conflicting rules.

Check Listening Services

Before scanning, know what is actually listening:

sudo ss -tlnp

-t TCP, -l listening only, -n numeric ports, -p show process. Any port in this output is a candidate for an open firewall rule. If a port appears here that you did not intend to expose, stop the service or bind it to 127.0.0.1 only.

Scan from Another Machine

The definitive test is an external port scan from a machine outside your network:

# From a separate machine (replace with your server's IP)
nmap -sS -p 1-65535 203.0.113.10

-sS performs a TCP SYN scan. Ports your firewall is blocking will appear as filtered; ports with running services and open rules will appear as open. You should see only your intended ports (22, 80, 443) as open and everything else as filtered.

If you do not have a second machine, Nmap can scan localhost from the server itself, but this bypasses the firewall (loopback traffic is not filtered by INPUT rules for the external interface). An external scan is the only reliable verification.

Check ufw Status in Detail

sudo ufw status verbose
sudo ufw show raw     # shows the underlying iptables rules ufw generated

Check iptables Rule Counters

sudo iptables -L INPUT -v -n

The -v flag shows packet and byte counters per rule. After a few minutes of normal traffic, your ESTABLISHED,RELATED and service rules should show non-zero counters. If a rule has zero hits, either it is never matched or there is a rule above it absorbing all relevant traffic.


10. How FastSox Complements Firewall Hardening

A firewall controls which ports are reachable — but it cannot protect the content of the traffic that passes through those open ports. An attacker on the same network segment, a malicious ISP, or a compromised upstream router can still intercept or tamper with unencrypted traffic on ports 80 or 443.

FastSox addresses the layer above the firewall: it routes your traffic through encrypted tunnels (WireGuard and HyperSox) between your machines and FastSox gateways, ensuring that even traffic on open, approved ports is encrypted end-to-end. Combined with a properly hardened firewall baseline, FastSox provides defence in depth: the firewall limits exposure at the transport layer, and FastSox encrypts the traffic that is permitted through.

FastSox gateways also allow you to bind services to the VPN interface exclusively — meaning port 22 (SSH), database ports, or internal APIs are not exposed to the public internet at all, accessible only through the encrypted tunnel. This effectively removes entire service classes from your public firewall ruleset.

This guide covers the infrastructure layer. OneDotNet Ltd, the company behind FastSox, builds tools that integrate with your hardened baseline to provide end-to-end encrypted connectivity across your infrastructure.


11. Going Further: Full Server Hardening

A firewall is the first line of defence, not the only one. Once your firewall baseline is in place, continue with the broader server hardening checklist covered in Best Practices to Secure a Linux Server:

  • Disable root SSH login (PermitRootLogin no)
  • Enforce SSH key authentication only (PasswordAuthentication no)
  • Install and configure fail2ban for application-level brute-force detection
  • Keep packages current with unattended-upgrades
  • Audit running services (systemctl list-units --type=service --state=running)
  • Configure auditd for syscall-level event logging
  • Apply kernel hardening via sysctl (ASLR, restrict dmesg, disable SysRq)

Conclusion

Getting from a default-open fresh install to a default-deny firewall baseline takes under five minutes with ufw, or fifteen minutes if you build the iptables ruleset manually. Either path gives you the same fundamental property: only the ports you explicitly name are reachable, and everything else is dropped.

The critical steps are:

  1. Set default deny incoming before enabling the firewall.
  2. Allow SSH first — always, before enabling.
  3. Use ufw limit ssh or iptables --syn -m limit to blunt brute-force attacks.
  4. Add ICMP and SYN flood mitigations if the server is directly internet-facing.
  5. Verify with an external nmap scan, not just ufw status.
  6. Save your rules so they survive a reboot.

A firewall baseline is not the end of server security — but it is the correct place to start. Everything else builds on top of a controlled, auditable set of open ports.

#linux#iptables#ufw#firewall#security#hardening#beginner

Related Articles