Firewall and Fail2ban: locking down NAS network access
- Published on
- ·6 min read
Why bother with a firewall on a home NAS?
A NAS is a server that's always on. No shame in securing it properly. It hosts sensitive stuff: file shares, media server, maybe home automation. Without a firewall, all ports are wide open and anything on your network can try to connect. Add port forwarding for remote access and suddenly you've got a problem.
I set up two complementary layers on my TerraMaster F4-424: UFW for port filtering, and Fail2ban for detecting and banning intrusion attempts.
UFW: a human-readable firewall
UFW (Uncomplicated Firewall) is a frontend for iptables/nftables. Main advantage: you can actually read the rules. Unlike iptables syntax which makes you want to throw your laptop out the window.
Default policy
First step: lock everything down:
ufw default deny incoming
ufw default allow outgoing
Deny incoming means all inbound traffic is blocked unless explicitly allowed. Allow outgoing lets the NAS talk to the outside world (updates, DNS, NTP...).
SSH: rate limiting
Instead of plain allow, I use limit which caps connections to 3 per minute from a single IP:
ufw limit ssh
First line of defense against brute-force, before Fail2ban even kicks in.
LAN-only services
File sharing doesn't need to be internet-accessible. Restrict them to the local subnet:
# Samba (Windows/macOS shares)
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 shares)
ufw allow from 192.168.1.0/24 to any port 2049
Simple. Takes 30 seconds. Saves a lot of headaches.
Open Docker services
Some containers need to be accessible remotely (VPN, remote access):
ufw allow 8096/tcp # Jellyfin
ufw allow 6881/tcp # qBittorrent
ufw allow 22000/tcp # Syncthing
ufw allow 8123/tcp # Home Assistant
Verification
Run ufw status verbose and see everything clearly:
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
Clean. Readable.
The Docker + UFW trap
Classic gotcha. Everyone falls for it eventually. Docker completely bypasses UFW. When you publish a port (-p 8080:80), Docker injects rules directly into iptables before UFW evaluates anything. Result? Your containers are accessible from anywhere even if UFW blocks the port.
Two solutions:
Option 1: Disable Docker's iptables management in /etc/docker/daemon.json:
{
"iptables": false
}
Caveat: Docker no longer handles container NAT. You need to configure networking manually.
Option 2 (my preference): Add rules to the DOCKER-USER chain, which evaluates before Docker's rules:
# /etc/ufw/after.rules (append to the end)
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -s 192.168.1.0/24 -j ACCEPT
-A DOCKER-USER -j DROP
COMMIT
Docker keeps working, containers are limited to LAN access only. Took me 2 hours to figure out why Jellyfin was accessible from the internet. This is important.
Fail2ban: banning intruders
UFW filters ports. It doesn't detect brute-force attacks on permitted services. That's where Fail2ban comes in: monitor logs and temporarily ban suspicious IPs.
SSH jail configuration
Main config file: /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 duration, use progressive bans. Logic: someone who misses their password once gets banned 10 minutes. An attacker keeps trying, gets banned for longer and longer:
# Progressive ban
bantime.increment = true
bantime.multipliers = 1 5 30 60 180 360 720
bantime = 600
With base bantime of 600 seconds (10 min) and these multipliers:
| Offense | Multiplier | Ban duration |
|---|---|---|
| 1st ban | x1 | 10 minutes |
| 2nd ban | x5 | 50 minutes |
| 3rd ban | x30 | 5 hours |
| 4th ban | x60 | ~10 hours |
| 5th ban | x180 | ~30 hours |
| 6th ban | x360 | ~2.5 days |
| 7th ban | x720 | ~5 weeks |
A persistent bot gets banned for 5 weeks. In practice, most quit way earlier.
Ban action
Default action uses iptables to block the IP. Add email notifications too:
action = %(action_mwl)s
action_mwl means: ban + email with logs and attacker whois. Useful for monitoring.
Verification
Check SSH 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
47 failed attempts, 12 total bans, 2 IPs currently banned.
Ansible automation
Like everything else, firewall and Fail2ban are deployed via Ansible. The role is idempotent: run it as many times as you want, no side effects.
# 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]
Rules live in role variables, making the configuration declarative and git-versioned.
Bottom line
A firewall on a home NAS isn't paranoia - it's basic housekeeping. UFW makes port filtering approachable, Fail2ban adds active detection, and Ansible ensures this survives reinstalls. The Docker/UFW trap is the critical point: if you're running Docker without handling this, your containers are probably exposed and you don't even know it. Happens to almost everyone setting this up.
Debian NAS from scratch series — This article is part of a complete series on building a Debian NAS.
Previous: Docker on NAS: network architecture and best practices | Next: Hardening your NAS Linux kernel to CIS Level 2 standards