새 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포트 열었는데 안 되는 5단계 진단 플로우 — SG → NACL → Route Table → IGW/NAT → OS 방화벽
- 4VPC Flow Logs에서 REJECT 로그를 읽고 차단 지점 특정하기
- 5NACL vs Security Group 비교 — Stateless vs 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 방화벽의 핵심입니다.
클라이언트 (10.0.1.50) EC2 인스턴스 (10.0.2.100)
Security Group:
인바운드: TCP 8080 허용
아웃바운드: 전체 허용 (기본값)
SYN ──────────────────────────► SG 인바운드 검사: TCP 8080 ✓ ALLOW
연결 추적 테이블에 기록
SYN-ACK ◄────────────────────── SG 아웃바운드 검사: SKIP (응답 트래픽)
ACK ──────────────────────────► 연결 수립 완료
HTTP GET /api ────────────────► 연결 추적 테이블 매칭 → ALLOW
HTTP 200 OK ◄────────────────── 연결 추적 테이블 매칭 → ALLOW (아웃바운드 규칙 무시)
Stateful이 실무에서 의미하는 것
아웃바운드 기본 규칙이 "전체 허용(0.0.0.0/0)"인 경우, Security Group 설정에서 인바운드 규칙만 신경 쓰면 됩니다. 인바운드에서 허용한 연결의 응답은 아웃바운드 설정과 무관하게 돌아갑니다.
아웃바운드를 제한하는 경우 — 예를 들어 DB 서버가 외부 인터넷으로 나가는 것을 막고 싶을 때 — 에는 아웃바운드 규칙을 수정합니다. 이때도 인바운드에서 허용된 연결의 응답은 영향을 받지 않습니다. Stateful이기 때문입니다.
아웃바운드 기본 규칙을 제거하면 어떻게 되나
아웃바운드 규칙을 모두 제거한 경우:
1. 외부에서 이 EC2로 들어오는 인바운드 연결 → 응답 트래픽은 자동 허용 (Stateful)
2. 이 EC2에서 외부로 나가는 아웃바운드 연결 → 차단됨 (규칙 없음)
즉, 아웃바운드 규칙 제거는 "이 서버가 먼저 연결을 시작하는 것"만 막습니다.
외부에서 들어온 연결에 대한 응답은 여전히 나갑니다.
이 동작 원리를 이해하지 못하면 "아웃바운드를 다 막았는데 왜 응답이 가지?"라는 혼란이 생깁니다. 연결을 시작한 방향이 중요한 것이 Stateful의 핵심입니다.
실습 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 │ ← EC2에 부착된 Stateful 방화벽
│ 인바운드 규칙 │ 규칙에 없으면 기본 차단 (REJECT)
└──────────┬──────────┘
│ 통과
▼
┌─────────────────────┐
│ 2. Network ACL │ ← 서브넷 경계의 Stateless 방화벽
│ (NACL) │ 모든 방향 각각 명시적 허용 필요
└──────────┬──────────┘
│ 통과
▼
┌─────────────────────┐
│ 3. Route Table │ ← 트래픽 경로 존재 여부
│ │ 경로 없으면 패킷 드롭 (조용히 사라짐)
└──────────┬──────────┘
│ 통과
▼
┌─────────────────────┐
│ 4. IGW / NAT GW │ ← 인터넷 연결 게이트웨이
│ │ Public Subnet = IGW, Private = NAT
└──────────┬──────────┘
│ 통과
▼
┌─────────────────────┐
│ 5. OS 방화벽 │ ← 인스턴스 내 iptables / firewalld / ufw
│ (iptables 등) │ 클라우드 바깥의 마지막 관문
└──────────┬──────────┘
│ 통과
▼
EC2 애플리케이션
각 단계별 점검 포인트
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 레코드 형식은 다음과 같습니다:
version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status
실제 예시:
2 123456789012 eni-0ab1c2d3e4f56789 203.0.113.50 10.0.2.100 52341 8080 6 1 40 1620000000 1620000060 REJECT OK
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ 시작시각 종료시각 차단 정상기록
│ │ │ 소스IP 목적지IP 소포 목포 │ 패 바이
│ │ ENI ID 프로토콜(6=TCP)
│ 계정 ID
버전
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)**를 소스 포트로 사용합니다. 서버 응답은 이 포트로 돌아가야 합니다.
클라이언트 (Subnet A) 서버 (Subnet B)
src: 10.0.1.50:52341 dst: 10.0.2.100:8080
─────────────────────────────────►
요청: TCP 8080 연결
src: 10.0.2.100:8080 dst: 10.0.1.50:52341
◄─────────────────────────────────
응답: 포트 52341로 돌아옴
NACL 설정에서 이 응답 트래픽을 허용하려면:
Subnet A의 NACL 인바운드 규칙:
허용: TCP 1024-65535 (ephemeral 포트 범위)
이 규칙이 없으면 응답이 Subnet A에 들어오는 순간 차단됩니다.
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를 미리 설계합니다.
클라우드 인프라 담당자의 신규 서비스 배포 전 보안그룹 체크리스트
신규 서비스를 배포할 때마다 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
1. IP 대신 SG ID를 써야 하는데 IP를 씀
→ EC2 IP는 재시작 시 바뀔 수 있음
→ 다른 EC2 허용 시 항상 SG ID를 소스로 사용
2. 인바운드만 확인하고 아웃바운드를 제거함
→ 누군가 "보안 강화"로 아웃바운드를 다 지웠을 때
→ EC2에서 외부 패키지 설치(yum, apt)가 안 됨
→ Stateful이므로 인바운드 응답은 영향 없지만 EC2 시작 요청은 차단됨
3. SG를 직접 수정했는데 ALB/RDS에 적용 안 됨
→ ALB와 RDS는 자체 ENI와 SG를 가짐
→ EC2 SG 수정이 ALB SG에 영향을 주지 않음
→ aws ec2 describe-network-interfaces로 실제 부착 위치 확인 필요
다음 모듈에서는 컨테이너 네트워크 디버깅을 다루며, Docker/Kubernetes 환경에서 서비스 간 통신이 끊겼을 때 계층적으로 원인을 좁히는 방법을 살펴봅니다.