Firewall Configuration with ufw and iptables
AI generated
$
/etc
Linux · Firewall · Security · Server Administration
Firewall Configuration with ufw and iptables
A baseline ruleset for web servers that survives a reboot

A correctly configured firewall is the first line of defense for any server on the internet. This article shows in practice how iptables works as the kernel packet filter, how ufw sits on top as a simple frontend, which baseline rules a production web server actually needs for SSH, HTTP, and HTTPS, and how to reliably verify that these rules stay active and survive every reboot intact.

14 min read ufw · iptables · netfilter · SSH hardening Ubuntu 24.04 · Debian 12 · systemd

1. Why every server needs a firewall

Any Linux server that sits on a public IP address on the internet gets picked up by automated port scanners within minutes of its first boot. Default installations often expose more open services than are actually needed: SSH on port 22, sometimes a database port, occasionally a test service that was never shut down again. Every open port without a deliberate reason is a potential attack surface, regardless of whether the application behind it is otherwise bug free. A firewall is therefore not an optional hardening step for later, it is the first step right after the operating system installation, well before deploying the actual application.

On Linux, the kernel itself handles packet filtering through the netfilter framework, controlled by the command line tool iptables. ufw (Uncomplicated Firewall) sits on top as a frontend and translates simple commands such as ufw allow 80/tcp into the corresponding iptables rules behind the scenes. For everyday work on a web server, ufw is almost always sufficient, while direct iptables remains necessary for more complex scenarios such as port forwarding or fine grained rule chains. This article covers both layers and a battle tested baseline configuration.

2. iptables as a kernel firewall: tables, chains, rules

iptables configures the netfilter subsystem of the Linux kernel through so called tables and chains. The most important table for a firewall is filter, it contains the three default chains INPUT for incoming packets addressed to the local host, OUTPUT for outgoing packets, and FORWARD for packets routed through the host, for example on a gateway. Each chain has a default policy, usually ACCEPT or DROP, which applies whenever no rule in the chain matches. Rules are processed top to bottom, the first matching rule decides the packet's fate, and every subsequent rule is skipped for that packet.

Every rule consists of match criteria such as protocol, source and destination IP, port, and connection state, plus a target such as ACCEPT, DROP, or REJECT. The difference between DROP and REJECT matters: DROP silently discards the packet, the sender receives no response and waits for a timeout, while REJECT actively sends back a rejection. At a server's outer boundary, DROP is usually the better choice because it slows down port scans and reveals no information about whether a service exists at all.


#!/usr/bin/env bash
# Inspect the current netfilter state before making any changes
iptables -L -n -v --line-numbers

# List only the filter table's INPUT chain with packet counters
iptables -L INPUT -n -v

# Show the currently active default policies per chain
iptables -S | grep '^-P'

# Count total rules across all filter-table chains
iptables -L -n | grep -c 'ACCEPT\|DROP\|REJECT'

3. ufw as a simple frontend for iptables

ufw has shipped as part of the default install since Ubuntu 8.04 and is now available on Debian based distributions as well. It is not a standalone packet filter but merely a management layer on top of iptables, or nftables on newer systems, that translates complex rule syntax into readable commands. Installation happens via apt install ufw, and on many Ubuntu systems the package is already preinstalled but disabled by default. The default state after installation permits all connections, so ufw must first be configured and switched on before it actually protects anything.

The core syntax reads almost like plain English: ufw allow 22/tcp opens a port, ufw deny 23/tcp blocks it, and ufw status verbose shows the current state including every active rule in the order it applies. Instead of port numbers you can also use registered application profiles, for example ufw allow OpenSSH or ufw allow Nginx Full, defined under /etc/ufw/applications.d/. This level of abstraction covers the vast majority of production web servers and noticeably reduces the error rate compared with hand written iptables rules.

4. The baseline ruleset for web servers: SSH, HTTP, HTTPS

The baseline configuration for a typical web server follows a simple principle: deny incoming traffic by default, allow outgoing connections, and then deliberately open the three ports that are actually needed, 22 for SSH administration, 80 for HTTP, and 443 for HTTPS. The order of the commands is critical. The SSH rule must be in place before ufw is enabled, otherwise you lock yourself out of the running SSH session the moment the default policy for incoming traffic switches to deny. Keeping a second terminal window with an open connection as a fallback is always recommended during the initial configuration.

After setting the three rules, ufw status numbered shows a numbered list of every active rule, which lets you remove individual entries later with ufw delete <number>. This baseline covers the standard case of a pure web server. Every additional service, such as a database port for replication or a monitoring agent, has to be opened deliberately and, ideally, restricted to a specific source IP, rather than being opened for every address on the internet by default.


#!/usr/bin/env bash
set -euo pipefail

# Reset to a known clean state before building the baseline
ufw --force reset

# Deny everything incoming by default, allow everything outgoing
ufw default deny incoming
ufw default allow outgoing

# Allow SSH first, otherwise the next command locks out the current session
ufw allow OpenSSH
ufw allow 80/tcp comment 'HTTP for ACME challenge and redirects'
ufw allow 443/tcp comment 'HTTPS production traffic'

# Enable last, only after SSH access is confirmed reachable
ufw --force enable
ufw status numbered

5. Advanced ufw rules: rate limiting, whitelisting, logging

Beyond the baseline, ufw offers three mechanisms that are needed regularly in production. Rate limiting with ufw limit ssh automatically blocks a source IP for a while if more than six connection attempts arrive from it within 30 seconds, a simple but effective defense against SSH brute force attacks without needing fail2ban at all. IP whitelisting with ufw allow from 203.0.113.10 to any port 22 proto tcp restricts a sensitive port to a known source address, for example an office network or a bastion host, instead of leaving it open to the entire internet.

Logging can be tuned granularly with ufw logging low, medium, high, or full, where high and full noticeably increase system load and log volume and are usually only worthwhile for short lived troubleshooting. The central configuration file /etc/default/ufw permanently controls fundamental options such as IPv6 support and the default log level, while individual rules continue to be managed through the ufw command line. Changes to this file require restarting ufw with systemctl restart ufw to take effect.


# /etc/default/ufw - core ufw configuration
# Apply changes with: systemctl restart ufw

# Enable IPv6 filtering alongside IPv4
IPV6=yes

# Default policies, individual "ufw allow/deny" rules override these
DEFAULT_INPUT_POLICY="DROP"
DEFAULT_OUTPUT_POLICY="ACCEPT"
DEFAULT_FORWARD_POLICY="DROP"

# Logging level: off, low, medium, high, full
LOGLEVEL=low

# Kernel modules loaded for connection tracking
IPT_MODULES="nf_conntrack_ftp nf_nat_ftp"

6. When ufw is not enough: using iptables directly

ufw covers the vast majority of use cases but hits its limits once more complex requirements come into play: port forwarding through the nat table, custom chains for structured rule sets, per source IP connection limiting through the connlimit module, or coexisting with Docker, which manages its own iptables rules in a separate chain named DOCKER-USER and can under some circumstances bypass ufw rules entirely. In those cases there is no way around direct iptables calls, either manually on the command line or declaratively through a configuration management tool such as Ansible.

The advantage of Ansible over manually typed iptables commands lies in reproducibility across multiple servers and in version controlling the ruleset in Git. The ansible.builtin.iptables module maps the most important iptables options to declarative tasks, runs idempotently, and integrates cleanly into existing provisioning pipelines, without anyone having to manually verify at the end which rule actually ended up on which server.


---
# playbook: harden-firewall.yml
# Apply the same baseline iptables ruleset across all web servers
- name: Configure baseline firewall with iptables
  hosts: webservers
  become: true
  tasks:
    - name: Allow established and related connections
      ansible.builtin.iptables:
        chain: INPUT
        ctstate: ESTABLISHED,RELATED
        jump: ACCEPT

    - name: Allow loopback traffic
      ansible.builtin.iptables:
        chain: INPUT
        in_interface: lo
        jump: ACCEPT

    - name: Allow SSH from the management network only
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "22"
        source: 10.0.5.0/24
        jump: ACCEPT

    - name: Allow HTTP and HTTPS from anywhere
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "{{ item }}"
        jump: ACCEPT
      loop:
        - "80"
        - "443"

    - name: Drop everything else on INPUT
      ansible.builtin.iptables:
        chain: INPUT
        jump: DROP

7. Ensuring persistence: rules that survive the reboot

iptables rules set by hand on the command line live only in kernel memory and vanish without a trace on the next reboot, a frequently overlooked pitfall that leaves a server sitting wide open again right after a planned restart. The package iptables-persistent, installable on Debian and Ubuntu via apt install iptables-persistent, solves this by writing the current ruleset with netfilter-persistent save into the files /etc/iptables/rules.v4 and rules.v6, which are then read back in automatically at system startup.

ufw solves the same problem at a higher level: the command ufw enable does not just activate the current session, it also sets up a systemd service that starts automatically at boot and loads the rules stored under /etc/ufw/. Checking with systemctl is-enabled ufw should return enabled, otherwise the configuration will not survive the next maintenance reboot. Anyone using both tools in parallel should consistently designate one of them as the source of truth, since uncoordinated changes on both layers at once quickly produce contradictory and hard to trace rulesets.

8. Verification: are the rules actually active?

A rule that has been set is not automatically a rule that is effective, which is why verification belongs firmly in the configuration process. ufw status verbose shows ufw's own view of the active rules, but it does not necessarily reflect the actual kernel state if iptables has also been used directly in parallel. The more reliable view comes from iptables -L -n -v, which shows the chains and rules that are genuinely active in the kernel along with packet counters, regardless of which tool set them. A packet counter of 0 on a DROP rule after a few minutes of uptime is normal, a steadily climbing counter indicates traffic being actively blocked.

The external view matters at least as much as the local one: a port scan from a second server, for example with nmap -p 22,80,443,3306 <server-ip>, shows which ports are actually reachable from the outside, independent of the local configuration. Only this way can you rule out that an upstream cloud firewall, such as an AWS security group or a Hetzner Cloud firewall ruleset, is overriding or contradicting the local ufw configuration. For automated monitoring, a regular cron job that logs the firewall status in a structured way and alerts on deviations from the expected state is well worth setting up.


{
  "hostname": "web-prod-01",
  "checked_at": "2026-07-12T06:00:00Z",
  "ufw_active": true,
  "default_incoming_policy": "deny",
  "open_ports": [
    { "port": 22, "protocol": "tcp", "source": "10.0.5.0/24", "rule": "allow" },
    { "port": 80, "protocol": "tcp", "source": "any", "rule": "allow" },
    { "port": 443, "protocol": "tcp", "source": "any", "rule": "allow" }
  ],
  "unexpected_listeners": [],
  "external_scan_matches_local_rules": true
}

9. ufw versus iptables head to head

Choosing between ufw and direct iptables calls is not a question of right or wrong, it depends on how complex the requirement is. For the vast majority of everyday firewall tasks on a web server, ufw delivers shorter, less error prone commands, while for complex network scenarios iptables remains the only option offering full control over the netfilter framework.

Task Insecure / Cumbersome Recommended pattern Benefit
Setting a new rule iptables -A INPUT -p tcp --dport 80 -j ACCEPT ufw allow 80/tcp Shorter, less error prone
Keeping rules after reboot Rules are lost without an extra package ufw enable or netfilter-persistent save Survives every restart automatically
Protecting SSH from brute force No built in protection, requires external fail2ban ufw limit ssh Automatic temporary block after too many attempts
Restricting access to a known IP Long iptables syntax with several options ufw allow from <IP> to any port 22 Readable, quick to review
NAT / port forwarding ufw hits its functional limits iptables -t nat -A PREROUTING ... Full control over netfilter tables

In practice, a hybrid approach has proven itself: ufw for day to day management of simple rules, direct iptables interventions or Ansible playbooks for anything that goes beyond plain port openings, such as NAT or Docker integration. What matters is that both layers stay consistent and that nobody sets contradictory rules on one side without knowing about the other.

Mironsoft

Server hardening, firewall audits, and deployment infrastructure for Magento and Hyvä

A firewall configuration that actually holds up?

We review existing ufw and iptables configurations, close unnecessarily open ports, and set up a reproducible, persistent baseline for your server and deployment stack.

Firewall audit

Cross checking ufw status, iptables rules, and an external port scan

Baseline hardening

SSH hardening, rate limiting, and a minimal attack surface for web servers

Persistence & automation

Ansible playbooks for reproducible rules across the entire server fleet

10. Summary

Firewall configuration with ufw and iptables solves a core problem for every internet facing server: unnecessarily open ports are an avoidable attack surface. iptables handles the actual packet filtering at the kernel level through tables and chains, while ufw translates the most common use cases into readable commands such as ufw allow OpenSSH. A baseline of SSH, HTTP, and HTTPS with a default deny policy for incoming traffic fully covers the standard case of a web server, as long as the SSH rule is set before enabling ufw.

The decisive difference between a configuration that looks secure and one that actually is secure lies in verification: iptables -L -n -v for the local kernel state, an external port scan for the outside view, and a persistent systemd service or netfilter-persistent to guarantee that the rules survive the next reboot too. Combining these three steps consistently means running a firewall that is not just set once, but stays effective for good.

Firewall Configuration with ufw and iptables: The Key Takeaways

Core principle

iptables filters at the kernel level through tables and chains. ufw is a frontend that translates complex syntax into readable commands.

Web server baseline

ufw default deny incoming, then allow SSH, 80/tcp, and 443/tcp, always set the SSH rule before ufw enable.

Persistence

ufw enable sets up a systemd service. Plain iptables needs iptables-persistent with netfilter-persistent save.

Verification

iptables -L -n -v for the kernel state, an external scan with nmap for actual reachability from outside.

11. FAQ: Firewall Configuration with ufw and iptables

1What is the difference between ufw and iptables?
ufw is a frontend that translates commands into iptables rules. iptables directly controls the kernel's netfilter subsystem.
2Does ufw completely replace iptables?
No. ufw uses iptables behind the scenes. Direct iptables is still needed for NAT, custom chains, or Docker integration.
3How do I allow SSH, HTTP, and HTTPS and deny everything else?
ufw default deny incoming, then ufw allow OpenSSH, 80/tcp, 443/tcp, finally ufw enable. Always set the SSH rule first.
4How do I make sure rules survive a reboot?
ufw enable sets up a systemd service. Plain iptables needs iptables-persistent with netfilter-persistent save for durable rules.
5How do I check whether a rule is actually active?
iptables -L -n -v shows the kernel state with packet counters. An external port scan with nmap confirms actual reachability.
6What do I do if I lock myself out over SSH?
Log in via the hosting provider's server console and run ufw disable. That is why the SSH rule must always be set before enabling.
7Difference between ufw deny and ufw reject?
deny silently discards packets, reject actively sends back a rejection. For the outer boundary, deny is usually the safer choice.
8How do I protect SSH against brute force attacks?
ufw limit ssh automatically blocks a source IP after more than six attempts in 30 seconds. fail2ban works well as an addition.
9Do I need iptables directly if I already use ufw?
Only for NAT, port forwarding, or Docker integration. For normal web server operation, ufw is completely sufficient.
10How do I log blocked connections?
ufw logging medium logs to /var/log/ufw.log. For more granular analysis, a LOG target in iptables before the DROP rule works well.