# UFW firewall on an Ubuntu VPS: setup without locking out

URL: https://cloudj.net/blog/ufw-firewall-vps
Published: 2026-11-11 · Updated: 2026-11-11

> Set up a UFW firewall on an Ubuntu or Debian VPS: default deny, allow SSH first, common rules for web and databases, rate limiting and how to check it.

A UFW firewall is the simplest way to protect an Ubuntu or Debian VPS. UFW (Uncomplicated Firewall) is a front end for the kernel's packet filter that turns rules like "allow SSH, allow HTTPS, block everything else" into one-line commands. The only real danger is the order of steps: enable it before allowing SSH and you cut off your own connection. This guide follows the safe order, then covers the rules most servers need, rate limiting, and the Docker gotcha that surprises many people.

## How a UFW firewall decides

UFW applies a default policy to anything that does not match a rule, and checks rules in the order they were added. The usual setup for a server is:

- **Incoming: deny by default.** Only ports you explicitly allow are reachable.
- **Outgoing: allow by default.** The server can still fetch updates, call APIs and send mail.
- **Explicit allows** for the few services you run, such as SSH, HTTP and HTTPS.

That is it. Most mistakes come from allowing too much ("allow from anywhere to the database") rather than too little.

## Step 1: install and check the status

UFW ships with Ubuntu but is inactive by default. On Debian, install it first.

```bash
sudo apt update
sudo apt install -y ufw
sudo ufw status
```

You should see `Status: inactive`. Do not enable it yet.

## Step 2: set the default policies

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
```

These commands do not take effect until the firewall is enabled, so they are safe to run over SSH.

## Step 3: allow SSH before anything else

This is the step that prevents a lockout. Use the `limit` rule rather than a plain `allow`, which also gives you basic brute-force protection (see below):

```bash
sudo ufw limit OpenSSH
```

`OpenSSH` is an application profile that means TCP port 22. If you moved SSH to another port, use the number instead, for example `sudo ufw limit 2222/tcp`. List available profiles with `sudo ufw app list`.

If you want a safety net on a server you cannot afford to lose access to, schedule an automatic disable before you enable the firewall, and cancel it once you have confirmed access:

```bash
sudo apt install -y at
echo "/usr/sbin/ufw disable" | sudo at now + 5 minutes
```

## Step 4: enable and verify

```bash
sudo ufw enable
sudo ufw status verbose
```

UFW warns that the command may disrupt existing SSH connections; answer `y`. Your current session stays up. Now open a second terminal and connect again. If the new SSH session works, the rule is right. If you scheduled the safety net, remove it with `sudo atq` to find the job number and `sudo atrm` followed by that number.

`status verbose` should show `Default: deny (incoming), allow (outgoing)` and your SSH rule for both IPv4 and IPv6. UFW manages IPv6 rules too, as long as `IPV6=yes` is set in `/etc/default/ufw`, which is the default on Ubuntu.

## Common rules, ready to copy

| Need | Command |
|---|---|
| Web server on ports 80 and 443 | `sudo ufw allow 'Nginx Full'` or `sudo ufw allow 80,443/tcp` |
| A single TCP port | `sudo ufw allow 8443/tcp` |
| A port range | `sudo ufw allow 6000:6010/tcp` |
| A UDP service such as WireGuard | `sudo ufw allow 51820/udp` |
| Database reachable from one app server only | `sudo ufw allow from 203.0.113.20 to any port 5432 proto tcp` |
| Admin access from your office IP only | `sudo ufw allow from 203.0.113.50 to any port 22 proto tcp` |
| Block one abusive address | `sudo ufw insert 1 deny from 198.51.100.7` |

Two habits keep the rule set clean. Allow databases, Redis and admin panels only from specific addresses, never from anywhere. And use `insert 1` for deny rules, because UFW stops at the first match and an earlier allow would win otherwise.

To remove a rule, list rules with numbers and delete by number:

```bash
sudo ufw status numbered
sudo ufw delete 3
```

Numbers shift after each deletion, so list again before deleting another.

## Rate limiting with ufw limit

The `limit` rule denies a connection from an IP address that has tried to open 6 or more connections in the last 30 seconds. It is a good default for SSH: real users rarely hit it, while password-guessing bots do.

It is not a replacement for proper SSH security. Disable password login and use keys, as described in [setting up SSH keys on a VPS](https://cloudj.net/blog/ssh-key-vps-setup), and work through the rest of the [secure new VPS checklist](https://cloudj.net/blog/secure-new-vps-checklist). Do not use `limit` for HTTP, where browsers legitimately open many connections at once.

## The Docker gotcha

When you publish a container port with `docker run -p 8080:80` or a `ports:` entry in Compose, Docker writes its own packet filter rules that are evaluated before UFW's. The port becomes reachable from the internet even if UFW has no allow rule for it, and `ufw status` will not show it.

The simplest fix is to publish ports on the loopback address only and put a reverse proxy in front:

```yaml
services:
  app:
    image: myapp:latest
    ports:
      - "127.0.0.1:8080:80"
```

Nginx then reaches the app on `127.0.0.1:8080`, and only ports 80 and 443 face the internet. Our guides to [installing Docker on a VPS](https://cloudj.net/blog/install-docker-vps) and [an Nginx reverse proxy with SSL](https://cloudj.net/blog/nginx-reverse-proxy-ssl-vps) show both halves.

## Checking what is really open

Rules describe intent; check reality from both sides.

```bash
sudo ss -tulpn
```

This lists every listening port and the process behind it. Anything listening on `0.0.0.0` or `[::]` that you did not mean to expose should either be bound to `127.0.0.1` or blocked. Then test from your own computer, not from the server, with `nc -vz 203.0.113.10 5432`. A connection that times out is filtered, which is what you want for private services.

To see blocked attempts, turn on logging at a low level and read the kernel log:

```bash
sudo ufw logging low
sudo journalctl -k -g UFW --since "1 hour ago"
```

On systems with rsyslog, the same entries also land in `/var/log/ufw.log`.

## Troubleshooting

- **Locked out after enabling.** If you scheduled the safety net, wait for it to run and reconnect. This is why you keep the first session open until a second one connects.
- **A rule seems ignored.** Check the order with `sudo ufw status numbered`; an earlier rule may match first.
- **Changes did not apply.** After editing files under `/etc/ufw/` by hand, run `sudo ufw reload`.
- **Starting over.** `sudo ufw reset` disables the firewall and removes all rules. Re-add SSH before enabling again.

## FAQ

### Do I still need UFW if my app only listens on localhost?

Yes. It is a second layer that catches services you forget about, such as a database installed with default settings that listens on all interfaces.

### Does UFW slow the server down?

For a typical VPS with a handful of rules, the overhead is negligible.

### Is UFW available on AlmaLinux or Rocky Linux?

Those distributions use firewalld by default. UFW is mainly an Ubuntu and Debian tool, and mixing two firewall managers on one server causes confusion.

### Should I open ping?

UFW allows ICMP echo by default through its `before.rules`. Keeping it helps with diagnostics such as those in [testing VPS latency with ping and mtr](https://cloudj.net/blog/test-vps-latency-ping-mtr).

## Start with a clean server

A firewall is easiest to get right on day one, before services pile up. Pick a location with the [VPS locations guide](https://cloudj.net/blog/vps-locations-guide), deploy an Ubuntu or Debian [cloud server](https://cloudj.net/cloud-servers) in the [Cloudj console](https://app.cloudj.net), and run the four steps above before you install anything else.
