When hardening a server, the cleanest starting point is a simple policy: deny all incoming traffic, allow outgoing traffic, then open only the services you actually need. On Ubuntu/Debian (and derivatives), UFW (Uncomplicated Firewall) is the most straightforward way to do that.
The key is order of operations: configure first, enable last. If you enable UFW before allowing SSH, you can lock yourself out in seconds.
0) Two rules that prevent most disasters
- Always keep a fallback: if possible, do this from an out-of-band console (IPMI/iLO/KVM) or at least keep a second SSH session open while testing.
- Firewall ≠ access control: UFW is a network gate. You still need proper SSH hygiene (keys, restricted access, etc.).
1) Check whether UFW is active
Before changing anything, verify the current state:
ufw status
On a fresh install you’ll often see Status: inactive, which is ideal—you can prepare rules without affecting connectivity.
For more detail:
ufw status verbose
2) Set the default policy: block inbound, allow outbound
This is the baseline “closed by default” posture:
ufw default deny incoming
ufw default allow outgoing
Code language: JavaScript (javascript)
deny incoming: blocks any inbound connection unless explicitly allowed.allow outgoing: lets the server reach the outside world (updates, DNS, NTP, package repos, APIs, etc.).
In high-control environments (strict DMZ/compliance), you can also restrict outbound, but that requires mapping every dependency (DNS resolvers, NTP servers, repos, backup endpoints, monitoring, etc.).
3) Don’t lose access: allow SSH before enabling UFW
If you administer the server over SSH, this is the critical step.
Option A — Allow SSH from anywhere (fast, less restrictive)
ufw allow ssh
Equivalent and explicit:
ufw allow 22/tcp
Using allow ssh is simply clearer.
Option B — Restrict SSH to a specific IP (recommended when possible)
If you have a fixed public IP (office, bastion/jump host, VPN egress):
ufw allow from 100.100.100.100 to any port 22 proto tcp
Code language: CSS (css)
Or using the ssh alias:
ufw allow from 100.100.100.100 to any port ssh
Code language: CSS (css)
Sysadmin reality check: if your IP changes (home fiber, 4G/5G), this can lock you out. Safer approaches include:
- a bastion with a stable IP
- a VPN overlay (WireGuard/Tailscale/Netbird)
- a controlled corporate range
- key-only SSH + additional controls
Optional: rate-limit SSH to reduce brute-force noise
ufw limit ssh
This won’t replace a proper security stack, but it cuts down automated hammering.
4) Open only the ports you actually need
For a typical web server:
ufw allow http
ufw allow https
Equivalent by port:
ufw allow 80/tcp
ufw allow 443/tcp
Good practice: every open port should have a clear owner (“this is for Nginx reverse proxy”, “this is for SSH via bastion”, etc.). If it’s internal-only, consider keeping it off the public interface entirely and exposing it through a private network or VPN.
5) IPv6: decide explicitly (don’t forget it exists)
A common “surprise exposure” happens when admins secure IPv4 but overlook IPv6.
Check whether UFW manages IPv6:
grep -i ipv6 /etc/default/ufw
Code language: JavaScript (javascript)
If you see IPV6=yes, UFW applies rules to IPv6 as well.
If you don’t use IPv6 on that host, consider disabling it according to your policy, or ensure your ruleset is correct for both stacks.
6) Enable UFW (only after SSH and required services are allowed)
Now you can enable safely:
ufw enable
Verify:
ufw status
ufw status verbose
7) Logging: useful, but don’t melt your disks
Enable logging:
ufw logging on
Or set a lower verbosity level (often a sensible default on public servers):
ufw logging low
Quick operational checklist (what “good” looks like)
- SSH uses keys (preferably password auth disabled).
- SSH access is restricted (IP allowlist, bastion, or VPN).
- Minimal exposure: only ports you truly serve (often 22/80/443, or fewer).
- IPv6 is accounted for (enabled and controlled, or intentionally disabled).
- Confirm what’s listening:
ss -tulpnand align it with your firewall intent.
Common mistakes (and how to avoid them)
- Enabling UFW before allowing SSH → instant lockout.
- Opening ports “just in case” → unnecessary attack surface.
- Ignoring IPv6 → “secure” server still reachable via IPv6.
- SSH with passwords exposed to the internet → constant brute-force attempts.
- Not validating listening services → firewall is fine, daemon exposure isn’t.
Minimal, safe sequence (copy/paste workflow)
ufw status
ufw default deny incoming
ufw default allow outgoing
# Choose ONE:
ufw allow ssh
# or:
ufw allow from 100.100.100.100 to any port 22 proto tcp
ufw allow http
ufw allow https
ufw enable
ufw status verbose
Code language: PHP (php) 