Skip to content
DevOps devops security 6 min read

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 run ufw 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.

PortProtocolWhat it is forExpose to internet?
22SSHRemote login and adminYes, but lock it down (keys only, fail2ban)
80HTTPPlain web traffic; usually only to redirect to 443Yes
443HTTPSEncrypted web trafficYes

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.0 only 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 incoming and only add the ports you can name a reason for.
  • Allow 22/tcp before running ufw enable so 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, not 0.0.0.0, and verify with sudo ss -tlnp.
  • Restrict private-network ports to specific source IPs with ufw allow from <ip> to any port <port>.
  • Re-run sudo ufw status verbose after 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.
Last updated June 15, 2026
Was this helpful?