RE:NODE

Security13 min read

The firewall rules that actually matter, and the ones that do not

Default deny, a short allow list, and the three ports people leave open without meaning to. With the ufw and nftables rules to copy and the audit commands.

Updated

0 readers

A firewall is a list of what is allowed, and everything else is refused. Written that way it sounds trivial, and the ruleset that protects a real server is genuinely short: one or two public ports, administrative access restricted to addresses you control, and nothing else.

The mistake is almost never a missing rule. It is a rule added during troubleshooting at three in the morning and never removed, or a service that was never supposed to be listening on a public interface and quietly is. This post is the ruleset worth having, in ufw and in nftables, the ports that get left open without anybody deciding to, the reason Docker ignores your firewall entirely, and what any of this means when your server runs on somebody else's panel rather than your own machine.

What a firewall does, and the shape of a ruleset that survives#

A host firewall decides, per packet, whether the kernel hands it to a listening process. That is a narrow job and it is worth being clear about what falls outside it.

It does not stop a volumetric flood. By the time a packet reaches your machine it has already crossed your uplink, so dropping it locally does not un-saturate the link it consumed. Filtering large floods has to happen upstream - what we do about attacks is the honest account of what that catches, and DDoS attacks on game servers explained covers the attack types. It does not help against traffic that looks exactly like a player, because at the packet level it is a player. And it does nothing about a weak password, an unpatched plugin or a malicious mod, which are the ways most servers are actually lost.

What it does do is reduce your exposed surface to the two or three things you deliberately publish. That is worth a great deal, and it takes about ten minutes.

Five rules make the skeleton, in this order:

  1. Accept established and related traffic. Connection tracking means the replies to connections you opened are allowed without a rule for each one. Without this, a default-deny policy blocks the answer to every request your server makes.
  2. Drop invalid packets. Traffic that does not belong to any tracked connection and is not a legitimate new one.
  3. Accept everything on loopback. Local services talk to each other over 127.0.0.1 and ::1. Blocking it breaks things in confusing ways.
  4. Allow the ICMP you need. Echo requests so people can ping you, and, critically, the messages that make path MTU discovery work. Blanket-dropping ICMPv6 breaks IPv6 outright - IPv6 and game servers has the detail on why.
  5. Then a short allow list, then deny everything else by default.
what survivesallowed ports onlylooks like a playerThe internetanything at allUpstream filteringvolumetric floodsHost firewalldefault denyContainer rulespublished portsGame serverlistening socketApplicationpasswords, limits
What decides whether a packet reaches your game

The rules, port by port#

Every rule answers three questions: which port, which protocol, and open to whom. The third is the one people skip, and it is the one that matters.

PortProtocolOpen toWhat it is
22TCPYour addresses onlySSH
80, 443TCPEverybodyWeb traffic
25565TCPEverybodyMinecraft Java
19132UDPEverybodyMinecraft Bedrock or Geyser
27015UDPEverybodySource engine game and query
2456-2457UDPEverybodyValheim game and query
25575TCPNobody, or one addressMinecraft RCON
5432TCPThe application onlyPostgreSQL
27017TCPThe application onlyMongoDB
6379TCPLoopback onlyRedis
9200TCPLoopback onlyElasticsearch and friends
8080, 3000TCPNobodyWhatever you bound while testing

Two entries there deserve a note. Query ports have to be open or your server runs perfectly and never appears in anybody's browser, which is a failure mode that looks nothing like a firewall problem - game server ports explained covers why. And Source engine RCON is TCP on the same port number the game uses over UDP, which means you cannot separate them by port number alone: the rule has to distinguish the protocols.

"Open to whom" has three useful values. Everybody, for the service you are actually publishing. A specific address or prefix, for administration. And loopback only, which is not a firewall rule at all but a binding decision in the service's own configuration, and is always better than a firewall rule when it is available.

Writing it with ufw#

ufw is a front end for iptables that makes the common ruleset four commands. On Debian and Ubuntu it is usually already installed.

bash
$ ufw default deny incoming$ ufw default allow outgoing$ ufw allow from 203.0.113.5 to any port 22 proto tcp comment 'ssh admin'$ ufw allow 25565/tcp comment 'minecraft java'$ ufw allow 2456:2457/udp comment 'valheim game and query'$ ufw enable

A few things about that sequence:

  • Add the SSH rule before you enable it. Enabling a default-deny policy over SSH without an SSH rule locks you out of the machine, and the console in your provider's panel is the only way back in.
  • A port range needs a protocol. ufw allow 2456:2457/udp is valid, ufw allow 2456:2457 is not.
  • Use `comment`. In six months, a rule you cannot explain is a rule you will leave in place out of fear. A comment turns that into a decision.
  • `ufw limit 22/tcp` allows SSH but throttles an address making more than six connections in thirty seconds. Useful if you must expose SSH to the world, which you should try not to.
  • IPv6 is a separate ruleset, and ufw only manages it when IPV6=yes is set in /etc/default/ufw. On modern installs it is on by default, and it is still worth checking.

Then read back what you built, because the list is the documentation:

bash
$ ufw status numberedStatus: active     To                         Action      From     --                         ------      ----[ 1] 22/tcp                     ALLOW IN    203.0.113.5     # ssh admin[ 2] 25565/tcp                  ALLOW IN    Anywhere        # minecraft java[ 3] 2456:2457/udp              ALLOW IN    Anywhere        # valheim$ ufw delete 3

Turn logging on with ufw logging low and the denied packets land in /var/log/ufw.log. It is noisy - the internet scans every address constantly - but it is how you find out that something you expected to work is being blocked. The ufw guide walks through the tool properly, and the first hour on a new VDS puts it in the order the rest of the setup wants.

The same rules in nftables#

If you are building something you will maintain for years, nftables is the better foundation. One inet table covers IPv4 and IPv6 in the same ruleset, which removes an entire category of "works on v4, silently dropped on v6" bug.

/etc/nftables.conf
#!/usr/sbin/nft -fflush rulesettable inet filter {  chain input {    type filter hook input priority filter; policy drop;    ct state established,related accept    ct state invalid drop    iif lo accept    ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept    ip6 nexthdr icmpv6 accept    tcp dport 22 ip saddr 203.0.113.5 accept    tcp dport { 80, 443 } accept    tcp dport 25565 accept    udp dport 2456-2457 accept  }  chain forward { type filter hook forward priority filter; policy drop; }  chain output  { type filter hook output  priority filter; policy accept; }}

Load it with nft -f /etc/nftables.conf and check it with nft list ruleset. Enable nftables.service so it survives a reboot, and test the ruleset from a second session before you close the one you are working in.

Docker, and why your rules are being ignored#

This surprises people badly enough to be worth its own section. A container with a published port is often reachable from the internet even though ufw status shows a deny-by-default policy and no rule for it.

The reason is that Docker writes its own rules directly into iptables, in the nat table and in chains that are consulted before the ones ufw manages. Publishing a port with -p 5432:5432 sets up a destination NAT that bypasses the INPUT chain entirely. Your firewall is not broken and it is not being consulted.

There are three ways out, and the first is the one to use:

  1. Publish to loopback. -p 127.0.0.1:5432:5432 binds the published port to the local interface only, so the container is reachable from the host and from nowhere else. In a Compose file that is "127.0.0.1:5432:5432".
  2. Write rules in the `DOCKER-USER` chain, which Docker consults before its own and does not overwrite. This works and it is easy to get wrong, because the packets have already been through NAT and the addresses are not the ones you expect.
  3. Do not publish the port at all. Containers on the same Docker network reach each other by name on the container port. If only your app talks to the database, the database needs no published port whatsoever.

Docker on a VDS covers the rest of the setup, and the same reasoning applies to any container runtime that manages its own networking.

The ports people leave open, and how to find them#

Three of these turn up again and again, and all three were opened by a reasonable person for a reasonable reason.

A database port opened to test a connection from home. Postgres on 5432 and MongoDB on 27017 get exposed for ten minutes to run a query from a desktop client, and then stay exposed. For Postgres, the firewall is only half the control anyway: listen_addresses in postgresql.conf decides which interfaces it binds, and pg_hba.conf decides who may authenticate and how. A line such as host all all 0.0.0.0/0 md5 in pg_hba.conf is the actual door, and closing the firewall while leaving that line in place is a fix that lasts exactly as long as the firewall rule does. The database security checklist is the whole list, and connecting to PostgreSQL remotely does it the safe way.

An RCON port opened for a tool that was uninstalled a year ago. RCON was designed in a more trusting era: in most implementations the password crosses the network with very little protection, and anybody holding it can do anything the console can. If you have a panel console, you do not need RCON exposed at all - RCON, safely makes that case in full.

An admin or debug interface bound to all interfaces because that was the default. A web admin on 8080, a metrics endpoint on 9090, a development server on 3000, a profiler left running after an investigation. These are found by mass scanners within hours of being opened, not days.

Finding them takes two commands. From the machine itself:

bash
$ ss -ltnp$ ss -lunp

Anything bound to 0.0.0.0 or [::] is listening on every interface. Anything on 127.0.0.1 is local-only and is not your problem. Go through the list and justify each entry out loud.

Then check from outside, because what the machine believes and what the internet can reach are different questions:

bash
$ nmap -Pn -p- --open node.example.com$ nmap -Pn -sU --top-ports 50 node.example.com

Scan your own servers only. The UDP scan is slow and less reliable than the TCP one by the nature of the protocol, which is also why UDP services are so often forgotten.

Do this quarterly and after any incident. If you cannot say what a rule or a listening socket is for, remove it and see who complains - that is a faster and more honest audit than reading documentation you wrote yourself.

Egress: the rules nobody writes#

Almost every ruleset allows all outbound traffic, and for most servers that is the right call. It is worth knowing what you are accepting.

A compromised process - a malicious mod, a plugin from a forum, a dependency with a backdoor - uses outbound connections for everything that comes next: fetching a second stage, joining a botnet, sending your data somewhere, or relaying spam. Nothing on the inbound side of your firewall is involved.

Two egress rules are cheap and worth having on almost any server:

  • Block outbound TCP 25. Nothing on a game or app server legitimately sends mail directly. This rule turns "our box was used to send spam and now our address is on a blocklist" into a log line.
  • Block outbound connections from your database container, if you run one. A database has no business making connections to the internet, and if it starts, you want to know.

Full egress allow-listing - naming every destination a server may reach - is realistic for an appliance and painful for a game server, where mods download from a dozen hosts you have never heard of. Judge it by what the server does. If a mod update breaks because it could not reach a CDN, the rules cost more than they saved - keeping a modded server clean is the wider version of that trade.

If any of this is already too late, what to do when your server is hacked has the containment order: isolate first, rotate credentials second, restore third, find the door last.

On a panel, the firewall is not yours#

If your server runs on a managed panel rather than a machine you own, none of the ufw and nftables material above applies to you, and that is mostly good news: the parts of the job that get people compromised are already handled and not editable by mistake.

What you control instead:

  • Which ports exist. Each plan states its allocations, and ports are added and removed on the server's Network tab, with query and RCON included. A port that is not allocated is not reachable, which is a firewall rule expressed as a list you can read at a glance.
  • Isolation. One container per server, with CPU as a hard throttle to the share bought. Your neighbours are not on your machine in any sense that matters.
  • The console instead of RCON. The panel console is the same capability behind an account you already protect properly, rather than a second credential on an open port.
  • Account security, which is now the real perimeter. TOTP two-factor with single-use recovery codes, bcrypt password hashes, captcha and rate limits on login, a session list you can sign out of, and API keys that can be restricted by address. Two-factor on your panel account is five minutes well spent.
  • Least privilege for people. Subusers, roles and teams with granular permissions - console only, files only, no billing - time-boxed access, and a per-server activity log. That is the control that matters once more than one person has access, and it is covered in subusers and least privilege.

Secrets are the other half of the perimeter and are frequently the weakest part: a database password in a public repository undoes every rule above. Environment variables and secrets covers keeping them out of the places people look first.

If you want the full firewall back, that is the argument for a VDS, along with everything else you then have to maintain yourself - choosing between a VDS and a game panel weighs the two honestly.

FAQ#

Do I need a firewall if my server only runs one game?

If it is your own machine, yes. A game server rarely runs alone: SSH, the package manager's services, a monitoring agent and whatever you installed to debug something last month are all listening. Default deny is what makes the list of exposed services equal to the list you intended.

Does a firewall protect me from DDoS attacks?

No. A dropped packet has already used your bandwidth, so a host firewall cannot help against a flood that saturates the link. Filtering that size of traffic happens upstream of the machine, and even then only the obvious volumetric and reflection traffic is distinguishable.

Should I change the SSH port from 22?

It reduces log noise from automated scanners and it is not security. Key-only authentication with PasswordAuthentication no, restricting the port to addresses you control, and fail2ban on top are what actually matters. Moving the port is optional and changes nothing about who gets in.

Why can people reach my container even though ufw denies everything?

Because Docker writes its own iptables rules that are consulted before the chains ufw manages, so a published port bypasses your policy. Publish to 127.0.0.1 instead of all interfaces, or do not publish the port at all if only another container needs it.

How often should I review my firewall rules?

Quarterly, and after every incident or migration. The review is short: list the rules, list what is actually listening with ss -ltnp, and delete anything you cannot justify in one sentence. Most rulesets shrink every time.


Comments

Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.

0/2000