새 EC2에 8080 포트를 열었는데 외부에서는 계속 타임아웃이 납니다. Security Group은 분명 허용했지만, NACL·라우팅·OS 방화벽 중 어디가 막는지 아무도 확신하지 못합니다.
클라우드 네트워크 장애는 한 화면만 보고 끝나지 않습니다. 패킷이 통과하는 계층을 순서대로 좁혀야 합니다.
AWS 보안그룹 실전 디버깅
첫 번째 클라우드 인프라를 구성한 날, 팀장이 말했습니다. "80포트 열어서 웹 서버 올려봐." Security Group 인바운드에 TCP 80을 추가하고, EC2를 올렸는데 브라우저에 아무것도 뜨지 않습니다. telnet을 해봐도 Connection timed out. "분명히 열었는데 왜 안 되지?" 클라우드를 처음 다루는 엔지니어가 절반 이상 겪는 상황입니다. Security Group만 봐서는 이유를 모릅니다 — 클라우드 네트워크에는 트래픽이 통과해야 하는 관문이 Security Group 외에도 여러 겹 존재하기 때문입니다. 이 챕터에서는 "포트를 열었는데 안 된다"는 상황을 5단계 진단 플로우로 체계적으로 좁혀나가는 방법을 배웁니다.
- 1Security Group의 Stateful 동작 원리와 인바운드/아웃바운드 연결 추적 메커니즘을 설명할 수 있다
- 2aws ec2 describe-security-groups / describe-network-interfaces로 부착 상태를 확인할 수 있다
- 3포트를 열었는데 안 될 때 SG → NACL → Route Table → IGW/NAT → OS 방화벽 5단계 진단 플로우를 적용할 수 있다
- 4VPC Flow Logs에서 REJECT 로그를 읽고 차단 지점을 특정할 수 있다
- 5NACL과 Security Group을 Stateless/Stateful 관점에서 비교하고 실무 함의를 판단할 수 있다
- 6EC2 두 대 사이의 포트 차단·허용 시나리오를 실습할 수 있다
aws sts get-caller-identityaws --versionexport AWS_DEFAULT_REGION=ap-northeast-2aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,State.Name,PrivateIpAddress,PublicIpAddress]' --output tableAWS 계정, IAM 사용자(ec2:Describe* 권한, logs:FilterLogEvents 권한), AWS CLI v2 이상. EC2 인스턴스가 없는 경우 describe 명령은 aws-cli 출력 형식 학습 용도로 사용하세요.
Security Group Stateful 동작 원리
Security Group을 처음 배울 때 가장 많이 혼동하는 부분이 Stateful 동작입니다. 온프레미스 ACL이나 iptables에 익숙한 엔지니어라면 "응답 트래픽도 아웃바운드 규칙에서 허용해야 하지 않나?"라는 의문이 자연스럽게 생깁니다. 결론부터 말하면 — Security Group에서는 그럴 필요가 없습니다.
확대
연결 추적(Connection Tracking)이란
Security Group은 내부적으로 모든 연결의 상태를 추적합니다. TCP 3-way handshake가 성공해 연결이 수립되면, 이 연결의 응답 패킷은 아웃바운드 규칙을 확인하지 않고 자동으로 허용됩니다. 이것이 Stateful 방화벽의 핵심입니다.
확대
Stateful이 실무에서 의미하는 것
아웃바운드 기본 규칙이 "전체 허용(0.0.0.0/0)"인 경우, Security Group 설정에서 인바운드 규칙만 신경 쓰면 됩니다. 인바운드에서 허용한 연결의 응답은 아웃바운드 설정과 무관하게 돌아갑니다.
아웃바운드를 제한하는 경우 — 예를 들어 DB 서버가 외부 인터넷으로 나가는 것을 막고 싶을 때 — 에는 아웃바운드 규칙을 수정합니다. 이때도 인바운드에서 허용된 연결의 응답은 영향을 받지 않습니다. Stateful이기 때문입니다.
아웃바운드 기본 규칙을 제거하면 어떻게 되나
아웃바운드 규칙을 모두 제거한 경우:
1. 외부에서 이 EC2로 들어오는 인바운드 연결 → 응답 트래픽은 자동 허용 (Stateful)
2. 이 EC2에서 외부로 나가는 아웃바운드 연결 → 차단됨 (규칙 없음)
즉, 아웃바운드 규칙 제거는 "이 서버가 먼저 연결을 시작하는 것"만 막습니다.
외부에서 들어온 연결에 대한 응답은 여전히 나갑니다.
이 동작 원리를 이해하지 못하면 "아웃바운드를 다 막았는데 왜 응답이 가지?"라는 혼란이 생깁니다. 연결을 시작한 방향이 중요한 것이 Stateful의 핵심입니다.
패킷 한 개가 보안그룹을 통과하는 순서 — 도착부터 응답까지
앞에서 "SG는 stateful"이라는 개념을 봤다면, 이번에는 패킷 한 개가 실제로 어느 순서로 평가되는지를 따라갑니다. 이 순서를 알면 "인바운드만 열었는데 왜 응답까지 되지", "SG는 열었는데 왜 timeout이지"를 단계로 좁힐 수 있습니다. 클라이언트가 EC2의 8080으로 붙는 한 번의 연결에서, 보안그룹 안쪽에서는 아래 순서가 일어납니다.
[외부 클라이언트] TCP SYN → EC2:8080
│
① 패킷이 인스턴스 ENI에 도착
│ (NACL이 있으면 서브넷 NACL 인바운드를 먼저 통과해야 함)
│
② 인바운드 SG 규칙 평가 — 화이트리스트
│ → 허용 규칙 중 출발지·포트가 매칭되는 게 하나라도 있나?
│ → 있으면 통과 / 하나도 없으면 조용히 드롭 (거부 규칙 자체가 없음)
│
③ 연결 추적(conntrack) 등록 — 여기서부터 stateful
│ → 이 연결의 5-튜플(출발지·목적지 IP/포트/프로토콜)을 상태 테이블에 기록
│
④ 응답 패킷 (EC2 → 클라이언트)
│ → 아웃바운드 SG 규칙을 보지 않고 conntrack 매칭으로 자동 허용
│
⑤ (NACL이 있으면) 응답은 NACL 아웃바운드도 통과해야 함
│ → NACL은 stateless라 ephemeral 포트(1024-65535) 허용이 따로 필요
▼
[클라이언트] 응답 수신 → 연결 성립
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① ENI 도착 | 라우팅으로 패킷이 인스턴스 ENI까지 전달. NACL이 있으면 서브넷 인바운드 규칙을 먼저 평가 | 라우팅 없음·NACL deny → 응답 없이 Connection timed out |
| ② 인바운드 SG 평가 | 허용 규칙(출발지·포트)을 훑어 매칭이 하나라도 있으면 통과. SG는 화이트리스트라 거부 규칙이 없고, 매칭이 없으면 그냥 버린다 | 포트·출발지 규칙 없음 → 조용히 드롭 → Connection timed out (RST 아님) |
| ③ conntrack 등록 | 통과한 연결의 5-튜플을 상태 테이블에 기록. 이 기록이 stateful의 실체 | 거의 안 막힘 (초대량 연결로 conntrack 테이블이 포화되면 신규 연결 드롭) |
| ④ 응답 자동 허용 | 등록된 연결의 응답·후속 패킷은 아웃바운드 규칙을 보지 않고 conntrack 매칭으로 통과 | 아웃바운드를 다 지워도 이 응답은 나감 — 대신 인스턴스가 새로 여는 아웃바운드만 막힘 |
| ⑤ NACL 아웃바운드 | NACL을 쓴다면 응답은 stateless라 ephemeral 포트로 돌아가는 아웃바운드를 별도 허용해야 함 | ephemeral 미허용 → 요청은 갔는데 응답만 막혀 Connection timed out |
즉 SG만 보면 "인바운드 ②만 맞으면 응답 ④는 저절로 된다" 가 핵심입니다. 그래서 접속이 안 될 때 Connection timed out이면 ①·②·⑤(경로·인바운드 SG·NACL 응답) 중 하나가 패킷을 삼킨 것이고, Connection refused면 ①②는 통과해 인스턴스까지 도달했으나 그 포트에 앱이 없다는 뜻입니다 — SG/NACL 문제가 아니라 ss -tlnp로 넘어갈 신호입니다.
실습 1: Security Group 부착 상태 확인
보안그룹이 올바른 인스턴스에 붙어있는지, 어떤 규칙이 설정되어 있는지 AWS CLI로 직접 확인하는 방법입니다. 콘솔 UI보다 CLI로 확인하면 여러 인스턴스를 한 번에 비교하거나 자동화에 활용할 수 있습니다.
인스턴스 ID로 어떤 Security Group이 붙어있는지 확인합니다. 먼저 인스턴스 목록을 조회해 실습에 사용할 ID를 파악합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part5/exam_23 && cd /tmp/networking/part5/exam_23
# 인스턴스 목록에서 ID 확인
aws ec2 describe-instances \
--query 'Reservations[*].Instances[*].[InstanceId,State.Name,Tags[?Key==`Name`].Value|[0]]' \
--output table
# 인스턴스 ID로 부착된 Security Group 목록 조회
aws ec2 describe-instances \
--instance-ids i-0abc12345def67890 \
--query 'Reservations[*].Instances[*].SecurityGroups' \
--output table
# 출력 예시:
# -------------------------------------------------
# | DescribeInstances |
# +-------------------------+---------------------+
# | GroupId | GroupName |
# +-------------------------+---------------------+
# | sg-0a1b2c3d4e5f67890 | web-server-sg |
# | sg-0f9e8d7c6b5a43210 | shared-monitoring |
# +-------------------------+---------------------+
aws ec2 describe-instances --instance-ids i-0abc12345def67890 --query 'Reservations[*].Instances[*].SecurityGroups' --output table- GroupId, GroupName 목록 확인 — 예상한 Security Group이 부착됐는지 확인
- 예상과 다른 SG가 있거나, 필요한 SG가 없으면 attach/detach 필요
- ALB/RDS는 자체 ENI를 가지므로 EC2 SG와 별도로 확인해야 함
Security Group ID로 인바운드 규칙 전체를 조회합니다. 어느 포트가 어느 IP 대역에 허용됐는지 확인하는 가장 직접적인 방법입니다.
# Security Group ID로 규칙 상세 조회
aws ec2 describe-security-groups \
--group-ids sg-0a1b2c3d4e5f67890 \
--query 'SecurityGroups[*].{
Name:GroupName,
Inbound:IpPermissions[*].{
Protocol:IpProtocol,
FromPort:FromPort,
ToPort:ToPort,
CIDR:IpRanges[*].CidrIp
}
}' \
--output json
# 이름에 "web"이 포함된 Security Group 검색
aws ec2 describe-security-groups \
--filters "Name=group-name,Values=*web*" \
--query 'SecurityGroups[*].[GroupId,GroupName,VpcId]' \
--output table
# ALB나 RDS 같은 관리형 서비스의 ENI 조회
aws ec2 describe-network-interfaces \
--filters "Name=group-id,Values=sg-0a1b2c3d4e5f67890" \
--query 'NetworkInterfaces[*].{ENI:NetworkInterfaceId,Type:InterfaceType,Description:Description}' \
--output table
aws ec2 describe-security-groups --group-ids sg-0a1b2c3d4e5f67890 --query 'SecurityGroups[*].{Name:GroupName,Inbound:IpPermissions[*].{Protocol:IpProtocol,FromPort:FromPort,ToPort:ToPort,CIDR:IpRanges[*].CidrIp}}' --output json- Inbound 규칙에서 허용된 Protocol, FromPort, ToPort, CIDR 확인
- 0.0.0.0/0으로 열린 포트는 인터넷 전체 허용 — SSH(22), DB(3306/5432)는 위험
- eni type=elasticloadbalancing 이면 ALB ENI — EC2 SG 수정이 ALB SG에 영향 없음
'포트 열었는데 안 된다' 5단계 진단 플로우
Security Group만 보고 "열었는데 왜 안 되지?"를 반복하는 것은 그 외 4개의 관문을 확인하지 않았기 때문입니다. 클라우드에서 트래픽이 EC2에 도달하기까지 통과해야 하는 관문은 순서대로 5개입니다.
확대
5단계 진단 순서
확대
각 단계별 점검 포인트
1단계: Security Group
# 확인할 것:
# - 해당 포트가 인바운드 규칙에 있는가?
# - 소스 IP가 허용 범위에 포함되는가?
# - Security Group이 올바른 인스턴스에 붙어있는가?
aws ec2 describe-security-groups --group-ids sg-xxxxx \
--query 'SecurityGroups[*].IpPermissions'
2단계: Network ACL (NACL)
# NACL은 서브넷 단위로 적용됩니다
# 인스턴스가 속한 서브넷의 NACL을 확인합니다
aws ec2 describe-network-acls \
--filters "Name=association.subnet-id,Values=subnet-xxxxx" \
--query 'NetworkAcls[*].Entries' \
--output table
# 인바운드와 아웃바운드 각각 허용 규칙이 있는지 확인
# Egress: false = 인바운드, true = 아웃바운드
# RuleAction: allow | deny
3단계: Route Table
# 인스턴스가 있는 서브넷의 라우팅 테이블 확인
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-xxxxx" \
--query 'RouteTables[*].Routes[*].{Dest:DestinationCidrBlock,Target:GatewayId}' \
--output table
# 0.0.0.0/0 → igw-xxxx : Public 서브넷 (인터넷 직접 연결)
# 0.0.0.0/0 → nat-xxxx : Private 서브넷 (아웃바운드만)
# 경로 없음 : 인터넷 도달 불가
4단계: IGW / NAT GW 상태
# IGW 연결 상태 확인
aws ec2 describe-internet-gateways \
--filters "Name=attachment.vpc-id,Values=vpc-xxxxx" \
--query 'InternetGateways[*].{IGW:InternetGatewayId,State:Attachments[0].State}'
# NAT Gateway 상태 확인 (Private 서브넷의 아웃바운드용)
aws ec2 describe-nat-gateways \
--filter "Name=vpc-id,Values=vpc-xxxxx" \
--query 'NatGateways[*].{NAT:NatGatewayId,State:State,SubnetId:SubnetId}'
5단계: OS 방화벽
# EC2 내부에서 확인 (SSH 접속 후)
# Amazon Linux 2 / RHEL 계열
sudo firewall-cmd --list-all
# Ubuntu 계열
sudo ufw status verbose
# iptables 직접 확인 (모든 배포판)
sudo iptables -L INPUT -n -v --line-numbers
# 포트가 실제로 리스닝 중인지 확인
sudo ss -tlnp | grep :8080
에러 메시지로 단계 추측하기
Connection timed out (연결 타임아웃, 무반응):
→ Security Group 또는 NACL이 DROP/REJECT 중
→ Route Table에 경로가 없거나 IGW가 없음
→ OS 방화벽이 DROP으로 설정됨
Connection refused (연결 거부):
→ EC2는 도달했지만 해당 포트에서 아무것도 리스닝하지 않음
→ 또는 OS 방화벽이 REJECT로 응답
→ 애플리케이션이 실행되지 않았거나 다른 포트를 사용 중
No route to host:
→ Route Table에 목적지 경로가 없음
→ 또는 NACL이 DENY 응답을 보냄
실습 2: VPC Flow Logs에서 REJECT 로그 읽기
VPC Flow Logs는 ENI를 통과하는 트래픽을 기록합니다. Security Group이나 NACL이 패킷을 차단할 때 REJECT가 기록됩니다. 이 로그를 읽으면 어느 계층에서, 어느 방향으로 트래픽이 막히는지를 알 수 있습니다.
Flow Log 레코드 형식은 다음과 같습니다:
2 123456789012 eni-0ab1c2d3e4f56789 203.0.113.50 10.0.2.100 52341 8080 6 1 40 1620000000 1620000060 REJECT OK
각 필드의 의미는 다음과 같습니다.
| 필드 | 예시 값 | 의미 |
|---|---|---|
| version | 2 | 레코드 버전 |
| account-id | 123456789012 | 계정 ID |
| interface-id | eni-0ab1c2d3e4f56789 | ENI ID |
| srcaddr / dstaddr | 203.0.113.50 / 10.0.2.100 | 소스 IP / 목적지 IP |
| srcport / dstport | 52341 / 8080 | 소스 포트 / 목적지 포트 |
| protocol | 6 | 프로토콜 (6=TCP, 17=UDP) |
| packets / bytes | 1 / 40 | 패킷 수 / 바이트 수 |
| start / end | 1620000000 / 1620000060 | 시작 / 종료 시각 (epoch) |
| action | REJECT | 허용(ACCEPT) / 차단(REJECT) |
| log-status | OK | 로그 기록 상태 |
VPC Flow Logs가 CloudWatch에 저장된 경우, REJECT 패턴으로 필터링해 어느 트래픽이 차단됐는지 확인합니다.
# Flow Logs가 CloudWatch Logs에 저장된 경우
# 로그 그룹 목록 확인
aws logs describe-log-groups \
--log-group-name-prefix "/aws/vpc/flowlogs" \
--query 'logGroups[*].logGroupName'
# REJECT 로그만 필터링 (최근 1시간)
aws logs filter-log-events \
--log-group-name "/aws/vpc/flowlogs/vpc-xxxxx" \
--filter-pattern "REJECT" \
--start-time $(date -d '1 hour ago' +%s000) \
--query 'events[*].message' \
--output text
# 특정 소스 IP의 REJECT만 필터링 (실제 장애 대응 시 유용)
aws logs filter-log-events \
--log-group-name "/aws/vpc/flowlogs/vpc-xxxxx" \
--filter-pattern "203.0.113.50 REJECT" \
--query 'events[*].message' \
--output text
# 특정 목적지 포트에 대한 REJECT 확인
aws logs filter-log-events \
--log-group-name "/aws/vpc/flowlogs/vpc-xxxxx" \
--filter-pattern "8080 REJECT" \
--query 'events[*].message' \
--output text
aws logs filter-log-events --log-group-name '/aws/vpc/flowlogs/vpc-xxxxx' --filter-pattern 'REJECT' --start-time $(date -d '1 hour ago' +%s000) --query 'events[*].message' --output text- action=REJECT: Security Group 또는 NACL이 차단 — SG 규칙 점검
- 인바운드 REJECT: srcaddr가 외부 IP, dstaddr가 EC2 Private IP
- 아웃바운드 REJECT: srcaddr가 EC2 Private IP, dstaddr가 외부 IP
- Flow Logs에 항목 자체가 없으면: Route Table 문제로 패킷이 ENI까지 도달 못한 것
REJECT 로그로 차단 지점 판별
Flow Logs에서 REJECT가 보이는 경우:
→ Security Group 또는 NACL이 차단 중
→ VPC Flow Logs는 인스턴스 ENI 기준으로 기록
→ 인바운드 REJECT = ENI에 들어오기 전에 차단 = Security Group 또는 NACL
Flow Logs에서 ACCEPT가 보이는데 연결이 안 되는 경우:
→ Security Group/NACL은 통과했지만 인스턴스까지 도달
→ 원인: OS 방화벽(iptables), 앱이 리스닝하지 않음, 프로세스 크래시
Flow Logs에 항목 자체가 없는 경우:
→ Route Table 문제로 패킷이 ENI에 도달조차 못함
→ 또는 Flow Logs 자체가 설정되지 않음
실전 패턴: 인바운드 차단 vs 아웃바운드 차단 구분
# 인바운드 차단 로그 예시 (외부 → EC2)
# srcaddr가 외부 IP, dstaddr가 EC2 Private IP
2 123456789012 eni-abc12345 203.0.113.50 10.0.2.100 52341 8080 6 1 40 ... REJECT OK
# 아웃바운드 차단 로그 예시 (EC2 → 외부)
# srcaddr가 EC2 Private IP, dstaddr가 외부 IP
2 123456789012 eni-abc12345 10.0.2.100 52.95.110.50 34521 443 6 1 40 ... REJECT OK
# EC2 간 통신 차단 (같은 VPC 내)
2 123456789012 eni-abc12345 10.0.1.50 10.0.2.100 45678 3306 6 1 40 ... REJECT OK
NACL vs Security Group — Stateless vs Stateful의 실무 함의
VPC 모듈에서 두 방화벽의 개념적 차이를 배웠다면, 여기서는 그 차이가 실제 트러블슈팅에서 어떤 함정을 만드는지를 봅니다. NACL의 Stateless 특성은 경험이 없으면 절대 직관적으로 알 수 없습니다.
핵심 비교
| 항목 | Security Group | Network ACL (NACL) |
|---|---|---|
| 적용 단위 | 인스턴스(ENI) | 서브넷 |
| 상태 추적 | Stateful (연결 추적) | Stateless (연결 추적 없음) |
| 기본 동작 | 모든 인바운드 차단, 모든 아웃바운드 허용 | 모든 트래픽 허용 (기본 NACL) |
| 규칙 평가 | 모든 규칙 AND 조건으로 평가 | 규칙 번호 순서대로, 첫 매칭에서 결정 |
| 허용/거부 | 허용 규칙만 (명시적 거부 불가) | 허용과 거부 모두 설정 가능 |
| 응답 트래픽 | 자동 허용 | 별도 아웃바운드 규칙 필요 |
NACL Stateless가 만드는 함정 — ephemeral 포트
클라이언트가 TCP 연결을 시작할 때, OS는 임의의 **ephemeral 포트(임시 포트, 1024-65535)**를 소스 포트로 사용합니다. 서버 응답은 이 포트로 돌아가야 합니다.
확대
Security Group이라면 연결 추적을 통해 52341 포트 응답을 자동 허용합니다. NACL은 그렇지 않습니다.
언제 NACL을 쓰고 언제 Security Group을 쓰는가
Security Group으로만 충분한 경우:
- 대부분의 일반 워크로드
- 인스턴스별 세밀한 접근 제어가 필요한 경우
- 다른 Security Group을 소스로 지정하고 싶을 때 (예: app-sg에서 db-sg로만 허용)
NACL을 추가로 사용하는 경우:
- 서브넷 단위로 특정 IP 대역을 명시적으로 차단해야 할 때 (DDoS 방어)
- 규정 준수(Compliance)로 서브넷 경계에서의 명시적 차단 기록이 필요할 때
- Security Group 규칙 한도(기본 60개 per SG)를 초과하는 복잡한 구성
실무 권장 패턴:
기본 NACL은 전체 허용으로 두고 (기본값),
세밀한 접근 제어는 Security Group으로만 관리합니다.
NACL 수정은 ephemeral 포트 함정 때문에 위험하므로
명확한 필요가 있을 때만 건드립니다.
실습 3: EC2 두 대 사이 포트 차단·허용 시나리오
Security Group으로 EC2 간 통신을 제어하는 가장 실무적인 패턴입니다. "IP 대신 Security Group ID를 소스로 지정"하는 방법이 핵심입니다.
시나리오: 웹 서버(web-ec2)에서 DB 서버(db-ec2)의 MySQL(3306) 허용
# 1단계: web-ec2에 부착된 Security Group ID 확인
WEB_SG=$(aws ec2 describe-instances \
--instance-ids i-0abc12345 \
--query 'Reservations[*].Instances[*].SecurityGroups[0].GroupId' \
--output text)
echo "Web SG: $WEB_SG"
# 2단계: db-ec2의 Security Group에 web-sg를 소스로 3306 허용
DB_SG="sg-0db1234567890abcd"
aws ec2 authorize-security-group-ingress \
--group-id $DB_SG \
--protocol tcp \
--port 3306 \
--source-group $WEB_SG
# 성공 출력:
# { "Return": true, "SecurityGroupRules": [...] }
# 3단계: 규칙이 적용됐는지 확인
aws ec2 describe-security-groups \
--group-ids $DB_SG \
--query 'SecurityGroups[*].IpPermissions[?ToPort==`3306`]' \
--output json
# UserIdGroupPairs 섹션에 web-sg ID가 보이면 성공
# IP 대역이 아닌 SG ID가 소스로 지정됨
특정 규칙 제거 (차단 복원)
# 방금 추가한 규칙 제거 (차단 상태로 되돌리기)
aws ec2 revoke-security-group-ingress \
--group-id $DB_SG \
--protocol tcp \
--port 3306 \
--source-group $WEB_SG
# 제거 후 재확인
aws ec2 describe-security-groups \
--group-ids $DB_SG \
--query 'SecurityGroups[*].IpPermissions[?ToPort==`3306`]'
# [] 빈 배열이 나오면 규칙 제거 완료
EC2 내부에서 포트 통신 직접 검증
# web-ec2 내부에서 db-ec2로 포트 연결 테스트 (SSH 접속 후)
# nc(netcat)으로 TCP 연결만 테스트
nc -zv 10.0.2.100 3306 -w 3
# 성공: Connection to 10.0.2.100 3306 port [tcp/mysql] succeeded!
# 실패(SG 차단): nc: connect to 10.0.2.100 port 3306 (tcp) failed: Connection timed out
# 실패(앱 없음): nc: connect to 10.0.2.100 port 3306 (tcp) failed: Connection refused
# 여러 포트를 한 번에 점검하는 스크립트
DB_HOST="10.0.2.100"
for port in 3306 5432 6379 27017; do
result=$(nc -zv -w 2 $DB_HOST $port 2>&1)
if echo "$result" | grep -q "succeeded"; then
echo "PORT $port ($(grep -w $port /etc/services 2>/dev/null | head -1 | awk '{print $1}' || echo 'unknown')): OPEN"
else
echo "PORT $port: CLOSED/FILTERED"
fi
done
두 에러의 차이
보안그룹 디버깅에서 이 두 에러는 전혀 다른 원인을 가리킵니다. 에러 메시지만으로도 원인 계층을 절반까지 좁힐 수 있습니다.
Connection timed out
# 현상
nc -zv 10.0.2.100 8080 -w 5
# nc: connect to 10.0.2.100 port 8080 (tcp) timed out
curl --connect-timeout 5 http://10.0.2.100:8080
# curl: (28) Connection timed out after 5001 milliseconds
의미: 클라이언트가 SYN 패킷을 보냈지만 아무 응답이 없습니다. 패킷이 방화벽에 의해 조용히 DROP되고 있습니다.
원인 가설:
- Security Group 인바운드에 해당 포트 규칙 없음 (기본 DROP)
- NACL이 해당 트래픽을 DENY
- OS 방화벽(iptables)이 DROP 규칙으로 응답 없이 차단
- Route Table에 경로가 없어 패킷이 전달되지 않음
점검 순서:
# 1. SG 규칙 확인
aws ec2 describe-security-groups --group-ids sg-xxxxx \
--query 'SecurityGroups[*].IpPermissions[?ToPort==`8080`]'
# 2. Flow Logs에서 REJECT 확인
aws logs filter-log-events \
--log-group-name "/aws/vpc/flowlogs/vpc-xxxxx" \
--filter-pattern "10.0.2.100 REJECT"
# 3. EC2 내부 iptables 확인 (SSH 가능한 경우)
sudo iptables -L INPUT -n | grep 8080
Connection refused
# 현상
nc -zv 10.0.2.100 8080 -w 3
# nc: connect to 10.0.2.100 port 8080 (tcp) failed: Connection refused
curl --connect-timeout 5 http://10.0.2.100:8080
# curl: (7) Failed to connect to 10.0.2.100 port 8080: Connection refused
의미: 클라이언트의 SYN 패킷에 서버가 RST(Reset)으로 즉시 응답합니다. 패킷은 목적지에 도달했지만, 해당 포트에서 아무것도 리스닝하지 않고 있습니다.
원인 가설:
- 애플리케이션이 실행되지 않음 (프로세스 크래시, 미배포)
- 애플리케이션이 다른 포트에서 리스닝 중 (8080 대신 8081 등)
- OS 방화벽이 REJECT 설정 (DROP이 아닌 REJECT)
점검 순서:
# EC2 내부에서 포트 리스닝 상태 확인 (SSH 접속 후)
sudo ss -tlnp | grep :8080
# 아무 출력 없음 → 아무것도 리스닝하지 않음
# 실제로 뭐가 리스닝 중인지 전체 확인
sudo ss -tlnp
# 프로세스 상태 확인
ps aux | grep -E "java|node|python|nginx"
systemctl status my-app
빠른 판별 공식
Connection timed out = 패킷이 사라짐 = 방화벽 또는 라우팅 문제
Connection refused = 패킷이 도달함 = 앱 문제 (리스닝 안 함)
timed out → Security Group / NACL / Route Table / OS DROP 규칙 점검
refused → ss -tlnp / ps aux / systemctl status 점검
증상
같은 VPC 안에 있는 EC2 2대 중 하나에서만 RDS 연결이 됩니다. 동일한 앱, 동일한 연결 정보인데도 한쪽에서는 연결이 거부됩니다.
# EC2 A에서 (성공)
psql -h mydb.xxxx.ap-northeast-2.rds.amazonaws.com -U admin -d mydb
# psql (15.2)
# Type "help" for help. mydb=#
# EC2 B에서 (실패)
psql -h mydb.xxxx.ap-northeast-2.rds.amazonaws.com -U admin -d mydb
# psql: error: connection to server on host "mydb.xxxx.rds.amazonaws.com" failed:
# Connection refused
원인 진단
# RDS Security Group의 인바운드 규칙 확인
aws ec2 describe-security-groups \
--group-ids sg-rds-group-id \
--query 'SecurityGroups[].IpPermissions[]'
# 출력 결과 예시:
# "IpRanges": []
# "UserIdGroupPairs": [{ "GroupId": "sg-ec2-group-A" }]
# → EC2 A의 SG만 허용, EC2 B의 SG는 없음
# EC2 B의 Security Group ID 확인
aws ec2 describe-instances --instance-ids i-ec2-b-id \
--query 'Reservations[].Instances[].SecurityGroups'
# [{ "GroupId": "sg-ec2-group-B" }] ← 다른 SG
해결
# RDS SG에 EC2 B의 SG 추가
aws ec2 authorize-security-group-ingress \
--group-id sg-rds-group-id \
--protocol tcp \
--port 5432 \
--source-group sg-ec2-group-B
# 변경 후 즉시 테스트 (SG 규칙은 실시간 반영)
psql -h mydb.xxxx.rds.amazonaws.com -U admin -d mydb
예방: 동일 역할 EC2는 동일 SG를 사용하거나, 역할 기반 SG를 미리 설계합니다.
심화 — 규칙은 다 열었는데 "연결은 되고 큰 데이터만 멈춘다"
심화: 핸드셰이크는 되는데 큰 응답이 사라진다 — PMTUD 블랙홀과 MSS
SG도 열었고 Flow Log도 ACCEPT인데 "연결은 되는데 큰 데이터만 멈춘다"는 신고가 옵니다. 이건 규칙 문제가 아니라 패킷 크기 문제입니다. SG/NACL 다섯 관문을 아무리 다시 봐도 안 나오는 이유가 여기 있습니다.
- MTU와 DF 비트: 경로 상 가장 작은 MTU를 넘는 IP 패킷은 쪼개거나(fragment) 버려져야 합니다. TCP는 보통 DF(Don't Fragment) 비트를 세워 보내므로, 중간 라우터는 쪼개는 대신 발신자에게 ICMP type 3 code 4(Fragmentation Needed, MTU 이만큼)를 돌려줘야 합니다. 발신자는 이를 받고 세그먼트 크기를 줄입니다 — 이것이 Path MTU Discovery(PMTUD)입니다.
- 블랙홀의 성립: 그 ICMP를 NACL·SG·중간 방화벽이 막으면, 발신자는 "너무 크다"는 신호를 못 받고 같은 큰 패킷을 계속 재전송하다 조용히 유실합니다. 반면 핸드셰이크·짧은 요청 같은 작은 패킷은 통과하니 "연결은 되는데 큰 응답·업로드만 멈춘다"가 됩니다. VPN·Direct Connect·오버레이 네트워크처럼 터널 헤더로 MTU가 1500보다 줄어드는 구간에서 특히 흔합니다.
- Flow Log 함정: 이 경우 규칙 자체는 통과했으므로 Flow Log에는 ACCEPT로 찍힙니다. REJECT를 찾아 헤매면 원인을 영영 못 만납니다 — ACCEPT인데 안 되는 것이 오히려 단서입니다.
- 두 갈래 대책: ① ICMP Fragmentation Needed(type 3 code 4)를 NACL 인바운드/아웃바운드와 SG에 허용해 PMTUD를 살립니다. ② 그래도 안 되면 MSS clamping으로 애초에 큰 패킷을 안 만듭니다 — 리눅스는 iptables의 TCPMSS --clamp-mss-to-pmtu, 또는 인터페이스 MTU를 경로 최소값(예: 1400)에 맞춰 낮춥니다. TCP는 SYN에서 MSS를 협상하므로, MSS를 낮추면 처음부터 경로에 맞는 크기로만 보냅니다.
상황: 8080을 열고 Flow Log에서 ACCEPT까지 확인했습니다. 그런데 작은 헬스체크 curl은 200이 즉시 오는데, 수십 KB 이상 큰 응답은 중간에서 멈춥니다. SSH도 붙긴 하는데 긴 디렉터리 목록을 내면 화면이 특정 지점에서 얼어붙습니다. 하필 온프레미스와 VPC를 VPN으로 잇는 경로입니다.
원인: 경로 중간(VPN 터널)의 MTU가 1500보다 작은데, DF 비트가 켜진 큰 TCP 패킷이 그 구간에서 거절됩니다. 이를 알리는 ICMP Fragmentation Needed(type 3 code 4)가 NACL·SG에 막혀 PMTUD가 블랙홀에 빠졌습니다. 발신자는 세그먼트를 줄이라는 신호를 못 받고 큰 패킷만 반복 유실하며, 작은 패킷은 통과하니 겉으로는 "연결됨"으로 보입니다.
진단: ping에 DF 비트를 세워 크기를 스윕합니다 — ping -M do -s 1472 대상은 성공하는데 -s 1500 근처부터 실패하면 경로 MTU가 낮은 것입니다. tcpdump로 같은 큰 세그먼트가 재전송만 반복되고 ICMP 응답이 없는 것을 확인합니다. Flow Log는 ACCEPT라 여기서는 단서가 없다는 사실 자체가 방향을 알려줍니다.
해결: NACL과 SG에 ICMP type 3(destination unreachable), 특히 code 4를 허용해 PMTUD를 복구합니다. 그래도 통제가 어려우면 게이트웨이나 인스턴스에서 MSS clamping을 적용하거나(iptables -t mangle의 TCPMSS --clamp-mss-to-pmtu) 인터페이스 MTU를 경로 최소값에 맞춰 낮춥니다. 근본은 터널 MTU를 고려해 TCP MSS가 처음부터 작게 협상되도록 만드는 것입니다.
클라우드 인프라 담당자의 신규 서비스 배포 전 보안그룹 체크리스트
신규 서비스를 배포할 때마다 Security Group 설정을 처음부터 만드는 팀이 있고, 체크리스트대로 검증하는 팀이 있습니다. 장애가 적은 팀은 항상 후자입니다. 아래는 실제 배포 전 5분이면 확인할 수 있는 체크리스트입니다.
배포 전 확인 사항
#!/bin/bash
# sg-preflight-check.sh — 신규 서비스 배포 전 Security Group 점검 스크립트
INSTANCE_ID="${1:-}"
if [ -z "$INSTANCE_ID" ]; then
echo "사용법: $0 <instance-id>"
exit 1
fi
echo "=== Security Group 배포 전 체크리스트 ==="
echo ""
# 1. 부착된 SG 목록
echo "[1] 부착된 Security Group:"
aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query 'Reservations[*].Instances[*].SecurityGroups[*].[GroupId,GroupName]' \
--output table
# 2. 0.0.0.0/0으로 열린 인바운드 포트 (과도한 공개 접근 확인)
echo ""
echo "[2] 인터넷 전체(0.0.0.0/0)에 열린 인바운드 포트:"
SG_IDS=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query 'Reservations[*].Instances[*].SecurityGroups[*].GroupId' \
--output text)
for sg in $SG_IDS; do
aws ec2 describe-security-groups \
--group-ids $sg \
--query "SecurityGroups[*].IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']].[{Port:FromPort,Proto:IpProtocol}]" \
--output table
done
# 3. 22번 포트(SSH) 허용 범위 확인
echo ""
echo "[3] SSH(22) 접근 허용 범위:"
for sg in $SG_IDS; do
aws ec2 describe-security-groups \
--group-ids $sg \
--query 'SecurityGroups[*].IpPermissions[?FromPort==`22`].IpRanges[*].CidrIp' \
--output text
done
echo ""
echo "=== 체크리스트 완료 ==="
점검 기준
| 체크 항목 | 권장 설정 |
|---|---|
| SSH(22) 소스 IP | 0.0.0.0/0이면 즉시 수정 → 팀 VPN IP 대역 또는 Bastion SG ID만 허용 |
| DB 포트(3306/5432) 소스 | 0.0.0.0/0이면 즉시 수정 → 앱 서버 SG ID만 허용 |
| 서비스 포트(80/443) 소스 | 0.0.0.0/0 허용(Public 서비스) 또는 ALB SG ID만 허용(Private 배포) |
| 아웃바운드 규칙 | 기본 전체 허용 유지(대부분) → 규정상 제한 필요 시에만 수정 |
| SG당 규칙 수 | 30개 이하 유지(가독성) → 초과 시 SG 분리 검토 |
자주 하는 실수 TOP 3
-
IP 대신 SG ID를 써야 하는데 IP를 씀
- EC2 IP는 재시작 시 바뀔 수 있음
- 다른 EC2 허용 시 항상 SG ID를 소스로 사용
-
인바운드만 확인하고 아웃바운드를 제거함
- 누군가 "보안 강화"로 아웃바운드를 다 지웠을 때
- EC2에서 외부 패키지 설치(yum, apt)가 안 됨
- Stateful이므로 인바운드 응답은 영향 없지만 EC2 시작 요청은 차단됨
-
SG를 직접 수정했는데 ALB/RDS에 적용 안 됨
- ALB와 RDS는 자체 ENI와 SG를 가짐
- EC2 SG 수정이 ALB SG에 영향을 주지 않음
aws ec2 describe-network-interfaces로 실제 부착 위치 확인 필요
명령어·단축키 빠른 참조
이 모듈에서 다룬 보안그룹 디버깅 명령을 실전 옵션과 함께 모았습니다. 콘솔 대신 CLI로 확인하면 여러 계층을 한 번에 비교할 수 있습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
aws ec2 describe-security-groups | SG 인바운드/아웃바운드 규칙 조회 | --group-ids sg-xxx --query 'SecurityGroups[*].IpPermissions' |
aws ec2 describe-instances | 인스턴스에 부착된 SG 확인 | --instance-ids i-xxx --query 'Reservations[*].Instances[*].SecurityGroups' |
aws ec2 describe-network-acls | 서브넷 NACL 인/아웃 규칙 확인 | --filters "Name=association.subnet-id,Values=subnet-xxx" |
aws ec2 describe-route-tables | 0.0.0.0/0 → igw/nat 경로 확인 | --filters "Name=association.subnet-id,Values=subnet-xxx" |
aws ec2 describe-network-interfaces | ALB/RDS ENI 실제 부착 위치 | --filters "Name=group-id,Values=sg-xxx" |
aws ec2 authorize-security-group-ingress | SG ID를 소스로 포트 허용 | --group-id $DB_SG --protocol tcp --port 3306 --source-group $WEB_SG |
aws logs filter-log-events | Flow Logs에서 REJECT 필터 | --filter-pattern "8080 REJECT" (차단 지점 특정) |
nc -zv | EC2 내부 포트 도달성 테스트 | nc -zv 10.0.2.100 3306 -w 3 (timed out=방화벽 / refused=앱) |
ss -tlnp | 포트 LISTEN 여부 확인 | sudo ss -tlnp | grep :8080 (refused 원인) |
iptables -L INPUT -n -v | OS 방화벽 차단 확인 | sudo iptables -L INPUT -n | grep 8080 |
ping -M do -s | 경로 MTU/PMTUD 블랙홀 확인 | ping -M do -s 1472 <대상> (큰 패킷만 멈출 때) |
다음 모듈에서는 컨테이너 네트워크 디버깅을 다루며, Docker/Kubernetes 환경에서 서비스 간 통신이 끊겼을 때 계층적으로 원인을 좁히는 방법을 살펴봅니다.
보안 그룹을 가상 방화벽으로 설계·운영하는 큰 그림은 가상 서버(EC2)(Cloud Engineering 트랙)에서, 계정 차원의 권한 통제는 계정과 IAM에서 체계적으로 다룹니다.