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:


Table of Contents

  1. Context / Why This Matters
  2. Implementation / Core Content
  3. Practical Examples
  4. Troubleshooting & Common Pitfalls
  5. 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

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse