Add UFW - Uncomplicated Firewall rules

Self hosted PayloadCMS and PostgreSQL website on Docker

4 min read

Published Jun 17 2025, updated Jun 19 2025


10
0
0
0

CaddyDockerGitHub ActionsJavascriptNextJSPayloadCMSPortainerTailscaleUbuntuUFW

At the moment the server isn't locked down. So if you were to try and access the database using the public IP in the connection string, it would let you, or you could access the public IP with port 3000 to access the website etc.


What we want to do is shut out access to all the ports on the main public IP, apart from ports 80 and 443 for the website traffic coming through to Caddy. We also want to leave access to anything on our Tailscale network so we can still SSH in to the server, access the database etc. over the secure Tailscale VPN.


Run this command on the server to get a list of the network adapters:

ip link show

This will return a list of all the network adapters available on the server, here is an example:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp1s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd xx:xx:xx:xx:xx:xx
3: enp1s0f1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd xx:xx:xx:xx:xx:xx
4: enxbe3af2b6059f: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether xx:xx:xx:xx:xx:xx brd xx:xx:xx:xx:xx:xx
5: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 500
    link/none
6: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether xx:xx:xx:xx:xx:xx brd xx:xx:xx:xx:xx:xx
10: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether xx:xx:xx:xx:xx:xx brd xx:xx:xx:xx:xx:xx
12: vethc6a5a2f@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT group default
    link/ether xx:xx:xx:xx:xx:xx brd xx:xx:xx:xx:xx:xx link-netnsid 1

So in the example above, the adaptors we are interested in are enp1s0f0 which is the main adapter that the public IP points to, we can ignore enp1s0f1 as that is a spare network adapter that has a state of down, and the tailscale0 adapter which is for our Tailscale.


So what we want to setup is:

  • enp1s0f0 : Port 80 allow.
  • enp1s0f0 : Port 443 allow.
  • enp1s0f0 : All other ports deny.
  • tailscale0 : Allow all ports.

To do this we will add rules to the UFW, make sure you change the network adapter name to what yours is listed as, the order the rules happen matters (you want to say something specifics allowed before then denying all as they are processed in order).


Allow Tailscale on all ports:

sudo ufw allow in on tailscale0

Allow port 80 on the main network:

sudo ufw allow in on enp1s0f0 to any port 80 proto tcp

Allow port 443 on the main network:

sudo ufw allow in on enp1s0f0 to any port 443 proto tcp

Deny all other ports on main network:

sudo ufw deny in on enp1s0f0

Now enable the UFW:

sudo ufw enable

Now test the rules have been applied:

sudo ufw status numbered

Which lists a response like:

Status: active

     To Action From
     -- ------ ----
[ 1] Anywhere on tailscale0 ALLOW IN Anywhere
[ 2] 80/tcp on enp1s0f0 ALLOW IN Anywhere
[ 3] 443/tcp on enp1s0f0 ALLOW IN Anywhere
[ 4] Anywhere on enp1s0f0 DENY IN Anywhere
[ 5] Anywhere (v6) on tailscale0 ALLOW IN Anywhere (v6)
[ 6] 80/tcp (v6) on enp1s0f0 ALLOW IN Anywhere (v6)
[ 7] 443/tcp (v6) on enp1s0f0 ALLOW IN Anywhere (v6)
[ 8] Anywhere (v6) on enp1s0f0 DENY IN Anywhere (v6)

UFW, iptables & docker

In Linux, it is a thing called iptables that control a networks access. UFW just applies rules to the iptables.


However, docker also adds rules to the iptables, which get applied before the UFW rules. So even though you have just setup UFW to allow and disallow ports on the 2 network adapters, it wont actually fully lock the system down yet. Docker effectively punches holes through when a port is published, so even though you have UFW rules that restrict all ports apart from 80 and 443 on the main IP address, at the moment, any published docker port is also available on the main IP as the docker rules occur before the UFW rules and so override them.


What we need to do now is add our own iptable deny rules for all the ports we have published on docker. We can do that directly with ip tables by adding a line such as:

sudo iptables -I DOCKER-USER -i enp1s0f0 -p tcp --dport 5000 -j DROP

Which is saying, in the docker user chain part of iptables, all incoming requests on the main network, port 5000, drop the matching packets. So therefore blocking the incoming connection on the main IP for port 5000.


So we could go ahead and add one of them for the other ports we published which were 5000, 5432 and 3000. Wich will work for now and stop incoming requests on the main IP for those ports.


When the server is rebooted, the iptables data is reset, so any additional rules you have added, like the above, will have been removed.


What we can do is add these iptable deny rules in to the UFW before rules file /etc/ufw/before.rules . That way they will always be applied by UFW when the server is rebooted.


Edit the /etc/ufw/before.rules file and add the deny rules for each other the published ports to the top of the file, below the *filter line but as high up as you can so they are processed before any other rules, so find the *filter heading:

/etc/ufw/before.rules

*filter

# Create DOCKER-USER chain if it doesn't already exist
:DOCKER-USER - [0:0]

# Your custom drop rules
-A DOCKER-USER -i enp1s0f0 -p tcp -m tcp --dport 5000 -j DROP
-A DOCKER-USER -i enp1s0f0 -p tcp -m tcp --dport 3000 -j DROP
-A DOCKER-USER -i enp1s0f0 -p tcp -m tcp --dport 5432 -j DROP
-A DOCKER-USER -i enp1s0f0 -p tcp -m tcp --dport 8000 -j DROP
-A DOCKER-USER -i enp1s0f0 -p tcp -m tcp --dport 9000 -j DROP
-A DOCKER-USER -i enp1s0f0 -p tcp -m tcp --dport 9443 -j DROP

We also want to add the 8000, 9000 and 9443, as these are Portainers published port numbers, so we also want them denied on the main IP.


When thats saved, need to disable and reenable UFW to apply the changes:

sudo ufw disable
sudo ufw enable

Now check UFW is enabled:

sudo ufw status

Check UFW is set to start on boot:

sudo systemctl is-enabled ufw

If not then run:

sudo systemctl enable ufw

Your server access should now be more locked down, with main web traffic allowed through and more access over Tailscale.


If you need to debug any issues, you can list all active iptable rules:

sudo iptables -L -v -n

Or NAT rules:

sudo iptables -t nat -L -v -n



Products from our shop

Docker Cheat Sheet - Print at Home Designs

Docker Cheat Sheet - Print at Home Designs

Docker Cheat Sheet Mouse Mat

Docker Cheat Sheet Mouse Mat

Docker Cheat Sheet Travel Mug

Docker Cheat Sheet Travel Mug

Docker Cheat Sheet Mug

Docker Cheat Sheet Mug

Vim Cheat Sheet - Print at Home Designs

Vim Cheat Sheet - Print at Home Designs

Vim Cheat Sheet Mouse Mat

Vim Cheat Sheet Mouse Mat

Vim Cheat Sheet Travel Mug

Vim Cheat Sheet Travel Mug

Vim Cheat Sheet Mug

Vim Cheat Sheet Mug

SimpleSteps.guide branded Travel Mug

SimpleSteps.guide branded Travel Mug

Developer Excuse Javascript - Travel Mug

Developer Excuse Javascript - Travel Mug

Developer Excuse Javascript Embroidered T-Shirt - Dark

Developer Excuse Javascript Embroidered T-Shirt - Dark

Developer Excuse Javascript Embroidered T-Shirt - Light

Developer Excuse Javascript Embroidered T-Shirt - Light

Developer Excuse Javascript Mug - White

Developer Excuse Javascript Mug - White

Developer Excuse Javascript Mug - Black

Developer Excuse Javascript Mug - Black

SimpleSteps.guide branded stainless steel water bottle

SimpleSteps.guide branded stainless steel water bottle

Developer Excuse Javascript Hoodie - Light

Developer Excuse Javascript Hoodie - Light

Developer Excuse Javascript Hoodie - Dark

Developer Excuse Javascript Hoodie - Dark

© 2025 SimpleSteps.guide
AboutFAQPoliciesContact