Published on

Azure VM SSH 접속 불가 - NSG·UDR·Bastion 진단법

Authors

서버가 살아 있는데도 SSH가 안 붙는 상황은 운영에서 가장 시간을 잡아먹는 장애 중 하나입니다. 특히 Azure에서는 NSG(Network Security Group), UDR(User Defined Route), Public IP/NAT, Azure Bastion이 동시에 얽히면서 “분명 22 열었는데 왜 안 되지?”가 자주 발생합니다.

이 글은 “외부에서 VM으로 SSH 접속이 실패한다”를 전제로, 패킷 흐름(클라이언트 → Azure 경계 → 서브넷 → NIC → VM OS) 관점으로 원인을 좁혀가는 실전 진단 순서를 제공합니다.

> 네트워크 장애를 볼 때는 증상이 401/403 같은 애플리케이션 레벨이든, SSH 타임아웃이든 흐름을 쪼개어 관측 지점을 늘리는 것이 핵심입니다. (참고로 비슷한 접근으로 egress를 추적하는 글도 있습니다: EKS Pod egress 간헐 끊김 - SNAT·NAT GW 추적법)

1) 먼저 증상을 분류: Timeout vs Refused vs 인증 실패

SSH 실패는 크게 세 갈래로 나뉩니다.

  • Timeout: 네트워크 경로/필터(NSG/UDR/방화벽) 문제 가능성이 큼
  • Connection refused: VM까지 도달했지만 sshd가 리슨 안 함 또는 OS 방화벽/포트 문제
  • Permission denied (publickey): 네트워크는 정상, 인증/계정/키 문제

클라이언트에서 아래처럼 빠르게 구분합니다.

# 네트워크 레벨 확인(타임아웃/리셋을 빨리 보기)
ssh -vvv -o ConnectTimeout=5 azureuser@<public-ip>

# 포트 오픈 여부만 빠르게 확인
nc -vz -w 3 <public-ip> 22
  • nc가 타임아웃이면 NSG/UDR/Public IP 경로부터 의심
  • nc가 연결되는데 SSH만 실패하면 sshd/인증으로 범위를 좁힙니다.

2) Azure 구조부터 확인: Public IP가 있는가? 어디에 붙어 있는가?

Azure VM에 외부에서 직접 SSH를 붙이려면 보통 다음 중 하나여야 합니다.

  1. VM NIC에 Public IP가 직접 연결
  2. Load Balancer(Inbound NAT Rule) 또는 NAT Gateway 등을 통해 노출
  3. Azure Bastion으로 사설 IP에 접속(외부 22 노출 불필요)

가장 흔한 실수:

  • VM에는 Public IP가 없는데 Public IP로 접속 시도
  • Public IP는 있지만 NSG에서 22 인바운드가 막힘
  • LB/NAT 규칙이 다른 백엔드/다른 포트로 연결됨

Azure CLI로 빠르게 확인합니다.

# VM의 NIC, private/public IP 요약
az vm list-ip-addresses -g <rg> -n <vmName> -o table

# NIC에 연결된 NSG 확인
az network nic show -g <rg> -n <nicName> --query "networkSecurityGroup.id" -o tsv

# 서브넷에 연결된 NSG 확인
az network vnet subnet show -g <rg> --vnet-name <vnet> -n <subnet> --query "networkSecurityGroup.id" -o tsv

3) NSG 진단: “NIC NSG + Subnet NSG” 둘 다 봐야 한다

Azure NSG는 NIC 레벨Subnet 레벨에 각각 붙을 수 있고, 둘 중 하나라도 막으면 트래픽이 차단됩니다.

3.1 인바운드 규칙의 핵심 체크 포인트

  • 대상: Destination port 22
  • 소스: 내 공인 IP(가능하면 CIDR로 제한)
  • 우선순위: Deny보다 낮은 숫자(우선 적용)
  • 프로토콜: TCP
  • 방향: Inbound

CLI로 실제 적용 규칙을 점검합니다.

# NSG 규칙 목록
az network nsg rule list -g <rg> --nsg-name <nsg> -o table

# NIC에 유효한 보안 규칙(Effective security rules)
az network nic list-effective-nsg -g <rg> -n <nicName> -o json

자주 놓치는 포인트

  • 규칙이 있어도 우선순위(priority) 때문에 아래에서 Deny에 걸림
  • 소스가 Internet으로 열려 있지만, 다른 규칙에서 더 먼저 막음
  • 포트가 22가 아니라 2222 등으로 바뀌었는데 NSG는 22만 열어둠

3.2 NSG Flow Logs / Traffic Analytics로 “실제로 막히는지” 확인

Network Watcher의 NSG flow logs를 켜면 “허용/거부”가 기록됩니다. 운영 환경에서는 이게 가장 빠른 증거가 됩니다.

  • Flow log에서 Deny가 찍히면: NSG/ASG/우선순위 문제
  • Flow log에 흔적이 없으면: 그 이전(라우팅/공인IP/LB) 또는 VM OS 단 문제

4) UDR(사용자 정의 라우트) 진단: 0.0.0.0/0가 함정이 된다

SSH가 타임아웃인데 NSG가 정상이라면, 다음으로 흔한 원인이 UDR로 인한 비대칭 라우팅입니다.

4.1 대표적인 장애 패턴

  • 서브넷에 UDR이 있고 0.0.0.0/0 -> NVA(방화벽 어플라이언스)로 보냄
  • 인바운드 패킷은 들어오는데, 리턴 트래픽이 NVA로 빠져나가 세션이 성립하지 않음
  • 또는 NVA가 22를 정책으로 막음

4.2 Effective routes로 실제 라우팅을 확인

Azure에서는 “내가 만든 라우트”가 아니라 Effective routes를 봐야 합니다.

# NIC 기준 유효 라우트 확인(Effective routes)
az network nic show-effective-route-table -g <rg> -n <nicName> -o table

여기서 확인할 것:

  • 0.0.0.0/0의 Next Hop이 Internet인지, VirtualAppliance인지
  • VirtualAppliance라면 해당 NVA가 return path를 보장하는지(세션 테이블/정책)

4.3 UDR 설계 팁(SSH 관점)

  • 인바운드 SSH를 허용할 거면, 최소한 리턴 경로가 동일하게 나가도록 설계
  • NVA를 쓴다면, NVA에서 22/TCP 정책 허용 + SNAT/라우팅 정책 정합성 확보
  • 가능하면 운영에서는 외부 22 노출 대신 Bastion/Private 접근을 권장

5) Azure Bastion 진단: “NSG 열었는데도”가 아니라 “NSG를 닫아도” 된다

Bastion을 쓰는 경우, 보통 VM에 Public IP가 없어도 SSH가 됩니다. 대신 Bastion이 정상이어야 합니다.

5.1 Bastion의 기본 조건

  • Bastion은 전용 서브넷 AzureBastionSubnet 필요
  • Bastion 서브넷 NSG/UDR이 Bastion 동작을 방해하면 연결 실패
  • 사용자는 포털/클라이언트에서 443으로 Bastion에 접속

오해 포인트

  • Bastion을 쓰는데도 VM NSG에서 22를 Internet에 열어두는 경우가 많습니다. Bastion 경유라면 보통 VM NSG는 Bastion 서브넷만 소스로 22 허용하고 Internet은 닫는 게 안전합니다.

예: VM NSG 인바운드 규칙 권장 형태

ALLOW TCP 22 FROM <AzureBastionSubnet CIDR> TO <VM NIC> priority 100
DENY  TCP 22 FROM Internet TO <VM NIC> priority 200

5.2 Bastion 연결 실패 시 체크리스트

  • Bastion 리소스 상태(프로비저닝 실패/중지)
  • AzureBastionSubnet에 잘 붙었는지
  • Bastion 서브넷에 UDR로 강제 라우팅이 걸려 있지 않은지
  • VM 쪽 NSG에서 Bastion 서브넷 소스의 22가 허용되는지

6) VM OS 레벨 진단: 네트워크가 뚫렸는데도 안 될 때

NSG/UDR/Bastion이 정상인데 SSH가 안 되면, 이제 VM 내부입니다.

6.1 sshd가 리슨 중인가

Bastion(또는 Serial Console/Run Command)로 들어갈 수 있다면 다음을 확인합니다.

# SSH 데몬 상태
sudo systemctl status sshd || sudo systemctl status ssh

# 22 포트 리슨 확인
sudo ss -lntp | grep ':22'

# OS 방화벽(UFW/Firewalld)
sudo ufw status verbose 2>/dev/null || true
sudo firewall-cmd --list-all 2>/dev/null || true

6.2 cloud-init/확장 기능으로 SSH 설정이 바뀐 경우

  • sshd_config에서 PasswordAuthentication no, PermitRootLogin no 등 정책 변경
  • 이미지/하드닝 스크립트로 포트 변경
sudo grep -E '^(Port|PasswordAuthentication|PermitRootLogin)' /etc/ssh/sshd_config

7) Azure Network Watcher 도구로 “증거 기반”으로 끝내기

감으로 추측하면 오래 걸립니다. Network Watcher는 원인 규명 시간을 크게 줄입니다.

7.1 IP Flow Verify: 특정 5-tuple이 허용/거부인지 즉시 확인

  • Source IP/Port, Destination IP/Port(22), Protocol을 넣고
  • NIC 기준으로 NSG가 허용하는지 결과를 반환

CLI 예시(개념):

az network watcher test-ip-flow \
  --resource-group <rg> \
  --vm <vmName> \
  --direction Inbound \
  --protocol TCP \
  --local <vm-private-ip>:22 \
  --remote <my-public-ip>:54321

결과가 Deny면 NSG/우선순위 문제로 확정할 수 있습니다.

7.2 Connection Troubleshoot: 경로/라우팅/NSG까지 종합 진단

  • 소스(클라이언트 또는 다른 VM)에서 대상 VM:22로 테스트
  • 중간에 막히는 지점을 리포트로 보여줌

이 접근은 “네트워크 흐름을 쪼개서 관측”한다는 점에서, 인증/권한 문제를 체계적으로 분해하는 방식과 유사합니다. (애플리케이션 레벨에서 401 원인을 체계적으로 분해하는 예: Spring Security 6 JWT 401/403 원인 9가지)

8) 가장 흔한 원인 TOP 8 (현장 기준)

  1. NIC NSG는 열었는데 Subnet NSG에서 막힘(또는 반대)
  2. NSG 규칙은 있는데 Priority 때문에 Deny가 먼저 적용
  3. 소스 IP를 제한해뒀는데 내 공인 IP가 바뀜(회사 VPN/통신사 NAT)
  4. VM에 Public IP가 없음(또는 다른 NIC에 붙음)
  5. LB/NAT Rule의 백엔드/포트 매핑 오류
  6. UDR 0.0.0.0/0 -> NVA리턴 패스가 꼬여 타임아웃
  7. Bastion 사용 중인데 VM NSG에서 Bastion 서브넷 소스 22 미허용
  8. VM 내부에서 sshd 다운/포트 변경/OS 방화벽 차단

9) 운영 권장 아키텍처(보안 포함)

  • 가능하면 Public IP + 22 오픈을 피하고,
    • Bastion
    • Point-to-Site VPN
    • Private Endpoint/Jumpbox 같은 방식으로 사설망 기반 접근을 기본값으로 둡니다.
  • 부득이하게 Public SSH가 필요하면:
    • NSG에서 소스를 고정 공인 IP/CIDR로 제한
    • JIT(Just-In-Time) 접근(Defender for Cloud)
    • 로그인 실패/접속 로그 수집(SSHD 로그, NSG flow logs)

10) 결론: “NSG만 보지 말고, 흐름으로 본다”

Azure VM SSH 장애는 대개 한 군데가 아니라 **NSG(필터) + UDR(경로) + Bastion(접근 방식) + VM OS(서비스)**가 합쳐져 발생합니다.

가장 빠른 해결 루트는 다음 순서입니다.

  1. 증상 분류(Timeout/Refused/Auth)
  2. Public IP/LB/Bastion 중 어떤 경로인지 확정
  3. NIC+Subnet Effective NSG로 허용 여부 확정
  4. Effective routes로 UDR/NVA 경로 확인
  5. Bastion이면 AzureBastionSubnet/VM NSG 소스 허용 점검
  6. 마지막으로 VM 내부(sshd/방화벽)

이 순서대로만 보면 “왜 안 되지?”가 아니라 “어디서 막히는지”가 남고, 그 순간부터 해결은 빨라집니다.