A web server needs few open doors: deny inbound by default, open 80 and 443 to the world, open 22 only to the addresses you administer from, and nothing else. MySQL 524 (3306, 33060), PHP-FPM (9000), Redis 2,763 (6379) and Memcached 4,600 (11211) serve local processes and must never face the internet. Keep the policy in a script that can be reviewed and rerun on the next server:
#!/usr/bin/env bash
# firewall.sh: the complete inbound policy for one web server (run as root)
set -euo pipefail
ADMIN_IP="${ADMIN_IP:?set ADMIN_IP to the address you administer from}"
ufw --force reset > /dev/null # start from nothing; rules are re-created below
ufw default deny incoming
ufw default allow outgoing
ufw default deny routed
ufw limit from "$ADMIN_IP" to any port 22 proto tcp comment 'SSH, admin only'
ufw allow 80,443/tcp comment 'Web'
ufw logging low
ufw --force enable
ufw status verbose... To Action From -- ------ ---- 22/tcp LIMIT IN 198.51.100.7 # SSH, admin only 80,443/tcp ALLOW IN Anywhere # Web 80,443/tcp (v6) ALLOW IN Anywhere (v6) # Web
Run it as sudo ADMIN_IP=198.51.100.7 bash firewall.sh. The SSH rule has no v6 twin because its source is an IPv4 address, and status verbose reports routed traffic as disabled because this kernel forwards no packets. ufw 280 reset saves a timestamped copy of each rules file in /etc/ufw/, so a bad run can be undone. After every change, rerun the probe from Port Closed? from outside, and before tightening SSH on a cloud server, make sure you have a way in that does not use port 22, such as the provider's browser console.