Foundations Chapter 4 45 min
Default deny
Decide what can reach your server, and test it from another machine.
By the end of this chapter you should be able to
- Say what your machine currently allows in, and how you found out
- Turn a permissive default into deny-by-default without cutting off your own session
- Choose between dropping and refusing a packet, and say why you picked one
- Prove a rule works by trying to get past it from somewhere else
Right now if a service starts on your server and listens on a port, anyone on your network can reach it. The default on Linux is to accept, and we are going to switch that off. You already saw it in chapter 2: two things were listening and nothing stood in front of either.
What is filtering your traffic right now #
Install UFW if the server image omitted it, then check rather than assume:
$ sudo apt update
$ sudo apt install ufw
$ grep '^IPV6=' /etc/default/ufw
Require IPV6=yes when the host has IPv6. UFW otherwise applies the policy only to IPv4 and can leave a global IPv6 listener reachable.
$ sudo ufw status verbose
If yours is already active, list the numbered rules and write down what each one permits:
$ sudo ufw status numbered
Do not reset an active firewall blindly. Preserve rules you recognise, investigate rules you do not, and add the policy below only after you know which existing services depend on them. The clean-install path shown here ends with one LAN SSH rule; an existing machine may deliberately retain more.
ufw, the Uncomplicated Firewall, is a front end for the firewall built into the Linux kernel. The package is available on Ubuntu and Debian; the commands above install it explicitly rather than assuming the image included it.
systemd will tell you this:
$ systemctl is-enabled ufw
$ systemctl is-active ufw
Compare the two systemd states with ufw status. An active unit means the service loaded its configured rules; it does not tell you whether the resulting policy permits everything. Only the firewall's own status and a network test settle that.
A running firewall service is not a firewall
This catches people in exactly the wrong direction: they check systemd, see green, and stop looking. Always ask the firewall itself.
Underneath ufw is netfilter, the packet filtering machinery in the kernel, which you talk to through iptables or its successor nftables. You do not need to be fluent in either, but you should be able to look:
$ sudo iptables -S
-P marks a built-in chain policy: the decision applied when no rule matched. Inspect the INPUT, FORWARD and OUTPUT policies and every jump before describing your starting position; an existing installation may already have UFW, Docker or another manager's chains.
Why an allowlist and not a blocklist #
There are two ways to write a firewall rule set. A blocklist allows everything and names what you want stopped, which means you have to think of everything: every service you forget, every port a package opens during an upgrade. An allowlist denies everything and names what you want through. Forget something in an allowlist and it breaks loudly, in front of you. Forget something in a blocklist and it stays open.
The first question in that diagram is the one people forget. Traffic that belongs to a connection you already established is allowed back in, and it does not need a rule of its own. That is why "deny all incoming" does not stop your server from downloading updates: the reply packets are part of a conversation the machine started, and the kernel tracks that. Without connection tracking, deny-by-default would break every outbound connection.
Setting the policy without cutting yourself off #
The order of the next three commands matters. Get it wrong and you lock yourself out.
Add the rule that keeps you in before you turn the policy on
Enabling a deny-by-default firewall over SSH, without an SSH allow rule already in place, cuts your own connection. Your session dies mid-command and the machine is now refusing new ones. Do the allow first, the enable second, and keep a second session open the whole time.
Set the defaults first. This writes them down without applying anything, because ufw is not enabled yet:
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
Now the rule that keeps you in the room. Use your own subnet, the one you wrote on the fact sheet in chapter 2:
$ sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp comment 'SSH from the LAN'
Read that as: from anywhere in my home network, to any address this machine has, on TCP port 22. The comment costs nothing and will one day tell you why a rule you no longer recognise exists.
Why scope it to a subnet rather than just allowing port 22?Show me why
ufw allow 22 opens SSH to anything that can route a packet to your machine. Today, on a home network behind a router that forwards nothing, the practical difference is small. It stops being small the moment you add a VPN, a second network interface, a guest network, or a router with a port forward somebody set up in 2019 and forgot.
Scoping to your own subnet costs you one extra clause and means the rule keeps meaning what you intended when the network around it changes. The cost is real, though: change subnets and the rule stops matching, which is why the next chapter puts it somewhere you can edit and re-apply in one command.
If your server is on wifi and you plan to move it to a cable
Wired and wireless can land you on different subnets, and will certainly give you a different interface name. If you move the machine later, this rule may stop matching and you will be locked out from the LAN. Check ip -brief addr after any such move, and consider adding a rule for the new subnet before you unplug anything.
Then, with a second session open and connected:
$ sudo ufw enable
Read the warning, confirm only after the safety session and SSH rule are in place, and require the command to report that the firewall is active.
Now look at what you have:
$ sudo ufw status verbose
You want to see the incoming policy as deny, the outgoing as allow, and one rule for port 22 scoped to your subnet. If any of those is not what you expect, fix it now while your second session is still alive.
I enabled the firewall and lost my connection
Work out which firewall rule cut off SSH and plan a safe recovery
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
Dropped or refused #
There are two ways to say no to a packet, and ufw exposes both.
deny drops it. Nothing goes back. The client sits there until it gives up, which takes a while.
reject refuses it. The kernel sends back an ICMP message, or a TCP reset, saying that nothing is listening. The client fails immediately with a clear error.
| What the sender sees | Time to fail | Reveals the machine exists | |
|---|---|---|---|
deny (drop) |
nothing at all | seconds, then a timeout | no |
reject (refuse) |
connection refused | instant | yes |
The usual argument for dropping is stealth, and you should be sceptical of it. A port scanner learns almost as much from a timeout as from a refusal, and anything that has already routed a packet to your address knows the address is in use. Dropping does not make your server invisible.
Dropping produces less reply traffic. Reflection and amplification require a service that sends useful replies to spoofed source addresses; a generic drop policy is not, by itself, a reflection-attack control.
What refusing buys you is a debugging experience that does not involve waiting thirty seconds to find out something is wrong. On a network where the only clients are yours, that is worth a lot.
A defensible split
Drop from the internet, refuse on your own network. You get quiet where the noise is and fast failures where you are the one doing the debugging. ufw supports both: deny and reject in the same rule set, scoped by source.
Your connection to a service on the server hangs for thirty seconds and then times out. What does that tell you?Show answer
Most likely something dropped your packet rather than refusing it: a firewall with a DROP policy on the path, or a route that goes nowhere. A refusal, by contrast, comes back instantly, and usually means the packet arrived at a machine that had nothing listening on that port.
That distinction is genuinely useful when debugging. Instant "connection refused" means you reached the host and the service is not there. A long hang means you probably did not reach the host at all, or something silently ate the packet in between.
Prove it from somewhere else #
You cannot test a firewall from behind it. Everything works from the machine itself, because packets to your own address never leave.
Go to your laptop:
$ nc -vz 192.168.1.20 22
$ nc -vz 192.168.1.20 8080
nc, or netcat, opens a connection and reports what happened. -v makes it say so, -z means do not send anything, just see whether it connects. The first should succeed. The second should hang until it gives up, because nothing is listening on 8080 and, more to the point, your policy would deny it even if something were.
If you have nmap to hand, it gives a sharper answer:
$ nmap -Pn -p 22,80,443,8080 192.168.1.20
Read the state column carefully, because the three words mean different things:
- open means something is listening and accepted your connection.
- closed means the machine answered and said nothing is there. Your packet reached it. That is a refusal.
- filtered means nothing came back at all. Something is dropping it, which is what a deny policy looks like from outside.
Going from closed to filtered on a port is how you see your policy take effect from the far side.
My rule looks right but the connection still fails
Work out where the packet is actually being stopped
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
sudo ufw status verbose shows the rule. I would like to work out whether the packet is being blocked at the firewall, never arriving, or being refused by something else.What you have not restricted #
Outgoing traffic. The default you set was allow outgoing, and it is worth being clear about what that means: anything running on this machine can open a connection to anywhere on the internet.
For a server that installs its own updates and will later fetch container images, that is the practical choice. But it is a real trade. If something on this machine is ever compromised, nothing at the network layer stops it from calling home, joining a botnet, or exfiltrating whatever it can read.
Locking down egress is possible and unpleasant: you need to know every host every service talks to, and that list changes when software updates. Most home servers, including this one, accept the risk and spend the effort on not being compromised in the first place. Make that trade knowingly rather than by default.
The rule that will not save you #
Everything in this chapter governs traffic arriving for this machine. Traffic routed through the machine to somewhere else takes a different path, and your INPUT rules do not apply to it.
That matters in chapter 6. Container runtimes route traffic rather than receive it. You will publish a port, it will be reachable from your whole network, and ufw status will still report incoming traffic as denied. Both are true. Do not try to fix it now.
Done when
-
sudo ufw status verboseshows incomingdeny, outgoingallow - The LAN-scoped SSH rule exists, and every other retained rule has a recorded purpose
-
sudo iptables -S | headshows rules where there used to be none -
nc -vz <server> 22from your laptop succeeds - A port you have not allowed hangs rather than refusing, or shows
filteredunder nmap - You can say which of
denyandrejectyou are using, and why - You still have a second SSH session open, and you closed it only after a fresh connection worked
What you picked up
- The default is accept. Deciding otherwise is your job.
- A running firewall service tells you nothing about whether any rules are loaded.
- Allowlists fail towards broken, blocklists towards open. You notice broken.
- Connection tracking is why deny-by-default does not break outbound traffic.
- Add the rule that keeps you in before you turn the policy on.
- Dropping is quiet, refusing is fast to debug, and stealth is not the reason to pick either.
- You cannot test a firewall from behind it.
INPUTrules do not govern traffic being routed through the machine, which matters in chapter 6.