Firewall & Network Security
Every open port on your server is a door. A firewall (software that decides which network traffic is allowed in or out) is how you lock the doors you do not use. The single most effective thing you can do to secure a Linux server is to stop exposing services to the internet that nobody outside the machine needs to reach. This page shows you how to think about your attack surface (the total set of ways an attacker could try to get in) and how to shrink it on Ubuntu 22.04/24.04 LTS.
The deny-by-default principle
A firewall can work in two ways. It can allow everything and block a few known-bad things (a “blocklist”), or it can block everything and allow only a few known-good things (an “allowlist”). For servers, you always want the second one. This is called deny-by-default: every port is closed unless you have a specific reason to open it.
Why does this matter so much? Because you cannot predict every service that might start listening on your server. A package you install might quietly open a port. A misconfigured app might bind to all network interfaces. With deny-by-default, none of that is reachable from the internet unless you explicitly allowed it. You make a small, deliberate list of open ports, and everything else is invisible.
When to use this: Always, on every internet-facing server. There is no production scenario where “allow everything” is the right default. The only time you relax it is on a fully private network behind another firewall, and even then deny-by-default is the safer habit.
Set up ufw with deny-by-default
Ubuntu ships with ufw (Uncomplicated Firewall), a friendly wrapper around the kernel’s iptables/nftables rules. It is the standard tool and you should use it. Start by setting the default policies before you enable anything, so you never lock yourself out.
sudo ufw default deny incoming
sudo ufw default allow outgoing
This says: block all traffic coming in, allow all traffic going out. Outgoing is usually fine to allow because it is your server reaching out (to fetch updates, call APIs, send email). Now allow only the ports you actually serve. For a typical web server you need SSH and HTTP/HTTPS:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Then turn the firewall on and check it:
sudo ufw enable
sudo ufw status verbose
Output:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
Gotcha: Always allow your SSH port (
22/tcp) before you runufw enable. If you enable the firewall over an SSH connection without allowing SSH first, you will be disconnected and locked out of the server.
Expose only 22, 80, and 443
These three ports cover almost every web workload, so it is worth knowing exactly what they are.
| Port | Protocol | What it is for | Expose to internet? |
|---|---|---|---|
| 22 | SSH | Remote login and admin | Yes, but lock it down (keys only, fail2ban) |
| 80 | HTTP | Plain web traffic; usually only to redirect to 443 | Yes |
| 443 | HTTPS | Encrypted web traffic | Yes |
Notice what is not on this list: databases, caches, internal APIs, and admin dashboards. A common and dangerous mistake is opening port 5432 (PostgreSQL) or 3306 (MySQL) or 6379 (Redis) to the internet. Attackers run automated scanners that find these in minutes and try default passwords. Your database should never appear in your ufw status allow list.
If you need a service reachable only by you, do not open its port. Instead, tunnel it over SSH from your laptop:
ssh -L 5432:localhost:5432 deploy@your-server-ip
This forwards your local port 5432 to the server’s localhost:5432 through the encrypted SSH connection, so the database stays closed to the world.
Bind services to localhost
A firewall blocks traffic at the door, but it is even better if the service never listens on a public door in the first place. Most servers have at least two network interfaces: lo (loopback, the 127.0.0.1 address that only the machine itself can reach) and a public interface (your real IP). When a service binds (attaches its listener) to 127.0.0.1 instead of 0.0.0.0 (all interfaces), it is physically unreachable from outside the machine, firewall or not. This is defence in depth (multiple layers, so one mistake does not expose you).
Databases are the most important case. PostgreSQL on Ubuntu binds to localhost by default, but verify it. Open the config:
sudo nano /etc/postgresql/16/main/postgresql.conf
Make sure the listen line is restricted:
listen_addresses = 'localhost'
For Redis, check /etc/redis/redis.conf:
bind 127.0.0.1 ::1
protected-mode yes
After editing, restart and confirm what is listening with ss (a socket-statistics tool):
sudo systemctl restart postgresql
sudo ss -tlnp
Output:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* postgres
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* sshd
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* nginx
Here PostgreSQL is on 127.0.0.1:5432 (good, private), while SSH and Nginx are on 0.0.0.0 (public, intended). If you ever see a database on 0.0.0.0, fix the bind address immediately.
When to use this: Bind to localhost whenever the only thing talking to a service lives on the same machine — databases, caches, metrics exporters, and internal app servers behind a reverse proxy. Use
0.0.0.0only for the public front door (Nginx, Apache) and SSH.
Private networks
If your app server and database run on separate machines, you do not want the database open to the public internet just so the app can reach it. The answer is a private network (also called a VPC, virtual private cloud) — an internal network where your servers get private IP addresses (like 10.0.0.x) that are not routable from the internet.
Cloud providers (AWS, DigitalOcean, Hetzner) give you this for free. Put your database on the private IP, bind it to that interface, and use a firewall rule that allows the database port only from your app server’s private IP:
sudo ufw allow from 10.0.0.5 to any port 5432 proto tcp
This opens PostgreSQL to a single trusted host on the private network and nobody else. Combined with listen_addresses set to the private IP, the database is reachable by your app but invisible to the internet.
Best practices
- Start with
ufw default deny incomingand only add the ports you can name a reason for. - Allow
22/tcpbefore runningufw enableso you never lock yourself out. - Never expose database or cache ports (5432, 3306, 6379) to the internet — use SSH tunnels or private networks.
- Bind internal services to
127.0.0.1, not0.0.0.0, and verify withsudo ss -tlnp. - Restrict private-network ports to specific source IPs with
ufw allow from <ip> to any port <port>. - Re-run
sudo ufw status verboseafter any deployment to confirm your open ports have not drifted. - Pair the firewall with SSH hardening and fail2ban — the firewall reduces doors, those tools protect the doors you keep.