나는 이번에 내가 항상 리부트를 하게되면 nftables가 systemctl enable을 해뒀는데도 재부팅할 때면 그게 제대로 안 켜지는 문제가 있어서, 이번에는 로그를 뜯어보면서 분석해보기로 했다.
일단 먼저 journalctl로 네트워크 인터페이스와 nftables 관련 로그를 살펴보기로 했다:
alvin@alvinserver:~$ journalctl -b | grep -E "enp5s0|NetworkManager|networkd.*link|nftables"
이렇게 해서 보니까 다음과 같은 순서로 이벤트들이 발생하고 있었다:

enp5s0 인터페이스가 eth0에서 이름이 변경됨enp5s0 Link UPenp5s0 carrier 획득 및 2500Mbps 링크 활성화여기서 문제를 알 수 있었는데, 내 nftables 규칙을 보면 iifname "enp5s0"를 사용하고 있는데, nftables가 완료(21:06:41)된 시점에는 아직 인터페이스가 제대로 UP(21:06:42)되지도 않았고, IP 주소도 할당(21:06:49)되지 않은 상태였다.
더 자세히 알아보기 위해 systemd의 서비스 의존성을 확인해보았다:
systemd-analyze critical-chain systemd-networkd-wait-online.service
systemd-analyze critical-chain nftables.service

이걸 봐도 문제를 명확하게 알 수 있었다:
nftables.service: @251ms (+21ms)
systemd-networkd.service: @1.724s (+60ms)
systemd-networkd-wait-online.service: @1.728s (+8.876s)
의존성 체인을 보니:
이를 통해 알 수 있는 것은:
특히 중요한 점은 nftables가 systemd-networkd나 network-pre.target에 대한 의존성이 없이 단순히 journald.socket만 기다린다는 것이었다. 이는 네트워크 설정이 전혀 준비되지 않은 상태에서 nftables가 시작될 수 있다는 것을 의미했다.
그래서 현재 설정을 확인해보기 위해 sudo systemctl edit --full nftables를 실행해봤다:
[Unit]
Description=nftables
Documentation=man:nft(8) http://wiki.nftables.org
Wants=network-pre.target
Before=network-pre.target shutdown.target
Conflicts=shutdown.target
DefaultDependencies=no
[Service]
Type=oneshot
RemainAfterExit=yes
StandardInput=null
ProtectSystem=full
ProtectHome=true
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
ExecStop=/usr/sbin/nft flush ruleset
[Install]
WantedBy=sysinit.target
문제를 해결하기 위해 다음과 같은 새로운 설정을 만들었다:
[Unit]
Description=nftables
Documentation=man:nft(8) http://wiki.nftables.org
After=network.target systemd-networkd.service
Wants=network-online.target
Before=network-online.target
DefaultDependencies=yes
[Service]
Type=oneshot
RemainAfterExit=yes
StandardInput=null
ProtectSystem=full
ProtectHome=true
# 두 단계 로드 적용
ExecStartPre=/usr/sbin/nft -f /etc/nftables.conf.early
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
ExecStop=/usr/sbin/nft flush ruleset
[Install]
WantedBy=multi-user.target
이전 설정과 새로운 설정의 차이를 종합적으로 분석해보면:
이전 설정은:
Wants=network-pre.target
Before=network-pre.target
WantedBy=sysinit.target
DefaultDependencies=no
이렇게 되어있어서:
새로운 설정은:
After=network.target systemd-networkd.service
Wants=network-online.target
Before=network-online.target
WantedBy=multi-user.target
DefaultDependencies=yes
이렇게 바꿔서:
이전 설정:
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
새로운 설정:
ExecStartPre=/usr/sbin/nft -f /etc/nftables.conf.early
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
nftables.conf.early에는 기본적인 보안 규칙들(인터페이스 독립적인)을 넣고nftables.conf에는 완전한 규칙 세트(인터페이스 의존적인)를 넣었다특히 nftables.conf.early에는 다음과 같은 기본적인 보안 규칙들을 포함시켰다:
# /etc/nftables.conf.early
# 모든 IPv4/IPv6 테이블 초기화
flush ruleset
table inet filter {
# 기본 입력 체인
chain input {
type filter hook input priority filter;
# 기본 정책: 모든 것을 차단
policy drop;
# 로컬호스트만 허용
iifname "lo" accept
# 이미 연결된 세션만 허용 (기존 SSH 연결 유지 등)
ct state {established, related} accept
# 모든 새로운 연결 차단
ct state new drop
# 기타 모든 것 차단 및 로깅
log prefix "nftables-early-drop: " drop
}
# 포워딩 체인
chain forward {
type filter hook forward priority filter;
# 기본 정책: 모든 것을 차단
policy drop;
# 모든 포워딩 차단 및 로깅
log prefix "nftables-early-forward-drop: " drop
}
# 출력 체인
chain output {
type filter hook output priority filter;
# 기본 정책: 모든 것을 차단
policy drop;
# 로컬호스트 허용
oifname "lo" accept
# DNS 쿼리 허용 (시스템 기능 유지를 위해)
udp dport 53 ct state new accept
tcp dport 53 ct state new accept
# DHCP 클라이언트 허용
udp dport 67-68 ct state new accept
# 이미 연결된 세션만 허용
ct state {established, related} accept
# 나머지 모든 출력 차단 및 로깅
log prefix "nftables-early-output-drop: " drop
}
}
# NAT 테이블도 초기화하여 모든 NAT 차단
table ip nat {
chain prerouting {
type nat hook prerouting priority dnat;
policy accept;
}
chain postrouting {
type nat hook postrouting priority srcnat;
policy accept;
}
}
# IPv6 완전 차단을 위한 테이블
table ip6 filter {
chain input {
type filter hook input priority filter;
policy drop;
log prefix "nftables-early-ipv6-drop: " drop
}
chain forward {
type filter hook forward priority filter;
policy drop;
log prefix "nftables-early-ipv6-forward-drop: " drop
}
chain output {
type filter hook output priority filter;
policy drop;
log prefix "nftables-early-ipv6-output-drop: " drop
}
}


이렇게 변경하고 나서 실제 실행 순서를 보면:
이전 설정은:
새로운 설정은:
systemd 초기화 시작 (@243ms system.slice)
네트워크 기본 초기화
네트워크 인터페이스 활성화
nftables.service 시작 (@10.283s - basic.target 이후)
기본 방화벽 규칙 적용 (ExecStartPre)
# 엄격한 early 규칙 적용
# - 모든 새로운 연결 차단
# - established/related 연결만 허용
# - DHCP/DNS 기본 통신만 허용
완전한 방화벽 규칙 적용 (ExecStart)
# 기존 nftables.conf 규칙 적용
# - 인터페이스 기반 규칙 (enp5s0)
# - SSH, VPN 등 포트 기반 규칙
# - 로깅 및 보안 정책
이렇게 바꾸고 나니 장단점이 명확했다:
이전 설정은:
새로운 설정은:
결과적으로 이런 변경은 전체적으로 더 안정적이고 예측 가능한 방화벽 동작을 제공하게 되었고, 특히 인터페이스 기반 규칙이 많은 내 환경에서는 큰 도움이 되었다. 이번 경험을 통해 systemd의 서비스 의존성과 타이밍이 얼마나 중요한지 다시 한번 깨달을 수 있었다.