AWS 보안그룹 공개 규칙을 안전하게 줄이는 순서
VPC Flow Logs로 실제 통신을 확인하고 좁은 인바운드 규칙을 먼저 추가한 뒤 0.0.0.0/0 공개 규칙을 제거하는 방법을 설명한다.
이 글의 목차보안그룹이 리소스에 닿는 순서6
가게 출입문이 너무 넓게 열려 있다고 해서 영업 중에 문부터 잠가 버릴 수는 없다.
배달하던 사람까지 함께 막힐 수 있다.
필요한 사람에게 다른 출입구를 마련하고 그 길이 작동하는지 본 뒤 기존 문을 닫아야 한다.
AWS 보안그룹을 정리할 때도 같은 순서를 택했다.
보안그룹은 허용할 네트워크 통신을 정하는 가상 방화벽이다.
외부에서 리소스로 들어오는 통신을 인바운드, 리소스에서 밖으로 나가는 통신을 아웃바운드라고 부른다.
이번 작업은 공개된 인바운드 규칙을 줄이는 일이었다.
0.0.0.0/0은 해당 규칙의 포트와 프로토콜에 대해 모든 IPv4 주소를 출발지로 허용한다.
모든 포트를 한꺼번에 여는 값은 아니지만 출발지 범위는 가장 넓다.
이유 없이 남길 수 없었고, 실제 호출자를 확인하지 않은 채 지울 수도 없었다.
IPv6를 쓰는 환경에서는 모든 IPv6 주소를 뜻하는 ::/0도 같은 점검 대상이다.
IPv4 규칙만 좁혀도 IPv6 공개 규칙이 남아 있으면 외부 접근 경로는 닫히지 않는다.
예를 들어 애플리케이션 서버만 접근해야 하는 데이터베이스 포트가 인터넷 전체에 열려 있다면, 데이터베이스 비밀번호가 별도로 있어도 불필요한 접속 시도와 취약점 탐색을 계속 받는다.
출발지를 애플리케이션 서버가 속한 보안그룹으로 좁히면 인증은 그대로 유지하면서 네트워크에 도달할 수 있는 대상부터 줄일 수 있다.
보안그룹이 리소스에 닿는 순서
보안그룹 한 줄만 보면 통신 전체를 이해하기 어렵다.
실제 흐름은 다음 순서로 이어진다.
- 외부 인터넷 또는 VPC 내부 호출자
- IP 주소 범위나 다른 보안그룹으로 출발지를 표현한다.
- 보안그룹의 인바운드 규칙
- 출발지, 프로토콜, 포트가 맞는 통신을 허용한다.
- ENI(Elastic Network Interface)
- 서버나 로드 밸런서에 붙는 가상 네트워크 카드다.
보안그룹은 이 ENI에 연결된다.
- 서버나 로드 밸런서에 붙는 가상 네트워크 카드다.
- EC2·로드 밸런서·데이터베이스 같은 리소스
- ENI를 통해 실제 요청을 받는다.
한 리소스에 여러 보안그룹이 붙으면 허용 규칙이 합쳐진다.
직접 연결된 ENI가 없는 그룹도 다른 보안그룹의 출발지로 참조될 수 있다.
IP 대신 보안그룹 ID로 호출자를 지정하는 관계다.
그래서 공개 규칙을 찾을 때는 보안그룹 하나만 보지 않았다.
대상 ENI에 연결된 모든 그룹의 IPv4 0.0.0.0/0, IPv6 ::/0, 여러 네트워크 대역을 하나로 묶은 prefix list와 다른 보안그룹 참조를 합쳐 실제 허용 경로를 확인해야 한다.
한 그룹의 공개 규칙을 지워도 같은 ENI의 다른 그룹이 같은 포트를 열고 있으면 외부 접근은 그대로다.
80개를 세고 Flow Logs부터 켰다
작업 시작 시 보안그룹은 80개였고 공개 인바운드를 가진 그룹은 29개였다.
삭제보다 먼저 세 VPC에 Flow Logs를 켰다.
VPC는 AWS 안에 만든 격리된 가상 네트워크다.
Flow Logs는 네트워크 인터페이스를 오간 흐름의 출발지·목적지·포트·허용 여부 같은 메타데이터를 남긴다.
패킷 내용 전체를 저장하는 도구는 아니다.
트래픽 유형은 허용과 거부를 모두 기록하는 ALL로 설정했고 세 개 모두 ACTIVE가 된 것을 확인했다.
다만 ACTIVE는 설정이 활성화됐다는 뜻이지 모든 레코드가 목적지에 전달됐다는 증거는 아니다.
Flow Logs는 가능한 범위에서 전달하는 best-effort 방식이며 실시간 기록도 아니라서 로그가 늦거나 일부를 건너뛴 SKIPDATA가 생길 수 있다.
describe-flow-logs의 DeliverLogsErrorMessage, 실제 수신 레코드의 action과 log-status까지 확인해야 한다.
Flow Logs에 기록이 없다는 사실만으로 사용하지 않는 경로라고 판단하지는 않았다.
활성 트래픽이 있어야 레코드가 생기고, AWS가 밝힌 제외 트래픽과 건너뛴 레코드도 있다.
주소를 변환하는 NAT(Network Address Translation), 로드 밸런서나 다른 중간 장비를 지나는 경로에서는 기본 srcaddr가 원래 클라이언트 주소가 아닐 수 있다.
이때는 사용자가 기록할 필드를 고르는 custom format에 pkt-srcaddr와 pkt-dstaddr를 넣어 원래 패킷 주소도 함께 본다.
좁은 규칙을 먼저 추가했다
실제 호출자가 사용하는 사설 주소 범위를 확인한 뒤 기존 공개 규칙과 같은 포트에 더 좁은 규칙을 먼저 추가했다.
aws ec2 authorize-security-group-ingress \
--group-id "<security-group-id>" \
--protocol tcp \
--port "<service-port>" \
--cidr "<private-cidr>"--group-id는 바꿀 보안그룹을, --protocol과 --port는 통신 방식과 서비스 입구를 고른다.
--cidr는 허용할 출발지 주소 범위다.
호출 주체가 같은 VPC나 지원되는 연결 경로의 AWS 리소스로 명확하다면 넓은 사설 CIDR보다 source security group을 우선 검토할 수 있다.
이 참조는 상대 그룹의 규칙을 복사하는 기능이 아니라, 그 그룹이 붙은 ENI에서 오는 트래픽을 출발지로 지정하는 방식이다.
중간 장비를 거치는 경로에서는 기대대로 동작하지 않을 수 있어 실제 네트워크 경로를 먼저 확인한다.
출발지 표현은 다음 기준으로 고른다.
| 출발지 | 적합한 상황 | 주의할 점 |
|---|---|---|
| source security group | 같은 VPC 또는 지원되는 연결에서 AWS 리소스끼리 통신 | 중간 장비를 거치면 원래 그룹 참조가 보존되는지 확인 |
| 사설 CIDR | 온프레미스·VPN처럼 네트워크 대역이 호출자를 대표 | 대역 안의 모든 주소가 허용되므로 범위를 불필요하게 넓히지 않음 |
고정 공인 IP /32 | 회사 관리망처럼 외부 고정 주소 한 곳에서 접근 | 주소 변경 절차와 IPv6 경로를 함께 관리 |
0.0.0.0/0 또는 ::/0 | 인터넷에 공개해야 하는 웹 입구 | 필요한 포트만 열고 로드 밸런서·WAF·인증 같은 다른 방어 계층을 둠 |
인터넷 사용자에게 공개해야 하는 443 포트와 내부 애플리케이션만 접근해야 하는 DB 포트를 같은 기준으로 닫을 수는 없다.
공개 자체가 잘못인지보다 그 리소스가 공개 입구 역할을 해야 하는지부터 확인한다.
규칙을 추가한 다음 곧바로 공개 규칙을 지우지 않았다.
- 새 연결로 서비스 기능을 호출했다.
- 애플리케이션 상태와 오류를 확인했다.
- Flow Logs에서 새 사설 경로의 흐름을 확인했다.
- 기존 연결과 새 연결을 나눠 확인했다.
- 문제가 생기면 되돌릴 공개 규칙을 준비했다.
보안그룹은 상태 저장 방식이라 허용된 연결의 응답은 별도 반대 방향 규칙 없이 돌아갈 수 있다.
연결 추적 때문에 이미 이어진 연결과 변경 후 새로 맺은 연결이 다르게 보일 수 있어 새 연결 검사가 필요했다.
확인 뒤 공개 규칙을 제거했다
새 경로의 통신을 확인한 다음 기존 공개 규칙을 제거했다.
이 명령은 필요한 통신까지 끊을 수 있으므로 앞 단계의 검증과 되돌리기 준비 없이 실행할 대상이 아니다.
먼저 사람이 다시 입력한 포트와 CIDR 대신 AWS가 각 규칙에 부여한 sgr-... ID를 조회했다.
규칙 ID는 비슷한 규칙이 여러 개 있어도 제거할 한 줄을 정확히 가리킨다.
aws ec2 describe-security-group-rules \
--filters "Name=group-id,Values=<security-group-id>" \
--query 'SecurityGroupRules[?IsEgress==`false`].[SecurityGroupRuleId,IpProtocol,FromPort,ToPort,CidrIpv4,CidrIpv6,ReferencedGroupInfo.GroupId]' \
--output table승인한 ID를 현재 조회 결과와 한 번 더 대조한 다음 그 ID만 제거한다.
aws ec2 revoke-security-group-ingress \
--group-id "<security-group-id>" \
--security-group-rule-ids "<security-group-rule-id>"--dry-run은 현재 자격 증명에 제거 권한이 있는지 확인하는 데는 쓸 수 있지만 통신이 안전한지는 검사하지 않는다.
속성 방식으로 제거할 경우 기본 VPC에서는 일치하지 않는 규칙이 있어도 오류 없이 반환될 수 있다.
어느 방식을 쓰든 명령의 성공 여부만 보지 않고 describe-security-group-rules를 다시 실행해 해당 ID가 사라졌는지 확인해야 한다.
규칙 전파 시간을 둔 뒤 허용 소스에서는 새 연결이 성공하고, 차단할 외부 소스에서는 새 연결이 실패하는지도 같은 포트로 확인한다.
같은 점검에서 ENI 연결이 없는 보안그룹 13개와 사용하지 않는 클러스터 관련 포트 규칙 4개도 제거했다.
다만 ENI 미연결만으로는 미사용이라고 판단할 수 없다.
다른 보안그룹이 출발지로 참조하는지, 새 서버 생성 설정인 launch template, 서버 수 자동 조절 설정인 Auto Scaling, 컨테이너 배포 설정인 ECS·EKS가 다음 배포에서 사용할 그룹인지까지 확인해야 한다.
삭제 전 규칙과 소비자 목록도 남긴다.
같은 이름으로 다시 만들어도 보안그룹 ID가 달라져 기존 참조가 자동 복구되지는 않기 때문이다.
작업 후 전체 보안그룹은 80개에서 67개, 공개 인바운드를 가진 그룹은 29개에서 14개가 됐다.
세 VPC의 Flow Logs는 모두 ACTIVE였고 기록된 대상 규칙 변경 명령 실패는 0건이었다.
이 두 상태만으로 통신 안전성이 증명되는 것은 아니어서 실제 Flow Logs 도착과 허용·차단 새 연결을 별도 완료 조건으로 봤다.
절반으로 줄인 것은 위험 점수가 아니라 공개 인바운드 그룹 수다
작업 결과는 숫자로 확인할 수 있다.
전체 보안그룹은 80개에서 67개로 13개, 16.25% 줄었다.
공개 인바운드를 가진 그룹은 29개에서 14개로 15개, 51.7% 줄었다.
세 VPC의 Flow Logs는 모두 ACTIVE였고 기록된 대상 규칙 변경 명령 실패는 0건이었다.
해결한 문제는 실제 호출자를 확인하지 않은 공개 규칙과 소유자 없는 보안그룹을 그대로 두던 상태였다.
51.7%를 보안 위험 감소율로 해석할 수는 없다.
여러 보안그룹이 한 리소스에 붙으면 허용 규칙이 합쳐지고, 같은 접근을 허용하는 다른 규칙이 남아 있을 수 있기 때문이다.
다만 출발지 주소 범위만 계산하면 0.0.0.0/0을 /p인 사설 CIDR로 바꿀 때 허용 후보 주소 수는 2^32개에서 2^(32-p)개로 줄어든다.
예를 들어 /24라면 계산상 1,677만 7,216분의 1이다.
이것도 주소 공간의 차이일 뿐 실제 공격 가능성이나 피해 규모의 측정값은 아니다.
남은 공개 그룹 14개도 포트·연결 자산·인증 수단을 검토해야 노출 정리가 끝난다.
서비스가 동작 중인 상태에서 허용 범위를 좁힐 때는 기존 세션만 보고 성공으로 판단하지 않고 새 연결을 만들어 확인해야 한다.
계획된 변경에서는 ‘좁은 규칙 추가 → 새 연결 검증 → 공개 규칙 제거’를 기본 절차로 삼고, 침해 대응에서는 이 순서보다 즉시 차단을 우선한다.
이번 경험에서 재사용할 수 있는 결론은 공개 규칙의 개수를 줄이는 것보다 필요한 통신을 더 좁은 출발지로 옮긴 뒤 실제 새 연결로 검증하는 것이 중요하다는 점이다.