Docker 격리 규칙에 가려진 iptables ACCEPT — DOCKER-USER 체인

네트워킹4분조회

사용자가 환경을 만들면 서버에 컨테이너가 하나 뜨고, 거기에 SSH 로 붙어 작업하는 서비스입니다. 그래서 컨테이너마다 SSH 포트를 호스트의 임의 포트에 하나씩 열어 줘야 하는데, Docker 의 포트 게시(-p)를 쓰지 않고 iptables 규칙을 직접 넣어 매핑하는 커스텀 로직을 쓰고 있었습니다.

보안 인증 요건 때문에 이 환경을 다른 클라우드에 새로 구축하면서 Docker 도 최신 버전으로 올렸는데, 그 직후부터 컨테이너는 멀쩡히 뜨는데 매핑 포트로 다른 서버에서 접속하면 전부 타임아웃이었습니다.

조용히 버려지는 SYN — 호스트 내부 방화벽

먼저 어디서 막히는지부터 좁혔습니다.

  • 슬레이브 자기 자신에서 nc -zv 127.0.0.1 <port> 는 성공.
  • 다른 서버에서 그 포트로 접속하면 타임아웃. 외부발만 막힙니다.

클라우드 방화벽(보안 그룹)과 네트워크 ACL 을 해당 포트로 다 열어도 그대로였습니다. 그래서 패킷이 실제로 어디까지 오는지 tcpdump 로 봤습니다.

In  10.x.x.160.53328 > 10.x.x.201.<port>: Flags [S]   ← SYN 도착
In  (재전송)
→ SYN-ACK 없음, RST 도 없음 (조용히 버려짐)

패킷은 NIC 까지 도착하는데 응답이 없습니다. 포트가 닫혀 있으면 보통 RST 로 거절당하는데, RST 조차 없이 조용히 사라지는 건 방화벽이 DROP 하고 있다는 신호입니다. 그리고 클라우드 방화벽은 이미 열었으니, 남은 건 호스트 안쪽의 방화벽입니다.

Docker 격리 규칙이 ACCEPT보다 앞

호스트의 nftables 규칙을 통째로 봤습니다. filter 테이블의 DOCKER 체인이 이렇게 돼 있었습니다.

chain DOCKER {
    iifname != "docker0" oifname "docker0" drop      ← 카운터가 테스트 횟수와 일치
    ... tcp dport <port> accept                       ← 이 규칙까지 도달을 못 함
}

맨 앞의 DROP 은 Docker 가 자동으로 넣는 컨테이너 격리(anti-spoofing) 규칙 입니다. “docker0 이 아닌 인터페이스로 들어와 docker0(컨테이너 쪽)으로 나가는 트래픽을 막는다” — 최근 Docker 버전부터 이게 DOCKER 체인 맨 앞에 추가됩니다.

문제는 커스텀 로직이 포트 매핑 ACCEPT 규칙을 iptables -A DOCKER ..., 즉 APPEND(맨 끝) 로 넣고 있었다는 겁니다. iptables·nftables 는 위에서부터 순서대로 평가해서 먼저 매치되면 거기서 끝입니다. 그러니 맨 앞의 격리 DROP 에 먼저 걸려서, 뒤에 있는 ACCEPT 는 죽은 규칙이나 마찬가지였습니다. DROP 규칙 카운터가 접속 시도 횟수와 정확히 맞아떨어져서 확인됐습니다.

해결 — DOCKER-USER 체인으로 이동

Docker 는 이럴 때 쓰라고 DOCKER-USER 체인을 따로 둡니다. Docker 가 절대 자동으로 덮어쓰거나 순서를 바꾸지 않는 커스터마이징 전용 체인이고, FORWARD 에서 격리 규칙이 있는 체인보다 항상 먼저 평가됩니다.

그래서 포트 매핑 규칙을 DOCKER 가 아니라 DOCKER-USER 체인에, 그것도 APPEND 가 아니라 Insert(맨 앞) 로 넣도록 바꿨습니다. 중복 방지 체크도 같이 붙였습니다.

// 기존: filter 테이블의 DOCKER 체인에 Append
// 변경: DOCKER-USER 체인에 Insert(맨 앞) + 중복 체크
chain := "DOCKER-USER"
if exists, _ := itb.Exists("filter", chain, rule...); !exists {
    itb.Insert("filter", chain, 1, rule...)
}

포트 DNAT 를 하는 nat 테이블 규칙은 이 문제와 무관하므로 그대로 DOCKER 체인에 뒀습니다. 이번 건 어디까지나 filter(허용/차단) 쪽 순서 문제였습니다.

바꾼 뒤 DOCKER-USER 에 ACCEPT 가 격리 DROP 보다 먼저 평가되면서, 외부에서 매핑 포트로 접속이 바로 됐습니다.

커스텀 규칙 자리 — DOCKER가 아니라 DOCKER-USER

Docker 가 관리하는 DOCKER 체인에 직접 규칙을 APPEND 하면, Docker 가 나중에 그 앞에 넣는 규칙에 가려질 수 있습니다. 컨테이너 트래픽에 손대는 커스텀 방화벽 규칙은 DOCKER 가 아니라 DOCKER-USER 체인에 넣어야 합니다. 거기는 Docker 가 안 건드리고, 격리 규칙보다 먼저 평가됩니다.

그리고 “포트가 외부에서만 안 되는데 RST 도 안 온다” 는 증상은 방화벽 DROP 을 가리킵니다. 클라우드 보안 그룹까지 다 열었는데도 그렇다면, 시선을 호스트 안쪽 방화벽으로 옮겨야 합니다. tcpdump 로 SYN 이 도착하는지, 응답이 RST 인지 무응답인지만 봐도 어느 층인지 절반은 갈립니다.

참고

  1. 불러오는 중