UFW (Uncomplicated Firewall) is a firewall right inside the VPS that you control with easy-to-read commands. You'll set it up so that only what you want is reachable from outside, without locking yourself out of SSH. At the end you'll find out why Docker bypasses UFW, plus a sample nftables configuration for advanced users.
What you'll need
- A StormHost VPS with Ubuntu 24.04 or Debian 13
- An SSH connection as root (guide)
- The browser console open, just in case
- A list of the ports your services use
- About 20 minutes
UFW in a nutshell#
Linux filters packets in the kernel (the netfilter subsystem, controlled through nftables or iptables). Writing rules directly is complicated; UFW does it for you: it turns the command ufw allow 22/tcp into the right rules for both IPv4 and IPv6.
UFW is one of two options. The other is the firewall in the customer area, which filters traffic before it even reaches the VPS. If you use both, a port has to be open in both.
Installation and default policy#
Ubuntu 24.04
UFW is installed but turned off. Check it:
ufw statusThe output Status: inactive means the firewall isn't blocking anything yet.
Debian 13
Install UFW:
apt install ufwAfter installation it's turned off (ufw status prints Status: inactive).
Set the default policy, meaning what happens to traffic no rule mentions:
ufw default deny incoming
ufw default allow outgoingufw default deny incoming– block new incoming connections unless a rule allows them.ufw default allow outgoing– allow outgoing connections from the VPS (updates, DNS, downloads).
UFW is stateful: replies to connections the VPS opened itself get through even with deny incoming.
Allow SSH before you turn the firewall on#
Without an SSH rule you lock yourself out when you turn it on
ufw enable with the deny incoming policy immediately blocks everything that isn't allowed. If you don't have a rule for SSH at that moment, the connection drops. You can then get into the VPS only through the browser console. If your SSH runs on a port other than 22, allow that one.
ufw allow 22/tcp comment 'SSH'ufw allow– adds an allow rule for incoming traffic.22/tcp– port 22, TCP protocol.comment 'SSH'– a note that shows in the rule list.
Alternatively you can use an application profile. ufw app list lists the profiles (for example OpenSSH, and after installing Nginx also Nginx Full). You then add the rule with ufw allow OpenSSH.
Turning it on and checking#
Turn the firewall on:
bash ufw enableUFW asks
Command may disrupt existing ssh connections. Proceed with operation (y|n)?. If you have the SSH rule, typeyand press Enter.You'll see
Firewall is active and enabled on system startup. The firewall is now running and also starts after a restart.In a new window, check that you can connect over SSH.
Show the detailed status:
bash ufw status verboseOutputtext Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere # SSH 22/tcp (v6) ALLOW IN Anywhere (v6) # SSH
| Line | Meaning |
|---|---|
Status: active | The firewall is running. |
Logging: on (low) | Writes blocked packets to the log, with a limit on the number of entries. |
Default: deny (incoming) | Incoming traffic without a rule is dropped. |
allow (outgoing) | Outgoing traffic is allowed. |
disabled (routed) | UFW doesn't handle forwarded traffic (the VPS as a router). |
New profiles: skip | New application profiles aren't allowed automatically. |
22/tcp ALLOW IN Anywhere | Port 22/TCP is allowed from anywhere over IPv4. |
(v6) | The same rule for IPv6. UFW creates it automatically. |
Rules for services#
| What you want | Command |
|---|---|
| Web (HTTP and HTTPS) | ufw allow proto tcp from any to any port 80,443 comment 'Web' |
| Minecraft Java | ufw allow 25565/tcp comment 'Minecraft' |
| Bedrock (Geyser) | ufw allow 19132/udp comment 'Geyser' |
| FiveM (TCP and UDP) | ufw allow 30120 comment 'FiveM' |
| Rust (game and query) | ufw allow proto udp from any to any port 28015,28017 comment 'Rust' |
| Port range | ufw allow proto tcp from any to any port 25565:25600 comment 'Game servers' |
| Port only from one IP address | ufw allow proto tcp from 198.51.100.25 to any port 3306 comment 'DB from office' |
| Block an IP address | ufw insert 1 deny from 198.51.100.25 comment 'Attacker' |
How to read the parts of the commands:
25565/tcp– port and protocol. Without a protocol (ufw allow 30120) the rule applies to both TCP and UDP.proto tcp– the protocol in the extended syntax. A range or list of ports (80,443,25565:25600) requires a protocol.from any/from 198.51.100.25– where the connection comes from: anywhere, or from one address (it can also be a range198.51.100.0/24).to any port 3306– which VPS address and port.any= all VPS addresses.insert 1– inserts the rule in first place. UFW evaluates rules from the top and the first match wins, so blocking an address must come above the allow rules. You block an IPv6 address withufw prepend deny from 2001:db8::/64, which puts the rule at the start of the IPv6 rules.deny– silently drop. Therejectvariant tells the sender the connection was refused.
In comments (comment), use only plain letters without accents, digits and spaces.
Limiting the number of connections (limit)#
ufw limit 22/tcp comment 'SSH'ufw limit– allows the port but blocks an IP address that tries to open 6 or more connections within 30 seconds. This slows down password guessing.
If you already have an allow rule for port 22, delete it first (see the next section), otherwise the older rule would win. Limit isn't suitable for services with many legitimate connections (web, games). For SSH, fail2ban is better, because it reacts to actual failed logins.
Numbered rules and deleting#
ufw status numbered To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere # SSH
[ 2] 25565/tcp ALLOW IN Anywhere # Minecraft
[ 3] 22/tcp (v6) ALLOW IN Anywhere (v6) # SSH
[ 4] 25565/tcp (v6) ALLOW IN Anywhere (v6) # MinecraftDeleting a rule by number:
ufw delete 2ufw delete 2– deletes rule number 2. UFW asks for confirmation (y).
After each deletion the numbers shift, so run ufw status numbered again before deleting the next one. The IPv4 and IPv6 variants are two rules. You delete both at once by writing out the rule: ufw delete allow 25565/tcp.
Other useful commands:
| Command | What it does |
|---|---|
ufw reload | Reloads the rules (after editing the files in /etc/ufw/ by hand). |
ufw disable | Turns the firewall off; the rules stay saved. |
ufw --dry-run allow 80/tcp | Shows what the command would do, but changes nothing. |
ufw reset | Turns the firewall off and deletes all rules. Use it only when starting over. |
IPv6#
UFW has IPv6 turned on by default: the file /etc/default/ufw has the line IPV6=yes. Every rule without a specific IPv4 address is then also created for IPv6 (the (v6) lines). Don't set IPV6=no. The VPS has a public IPv6 address, and without IPv6 rules IPv6 traffic wouldn't be filtered by your rules.
Docker bypasses UFW#
When you publish a port in Docker (-p 8080:80 or ports: in compose.yaml), it's reachable from the internet even if UFW blocks it. Docker redirects the traffic to the container before UFW's rules get to it. Docker's documentation describes this as an incompatibility between Docker and UFW.
How to avoid it:
Publish ports only on
127.0.0.1. Then they're reachable only inside the VPS and you expose them through Nginx (see Nginx and HTTPS):compose.yamlYAML services: app: ports: - "127.0.0.1:8080:80"127.0.0.1:8080:80– port 8080 on the VPS, but only on the local address, forwarded to port 80 in the container.
Don't publish databases and internal services at all. Containers talk to each other over Docker's network; they don't need a port to the outside.
Use the firewall in the customer area. It filters traffic before it reaches the VPS, so it also catches ports published by Docker.
Don't use the Docker setting "iptables": false; it breaks container networking. Advanced filtering of traffic to containers (the DOCKER-USER chain) is described in Docker's documentation (opens in a new window). More about Docker in the Docker and Docker Compose guide.
Logging#
ufw logging lowlow– logs blocked packets, with a limit on the number of entries. Higher levels (medium,high,full) log more and the log grows fast.ufw logging offturns logging off.
Where to find the entries:
Ubuntu 24.04
tail -f /var/log/ufw.logtail -f– keeps printing new lines of the file. Quit with Ctrl+C.
Debian 13
journalctl -k -f --grep "UFW"journalctl -k– kernel messages (by default Debian has no separate/var/log/ufw.logfile).-f– keep printing new entries.--grep "UFW"– only lines from UFW.
Example entry: [UFW BLOCK] IN=eth0 … SRC=198.51.100.25 DST=203.0.113.10 … PROTO=TCP SPT=51234 DPT=23. SRC is the sender's address, DST your VPS, PROTO the protocol and DPT the port it tried to connect to.
For advanced users: nftables#
nftables is the modern tool for firewall rules in the kernel. On Debian it's the default framework, and UFW itself uses it under the hood (through the iptables-nft layer). Writing rules directly in nftables is useful when you need more than UFW can do.
Don't mix UFW and your own nftables
Use either UFW or your own /etc/nftables.conf. Turn UFW off (ufw disable) before switching to nftables. The configuration below starts with the flush ruleset command, which deletes all rules in the kernel, including Docker's and fail2ban's rules. So after every restart of the nftables service, restart Docker and fail2ban too. With Docker on the same VPS, you're better off staying with UFW or the firewall in the customer area.
A sample file for a server with SSH and a website:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
meta l4proto { icmp, ipv6-icmp } accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}| Line | What it does |
|---|---|
flush ruleset | Deletes all existing rules so the file always loads from a clean state. |
table inet filter | The filter table for the inet family, meaning IPv4 and IPv6 at once. |
type filter hook input … policy drop | The chain for incoming traffic. Anything no rule allows is dropped. |
ct state established,related accept | Lets through replies to connections already established. Without this, nothing the VPS opens itself would work. |
ct state invalid drop | Drops damaged or nonsensical packets. |
iif "lo" accept | Allows traffic inside the VPS (the loopback, 127.0.0.1). Without it, local databases and services stop working. |
meta l4proto { icmp, ipv6-icmp } accept | Allows ICMP: ping and the messages needed for the network to work properly. IPv6 doesn't work without ICMPv6 (neighbor discovery). |
tcp dport 22 accept | SSH. If you use a different port, change it. |
tcp dport { 80, 443 } accept | Web. A set of ports in curly braces. For UDP use udp dport. |
chain forward … policy drop | The VPS won't forward traffic (it isn't a router). |
chain output … policy accept | Outgoing traffic without restrictions. |
Deployment (applies to both Debian 13 and Ubuntu 24.04):
Install the tool:
apt install nftables.Turn UFW off if it's running:
ufw disable.Save the configuration to
/etc/nftables.conf(nano /etc/nftables.conf).Check it without applying it:
bash nft -c -f /etc/nftables.conf-c– check only; nothing changes. If everything is fine, it prints nothing.-f– load the rules from a file.
Enable the service that loads the rules right away and after every boot:
bash systemctl enable --now nftablesenable– load the rules when the VPS boots (the service is off by default).--now– and load them right away.
Check SSH in a new window.
nft list rulesetprints the current rules.
Verification#
ufw status verboseshowsStatus: activeand rules for all your services.- You can connect from your computer over SSH and the services work.
- A port that isn't allowed doesn't respond from outside (test from your computer: on Windows
Test-NetConnection <VPS_IP_ADDRESS> -Port 3306, on macOS and Linuxnc -vz -w 5 <VPS_IP_ADDRESS> 3306; a closed port ends with a timeout).
If you lock yourself out#
- Open the browser console and log in as root.
- Allow SSH:
ufw allow 22/tcp, or turn the firewall off temporarily:ufw disable. - With nftables:
systemctl stop nftablesand thennft flush ruleset(deletes all rules; traffic will be allowed). - Fix the rules and turn the firewall back on.
Troubleshooting#
ERROR: Must specify 'tcp' or 'udp' with multiple ports#
A range or list of ports requires a protocol. Write the rule like this: ufw allow proto tcp from any to any port 25565:25600. For both TCP and UDP, add two rules.
The rule is added, but the port is still closed#
- Check the order (
ufw status numbered). There must be nodenyrule above it that drops the same traffic. - Check that the application listens on the port:
ss -tulpn | grep <PORT>. The address127.0.0.1means it's reachable only inside the VPS. - Check the firewall in the customer area. If it's in the Block everything except allowed mode, the port must be allowed there too.
A Docker port is open even though UFW blocks it#
That's the expected behavior, see the Docker bypasses UFW section.
ERROR: initcaps when turning it on#
UFW can't use the kernel modules. Typically you updated the kernel and the VPS isn't running the new one yet. Restart the VPS and turn UFW on again.
Docker or fail2ban stopped working after restarting the nftables service#
flush ruleset deleted their rules. Restart them: systemctl restart docker fail2ban.
Summary
- First
ufw allow 22/tcp, only thenufw enable. - Default policy:
deny incoming,allow outgoing; rules apply from the top and the first match wins. ufw status numberedandufw delete <number>for managing rules,ufw status verbosefor checking.- IPv6 is on (
IPV6=yes); rules are created for both versions. - Ports published by Docker bypass UFW: publish on
127.0.0.1, or use the firewall in the customer area. - nftables only for advanced users and never together with UFW.