nftables 부팅 문제 해결기

나는 이번에 내가 항상 리부트를 하게되면 nftables가 systemctl enable을 해뒀는데도 재부팅할 때면 그게 제대로 안 켜지는 문제가 있어서, 이번에는 로그를 뜯어보면서 분석해보기로 했다.

일단 먼저 journalctl로 네트워크 인터페이스와 nftables 관련 로그를 살펴보기로 했다:

alvin@alvinserver:~$ journalctl -b | grep -E "enp5s0|NetworkManager|networkd.*link|nftables"

이렇게 해서 보니까 다음과 같은 순서로 이벤트들이 발생하고 있었다:

  • 21:06:40 - enp5s0 인터페이스가 eth0에서 이름이 변경됨
  • 21:06:40 - nftables 서비스 시작
  • 21:06:41 - nftables 서비스 완료
  • 21:06:42 - enp5s0 Link UP
  • 21:06:45 - enp5s0 carrier 획득 및 2500Mbps 링크 활성화
  • 21:06:47 - IPv6LL 주소 획득
  • 21:06:49 - DHCPv4로 주소 할당

여기서 문제를 알 수 있었는데, 내 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는 단순히 journald.socket에만 의존하고 있고
  • networkd는 network-pre.target를 기다리며
  • networkd-wait-online은 networkd 서비스를 기다리고 있었다

이를 통해 알 수 있는 것은:

  1. nftables가 너무 일찍(251ms) 시작되고 있다는 것
  2. networkd는 그보다 훨씬 나중(1.724s)에 시작된다는 것
  3. 네트워크가 완전히 준비되는 건 더 나중(1.728s + 8.876s = 약 10.6s)이라는 것

특히 중요한 점은 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

이전 설정과 새로운 설정의 차이를 종합적으로 분석해보면:

  1. 시작 타이밍과 의존성이 변경되었다:

이전 설정은:

Wants=network-pre.target
Before=network-pre.target
WantedBy=sysinit.target
DefaultDependencies=no

이렇게 되어있어서:

  • 매우 이른 시점(sysinit.target)에서 실행되고
  • 네트워크 초기화 이전에 실행되며
  • 기본 의존성 없이 동작했다

새로운 설정은:

After=network.target systemd-networkd.service
Wants=network-online.target
Before=network-online.target
WantedBy=multi-user.target
DefaultDependencies=yes

이렇게 바꿔서:

  • 네트워크 기본 초기화 후에 실행되고
  • systemd-networkd 서비스 완료 후 실행되며
  • 완전한 시스템 초기화 시점에 실행되도록 했다
  1. 규칙 로드 방식도 변경했다:

이전 설정:

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


이렇게 변경하고 나서 실제 실행 순서를 보면:

이전 설정은:

  1. systemd 초기화 시작
  2. nftables 규칙 로드 시도 (약 251ms)
    • 인터페이스가 준비되지 않은 상태
    • iifname 규칙이 실패할 수 있음
  3. 네트워크 초기화 (약 1.7초)
  4. 네트워크 인터페이스 활성화 (약 10.6초)

새로운 설정은:

  1. systemd 초기화 시작 (@243ms system.slice)

    • journald.socket (@266ms)
    • lvm2-monitor.service (@281ms)
    • local-fs-pre.target (@369ms)
  2. 네트워크 기본 초기화

    • ufw.service (@743ms)
    • network-pre.target (@1.946s)
    • systemd-networkd.service (@1.950s)
  3. 네트워크 인터페이스 활성화

    • systemd-networkd-wait-online.service 시작 (@1.997s)
    • 네트워크 인터페이스 준비 완료될 때까지 대기 (+8.114s)
    • 네트워크 완전 활성화 (@~10.111s)
  4. nftables.service 시작 (@10.283s - basic.target 이후)

    • 기본 방화벽 규칙 적용 (ExecStartPre)

      # 엄격한 early 규칙 적용
      # - 모든 새로운 연결 차단
      # - established/related 연결만 허용
      # - DHCP/DNS 기본 통신만 허용
    • 완전한 방화벽 규칙 적용 (ExecStart)

      # 기존 nftables.conf 규칙 적용
      # - 인터페이스 기반 규칙 (enp5s0)
      # - SSH, VPN 등 포트 기반 규칙
      # - 로깅 및 보안 정책
  • 네트워크 초기화는 약 2초에 시작해서 10초 정도에 완료
  • nftables는 네트워크가 완전히 준비된 이후인 10.283초에 시작
  • 전체 과정이 약 10.6초 정도 소요

이렇게 바꾸고 나니 장단점이 명확했다:

이전 설정은:

  • 장점:
    • 빠른 초기 실행
    • 단순한 설정
  • 단점:
    • 규칙 적용이 실패할 수 있음
    • 인터페이스 의존적 규칙에 문제가 있음
    • 불안정한 동작 가능성이 있음

새로운 설정은:

  • 장점:
    • 안정적인 규칙 적용
    • 단계적 보안 구현
    • 인터페이스 준비 상태가 보장됨
    • 실패했을 때 복구 가능성이 증가
  • 단점:
    • 부팅 시간이 약간 증가
    • 설정 파일을 더 관리해야 함

결과적으로 이런 변경은 전체적으로 더 안정적이고 예측 가능한 방화벽 동작을 제공하게 되었고, 특히 인터페이스 기반 규칙이 많은 내 환경에서는 큰 도움이 되었다. 이번 경험을 통해 systemd의 서비스 의존성과 타이밍이 얼마나 중요한지 다시 한번 깨달을 수 있었다.

0개의 댓글