Skip to content

필사 모드: A complete guide to Linux firewalls and access control: handling nftables, firewalld, and ufw without locking yourself out

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction

The real danger in firewall work is not writing a rule incorrectly. It is the moment you apply a rule remotely, block yourself, and have no way left to undo it. If you cannot get to the data center and have no console access, that server is effectively lost.

So this article puts procedure ahead of rule syntax. What you apply in which order, and how to make things roll back automatically when you get it wrong, is the core. Syntax comes after that.

The scope covers three layers. On top of the kernel netfilter there is nftables, and on top of that there are management tools such as firewalld and ufw. Once you understand that whichever tool you use, the rules ultimately land on the same kernel hooks, moving between tools becomes easy.

The baseline is nftables 1.0 or later, firewalld 1.x, and ufw 0.36 or later. On RHEL-family 8 and later, firewalld is the default and its backend is nftables. On Debian and Ubuntu, ufw is the convention, and it also runs on top of nftables. Even on distributions where the iptables command still exists, it is usually a compatibility layer that translates into nftables.


1. The procedure that keeps you from locking yourself out

Before the technical material, get this procedure into muscle memory. The order is everything.

Step 1 — schedule the rollback first. Before you apply rules, schedule an automatic restore for some time later. If your connection drops, the schedule fires and the box returns to its original state.

# nftables: save the current state, then schedule an automatic restore in 10 minutes
sudo nft list ruleset > /root/nft-backup-2026-08-15.nft
echo "nft -f /root/nft-backup-2026-08-15.nft" | sudo at now + 10 minutes

In an environment without at, a background shell does the same job.

sudo setsid bash -c 'sleep 600; nft -f /root/nft-backup-2026-08-15.nft' >/dev/null 2>&1 &

Step 2 — validate, then apply. nftables can syntax-check an entire file.

sudo nft -c -f /etc/nftables/main.nft

According to the documentation, -c checks validity only, without applying. Always go through it.

Step 3 — verify connectivity. Do not close your existing session; try connecting from a new terminal.

Step 4 — once verification is done, cancel the schedule and save permanently.

atq
sudo atrm 3
sudo systemctl enable --now nftables

firewalld has this procedure built in as a feature. Give it --timeout and the rule disappears automatically after the specified time.

sudo firewall-cmd --zone=public --add-port=8080/tcp --timeout=5m

According to the documentation, --timeout cannot be used together with --permanent. That is because it applies to runtime rules only. This combination is one of the biggest practical reasons to use firewalld.

Know the emergency commands as well.

sudo firewall-cmd --panic-on
sudo firewall-cmd --panic-off

As the documentation puts it, --panic-on drops all incoming and outgoing packets. It exists for immediate isolation during incident response, and running it remotely cuts your connection on the spot. Use it only when console access is secured.


2. The structure of nftables — tables, chains, rules

nftables is composed of three layers.

  • table: a container per address family. The families are ip, ip6, inet, arp, bridge, and netdev. inet handles IPv4 and IPv6 together, so this is the one you use in most cases.
  • chain: a bundle of rules. There are base chains attached to kernel hooks, and regular chains reachable only through a jump.
  • rule: a combination of match conditions and a verdict.

Declaring a base chain requires a type, a hook, and a priority.

sudo nft add table inet filter
sudo nft add chain inet filter input '{ type filter hook input priority filter; policy drop; }'
sudo nft add chain inet filter forward '{ type filter hook forward priority filter; policy drop; }'
sudo nft add chain inet filter output '{ type filter hook output priority filter; policy accept; }'

The hook names are prerouting, input, forward, output, postrouting, ingress, and egress. Priorities can be given as keywords, and the documented values are raw (-300), mangle (-150), dstnat (-100), filter (0), security (50), and srcnat (100).

The default policy is accept (the default value) or drop. The moment you declare policy drop, every packet entering that hook is blocked, so you must keep the order of inserting allow rules first and changing the policy afterward.

Adding rules looks like this.

sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input iif lo accept
sudo nft add rule inet filter input tcp dport 22 accept
sudo nft add rule inet filter input ip saddr 10.0.0.0/8 tcp dport 5432 accept
sudo nft add rule inet filter input ip protocol icmp accept
  • ct state established,related accept is the rule that has to come first. It permits the response packets of connections that are already established. Without it, even the responses to requests the server sent outbound are blocked.
  • iif lo accept permits loopback. Leave it out and communication between local services breaks, producing a failure whose cause is hard to track down.
  • The verdicts are accept, drop, reject, jump, and goto.

Know the difference between drop and reject. drop discards without a response, so the client waits until it times out. reject sends a refusal response and fails immediately. For externally exposed ports, drop is commonly chosen to frustrate scanning; on internal networks, reject is commonly chosen for diagnostic convenience.

The operational impact of that choice is larger than you would expect. If you block communication between internal services with drop, the calling side holds a thread waiting until the connection times out. If the timeout is set to 30 seconds, the connection pool drains during that window, and in the end a single firewall rule spreads into latency across the whole service. Remember the principle that failing fast is better on an internal network.

Grouping addresses and ports into sets makes rules considerably more concise. A set can be defined separately and reused from several rules, and updating only its contents changes who is allowed without touching any rule.

sudo nft add set inet filter admin_ips '{ type ipv4_addr; flags interval; }'
sudo nft add element inet filter admin_ips '{ 10.0.3.0/24, 10.0.9.7 }'
sudo nft add rule inet filter input ip saddr @admin_ips tcp dport 22 accept
sudo nft list set inet filter admin_ips

flags interval permits range notation. When the administrator IP list changes you update only the set rather than the rules, which reduces operational mistakes.


3. Querying and modifying nftables

sudo nft list ruleset
sudo nft list table inet filter
sudo nft -a list ruleset
sudo nft list chain inet filter input

-a shows a handle number attached to each rule. Deletion can only be targeted precisely by handle, so this option is essential.

sudo nft -a list chain inet filter input
sudo nft delete rule inet filter input handle 5
sudo nft insert rule inet filter input position 4 tcp dport 443 accept

add puts a rule at the end of the chain; insert puts it at the front or at a specified position. Rules are evaluated top to bottom and the verdict is decided at the first match, so order changes the outcome.

A full reset looks like this.

sudo nft flush ruleset

Destructive command warning: as the documentation states explicitly, nft flush ruleset removes every table and all of their contents. If a chain whose default policy is drop disappears, you end up wide open instead; conversely, the rules another tool was managing get wiped out too. Never run it remotely without a backup and a scheduled restore.

The standard approach is to manage permanent configuration as a file and apply it atomically.

#!/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
    ip protocol icmp accept
    ip6 nexthdr ipv6-icmp accept

    ip saddr 10.0.0.0/8 tcp dport 22 accept
    tcp dport { 80, 443 } accept

    counter comment "dropped"
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}

Put this file at /etc/nftables/main.nft, check it, then apply it.

sudo nft -c -f /etc/nftables/main.nft
sudo nft -f /etc/nftables/main.nft
sudo systemctl enable nftables

nft -f applies the whole file atomically. If it fails partway, nothing is applied, so you never get a dangerous intermediate state with only half the rules loaded. That is why it is safer than running commands one line at a time.


4. firewalld — zone-based management

This is the default tool on the RHEL family. It assigns interfaces and sources to a zone, and each zone holds its own allow rules.

sudo firewall-cmd --state
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
sudo firewall-cmd --zone=public --list-all

The most important concept is the separation of runtime and permanent configuration.

# Runtime only (disappears on restart)
sudo firewall-cmd --zone=public --add-service=https

# Permanent configuration only (not applied right now)
sudo firewall-cmd --permanent --zone=public --add-service=https

# Apply the permanent configuration to the runtime
sudo firewall-cmd --reload

# Save the current runtime state as permanent
sudo firewall-cmd --runtime-to-permanent

As the documentation puts it, --runtime-to-permanent overwrites the permanent configuration with the currently active runtime configuration. The safe procedure is clear. Apply to the runtime only first and verify connectivity, then save permanently once verification is done. Using --permanent from the start and then running --reload is dangerous because it has no verification step.

Specifying ports and sources.

sudo firewall-cmd --zone=public --add-port=8080/tcp
sudo firewall-cmd --zone=internal --add-source=10.0.0.0/8
sudo firewall-cmd --zone=public --remove-service=cockpit
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.3.0/24" port port="5432" protocol="tcp" accept'

There is a syntax check for the permanent configuration as well.

sudo firewall-cmd --check-config

Know the difference between --reload and --complete-reload too. According to the documentation, --reload preserves state information, while --complete-reload reloads the netfilter kernel modules as well. --complete-reload loses connection tracking state and can drop existing connections, so use it only for troubleshooting.


5. ufw — where simplicity is what you need

This is the default management tool on Ubuntu. It suits cases where the rule count is small and the server role is simple.

sudo ufw status
sudo ufw status verbose
sudo ufw status numbered

Order is absolutely critical. You must allow SSH before you switch the default policy to deny.

# 1. Allow SSH first
sudo ufw allow 22/tcp

# 2. Then set the default policy
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 3. Enable it last
sudo ufw enable

Destructive command warning: reverse this order — that is, run ufw enable in a default deny incoming state without allowing SSH — and remote access is cut instantly. ufw enable does show a warning when it is run from an SSH session, but if you proceed without checking, you get locked out all the same.

The rule syntax.

sudo ufw allow 80/tcp
sudo ufw allow from 10.0.0.0/8 to any port 22 proto tcp
sudo ufw limit ssh/tcp
sudo ufw deny 3306
sudo ufw delete 3
sudo ufw insert 1 allow from 10.0.3.10
sudo ufw --dry-run allow 8080/tcp
  • --dry-run shows what changes would occur without actually applying them. Always go through it before applying.
  • ufw limit is rate limiting. According to the documentation, it denies an IP that attempts six or more connections within 30 seconds. It is a simple mitigation against SSH brute force.
  • When deleting with sudo ufw delete NUM, check the number with status numbered first. The numbers are renumbered every time a rule is deleted, so when deleting several, work from the highest number down.

Logging and reset.

sudo ufw logging on
sudo ufw logging medium
sudo ufw reload
sudo ufw reset

Destructive command warning: ufw reset deletes every rule and returns to the installation defaults. Run it remotely and your connection may be cut.


6. When rules do not take effect — the diagnostic order

Diagnosing "I added the rule and it is still blocked" or "I blocked it and traffic still gets in" works its way down layer by layer.

Step 1 — check the actual kernel rules. You have to look at nftables itself, not at the output of the management tool.

sudo nft list ruleset
sudo iptables -L -n -v --line-numbers
sudo iptables-save | head -40

Even if you use firewalld or ufw, the final rules appear in nft list ruleset. This is where you confirm the management tool translated things the way you intended.

Step 2 — count whether packets hit the rule. Attaching a counter makes it certain.

sudo nft add rule inet filter input tcp dport 8080 counter accept
sudo nft list chain inet filter input

If the counter is 0, packets never reached that rule. Either an earlier rule already decided the verdict, or the packets are not arriving in the first place.

Step 3 — check order and duplication. If there is a broader drop rule earlier on, the accept behind it is never evaluated.

Step 4 — check for tool conflicts. Turning on firewalld and ufw at the same time, or mixing with the rules Docker created, makes behavior hard to predict.

systemctl is-active firewalld ufw nftables iptables
sudo nft list tables

Docker inserts rules of its own. And those rules go into a chain other than the host firewall input chain, so a port you believed you had blocked with the host firewall ends up open through the container. Container port publishing policy has to be reviewed separately from the firewall.

Step 5 — check the layers above. In the cloud, a security group or a network ACL may be blocking first. If traffic does not even reach the host, the host firewall is innocent.

sudo tcpdump -ni eth0 'tcp port 8080' -c 20

If you see no packets at all, it is a network path problem; if you see them but get no response, the problem is inside the host. This one check cuts the investigation scope in half.

Step 6 — look at the logs. You can add a rule that records the packets being dropped.

sudo nft add rule inet filter input limit rate 5/minute log prefix '"nft-drop: "' drop
sudo journalctl -k -f -g 'nft-drop'

Without limit rate alongside it, the logs flood and fill the disk. Always apply a limit.


7. The remaining layers of access control

Access control is not complete with a firewall alone. Here it is broken out by layer.

LayerMechanismCharacteristics
Network boundaryCloud security groups, ACLsBlocks before the host
Host firewallnftables, firewalld, ufwPer port and source
Service configurationBind address, application ACLsThe most certain block
AuthenticationKeys, certificates, tokensConfirms who you are
AuthorizationUsers and groups, SELinux, sudoWhat you are allowed to do

The most effective access control is not the firewall but the bind address. If a service is bound only to loopback, it cannot be reached from outside even with no firewall rule at all.

sudo ss -tlnp

Find the services open on every interface, such as 0.0.0.0:5432, and review whether external access is genuinely needed. If it is not, restricting it in the configuration to 127.0.0.1 or to an internal interface address is more certain than a single firewall rule.

sudo privileges are part of access control too.

sudo -l
sudo visudo -c
sudo visudo -f /etc/sudoers.d/deploy

Always use visudo. A sudoers file with a syntax error makes sudo itself unusable, and that is its own kind of self-lockout accident. visudo checks the syntax before saving and prevents that accident. -c only checks the syntax of an existing file.

For how to design SSH access restrictions together with the firewall, see A complete guide to SSH operations.


Quiz: check your understanding

Quiz 1: You need to make a sweeping change to the firewall rules on a remote server. What is the first thing you do?

Answer: save the current rules and schedule an automatic restore for some time later

Explanation: preparing the rollback comes before writing the rules.

sudo nft list ruleset > /root/nft-backup-2026-08-15.nft
echo "nft -f /root/nft-backup-2026-08-15.nft" | sudo at now + 10 minutes

With firewalld, the --timeout option plays the same role.

sudo firewall-cmd --zone=public --add-port=8080/tcp --timeout=5m

Even if your connection drops, the box returns to its original state once the time passes, so you do not lose the server even in the worst case.

Quiz 2: You want to apply a default deny policy with ufw. What is the correct order?

Answer: allow SSH first, then change the default policy, and enable it last

Explanation: reverse the order and the connection drops the instant it is enabled.

sudo ufw allow 22/tcp
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw --dry-run enable
sudo ufw enable

Build the habit of previewing the rules that will be applied with --dry-run. If you need brute-force mitigation, ufw limit ssh/tcp is worth considering as well. According to the documentation, it denies an IP that attempts six or more connections within 30 seconds.

Quiz 3: Which two rules must sit near the front of an nftables input chain, and why?

Answer: the connection state allow and the loopback allow

Explanation: without these two rules, normal communication breaks on a broad scale.

sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input iif lo accept

Without the first rule, even the response packets to requests the server sent outbound are blocked, so package installs and API calls all fail. Without the second, communication between local services breaks and you get a failure whose cause is hard to track down.

Quiz 4: You blocked port 5432 with the firewall, but the database in a container is still reachable from outside. Why?

Answer: because the container runtime inserts its own rules into a separate chain

Explanation: runtimes like Docker insert their own forwarding-related rules when they publish a port. Those rules are evaluated on a different path than the host input chain, so blocking only the input chain does not stop the traffic.

sudo nft list ruleset | grep -i -A5 'docker'
sudo ss -tlnp | grep 5432

The fundamental fix is to publish the container port only on loopback rather than on every interface. Port publishing policy has to be reviewed separately from the firewall.

Quiz 5: You added the rule and the connection is still blocked. How do you narrow down the cause?

Answer: use a counter to check whether packets reach the rule, and tcpdump to check whether packets reach the host

Explanation: those two checks shrink the investigation scope dramatically.

sudo nft add rule inet filter input tcp dport 8080 counter accept
sudo nft list chain inet filter input
sudo tcpdump -ni eth0 'tcp port 8080' -c 20

If tcpdump shows no packets at all, they are not reaching the host, so it is a cloud security group or a routing problem. If packets are visible but the counter is 0, an earlier rule already decided the verdict, so look at the order.

Quiz 6: What blocks service exposure more reliably than a firewall rule?

Answer: bind the service only to loopback or to an internal interface

Explanation: a firewall is breached if a rule gets deleted by mistake or if tools conflict. By contrast, if the service never listens on the external interface in the first place, it cannot be reached regardless of the rules.

sudo ss -tlnp

Pull a list of the ports opened on 0.0.0.0 or [::] and review them one at a time. Management tools, metrics endpoints, and databases are commonly open on every interface. The firewall belongs on top of that as a second line of defense.


Closing

What matters in firewall work is not syntax but order. Schedule the rollback first, validate, apply to the runtime only, verify with a new session, and then save permanently. Follow these five steps and lockout accidents do not happen.

And choose the tool to match the scale. Ten rules on a single server and ufw is enough; if you need the concepts of zones and services, firewalld is better; if you have to manage rules as code and deploy them atomically, an nftables file is the right answer. Whichever you pick, remembering that the final result can be verified with nft list ruleset makes moving between tools easy enough.

One last thing. A firewall is not the last line of defense but one of several layers. Bind addresses, authentication, and authorization have to be designed together for the system to actually be safe.


References


Further reading

현재 단락 (1/226)

The real danger in firewall work is not writing a rule incorrectly. It is **the moment you apply a r...

작성 글자: 0원문 글자: 18,890작성 단락: 0/226