Skip to content

Split View: 리눅스 방화벽과 접근 제어 완전 가이드: nftables, firewalld, ufw를 잠기지 않고 다루기

|

리눅스 방화벽과 접근 제어 완전 가이드: nftables, firewalld, ufw를 잠기지 않고 다루기

들어가며

방화벽 작업의 진짜 위험은 규칙을 잘못 쓰는 것이 아닙니다. 원격에서 규칙을 적용한 순간 자기 자신이 차단되어 되돌릴 수단이 없어지는 것입니다. 데이터 센터에 갈 수 없고 콘솔 접근 수단도 없다면 그 서버는 사실상 잃은 것입니다.

그래서 이 글은 규칙 문법보다 절차를 앞세웁니다. 무엇을 어떤 순서로 적용하고, 실수했을 때 어떻게 자동으로 되돌아오게 만드는지가 핵심입니다. 문법은 그다음입니다.

다루는 범위는 세 층입니다. 커널의 netfilter 위에 nftables가 있고, 그 위에 firewalld와 ufw 같은 관리 도구가 있습니다. 어느 도구를 쓰든 최종적으로는 같은 커널 훅에 규칙이 들어간다는 점을 이해하면 도구 간 이동이 쉬워집니다.

기준은 nftables 1.0 이상, firewalld 1.x, ufw 0.36 이상입니다. RHEL 계열 8 이상은 firewalld가 기본이고 백엔드는 nftables입니다. Debian/Ubuntu는 ufw가 관례이며 역시 nftables 위에서 동작합니다. iptables 명령이 남아 있는 배포판에서도 대개 nftables로 변환해 처리하는 호환 계층입니다.


1. 자신을 잠그지 않는 절차

기술적인 내용에 앞서 이 절차를 먼저 몸에 익히세요. 순서가 전부입니다.

절차 1 — 되돌리기 예약을 먼저 겁니다. 규칙을 적용하기 전에 "일정 시간 뒤 자동으로 원복"을 예약해 둡니다. 접속이 끊기면 예약이 살아나 원래 상태로 돌아옵니다.

# nftables: 현재 상태 저장 후 10분 뒤 자동 복구 예약
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

at이 없는 환경이라면 백그라운드 셸로도 같은 일을 할 수 있습니다.

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

절차 2 — 검증한 뒤 적용합니다. nftables는 파일 전체를 문법 검사만 할 수 있습니다.

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

-c는 문서에 따르면 적용하지 않고 유효성만 확인합니다. 반드시 거치세요.

절차 3 — 접속을 확인합니다. 기존 세션을 닫지 말고 새 터미널에서 접속해 봅니다.

절차 4 — 확인이 끝나면 예약을 취소하고 영구 저장합니다.

atq
sudo atrm 3
sudo systemctl enable --now nftables

firewalld에는 이 절차가 기능으로 들어 있습니다. --timeout을 주면 지정 시간 뒤 규칙이 자동으로 사라집니다.

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

문서에 따르면 --timeout--permanent와 함께 쓸 수 없습니다. 런타임 규칙에만 적용되기 때문입니다. 이 조합이 firewalld를 쓰는 가장 큰 실무적 이유 중 하나입니다.

긴급 상황용 명령도 알아 두세요.

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

--panic-on은 문서 표현대로 모든 수신·송신 패킷을 버립니다. 침해 대응 시 즉시 격리하는 용도이며, 원격에서 실행하면 그 즉시 접속이 끊깁니다. 콘솔 접근이 확보된 상태에서만 쓰세요.


2. nftables의 구조 — 테이블, 체인, 규칙

nftables는 세 계층으로 구성됩니다.

  • 테이블: 주소 패밀리별 컨테이너입니다. 패밀리는 ip, ip6, inet, arp, bridge, netdev입니다. inet은 IPv4와 IPv6를 함께 처리하므로 대부분의 경우 이것을 씁니다.
  • 체인: 규칙의 묶음입니다. 커널 훅에 붙는 베이스 체인과, 점프로만 도달하는 일반 체인이 있습니다.
  • 규칙: 매치 조건과 판정(verdict)의 조합입니다.

베이스 체인 선언에는 타입, 훅, 우선순위가 필요합니다.

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; }'

훅 이름은 prerouting, input, forward, output, postrouting, ingress, egress입니다. 우선순위는 키워드로 지정할 수 있으며 문서 기준 값은 raw(-300), mangle(-150), dstnat(-100), filter(0), security(50), srcnat(100)입니다.

기본 정책은 accept(기본값) 또는 drop입니다. policy drop을 선언하는 순간 그 훅으로 들어오는 모든 패킷이 막히므로, 반드시 허용 규칙을 먼저 넣고 정책을 바꾸는 순서를 지켜야 합니다.

규칙 추가는 다음과 같습니다.

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가장 먼저 와야 하는 규칙입니다. 이미 성립한 연결의 응답 패킷을 허용합니다. 이것이 없으면 서버가 밖으로 보낸 요청의 응답도 막힙니다.
  • iif lo accept는 루프백을 허용합니다. 이것이 빠지면 로컬 서비스 간 통신이 끊겨 원인을 찾기 어려운 장애가 납니다.
  • 판정은 accept, drop, reject, jump, goto입니다.

dropreject의 차이를 알아 두세요. drop은 응답 없이 버리므로 클라이언트가 타임아웃까지 기다립니다. reject는 거부 응답을 보내 즉시 실패합니다. 외부 노출 포트에는 스캔 대응상 drop이, 내부망에서는 진단 편의상 reject가 흔히 선택됩니다.

이 선택이 운영에 미치는 영향은 생각보다 큽니다. 내부 서비스 간 통신을 drop으로 막으면, 호출하는 쪽은 연결 타임아웃까지 스레드를 붙잡고 기다립니다. 타임아웃이 30초로 잡혀 있다면 그동안 커넥션 풀이 고갈되고, 결국 방화벽 규칙 하나가 서비스 전체의 지연으로 번집니다. 내부망에서는 빠르게 실패하는 편이 낫다는 원칙을 기억하세요.

주소와 포트를 집합으로 묶으면 규칙이 훨씬 간결해집니다. 집합은 별도로 정의해 여러 규칙에서 재사용할 수 있고, 내용만 갱신하면 규칙을 건드리지 않고 허용 대상을 바꿀 수 있습니다.

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은 대역 표기를 허용합니다. 관리자 IP 목록이 바뀔 때 규칙이 아니라 집합만 갱신하면 되므로 운영 실수가 줄어듭니다.


3. nftables 조회와 수정

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

-a는 각 규칙에 핸들 번호를 붙여 보여 줍니다. 삭제는 핸들로만 정확히 지정할 수 있으므로 이 옵션이 필수입니다.

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는 체인 끝에, insert는 앞쪽 또는 지정 위치에 넣습니다. 규칙은 위에서 아래로 평가되고 첫 매치에서 판정이 결정되므로 순서가 결과를 바꿉니다.

전체 초기화는 다음과 같습니다.

sudo nft flush ruleset

파괴적 명령 경고: 문서가 명시하듯 nft flush ruleset모든 테이블과 그 내용을 전부 제거합니다. 기본 정책이 drop인 체인이 사라지면 오히려 전면 개방 상태가 되고, 반대로 다른 도구가 관리하던 규칙까지 날아갑니다. 원격에서는 백업과 복구 예약 없이 절대 실행하지 마세요.

영구 설정은 파일로 관리하고 원자적으로 적용하는 것이 정석입니다.

#!/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;
  }
}

이 파일을 /etc/nftables/main.nft로 두고 검사 후 적용합니다.

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

nft -f파일 전체를 원자적으로 적용합니다. 중간에 실패하면 아무것도 적용되지 않으므로, 규칙 절반만 들어간 위험한 중간 상태가 생기지 않습니다. 이것이 명령을 한 줄씩 실행하는 것보다 안전한 이유입니다.


4. firewalld — 존 기반 관리

RHEL 계열의 기본 도구입니다. 인터페이스와 출발지를 존(zone) 에 배정하고, 존마다 허용 규칙을 둡니다.

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

가장 중요한 개념은 런타임과 영구 설정의 분리입니다.

# 런타임에만 적용 (재기동 시 사라짐)
sudo firewall-cmd --zone=public --add-service=https

# 영구 설정에만 반영 (지금은 적용 안 됨)
sudo firewall-cmd --permanent --zone=public --add-service=https

# 영구 설정을 런타임에 반영
sudo firewall-cmd --reload

# 현재 런타임 상태를 영구로 저장
sudo firewall-cmd --runtime-to-permanent

--runtime-to-permanent는 문서 표현대로 현재 활성 런타임 설정을 영구 설정에 덮어씁니다. 안전 절차는 명확합니다. 먼저 런타임에만 적용해 접속을 확인하고, 확인이 끝난 뒤에 영구로 저장하는 것입니다. 처음부터 --permanent를 쓰고 --reload하는 방식은 검증 단계가 없어 위험합니다.

포트와 출발지 지정입니다.

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'

영구 설정의 문법 검사도 있습니다.

sudo firewall-cmd --check-config

--reload--complete-reload의 차이도 알아 두세요. 문서에 따르면 --reload는 상태 정보를 유지하고, --complete-reload는 netfilter 커널 모듈까지 다시 적재합니다. --complete-reload는 연결 추적 상태를 잃어 기존 연결이 끊길 수 있으므로 문제 해결 목적에서만 쓰세요.


5. ufw — 단순함이 필요한 곳

Ubuntu의 기본 관리 도구입니다. 규칙 수가 적고 서버 역할이 단순할 때 적합합니다.

sudo ufw status
sudo ufw status verbose
sudo ufw status numbered

순서가 절대적으로 중요합니다. 기본 정책을 거부로 바꾸기 전에 SSH를 먼저 허용해야 합니다.

# 1. 먼저 SSH 허용
sudo ufw allow 22/tcp

# 2. 그다음 기본 정책 설정
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 3. 마지막에 활성화
sudo ufw enable

파괴적 명령 경고: 이 순서를 뒤집으면, 즉 SSH 허용 없이 default deny incoming 상태에서 ufw enable을 실행하면 원격 접속이 즉시 끊깁니다. ufw enable은 SSH 세션에서 실행할 때 경고를 보여 주지만, 확인 없이 진행하면 그대로 잠깁니다.

규칙 문법입니다.

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은 실제로 적용하지 않고 어떤 변경이 일어날지 보여 줍니다. 적용 전에 반드시 거치세요.
  • ufw limit은 속도 제한입니다. 문서에 따르면 한 IP가 30초 안에 6회 이상 연결을 시도하면 거부합니다. SSH 무차별 대입에 대한 간단한 완화책입니다.
  • sudo ufw delete NUM으로 지울 때는 먼저 status numbered로 번호를 확인해야 합니다. 번호는 규칙을 지울 때마다 재배열되므로, 여러 개를 지울 때는 큰 번호부터 지우세요.

로깅과 초기화입니다.

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

파괴적 명령 경고: ufw reset은 모든 규칙을 지우고 설치 기본값으로 되돌립니다. 원격에서 실행하면 접속이 끊길 수 있습니다.


6. 규칙이 적용되지 않을 때 — 진단 순서

"규칙을 넣었는데 여전히 막힌다" 또는 "막았는데 여전히 들어온다"의 진단은 계층별로 내려갑니다.

1단계 — 실제 커널 규칙을 확인합니다. 관리 도구의 출력이 아니라 nftables 자체를 봐야 합니다.

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

firewalld나 ufw를 쓰더라도 최종 규칙은 nft list ruleset에 나타납니다. 관리 도구가 의도한 대로 변환했는지 여기서 확인합니다.

2단계 — 규칙에 패킷이 걸리는지 셉니다. 카운터를 붙이면 확실합니다.

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

카운터가 0이면 그 규칙까지 패킷이 도달하지 못한 것입니다. 앞선 규칙에서 이미 판정되었거나, 패킷이 애초에 도착하지 않는 것입니다.

3단계 — 순서와 중복을 확인합니다. 앞쪽에 더 넓은 drop 규칙이 있으면 뒤의 accept는 평가되지 않습니다.

4단계 — 도구 충돌을 확인합니다. firewalld와 ufw를 동시에 켜거나, Docker가 만든 규칙과 섞이면 예측이 어려워집니다.

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

Docker는 자체적으로 규칙을 삽입합니다. 그리고 그 규칙은 호스트 방화벽의 입력 체인이 아니라 다른 체인에 들어가므로, 호스트 방화벽으로 막았다고 생각한 포트가 컨테이너를 통해 열려 있는 상황이 생깁니다. 컨테이너 포트 공개 정책은 방화벽과 별도로 검토해야 합니다.

5단계 — 상위 계층을 확인합니다. 클라우드라면 보안 그룹이나 네트워크 ACL이 먼저 막고 있을 수 있습니다. 호스트에 도달조차 하지 않는다면 호스트 방화벽은 무고합니다.

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

패킷이 전혀 보이지 않으면 네트워크 경로 문제이고, 보이는데 응답이 없으면 호스트 안의 문제입니다. 이 한 번의 확인이 조사 범위를 절반으로 줄입니다.

6단계 — 로그를 봅니다. 버려지는 패킷을 기록하도록 규칙을 추가할 수 있습니다.

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

limit rate를 함께 쓰지 않으면 로그가 폭주해 디스크를 채웁니다. 반드시 제한을 거세요.


7. 접근 제어의 나머지 계층

방화벽만으로 접근 제어가 완결되지 않습니다. 계층을 나눠 정리합니다.

계층수단특징
네트워크 경계클라우드 보안 그룹, ACL호스트에 도달 전 차단
호스트 방화벽nftables, firewalld, ufw포트와 출발지 단위
서비스 설정바인드 주소, 애플리케이션 ACL가장 확실한 차단
인증키, 인증서, 토큰누구인지 확인
권한사용자·그룹, SELinux, sudo무엇을 할 수 있는지

가장 효과적인 접근 제어는 방화벽이 아니라 바인드 주소입니다. 서비스가 루프백에만 바인드되어 있으면 방화벽 규칙이 없어도 외부에서 접근할 수 없습니다.

sudo ss -tlnp

0.0.0.0:5432처럼 모든 인터페이스에 열려 있는 서비스를 찾아, 정말 외부 접근이 필요한지 검토하세요. 필요 없다면 설정에서 127.0.0.1이나 내부 인터페이스 주소로 제한하는 것이 방화벽 규칙 한 줄보다 확실합니다.

sudo 권한도 접근 제어의 일부입니다.

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

visudo를 반드시 쓰세요. 문법 오류가 있는 sudoers 파일은 sudo 자체를 못 쓰게 만들며, 이것도 일종의 자기 잠금 사고입니다. visudo는 저장 전에 문법을 검사해 이 사고를 막아 줍니다. -c는 기존 파일의 문법만 검사합니다.

SSH 접근 제한과 방화벽을 함께 설계하는 방법은 SSH 운영 완전 가이드를 참고하세요.


퀴즈: 실력을 확인해 보세요

퀴즈 1: 원격 서버의 방화벽 규칙을 크게 바꿔야 합니다. 첫 번째로 할 일은?

정답: 현재 규칙을 저장하고 일정 시간 뒤 자동 복구를 예약합니다

설명: 규칙 작성보다 되돌리기 준비가 먼저입니다.

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

firewalld라면 --timeout 옵션이 같은 역할을 합니다.

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

접속이 끊겨도 시간이 지나면 원래 상태로 돌아오므로, 최악의 경우에도 서버를 잃지 않습니다.

퀴즈 2: ufw로 기본 거부 정책을 적용하려 합니다. 올바른 순서는?

정답: SSH 허용을 먼저 넣고, 기본 정책을 바꾸고, 마지막에 활성화합니다

설명: 순서를 뒤집으면 활성화되는 순간 접속이 끊깁니다.

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

--dry-run으로 적용될 규칙을 미리 확인하는 습관을 들이세요. 무차별 대입 완화가 필요하면 ufw limit ssh/tcp도 고려할 수 있습니다. 문서에 따르면 30초 안에 6회 이상 연결을 시도하는 IP를 거부합니다.

퀴즈 3: nftables 입력 체인에서 반드시 앞쪽에 있어야 하는 두 규칙은 무엇이고 왜인가요?

정답: 연결 상태 허용과 루프백 허용입니다

설명: 이 두 규칙이 없으면 정상 통신이 광범위하게 깨집니다.

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

첫 규칙이 없으면 서버가 밖으로 보낸 요청의 응답 패킷까지 막혀 패키지 설치나 API 호출이 전부 실패합니다. 둘째가 없으면 로컬 서비스 간 통신이 끊겨 원인을 찾기 어려운 장애가 납니다.

퀴즈 4: 방화벽으로 5432 포트를 막았는데 컨테이너의 데이터베이스에 외부에서 접속됩니다. 왜일까요?

정답: 컨테이너 런타임이 별도 체인에 자체 규칙을 삽입하기 때문입니다

설명: Docker 같은 런타임은 포트를 공개할 때 자체적으로 전달 관련 규칙을 넣습니다. 그 규칙은 호스트의 입력 체인과 다른 경로로 평가되므로, 입력 체인만 막아서는 차단되지 않습니다.

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

근본 해결은 컨테이너 포트를 모든 인터페이스가 아니라 루프백에만 공개하는 것입니다. 포트 공개 정책은 방화벽과 별도로 검토해야 합니다.

퀴즈 5: 규칙을 넣었는데 여전히 접속이 막힙니다. 원인을 어떻게 좁히나요?

정답: 카운터로 패킷이 규칙에 도달하는지 확인하고, tcpdump로 패킷이 호스트에 오는지 확인합니다

설명: 두 확인으로 조사 범위가 크게 줄어듭니다.

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

tcpdump에 패킷이 전혀 보이지 않으면 호스트에 도달하지 않는 것이므로 클라우드 보안 그룹이나 경로 문제입니다. 패킷은 보이는데 카운터가 0이면 앞선 규칙에서 이미 판정된 것이므로 순서를 봐야 합니다.

퀴즈 6: 방화벽 규칙 대신 더 확실하게 서비스 노출을 막는 방법은?

정답: 서비스를 루프백이나 내부 인터페이스에만 바인드합니다

설명: 방화벽은 규칙이 실수로 지워지거나 도구가 충돌하면 뚫립니다. 반면 서비스가 애초에 외부 인터페이스에서 대기하지 않으면 규칙과 무관하게 접근할 수 없습니다.

sudo ss -tlnp

0.0.0.0이나 [::]로 열린 포트를 목록으로 뽑아 하나씩 검토하세요. 관리 도구, 메트릭 엔드포인트, 데이터베이스가 전체 인터페이스에 열려 있는 경우가 흔합니다. 방화벽은 그 위에 얹는 두 번째 방어선으로 두는 것이 맞습니다.


마치며

방화벽 작업에서 기억할 것은 문법이 아니라 순서입니다. 되돌리기를 먼저 예약하고, 검사하고, 런타임에만 적용하고, 새 세션으로 확인하고, 그다음에 영구 저장합니다. 이 다섯 단계를 지키면 잠기는 사고는 일어나지 않습니다.

그리고 도구를 고를 때는 규모에 맞추세요. 단일 서버에 규칙 열 줄이면 ufw로 충분하고, 존과 서비스 개념이 필요하면 firewalld가 낫고, 규칙을 코드로 관리하고 원자적으로 배포해야 하면 nftables 파일이 정답입니다. 어느 쪽을 고르든 최종 결과는 nft list ruleset에서 확인할 수 있다는 점을 기억하면 도구 사이를 오가기 어렵지 않습니다.

마지막으로 한 가지만 더. 방화벽은 마지막 방어선이 아니라 여러 겹 중 하나입니다. 바인드 주소, 인증, 권한이 함께 설계되어야 실제로 안전해집니다.


참고 자료


이어서 읽기

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

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