Firewall and Fail2ban: locking down NAS network access

Firewall and Fail2ban: locking down NAS network access

·6 min read·Updated on January 20, 2026

Why bother with a firewall on a home NAS

A NAS is a server running 24/7 that holds sensitive data: file shares, media server, sometimes home automation. Without a firewall, every port is open by default and any device on the network can reach it. The moment a port is exposed outward for remote access, the attack surface explodes.

Two complementary layers on this TerraMaster F4-424: UFW to filter ports, Fail2ban to detect and ban intrusion attempts.

UFW: a readable firewall

UFW is a frontend for iptables/nftables whose main advantage is readable rules, against iptables' cryptic syntax.

Default policy — lock everything down first:

ufw default deny incoming
ufw default allow outgoing

All incoming traffic is blocked unless explicitly allowed, while the NAS can still reach outward (updates, DNS, NTP).

SSH — rather than a plain allow, limit restricts to 3 connections per minute from the same IP, a first line of defence against brute-force before Fail2ban even runs:

ufw limit ssh

LAN-only services — file shares don't need to be reachable from the Internet:

# Samba (Windows/macOS)
ufw allow from 192.168.1.0/24 to any port 445
ufw allow from 192.168.1.0/24 to any port 139

# NFS (Linux)
ufw allow from 192.168.1.0/24 to any port 2049

Exposed Docker services — some containers must stay reachable, including over VPN:

ufw allow 8096/tcp   # Jellyfin
ufw allow 6881/tcp   # qBittorrent
ufw allow 22000/tcp  # Syncthing
ufw allow 8123/tcp   # Home Assistant

ufw status verbose gives the full picture:

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

To                         Action      From
--                         ------      ----
22/tcp                     LIMIT       Anywhere
445                        ALLOW       192.168.1.0/24
139                        ALLOW       192.168.1.0/24
2049                       ALLOW       192.168.1.0/24
8096/tcp                   ALLOW       Anywhere
6881/tcp                   ALLOW       Anywhere
22000/tcp                  ALLOW       Anywhere
8123/tcp                   ALLOW       Anywhere

The Docker + UFW trap

Docker bypasses UFW. When Docker publishes a port (-p 8080:80), it injects its rules straight into iptables before UFW. The result: containers are reachable from anywhere even though UFW blocks the port.

Option 1 — disable iptables in Docker, via /etc/docker/daemon.json:

{
  "iptables": false
}

Careful: Docker then no longer handles container NAT, which you must configure manually.

Option 2, preferable — add rules to DOCKER-USER, evaluated before Docker's own rules:

# /etc/ufw/after.rules (at the end of the file)
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -s 192.168.1.0/24 -j ACCEPT
-A DOCKER-USER -j DROP
COMMIT

Docker stays functional, but containers are confined to the LAN.

Fail2ban: banning intruders

UFW filters ports but doesn't detect brute-force attempts. Fail2ban watches the logs and temporarily bans suspicious IPs.

The SSH jail, in /etc/fail2ban/jail.local:

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
backend  = systemd

# 3 attempts max before ban
maxretry = 3

# Detection window: 10 minutes
findtime = 600

Progressive bans

Instead of a fixed ban, a progressive strategy: one mistyped password costs ten minutes, while a persistent attacker gets banned for longer and longer.

# Progressive ban
bantime.increment    = true
bantime.multipliers  = 1 5 30 60 180 360 720
bantime              = 600
Repeat offenceMultiplierBan duration
1st banx110 minutes
2nd banx550 minutes
3rd banx305 hours
4th banx60~10 hours
5th banx180~30 hours
6th banx360~2.5 days
7th banx720~5 weeks

A persistent bot ends up banned for five weeks; in practice nearly all of them give up before that.

The default action blocks the IP through iptables. action = %(action_mwl)s adds an email with the logs and a whois of the attacker, useful for monitoring.

Checking jail status:

fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     47
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 2
   |- Total banned:     12
   `- Banned IP list:   203.0.113.42 198.51.100.7

Ansible automation

The firewall and Fail2ban are deployed through an idempotent Ansible role:

# roles/firewall/tasks/main.yml
- name: Install UFW and Fail2ban
  ansible.builtin.apt:
    name:
      - ufw
      - fail2ban
    state: present
  tags: [firewall]

- name: Set UFW default policies
  community.general.ufw:
    direction: '{{ item.direction }}'
    policy: '{{ item.policy }}'
  loop:
    - { direction: incoming, policy: deny }
    - { direction: outgoing, policy: allow }
  tags: [firewall, ufw]

- name: Configure UFW rules
  community.general.ufw:
    rule: '{{ item.rule }}'
    port: '{{ item.port }}'
    proto: "{{ item.proto | default('tcp') }}"
    from_ip: "{{ item.from_ip | default('any') }}"
  loop: '{{ firewall_rules }}'
  tags: [firewall, ufw]

- name: Deploy Fail2ban jail configuration
  ansible.builtin.template:
    src: jail.local.j2
    dest: /etc/fail2ban/jail.local
    mode: '0644'
  notify: Restart fail2ban
  tags: [firewall, fail2ban]

The rules live in the role's variables: declarative and version-controlled in Git.

Conclusion

UFW makes port filtering approachable, Fail2ban adds active detection, and Ansible ensures it all survives reinstalls. The Docker/UFW trap remains the most critical point: without addressing it, your containers are probably exposed without you knowing.

ShareLinkedInXBluesky

Related articles