UFW Firewall - Rulesets for the fogserv Fleet
Status: Active
Last Updated: 2026-08-26
Category: Networking - Phase 2: Services
Prerequisites: firewall-basics, firewalls-nftables, docker-networking
Time: 1-2 hours
Tags: ufw, firewall, rate-limiting, docker, iptables, hardening
Summary
Build a consistent ufw ruleset for fleet hosts: default deny incoming, explicit allow for SSH and proxied services, connection-rate limiting on SSH, and the critical Docker caveat โ Docker's iptables rules bypass ufw unless you handle them deliberately.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Apply a standard baseline ruleset to any fleet host
- โ Rate-limit SSH and service ports
- โ Understand and fix the Docker/ufw bypass problem
- โ Choose between ufw and raw nftables per host
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- Next Steps / Ops Actions
Context / Why This Matters
Our hosts run nftables natively (firewalls-nftables); ufw is a friendly frontend that (on Ubuntu) writes iptables-nft rules under the hood. Use ufw where you want quick, auditable rule management on general-purpose hosts; drop to hand-written nftables where you need NAT/forwarding finesse or run Docker heavily. Either way the policy is the same: default deny inbound, allow only what a service needs, from only the subnets that need it. Zone design lives in network-segmentation.
Implementation / Core Content
Baseline ruleset (run on every host)
sudo apt install ufw
# 1. Defaults first โ never enable before defaults are set
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 2. Management access BEFORE enabling
sudo ufw limit 22/tcp comment 'SSH rate-limited'
sudo ufw allow from 192.168.10.0/24 to any port 22 proto tcp comment 'SSH from mgmt VLAN'
# 3. Service ports
sudo ufw allow 80,443/tcp comment 'web/proxy'
# 4. Enable
sudo ufw enable
sudo ufw status verbose
Rule syntax essentials
# Allow a specific subnet only (prefer over 0.0.0.0/0 wherever possible)
sudo ufw allow from 192.168.20.0/24 to any port 3000 proto tcp comment 'grafana LAN'
# Deny takes precedence over allow when more specific? No โ FIRST MATCH WINS.
# Order rules from most specific to least specific.
# Delete by number (safer than by text)
sudo ufw status numbered
sudo ufw delete 3
# Rate limiting: max 6 connections per 30s per IP, then block
sudo ufw limit 22/tcp
Application profiles
Ship reusable profiles in /etc/ufw/applications.d/fogserv:
[Fogserv Web]
title=Web entry (nginx/caddy/traefik)
description=HTTP + HTTPS entry points
ports=80,443/tcp
[Fogserv NodeExporter]
title=Prometheus node metrics
description=node_exporter scraped by Prometheus only
ports=9100/tcp
sudo ufw app update Fogserv Web
sudo ufw allow "Fogserv Web"
The Docker bypass problem
Docker inserts its own rules into the iptables FORWARD/nat chains. Published container ports (-p 8080:80) are reachable even if ufw says blocked, because Docker's DOCKER chain is evaluated before ufw's INPUT rules apply to forwarded traffic.
Three acceptable fixes:
Option A โ bind to loopback, publish via proxy (preferred)
# compose.yaml
services:
grafana:
ports:
- "127.0.0.1:3000:3000" # unreachable externally; proxy terminates traffic
Then ufw only needs 80/443 open for the proxy. See nginx-configuration.
Option B โ make ufw aware of Docker's chains
Set in /etc/default/ufw โ DEFAULT_FORWARD_POLICY="DROP" stays, but add before the *filter section in /etc/ufw/before.rules... actually the cleanest variant:
# /etc/ufw/after.rules โ append:
*filter
:ufw-user-forward - [0:0]
-A ufw-user-forward -i docker0 -o docker0 -j ACCEPT
COMMIT
Option C โ ufw-docker helper
# Grants external access to one published port explicitly
sudo ufw route allow proto tcp from any to any port 8080
Whatever you pick, verify with an external probe โ do not trust ufw status alone.
IPv6
ufw handles IPv6 if enabled in /etc/default/ufw: IPV6=yes. Our public services are behind Cloudflare (cloudflare-dns), so also consider allowing inbound 80/443 only from Cloudflare IP ranges when origin-pull protection matters.
Practical Examples
Full ruleset for a typical app host
sudo ufw reset >/dev/null
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp comment 'SSH'
sudo ufw allow from 192.168.10.0/24 to any port 22 proto tcp comment 'mgmt'
sudo ufw allow 80,443/tcp comment 'proxy'
sudo ufw allow from 192.168.20.0/24 to any port 9100 proto tcp comment 'node_exporter'
sudo ufw --force enable
Verify from outside
nmap -Pn -p 22,80,443,3000,9100 <host-ip>
# Expect: 22 open (rate-limited), 80/443 open, everything else filtered.
Audit drift across the fleet with ansible
ansible fleet -m command -a "ufw status numbered" --become
(See ansible-basics.)
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| Container port reachable despite no ufw allow | Docker bypasses INPUT via FORWARD chain | Bind to 127.0.0.1 or add explicit ufw route rules |
Locked out after ufw enable |
No SSH allow before enabling | Always ufw allow/limit SSH first; keep a console session |
| Rule added but still blocked | First-match-wins ordering | Check ufw status numbered, insert with ufw insert 1 ... |
| WireGuard peers can't connect | UDP 51820 not allowed | sudo ufw allow 51820/udp |
| Forwarded traffic dropped after reload | DEFAULT_FORWARD_POLICY=DROP | Add explicit forward allows for needed bridges |
| Slow SSH / DNS timeouts inside host | egress fine but logging noisy, or IPv6 half-configured | Ensure IPV6=yes matches reality; review /var/log/ufw.log |
Next Steps / Ops Actions
- Decide ufw vs nftables per host and document it: firewalls-nftables.
- Pair SSH limiting with fail2ban bans: fail2ban-setup.
- Define which subnets may reach which ports: network-segmentation.
Sources & Related
External references consulted:
- https://manpages.ubuntu.com/manpages/noble/en/man8/ufw.8.html
- https://docs.docker.com/engine/network/packet-filtering-firewalls/
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB build session.