지식

[Linux] ping은 되는데 SSH만 안 되는 서버들 — 방화벽 지층 발굴기

몇 년에 걸쳐 자란 사설 클라우드 클러스터를 원격으로 정비하다 보면, 문서에는 없고 서버에만 남아 있는 역사를 발굴하게 된다. 이 글은 그중 하나 — 같은 서브넷의 서버 20여 대 중 왜 어떤 노드는 SSH가 되고 어떤 노드는 안 되는가라는 사소해 보이는 의문을 따라갔다가, 노드마다 다른 "방화벽의 지층"을 발견하고 표준화까지 마친 기록이다.

증상: ping은 되는데, 일부만 SSH가 된다

관리 머신에서 클러스터 전 노드의 OS 버전을 일괄 수집하려고 SSH를 돌리는데, 노드마다 결과가 갈렸다. 어떤 노드는 바로 붙고, 어떤 노드는 timeout이 났다.

ssh: connect to host 10.20.0.21 port 22: Connection timed out

이상한 점은 세 가지였다.

  1. 전 노드가 같은 /24 대역이다. 라우팅이 문제라면 다 같이 안 되어야 자연스럽다.
  2. ping(ICMP)은 전 노드가 응답한다. 노드가 죽은 게 아니다.
  3. 안 되는 노드도 점프 호스트를 거치면 붙는다. SSH 데몬은 살아 있다.

먼저 의심한 것은 관리 머신의 라우팅이었다. 그런데:

$ ip route get 10.20.0.21   # 안 되는 노드
10.20.0.21 via 192.168.0.1 dev eth0 src 192.168.0.7

$ ip route get 10.20.0.46   # 되는 노드
10.20.0.46 via 192.168.0.1 dev eth0 src 192.168.0.7

두 노드 모두 같은 기본 게이트웨이로 나간다. 관리 머신 쪽에는 차이가 없다. 그렇다면 차이는 경로 상류 어딘가, 혹은 대상 노드 자체에 있다.

추적 1: 전수 프로브로 패턴부터 잡는다

개별 노드를 쑤시기 전에, 전 노드에 같은 프로브를 던져 패턴을 얻었다.

for ip in $(seq 21 48); do
  if timeout 3 bash -c "echo > /dev/tcp/10.20.0.$ip/22" 2>/dev/null; then
    r=OPEN; else r=FAIL; fi
  p=$(ping -c1 -W1 10.20.0.$ip >/dev/null 2>&1 && echo ping-ok || echo ping-no)
  echo "10.20.0.$ip  tcp22=$r  $p"
done

결과는 놀랄 만큼 선명했다. ping은 전원 응답, TCP는 정확히 "최근에 새로 설치한 노드들"만 OPEN. 몇 년 전에 구축된 노드는 전부 FAIL이었다. 설치 시기가 갈랐다면, 이건 네트워크 문제가 아니라 호스트 설정의 세대 차이다.

추적 2: DROP인가 REJECT인가 — timeout의 모양을 본다

TCP 연결 실패에도 결이 있다. 즉시 거절되면(RST 회신) 방화벽의 REJECT, 하염없이 기다리다 timeout이면 silent DROP이다.

$ time nc -zv -w4 10.20.0.21 22
nc: connect to 10.20.0.21 port 22 (tcp) timed out
real    0m4.006s        # 꽉 채운 timeout → silent DROP

$ time nc -zv -w4 10.20.0.46 22
Connection to 10.20.0.46 22 port [tcp/*] succeeded!
real    0m0.005s

silent DROP까지 확인했으면 다음 갈림길은 "어디서 버리는가"다. 대상 노드에서 SYN 패킷을 잡아 보면 세 가지로 갈린다.

  • SYN이 잡히는데 응답이 없다 → 대상 호스트 방화벽의 DROP
  • SYN이 잡히고 RST가 나간다 → 대상 호스트 방화벽의 REJECT
  • SYN이 아예 안 잡힌다 → 패킷이 도달조차 못 함 = 상류(스위치 ACL·라우터)에서 차단
# 대상 노드에서 (점프 호스트 경유로 접속해 실행)
sudo tcpdump -n -i any 'tcp port 22 and tcp[tcpflags] & tcp-syn != 0'

SYN은 잡혔고 응답이 없었다. 즉 호스트 방화벽이 범인이다. 그리고 이 tcpdump가 보너스 단서를 하나 더 줬다 — 잡힌 SYN의 소스 IP가 관리 머신의 로컬 주소(192.168.0.7)가 아니었다. 사설 주소가 중간에서 NAT로 변환되어, 대상 노드는 전혀 다른 공인 주소를 보고 있었다. 접속되는 노드에 로그인해 echo $SSH_CLIENT로 재확인해도 같은 주소가 나왔다.

원인: 방화벽 규칙의 "지층" — 한 옥텟 차이

이제 각 노드의 ufw를 직접 열어 봤다. 구세대 노드의 규칙:

Status: active
Default: deny (incoming), allow (outgoing)

To         Action      From
--         ------      ----
22/tcp     ALLOW IN    10.20.0.0/24        # 내부망
22/tcp     ALLOW IN    198.51.100.0/24     # 관리 대역 A
22/tcp     ALLOW IN    203.0.113.0/24      # 관리 대역 B
...

그리고 우리 트래픽이 NAT를 거쳐 도착하는 소스는 198.51.101.x였다. allowlist에 있는 건 198.51.100.0/24. 한 옥텟 차이. 이 allowlist는 수년 전, 지금과 다른 네트워크 지형에서 작성된 것이었다.

노드 전수를 조사하니 방화벽이 세 개의 지층으로 나뉘어 있었다.

┌─ 구세대 노드 (수년 전 구축) ─ ufw active, 옛 allowlist (현 NAT 대역 없음) → DROP
├─ 최근 재구축 노드 1대 ────── ufw active, 새 allowlist (현 NAT 대역 포함) → OPEN
└─ 신규 투입 노드들 ────────── ufw inactive (방화벽 자체가 없음)          → OPEN

처음의 의문 — "왜 일부만 라우팅이 살아 있지?" — 의 답은 이랬다. 라우팅이 살아 있는 게 아니라, 어떤 노드는 막는 규칙이 낡았고 어떤 노드는 막는 것 자체가 없었다. ping이 전부 통한 이유도 단순하다: ufw의 기본 before 규칙이 ICMP echo-request를 허용하기 때문이다. ping은 방화벽 유무를 증명하지 않는다.

특히 "방화벽 inactive" 상태로 내부망에 서 있던 신규 노드들은 명백한 보안 공백이었다 — 접속이 잘 된다고 좋아할 일이 아니었다.

해결: 표준 ruleset 통일 + 점프 호스트 일원화

두 가지를 결정했다.

1. NAT 대역은 열지 않는다. NAT 게이트웨이 주소는 관리 머신 전용이 아니라 그 게이트웨이 뒤 모든 호스트의 공유 출구다. 그 대역을 allowlist에 넣으면 의도보다 훨씬 넓은 문이 열린다. 게다가 NAT 출구는 경로에 따라 바뀔 수 있어 allowlist가 간헐적으로 빗나간다. 대신 모든 접속을 점프 호스트 경유(ProxyJump)로 일원화했다 — 점프 호스트에서 들어가면 소스가 내부망 주소라 기존 allowlist의 첫 줄로 항상 통한다.

관리 머신 ──> 점프 호스트 ──내부망 소스──> 전 노드     ← 표준 경로 (항상 통함)
관리 머신 ──NAT──직접──> ✗                             ← 쓰지 않음

2. 무방화벽 노드에 표준 ruleset을 적용한다. 기존 노드들과 동일한 allowlist로, 원격에서 잠기지 않도록 순서에 주의해서:

#!/bin/bash
# 순서 중요: enable 전에 SSH allow부터. 기존 세션은 established로 유지됨.
set -eu
ufw default deny incoming
ufw default allow outgoing
ufw allow proto tcp from 10.20.0.0/24    to any port 22 comment 'SSH from internal'
ufw allow proto tcp from 198.51.100.0/24 to any port 22 comment 'SSH from mgmt A'
# ... (표준 allowlist 나머지)
ufw logging low
ufw --force enable
ufw status verbose

ufw enable은 이미 맺어진 SSH 세션을 끊지 않는다(established 허용). 그래도 안전 수칙은 지켰다: 기존 세션을 열어 둔 채 적용하고, 새 터미널로 재접속을 확인한 뒤에 닫는다. 적용 후 외부 직접 TCP가 DROP되는 것과 점프 호스트 경유가 정상인 것을 다시 프로브로 검증했다.

교훈

  • ping과 TCP는 다른 질문이다. ping OK는 "노드 생존"만 말해 준다. 방화벽 진단은 반드시 해당 포트의 TCP로.
  • timeout의 모양(즉시 RST vs 4초 침묵) 만으로도 REJECT/DROP이 갈리고, 대상에서의 tcpdump로 호스트 방화벽/상류 차단이 갈린다. 이 3분법이면 "어디서 막히는가"는 몇 분 안에 판정된다.
  • 방화벽이 보는 소스 IP는 실측해야 한다. NAT가 끼면 ip route get이 보여주는 로컬 소스와 대상이 보는 소스는 다르다. allowlist를 만질 일이 있으면 tcpdump나 $SSH_CLIENT로 실제 도착 주소를 확인하라.
  • 접속이 잘 되는 노드를 의심하라. 이번 사례에서 "잘 되는" 노드의 절반은 방화벽이 아예 없는 것이었다. 잘 됨 ≠ 잘 설정됨.
  • 방화벽 규칙도 형상 관리 대상이다. 노드마다 설치 시점의 규칙이 화석처럼 남으면, 몇 년 뒤의 관리자는 오늘의 나처럼 지층 발굴부터 해야 한다. 신규 노드 프로비저닝 절차에 표준 ruleset 적용을 포함시키는 것이 재발 방지의 핵심이다.
조회 0댓글 0

댓글

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.