- Published on
Azure VM SSH 접속 불가 - NSG·UDR·Bastion 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 살아 있는데도 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를 붙이려면 보통 다음 중 하나여야 합니다.
- VM NIC에 Public IP가 직접 연결
- Load Balancer(Inbound NAT Rule) 또는 NAT Gateway 등을 통해 노출
- 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 (현장 기준)
- NIC NSG는 열었는데 Subnet NSG에서 막힘(또는 반대)
- NSG 규칙은 있는데 Priority 때문에 Deny가 먼저 적용
- 소스 IP를 제한해뒀는데 내 공인 IP가 바뀜(회사 VPN/통신사 NAT)
- VM에 Public IP가 없음(또는 다른 NIC에 붙음)
- LB/NAT Rule의 백엔드/포트 매핑 오류
- UDR
0.0.0.0/0 -> NVA로 리턴 패스가 꼬여 타임아웃 - Bastion 사용 중인데 VM NSG에서 Bastion 서브넷 소스 22 미허용
- 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(서비스)**가 합쳐져 발생합니다.
가장 빠른 해결 루트는 다음 순서입니다.
- 증상 분류(Timeout/Refused/Auth)
- Public IP/LB/Bastion 중 어떤 경로인지 확정
- NIC+Subnet Effective NSG로 허용 여부 확정
- Effective routes로 UDR/NVA 경로 확인
- Bastion이면
AzureBastionSubnet/VM NSG 소스 허용 점검 - 마지막으로 VM 내부(sshd/방화벽)
이 순서대로만 보면 “왜 안 되지?”가 아니라 “어디서 막히는지”가 남고, 그 순간부터 해결은 빨라집니다.