Skip to content
Networking

Firewall configuration

Deny inbound by default and open only the ports you serve, in ufw, firewalld or nftables directly — plus how not to lock yourself out doing it.

9 min readUpdated

One principle: deny inbound by default, allow outbound, open exactly the ports you serve. Everything below implements that.

ufw (Debian, Ubuntu)

apt -y install ufw
ufw default deny incoming
ufw default allow outgoing

ufw allow 22/tcp comment 'ssh'
ufw allow 80/tcp comment 'http'
ufw allow 443/tcp comment 'https'

ufw limit 22/tcp          # rate-limit repeated SSH attempts
ufw --force enable
ufw status verbose

ufw applies rules to IPv4 and IPv6 together, which is what you want and what people most often get wrong when hand-rolling rules.

firewalld (AlmaLinux, Rocky, Fedora)

systemctl enable --now firewalld
firewall-cmd --set-default-zone=drop
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
firewall-cmd --list-all

nftables directly

ufw and firewalld both compile down to nftables. Writing it yourself costs you their convenience and buys you a ruleset you can read end to end. Below is the shape we use on our own machines: one table per address family, so the IPv6 side is a thing you wrote rather than a thing you hoped a front end did for you.

# /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset

table ip guard {
  chain services {
    tcp dport { 80, 443 } counter accept comment "web"
    tcp dport 22 counter accept comment "ssh"
  }

  chain edge_in {
    type filter hook input priority filter; policy drop;

    iif "lo" counter accept comment "loopback, unconditionally"
    ct state invalid counter drop comment "no conntrack entry, no entry"
    ct state { established, related } counter accept comment "our own conversations"
    icmp type { echo-request, echo-reply, destination-unreachable,
                time-exceeded, parameter-problem } counter accept
    ct state new jump services
  }

  chain edge_fwd { type filter hook forward priority filter; policy drop; }
  chain edge_out { type filter hook output  priority filter; policy accept; }
}

table ip6 guard {
  chain services {
    tcp dport { 80, 443 } counter accept comment "web"
    tcp dport 22 counter accept comment "ssh"
  }

  chain edge_in {
    type filter hook input priority filter; policy drop;

    iif "lo" counter accept comment "loopback, unconditionally"
    ct state invalid counter drop comment "no conntrack entry, no entry"
    ct state { established, related } counter accept comment "our own conversations"
    icmpv6 type { echo-request, echo-reply, destination-unreachable,
                  packet-too-big, time-exceeded, parameter-problem,
                  nd-router-solicit, nd-router-advert,
                  nd-neighbor-solicit, nd-neighbor-advert,
                  mld-listener-query } counter accept comment "RFC 4890 floor"
    ct state new jump services
  }

  chain edge_fwd { type filter hook forward priority filter; policy drop; }
  chain edge_out { type filter hook output  priority filter; policy accept; }
}
nft -c -f /etc/nftables.conf    # syntax check FIRST
systemctl enable --now nftables

Reading order matters and this file is written to be read in order: trust the loopback, throw away anything conntrack cannot place, wave through traffic you started, keep ICMP working, and only then look at what a stranger is asking for. The services chain is the one place you edit when you open a port, which is the point of splitting it out — you are never editing a chain whose policy is drop.

priority filter is the named form of priority 0, and the counter on every rule costs nothing measurable while making nft list ruleset tell you which rule is doing the work. There is no rate limit on port 22 here on purpose; that job belongs to fail2ban in the SSH hardening guide, and doing it in both places means two answers to the question of why a login was refused.

Do not skip nft -c. A policy drop input chain with a typo locks you out instantly.

The ICMPv6 list is not padding. Neighbour discovery is how IPv6 resolves a link-layer address at all, and packet-too-big is how path MTU discovery works. Drop those and you get large requests that hang, transfers that stall at a fixed size, and intermittent failures that look nothing like a firewall problem. If you tighten anything in this ruleset, tighten it last.

Locking yourself out

Every NimbusVPS server has a browser KVM console in the control panel that works regardless of network configuration. That is your recovery path. Test that you can reach it before you need it.

A safer habit for remote changes

# Applies rules, then reverts in 5 minutes unless you cancel
( sleep 300 && nft flush ruleset ) &
REVERT=$!
nft -f /etc/nftables.conf
# ... confirm you still have access ...
kill $REVERT

Deploy in the next five minutes.

Pick a location, size the box, pay in crypto. No account signup wall, no ID, no waiting on a human.

7-day money-back guarantee · No KYC · Cancel any time from the panel