UFW Without SSH Lockout
The target is public TCP 80/443, SSH only on tailscale0, default deny incoming, and allow outgoing. Keep one known-good public session and one verified Tailscale session. An established connection may survive a rule deletion, so only a fresh connection proves the new policy.
Inspect without resetting:
sudo ufw status verbose
sudo ufw status numbered
Then stage the rules:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # temporary fallback
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow in on tailscale0 to any port 22 proto tcp
sudo ufw --force enable
sudo ufw status verbose
Open fresh public and Tailscale SSH sessions. If both pass, inspect rules again and remove the semantic generic rule:
sudo ufw delete allow 22/tcp
sudo ufw status numbered
Do not assume a saved rule number; numbering changes, and a numbered deletion normally removes only that specific IPv4 or IPv6 entry.
# Must pass
ssh -o ConnectTimeout=10 "$VPS_USER@$TAILSCALE_IPV4"
# Must time out or be rejected
ssh -o ConnectionAttempts=1 -o ConnectTimeout=5 \
"$VPS_USER@$VPS_PUBLIC_IPV4"
Docker warning: published container ports can bypass ordinary UFW rules. Future deployments must review Compose ports, listening addresses, docker ps, and the DOCKER-USER chain. Disabling Docker's iptables management usually breaks container networking.
If Tailscale SSH transport fails after removal, keep the surviving session, restore sudo ufw allow 22/tcp, verify fresh public-key SSH, gather diagnostics, and stop. Recovery means restoring a verified path, not resetting the firewall.