Advanced Firewalls: iptables, UFW, and nftables

Advanced Firewalls: iptables, UFW, and nftables

Bitnesia Aug 29, 2026 1 ID

Chapter 30 closes with an explicit promise: our second layer of defense in depth, namely the firewall, becomes the full focus of this chapter. Throughout this series, we have actually repeatedly run sudo ufw allow without ever discussing where that command comes from or where it actually goes inside the kernel. Custom SSH ports in Chapter 3, DNS and DHCP in Chapter 9 and Chapter 10, web servers and databases in Parts IV and V, up to Samba and NFS in Part VI, all have stacked waiting ufw allow rules, but UFW itself has never really been officially activated. This chapter completes that activation, while answering two pending promises: the reason why Docker containers in Chapter 27 could bypass UFW even though their ports were not allowed yet, and a deep discussion regarding application profiles that we postponed in Chapter 24.

We will start from the evolution of Linux firewall architecture, from classic iptables to nftables as its successor, to understand UFW's position between the two. After that, we get into basic iptables concepts that are still relevant to read in many old production scripts, officially activate UFW complete with rate limiting and custom application profiles, try native nftables syntax along with its migration method, summarize all firewall rules for services we installed throughout this series, and close with firewall logging and monitoring as preparation for detecting intrusion attempts before Chapter 32 discusses Fail2ban.

31.1 Evolution of Linux Firewall Architecture: from iptables to nftables

Before touching any command, it is good to understand the big picture first: the reason why Ubuntu Server 26.04 still recognizes iptables as well as nftables, and the way both relate to UFW which we have secretly used for a long time.

31.1.1 Netfilter, iptables, and Their Limitations

Netfilter is a framework inside the Linux kernel that provides hook points along the network packet path, starting from when a new packet enters a network interface until it leaves again, where filtering modules can inspect and decide the fate of each packet. iptables (along with its siblings ip6tables for IPv6, arptables, and ebtables) is a classic userspace utility that talks to Netfilter via an old framework called x_tables. For over two decades, iptables became the de facto standard of Linux firewalls, but its architecture carries real limitations: a separate utility for each protocol family, rules are evaluated one by one linearly from top to bottom without a built-in fast search data structure, and there is no truly atomic ruleset reload mechanism, making swapping many rules at once risky due to leaving brief windows during execution.

31.1.2 nftables as the Successor and iptables-nft as a Bridge

nftables entered the mainline Linux kernel in version 3.13 (2014) as the official successor to Netfilter, designed to address all the above limitations: one single utility nft for all protocol families via table family concepts (ip, ip6, inet for dual-stack simultaneously, arp, bridge, netdev), native support for sets and maps with O(1) lookup instead of linear, atomic ruleset reloads, and a declarative syntax that is much easier to read compared to series of short flags ala iptables.

Its transition in Ubuntu ran gradually. Since Ubuntu 16.04 LTS, the iptables package included a compatibility wrapper called iptables-nft that translates old iptables syntax into nftables rules behind the scenes. Since Ubuntu 20.10, this wrapper became the default backend: running iptables commands on Ubuntu Server 26.04 actually invokes iptables-nft, managed via the update-alternatives system, while the old backend is still available as iptables-legacy for specific kernel module compatibility cases. Both backends keep completely separate rulesets, so mixing both on the same server will only cause self-confusion during troubleshooting.

This fact also explains UFW's position more precisely. UFW calls iptables/ip6tables binaries to apply every rule we write, and because those binaries are now iptables-nft, UFW rules are ultimately saved as nftables data structures in the kernel. However, UFW itself, up to the writing of this series, does not have a native backend that talks directly to nft syntax. So if anyone says "UFW generates nftables rules", it does not mean UFW writes nft syntax directly, but rather UFW continues to write iptables-style rules which are then automatically translated by iptables-nft into nftables entries in the kernel.

Aspectiptables (x_tables / legacy)nftables
Utility per protocol familySeparate: iptables, ip6tables, arptables, ebtablesOne utility nft for all families
Rule evaluationLinear, read one by one from top to bottomSupports set/map with O(1) lookup
Ruleset reloadNot natively atomicAtomic via nft -f
Syntax styleProcedural, many short flagsDeclarative, closer to block structures

Practical Steps

  1. Check the currently active iptables backend on our server.
    iptables -V
  2. Inspect the alternatives symlink determining that backend choice.
    sudo update-alternatives --display iptables

Verification and Troubleshooting

  • The output of iptables -V on Ubuntu Server 26.04 contains the (nf_tables) indication, signaling that its active backend is indeed iptables-nft, not iptables-legacy.
  • If the output instead displays (legacy), the server was likely directed manually to the old backend via update-alternatives --config iptables, either by a third-party software installation requiring it or a previous configuration error. Revert it to the option pointing to iptables-nft unless there is a strong reason to maintain the legacy backend.

31.2 iptables: Basic Concepts and Real Case Behind Docker

Even though Ubuntu 26.04 uses nftables behind the scenes, mastering iptables syntax remains worthwhile for three concrete reasons: it is still widely used in legacy automation scripts like Ansible playbooks or old vendor images, it is the language used by UFW as we just discussed, and it is used directly by Docker to manipulate the firewall without UFW's knowledge, a real case that forms the focus of this section.

31.2.1 Anatomy of Tables, Chains, and Rules

iptables organizes rules into tables, each table contains a set of chains, and each chain contains a list of rules. The default table named filter contains three built-in chains: INPUT for packets destined for this host itself, OUTPUT for packets originating from this host, and FORWARD for packets merely passing through or routed through this host, a chain directly relevant to the Docker case in Section 31.2.3. Each chain has a default policy (ACCEPT, DROP, or REJECT) that applies if no rule matches, and rule lists inside are evaluated sequentially from top to bottom until the first matching rule is found (first-match-wins). A single rule consists of matching criteria, such as -p for protocol, --dport for destination port, -s for source IP, combined with targets via the -j flag such as ACCEPT, DROP, REJECT, or LOG. The filter table is not the only table known to iptables. The nat table, with PREROUTING and POSTROUTING chains, specifically handles address translation such as port forwarding, and this exact table is what Docker uses to publish container ports, not the filter table where UFW operates, as we will see directly in Section 31.2.3.

31.2.2 Reading and Testing Rules Directly

The safest practice to start getting acquainted with iptables is to read the running ruleset first, then try adding and deleting practice rules that break nothing.

Practical Steps

  1. View all active rules in the filter table chains along with packet counters matched by each rule.
    sudo iptables -L -n -v --line-numbers
  2. Add a practice rule, for example allowing port 8443/tcp.
    sudo iptables -A INPUT -p tcp --dport 8443 -j ACCEPT
  3. Remove that practice rule. Re-run the command from step one to see which line number this new rule resides on, because -A always appends rules at the end of the chain, then delete based on that number.
    sudo iptables -D INPUT 5

Verification and Troubleshooting

  • Rules added via -A live only in kernel memory and disappear after the next reboot, unlike UFW rules which are stored permanently in /etc/ufw/user.rules as discussed in Section 31.3.1. For raw iptables rules needing to survive reboots, the iptables-persistent package provides the netfilter-persistent service which saves rulesets to /etc/iptables/rules.v4 and rules.v6.
  • Avoid manually adding rules for the exact same port managed by UFW on the same server. Having two mechanisms managing identical ports from different sources will only confuse diagnostics when access issues occur later.

31.2.3 Case Study: DOCKER-USER Chain and Why Containers Can Bypass UFW

Section 27.2.3 gave a brief warning: if UFW is active, Docker Compose stack ports need to be allowed first. The reality is slightly more complex, and that is precisely where understanding iptables comes in handy. When a container publishes a port via the -p option in docker run or via the ports: block in docker-compose.yml, Docker Engine creates NAT rules in the PREROUTING chain of the nat table directing incoming packets to the internal container IP, then those packets pass through the FORWARD chain, not the INPUT chain where all UFW rules reside. Docker also creates two custom chains named DOCKER and DOCKER-USER, inserting jumps to both at the top of the FORWARD chain, so packets bound for containers are evaluated by Docker's chains long before UFW has a chance to intervene. Consequently, containers publishing ports remain accessible from outside even if UFW has never allowed those ports, because technically that traffic never passes through the path guarded by UFW.

Docker officially provides the DOCKER-USER chain specifically for this scenario. Docker intentionally never populates this chain automatically, making it safe for Sysadmins to insert custom access control rules that persist whenever the Docker daemon restarts.

Practical Steps

  1. Ensure this chain exists, meaning Docker Engine has previously run on this server as in Chapter 26 and Chapter 27.
    sudo iptables -L DOCKER-USER -n -v
  2. Restrict access to all containers publishing ports exclusively to trusted subnets, such as the office subnet 192.168.1.0/24 consistently used since Chapter 11. The sequence of the following three commands is important because -I inserts rules at the top, so the command executed last ends up at the very top. The ESTABLISHED,RELATED rule is intentionally executed last to end up on the first line, because the FORWARD chain containing DOCKER-USER is also traversed by return traffic from connections initiated by the container itself to the internet, such as when running apt-get update inside it. Without this rule, lower blocks would cut off container outbound access as well, not just the inbound access we want to limit.
    sudo iptables -I DOCKER-USER -j DROP
    sudo iptables -I DOCKER-USER -s 192.168.1.0/24 -j ACCEPT
    sudo iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
  3. Verify the order matches expectations.
    sudo iptables -L DOCKER-USER -n -v --line-numbers

Verification and Troubleshooting

  • The ACCEPT rule for ESTABLISHED,RELATED traffic must be on line 1, the ACCEPT rule from trusted subnets on line 2, and the DROP rule on line 3. If the order is broken, both inbound access from trusted subnets and container outbound internet access can be dropped, as the first matching rule wins.
  • Rules in DOCKER-USER persist across Docker daemon restarts by design, but do not automatically persist across host reboots unless made persistent via iptables-persistent as discussed in Section 31.2.2, or rewritten via boot automation.
  • Honest note on limitations: this solution operates at the raw iptables/nftables level, entirely outside UFW's reach. Up to the writing of this series, UFW does not provide a native way to manage the DOCKER-USER chain, meaning combining Docker and UFW always requires extra manual steps like above, rather than relying solely on ufw allow on published container ports.

31.3 UFW: Behind-the-Scenes Mechanics and Advanced Controls

After understanding raw iptables, we return to UFW, a tool secretly used since Chapter 3, but this time covering its inner workings and advanced features never touched before.

31.3.1 UFW as a Rule Generator and Official Activation

UFW (Uncomplicated Firewall) is a Python-based frontend that simplifies iptables/nftables syntax into concise commands, developed specifically for Ubuntu so daily firewall configuration does not require memorizing individual iptables flags. Every time we run ufw allow or ufw deny, UFW writes a new line to rule files in /etc/ufw/user.rules (and user6.rules for IPv6), regardless of whether UFW status itself is active or not. That is why all rules added since Chapter 3 remain saved and waiting, even though we never actually executed ufw enable. Once activated, UFW loads all those rule files via iptables-restore, forming its own chain structures such as ufw-before-input, ufw-user-input, and ufw-after-input, inserted into standard INPUT/OUTPUT/FORWARD chains.

Practical Steps

  1. Before activating anything, view all rules accumulated since Chapter 3. The standard ufw status command displays nothing except the word inactive when UFW is off, so use show added which reads rules directly from configuration files.
    sudo ufw show added
  2. Ensure the rule for the custom SSH port from Section 3.2.4 exists in that list before proceeding further. Losing SSH access due to a misconfigured firewall is one of the most common incidents experienced by Sysadmins, yet easiest to prevent.
  3. Officially activate UFW.
    sudo ufw enable
    This command outputs the confirmation prompt Command may disrupt existing ssh connections. Proceed with operation (y|n)?. Type y only after confirming SSH rules are present in the previous step.
  4. Verify full status along with default policies.
    sudo ufw status verbose

Verification and Troubleshooting

  • A healthy ufw status verbose output shows Status: active, followed by Default: deny (incoming), allow (outgoing), disabled (routed), then the list of accumulated rules from previous chapters.
  • Prove that UFW rules actually materialize as real iptables kernel rules, not just configuration on paper.
    sudo iptables -L ufw-user-input -n -v
  • If the SSH session drops right after enabling UFW, do not panic. Active connected sessions are usually not forcefully disconnected by rule changes immediately, but new sessions can fail to connect if SSH rules point to wrong ports or syntax. Console access from Section 3.4 remains the emergency path for this scenario.

31.3.2 Rate Limiting and Numbered Rule Management

Standard allow rules do not limit how frequently a single source IP attempts connections. Attackers running automated SSH brute-force attacks can try hundreds of username and password combinations in minutes without blockage, until Chapter 32 introduces Fail2ban as a log-detection solution. As a lightweight first line of defense, UFW provides built-in rate limiting mechanisms.

Practical Steps

  1. Apply rate limiting to our custom SSH port. Note that the ready-to-use alias ufw limit ssh or OpenSSH profile points to standard port 22, not our customized port 2222 from Section 3.2.4, so we must specify the port explicitly.
    sudo ufw limit 2222/tcp
  2. View all rules alongside line numbers.
    sudo ufw status numbered
  3. Delete a rule based on its line number, adjusting the number to match output from the step above.
    sudo ufw delete 3
  4. Insert a new rule at a specific position, for instance blocking documentation subnet 203.0.113.0/24 at position 1 before other allow rules are evaluated.
    sudo ufw insert 1 deny from 203.0.113.0/24 to any port 2222 proto tcp comment 'example suspicious subnet block'

Verification and Troubleshooting

  • UFW limit rules work by denying new connections from a source IP once that source attempts 6 or more connections within 30 seconds, a threshold not customizable within UFW itself. This limit is lenient enough for legitimate users occasionally mistyping passwords, but strict enough to slow down automated brute-force attacks.
  • Rule numbers shift whenever rules are deleted or inserted. Always re-run ufw status numbered before running delete or insert commands, rather than relying on stale output from previous checks.

31.3.3 Application Profile: Reading Built-in Profiles and Writing Custom Ones

Section 24.3 postponed in-depth UFW application profile discussions when opening Samba ports manually. Now is the time to finalize it, as well as build a custom profile for the Docker Compose stack from Chapter 27.

Practical Steps

  1. List all profiles registered on this server from various service installations throughout the series.
    sudo ufw app list
  2. Inspect details of the Samba profile avoided in Section 24.3.
    sudo ufw app info Samba
    Output shows Ports: 137,138/udp|139,445/tcp. This is the technical reason Section 24.3 chose manual ufw allow 445/tcp over ufw allow Samba: the default profile opens ports 137, 138, and 139 for NetBIOS, even though NetBIOS on our server was intentionally disabled in that section. Using default profiles there would reopen ports unused by active processes.
  3. Create a new profile for the Docker Compose stack from Section 27.2, using port 8090 for static sites and 8091 for Adminer.
    sudo nano /etc/ufw/applications.d/notes-compose
    Populate the file with the following format.
    [NotesCompose]
    title=Notes Compose Stack
    description=Static site and Adminer from Chapter 27 Docker Compose stack
    ports=8090,8091/tcp
  4. Register the new profile with UFW and apply it.
    sudo ufw app update NotesCompose
    sudo ufw allow NotesCompose

Verification and Troubleshooting

  • Confirm the new profile reads correctly.
    sudo ufw app info NotesCompose
  • Important note: the allow rule above controls UFW's INPUT chain, whereas per Section 31.2.3, containers publishing ports via ports: in Compose bypass INPUT and use FORWARD and DOCKER-USER chains instead. For the Chapter 27 Compose stack specifically, those containers were accessible from outside from the start, regardless of allow NotesCompose rules. Profiles remain useful as port documentation and take effect if the same port is later used by non-container processes listening directly on the host. For access control applying directly to port-published Compose containers, refer back to the DOCKER-USER approach in Section 31.2.3.

31.4 Native nftables: Modern Syntax and Migration from iptables

UFW suffices for most daily needs in this series, but Sysadmins sometimes require granular control missing in UFW, such as named sets containing dynamic IPs or condition-based rules unsupported by UFW syntax. Native nftables, the official successor discussed in Section 31.1.2, fills that role.

Because UFW and native nftables rules share identical Netfilter hooks in the kernel, running both simultaneously as main firewalls can cause confusion or conflicts. Treat the following practice section as a standalone lab: temporarily disable UFW first, then reactivate it after experiments conclude, maintaining a single source of firewall truth.

31.4.1 Installation and Ruleset Anatomy: Tables, Chains, Rules

Practical Steps

  1. Temporarily disable UFW to avoid conflicts with upcoming experiments.
    sudo ufw disable
  2. Install the nftables package if not present. Kernel and compatibility layers are likely active via iptables-nft, but the nft utility and systemd unit require explicit installation.
    sudo apt install nftables
  3. Create an input table and chain with default drop policy. The inet family is chosen to cover IPv4 and IPv6 simultaneously in one rule, unlike iptables requiring separate ip6tables. The priority 0 value sets execution order relative to other hooks attached to the input point, such as Docker or other nftables.service instances; smaller values execute earlier, and 0 is standard for main filtering chains.
    sudo nft add table inet filter
    sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
  4. Add base rules: loopback, established or related connections, and SSH on our custom port.
    sudo nft add rule inet filter input iif lo accept
    sudo nft add rule inet filter input ct state established,related accept
    sudo nft add rule inet filter input tcp dport 2222 accept

Verification and Troubleshooting

  • Display the complete ruleset just created.
    sudo nft list ruleset
  • Rule ordering inside base chains evaluates top-down just like iptables. Loopback and established/related rules are placed at the top so high-frequency traffic matches early without traversing remaining rules, a small optimization noticeable on busy servers.

31.4.2 Persistent Ruleset in /etc/nftables.conf

Rules added via nft add exist only in kernel memory and vanish upon reboot, matching raw iptables in Section 31.2.2. nftables provides a cleaner method: defining declarative rulesets inside /etc/nftables.conf, loaded automatically by nftables.service systemd unit during boot.

Practical Steps

  1. Rewrite the rules above into a declarative file.
    sudo nano /etc/nftables.conf
    Populate the file as follows.
    #!/usr/sbin/nft -f
    
    flush ruleset
    
    table inet filter {
        chain input {
            type filter hook input priority 0; policy drop;
    
            iif lo accept
            ct state established,related accept
            tcp dport 2222 accept
            ct state invalid drop
        }
    }
    The flush ruleset directive at the top ensures existing rulesets clear before loading, preventing duplicate rules in the kernel. A new line is included here, ct state invalid drop, absent in initial testing: this drops packets marked invalid by kernel connection tracking, such as out-of-order TCP fragments. Such packets almost always stem from port scans or corrupted packets, making early dropping safe before evaluation against lower rules.
  2. Load the file and enable it as a service starting automatically at boot.
    sudo nft -f /etc/nftables.conf
    sudo systemctl enable --now nftables.service

Verification and Troubleshooting

  • Ensure the service runs and kernel rulesets match file contents.
    sudo systemctl status nftables.service
    sudo nft list ruleset
  • On syntax errors, nft -f refuses file loading and displays problematic line numbers, proving far more informative than legacy iptables errors lacking context.
  • After completing experiments, revert the server state: disable nftables.service, flush rulesets, and re-enable UFW as the sole active firewall.
    sudo systemctl disable --now nftables.service
    sudo nft flush ruleset
    sudo ufw enable

31.4.3 Migrating Legacy Rules with iptables-translate

A common operational scenario: Sysadmins inherit servers containing raw legacy iptables rules and consider full migration to nftables. Manually rewriting rules is error-prone and time-consuming. The nftables package includes iptables-translate, converting single iptables rules into equivalent nft syntax without manual flag mapping lookup.

Practical Steps

  1. Translate a sample rule to inspect conversion patterns.
    iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
    This command outputs the equivalent nft line, roughly formatted as nft add rule ip filter INPUT tcp dport 22 ct state new counter accept, ready for execution or configuration file inclusion.
  2. For full ruleset migrations saved via iptables-save, use its restore translation variant to process entire files at once.
    sudo iptables-save > /root/rules-lama.v4
    sudo iptables-restore-translate -f /root/rules-lama.v4

Verification and Troubleshooting

  • Translated output must be reviewed manually before production deployment, particularly complex rules utilizing iptables modules lacking direct nft equivalents. This tool accelerates migration rather than replacing manual review entirely.

31.5 Firewall Rules for Common Services: Series Consolidation

Having understood underlying mechanisms, this section consolidates all firewall rules applied since Chapter 3 into a single concise reference usable without reviewing individual chapters.

31.5.1 Summary of Ports Opened Since Chapter 3

ChapterServicePort / ProtocolUFW Rule
3SSH (custom port)2222/tcpufw allow 2222/tcp
9DNS (BIND9)53 (tcp & udp)ufw allow 53
10DHCP67/udpufw allow 67/udp
11NTP (chrony)123/udp, subnet restrictedufw allow from 192.168.1.0/24 to any port 123 proto udp
12HTTP (Nginx)80/tcpufw allow 80/tcp
13HTTP & HTTPS (Apache)80, 443/tcp via profileufw allow 'Apache Full'
17HTTPS (Let's Encrypt)443/tcpufw allow 443/tcp
18PostgreSQL5432/tcp, subnet restrictedufw allow from 192.168.1.0/24 to any port 5432 proto tcp
19MySQL3306/tcp, subnet restrictedufw allow from 192.168.1.0/24 to any port 3306 proto tcp
24Samba445/tcp, manual non-profileufw allow 445/tcp
25NFS2049/tcp, subnet restrictedufw allow from 192.168.1.0/24 to any port 2049 proto tcp

A notable pattern emerges from the table above. Services requiring wide public access, such as HTTP, HTTPS, and DNS, open without source restrictions. Internal services like databases and file sharing remain restricted to the trusted 192.168.1.0/24 subnet rather than Anywhere. This reflects consistent application of the least privilege principle introduced in Chapter 30 and expanded in Chapter 34.

31.5.2 Default Deny Incoming Principle and Safe Rule Ordering

UFW's default policy verified in Section 31.3.1, deny incoming, means the server automatically rejects all incoming traffic not explicitly permitted. This is a whitelist philosophy: closing all access first, opening ports individually per requirement, proving far safer than blacklist models opening all traffic and blocking offending sources post-incident. UFW handles this ordering automatically without requiring manual rule ordering, internally placing specific allow rules before broad default deny rules. In raw iptables or nftables, rule order management rests entirely on the admin, as demonstrated in Section 31.2.3 where ACCEPT rules were explicitly inserted above DROP rules to function correctly.

Verification and Troubleshooting

  • Confirm active default policies.
    sudo ufw status verbose
  • Field note: never change default outgoing policies to deny without careful preparation. Processes like apt update, NTP synchronization, or outbound notifications will get blocked, often presenting subtle symptoms like failed package updates or gradual clock drift noticed long after changes.

31.6 Firewall Logging and Monitoring

Correct rules without visibility leave security risks. Firewall logs provide raw data showing active server connection attempts, including failed attacker probes blocked by rules established throughout this chapter.

31.6.1 UFW Logging Levels and Reading Log Entries

UFW supports five logging levels: off, low, medium, high, and full, with low active as default upon UFW enablement. The low level logs blocked packets matching no rules along with packets matching rules marked for logging, providing sufficient daily monitoring data without heavy disk usage.

Practical Steps

  1. Ensure logging is set to low. While default since Section 31.3.1, explicit configuration aids documentation.
    sudo ufw logging low
  2. Monitor logs in real-time.
    sudo tail -f /var/log/ufw.log

Verification and Troubleshooting

  • Locate rejected traffic entries using the [UFW BLOCK] keyword.
    sudo grep 'UFW BLOCK' /var/log/ufw.log | tail -20
  • If /var/log/ufw.log is missing despite normal rsyslog operation, inspect logs via journald.
    sudo journalctl -k | grep UFW
  • Levels medium and higher generate significant log volume by recording allowed traffic alongside blocks. Use higher levels for temporary troubleshooting sessions rather than permanent configurations to avoid quick disk saturation on busy systems.

31.6.2 Native Logging in nftables with Log Statements

When selecting native nftables per Section 31.4, logging does not run globally by default like UFW. Rules requiring logging must explicitly include log statements.

Practical Steps

  1. Add logging to the SSH rule in the nftables test ruleset from Section 31.4.1.
    sudo nft add rule inet filter input tcp dport 2222 log prefix "SSH-ATTEMPT " accept

Verification and Troubleshooting

  • Monitor newly appended log entries via journald.
    sudo journalctl -k -f
    Look for lines prefixed with SSH-ATTEMPT upon connection attempts to that port.
  • The log statement sits before final actions like accept or drop, passing packets to that action rather than replacing it. Statements attach to either accept or drop rules depending on monitored traffic.
  • For advanced analysis delegating logs to userspace consumers like ulogd, append the group parameter to log statements. Detailed ulogd setup falls outside this series' scope.

31.6.3 Case Study: Identifying Port Scanning Patterns from Logs

In production, repeated UFW BLOCK entries sharing identical source IPs across sequential ports within seconds strongly indicate automated scanners like nmap rather than misconfigured user addresses.

Practical Steps

  1. Identify top blocked source IPs during the current hour.
    sudo grep 'UFW BLOCK' /var/log/ufw.log | grep "$(date '+%b %e %H')" | grep -oP 'SRC=\K[0-9.]+' | sort | uniq -c | sort -rn | head -10

Verification and Troubleshooting

  • A single source IP attempting hundreds of connections across varied ports in brief windows warrants permanent manual block rules as an emergency measure.
    sudo ufw insert 1 deny from alamat_ip_scanner comment 'manually detected scanner'
  • Manual grep approaches remain reactive and unscalable for servers receiving thousands of daily connection attempts. Chapter 32 automates detection and blocking workflows using Fail2ban, eliminating manual log grepping.

At this point we have completed our second layer of defense in depth from Section 30.1.2: understanding the evolution from iptables to nftables, reading raw iptables rules directly to comprehend why Docker bypasses UFW, officially enabling UFW complete with rate limiting and custom application profiles, experimenting with native nftables syntax and migration methods, summarizing service ports opened since Chapter 3, and applying firewall logging and monitoring for early detection. A clean firewall still keeps one blind spot: it rejects only explicitly prohibited connections, but cannot automatically identify repeated attack patterns or block suspicious IPs without manual intervention like performed in Section 31.6.3. Chapter 32 addresses that blind spot using Fail2ban, the third layer in our defense in depth map, automating log pattern detection and real-time IP blocking without requiring daily manual log reviews.