Foundations / Chapter 4

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.

Side readingReading iptables outputEnough vocabulary to look under your firewall and understand what you are seeing, without becoming a netfilter expert.

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.

How the kernel decides what to do with an arriving packet A packet arrives. If it belongs to a connection this machine already started, it is let in. Otherwise, if an allow rule matches its source, port and protocol, it is let in. Otherwise the default policy decides, and yours now says deny. None of this applies to traffic being routed through the machine to somewhere else. A packet arrives Is it part of a conversation this machine already started? yes Let it in no rule needed no Does an allow rule match it? source, destination port, protocol yes Let it in you said so, explicitly no The default policy decides and yours now says deny Dropped nothing goes back None of this governs traffic being routed through the machine to somewhere else. That is a different path, and chapter 6 walks down it.
What happens to a packet arriving at your server, in the order the kernel asks the questions.

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.

I enabled ufw on my server and my SSH session died. New connections are not working either. I have physical access to the machine. I want to understand which rule I was missing rather than just turning the firewall off and walking away.

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.

I have added a ufw rule that I believe should allow a connection, and it is still not working from another machine. 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 verbose shows incoming deny, outgoing allow
  • The LAN-scoped SSH rule exists, and every other retained rule has a recorded purpose
  • sudo iptables -S | head shows rules where there used to be none
  • nc -vz <server> 22 from your laptop succeeds
  • A port you have not allowed hangs rather than refusing, or shows filtered under nmap
  • You can say which of deny and reject you 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.
  • INPUT rules do not govern traffic being routed through the machine, which matters in chapter 6.

Settings

Your values

The book is written with placeholder names so it makes sense to everybody. Put your own in and every chapter, every command and every copy-paste prompt updates to match.

Nothing here is sent anywhere. It is saved in this browser, so it comes back next time. A different browser or a private window gets the placeholders again.

Live preview

$ ssh admin@192.168.1.20
$ sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
$ sudo hostnamectl set-hostname homeserver
$ sudo timedatectl set-timezone Europe/Paris

Real commands from chapters 2, 3 and 4. They change as you type.

The account you log in as. Not root, and not necessarily the same name you use on your laptop.

Introduced in Chapter 2, Meet your server

The book's placeholder is admin

What the machine calls itself. You choose it, and it shows up in your shell prompt and your logs.

Introduced in Chapter 3, A safe front door

The book's placeholder is homeserver

The IP address your server has on your home network, from ip -brief addr.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.20

The address range and prefix shown by ip route or ip -brief addr, written in CIDR form. Copy the real prefix; do not guess /24.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.0/24

The address traffic goes to on its way out of your house, from ip route.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.1

In Region/City form, or Etc/UTC if you would rather read logs in UTC.

Introduced in Chapter 3, A safe front door

The book's placeholder is Europe/Paris

A registered name you control. Chapter 8 uses it for the LAN route; chapter 12 uses a separate private Tailscale name remotely.

Introduced in Chapter 8, One door, many rooms

The book's placeholder is example.com

The email identity allowed to administer the tagged server in your Tailscale policy.

Introduced in Chapter 10, Your own private network

The book's placeholder is you@example.com

The mailbox that should receive actionable home-server alerts.

Introduced in Chapter 14, Knowing it is alive

The book's placeholder is alerts@example.com

Once you save, the prose and the commands read with your names, the copy buttons copy your values, and the copy-paste prompts describe your machine accurately. That last one matters: an assistant told your network is 192.168.1.0/24 when it is not will send you chasing the wrong thing.

Anything you leave empty keeps the book's placeholder.