🔥 Security Group: 인스턴스 레벨 방화벽
강의 목차

보안 그룹은 deny를 적을 곳이 없다. 콘솔에서 처음 인바운드 규칙 화면을 열고 한참 막혔던 적이 있다. 한 줄을 통째로 막아야 할 것 같은 일이 생기면 어디에 적느냐고 물었다. 답은 적을 곳이 없다, 그게 곧 설계라는 사실이었다.
AWS 공식 문서 한 줄이 이 설계를 그대로 적어 둔다. "You can specify allow rules, but not deny rules." 보안 그룹은 허용만 적는다. 차단은 허용을 적지 않는 것으로 한다. 이게 정책 평가 흐름: Allow와 Deny가 만나면에서 다룬 IAM 정책의 Implicit Deny / Explicit Deny 비대칭과 겹치는 대목이긴 한데, 그쪽보다 더 단순하다. Explicit Deny가 없다. 이게 SG에 대한 첫 인상이다.

어디에 매달려 있는가, 인스턴스 ENI 레벨
보안 그룹이 연결되는 위치는 인스턴스 전체가 아니라 인스턴스의 ENI(네트워크 인터페이스) 한 장이다. EC2를 만들면 ENI가 따라 붙고 그 ENI에 SG를 묶는다. ENI 하나당 SG는 기본 5개까지, 한도를 늘리면 16개까지 묶을 수 있다.
Subnet: Public과 Private의 진짜 차이에서 본 것처럼 Subnet의 public/private 분류는 라우팅 테이블이 결정했다. SG는 그 위에 한 겹을 더 더하는 셈이다. Subnet에 들어왔다, 즉 라우팅 테이블이 "이 Subnet으로 보내라"를 통과시킨 다음, 그 다음에 SG가 ENI 앞에서 한 번 더 검사한다. 이게 SG의 동작 위치다.
같은 Subnet 안의 두 인스턴스 사이에도 SG가 작용한다. 내부 트래픽이라 안전하다는 가정은 SG에 통하지 않는다. 같은 Subnet인지가 아니라, 받는 ENI에 매달린 SG가 그 출발지로부터의 트래픽을 허용한다고 적어 놨는지가 전부다.
새로 만든 SG가 기본으로 갖는 구조
여기서 한 번 함정에 걸린 적이 있다. 새로 만든 보안 그룹과 VPC가 기본으로 들고 다니는 보안 그룹(default security group)이 같은 줄 알았는데 둘은 다르다.
새로 만들면, 그러니까 콘솔에서 Create security group을 누르면, 구성이 이렇다.
- 인바운드 규칙: 0줄. 아무것도 안 들어온다.
- 아웃바운드 규칙:
0.0.0.0/0모든 포트 모든 프로토콜로 전부 나가는 한 줄(VPC가 IPv6 CIDR을 갖고 있으면::/0도 같은 형식으로 한 줄을 더 추가한다).
이 둘이 묘하게 비대칭이다. 받는 쪽은 닫혀 있고 나가는 쪽은 활짝 열려 있다. 현실적으로 외부 호출이 전혀 필요 없는 인스턴스는 사실상 드무니까, 만든 직후의 SG도 곧장 어디든 부를 수는 있다. 단지 외부에서 누가 와서 두드릴 수는 없는 상태.
VPC가 기본으로 들고 있는 default SG는 구성이 또 다르다.
- 인바운드 한 줄: source가
자기 자신의 SG ID. 즉, default SG에 묶인 인스턴스끼리는 모든 포트로 서로 통신할 수 있다. - 아웃바운드:
0.0.0.0/0로 전부 나가는 한 줄(VPC가 IPv6 CIDR을 갖고 있으면::/0도 같은 형식으로 한 줄을 더 추가한다).
이 self-referencing 한 줄이 의외로 자주 사고를 친다. 학습용 default VPC에서는 편하다는 이유로 아무 인스턴스에나 default SG를 박았다가, 그게 운영으로 흘러가면 같은 SG에 묶인 인스턴스 전부가 전 포트로 서로 통신할 수 있게 된다. AWS 공식 가이드도 default SG에 기대지 말고 별도의 보안 그룹을 만들어 쓰라고 권한다. SG를 명시하지 않은 채 인스턴스를 띄우면 default SG가 자동으로 붙어 버리기 때문이다.

Stateful이라는 한 줄의 뜻
SG가 stateful이라는 말은, 한 번 인바운드 규칙을 거친 패킷의 짝(response)은 SG가 아웃바운드 규칙을 다시 검사하지 않고 그대로 보낸다는 뜻이다. 반대 방향도 마찬가지. 내가 인스턴스에서 어딘가로 요청을 던지면 그 응답은 인바운드 규칙과 무관하게 들어온다.
이 동작을 가능하게 해 주는 것이 connection tracking. AWS Nitro 시스템이 흐름의 상태를 기억해 두는 메커니즘이고, 응답 패킷이 도착하면 SG가 그 기억과 매칭해 그대로 내보낸다. 정확한 내부 구조까지 공식 문서가 공개하지는 않지만, 흐름 단위로 한 항목이 잠시 잡혀 있다가 idle timeout 이후 AWS가 항목을 비운다고 명시한다.
타임아웃은 protocol마다 다르다 (verified 2026-04-26).
- TCP established: 기본 350초(Nitrov6 인스턴스 타입, P6e-GB200 제외) / 432,000초 = 5일(그 외 Nitro 타입). 범위 60초 ~ 5일.
- UDP unidirectional(한 쪽만 흐른 UDP 흐름): 기본 30초, 범위 30~60초.
- UDP stream(양쪽이 다 흐른 UDP): 기본 180초, 범위 60~180초.
- ICMP: AWS 표현 그대로 "ICMP traffic is always tracked." 즉 outbound 규칙이 ICMP를 막고 있어도 들어온 인바운드 ICMP의 응답은 통과한다.
- TCP/UDP/ICMP가 아닌 프로토콜(예: ESP): IP와 프로토콜 번호만 추적. 같은 짝 트래픽이 600초 안에 도착하면 SG는 인바운드 규칙과 무관하게 응답을 수용한다.
이 타임아웃들 중 TCP established / UDP stream / UDP unidirectional 셋은 2023-11-20부터 ENI 단위로 조정할 수 있게 됐다 (Nitro 인스턴스 한정, ENI의 ConnectionTrackingSpecification 항목). NAT Gateway: Private Subnet의 외부 연결에서 본 350초 idle timeout은 NAT GW 자체의 고정값이고, 이쪽은 ENI 단위 conntrack이라 작동하는 box가 다른 위치에 있다. 둘이 둘 다 stateful이라 헷갈리는데, 연결되어 동작하는 위치가 다르다는 점만 잡고 있으면 된다.
한 가지 미묘한 예외가 있다. untracked connection이라 부르는 경우. 인바운드와 아웃바운드 양쪽 모두 0.0.0.0/0 (또는 ::/0) × 모든 포트로 활짝 열려 있고, 흐름이 그 규칙과 정확히 일치한다면, 그 흐름은 conntrack에 등록되지 않는다. 등록되지 않았다는 건, 규칙을 수정하거나 지우면 진행 중인 흐름까지 그 즉시 함께 멈춘다는 뜻이다. 한도 가까이 conntrack을 쓰는 워크로드에서 의도적으로 untracked로 빼는 패턴이 있긴 한데, 운영에서는 부작용을 정확히 알고 써야 하는 패턴이다.
SG를 SG로 참조하는 패턴이 운영의 주력
SG의 source/destination 칸에는 CIDR도 들어가지만, 다른 SG의 ID도 들어간다. 이게 운영 SG의 진짜 주력 패턴이다.
두 방식을 나란히 두면 차이가 명확하다. 웹 서버 ASG와 RDS 사이에서 RDS는 웹 서버에서 오는 5432 포트만 받는다고 적고 싶다. 두 가지 길이 있다.
- CIDR로 박기: RDS의 SG 인바운드에
10.0.1.0/24, TCP, 5432한 줄. 그런데 웹 서버 Subnet이 늘어나거나, 다른 AZ로 ASG가 확장되면 그때마다 사람이 CIDR을 추가해야 한다. - SG로 박기: RDS의 SG 인바운드에
sg-web, TCP, 5432한 줄. ASG가 새 인스턴스를 띄우면 그 인스턴스에는 sg-web이 자동으로 매달리고, RDS 룰은 자동으로 그 인스턴스를 받아준다. 룰 자체는 한 번도 손대지 않는다.
SG 참조는 같은 VPC 안에서 자유롭게 쓰이고, VPC Peering으로 연결된 다른 VPC에서도 인바운드와 아웃바운드 양쪽으로 가능하다. Transit Gateway로 묶인 VPC들 사이에서는 SG 참조가 인바운드 룰에서만 지원되니 비대칭이라는 점만 잡고 있으면 된다. Cross-account는 123456789012/sg-1a2b3c4d 형식으로 적는다. 다만 공인 IP 트래픽에는 적용되지 않는다. 이건 사설 통신 한정의 약속이다.
지금까지 반복해서 짚는 관리형이 감추는 비용 흐름에서 보면, SG 참조는 그 비용을 살짝 줄이는 한 가지 방법이다. SG 자체는 무료지만, ENI당 5개 SG × 60줄 인바운드 × 60줄 아웃바운드 한도가 생각보다 빨리 찬다. 한 줄을 인스턴스 IP로 적는 대신 SG로 적으면, 그 한 줄이 인스턴스 N개를 커버한다. 운영에서 룰 갱신을 사람이 따라가지 않아도 되는 운영 비용 절감이 SG 참조의 진짜 값.

한도, 곱셈으로 빨리 차는 항목
숫자는 외워둘 만하다 (verified 2026-04-26).
- 리전당 VPC 보안 그룹: 2,500개 (조정 가능. VPC당이 아니라 리전당이 정확한 표현)
- SG당 인바운드 룰: 60줄 (조정 가능)
- SG당 아웃바운드 룰: 60줄 (조정 가능, 인바운드와 별개로 카운트)
- ENI당 SG: 5개, 한도 늘리면 16개까지 (조정 가능)
- 단, SGs per ENI × rules per SG ≤ 1,000이 하드 한도
기본값이면 ENI 하나당 5 × 60 = 300줄. 한도를 둘 다 늘려도 1,000줄까지가 한계다. 마이크로서비스가 늘어나면서 룰을 CIDR로 박아 두면 이 한도가 의외로 빨리 닿는다. 그래서 AWS와 운영자 모두 SG 참조 패턴을 권장한다.
룰을 적을 때 prefix list (관리형 IP 묶음)를 source로 쓰는 옵션도 있는데, prefix list 한 줄은 그 안의 최대 엔트리 수만큼 룰 카운트에 함께 들어간다. AWS 관리형 prefix list 중 weight가 10이면 그 한 줄이 10줄 분량으로 룰 카운트를 차지한다. 이걸 모르고 박았다가 한도에 걸려 SG 생성이 실패하는 일이 있다.
SG와 NACL이 다른 일을 한다는 점
세 가지가 다르다.
- 적용 레벨: SG는 ENI. Network ACL(NACL)은 Subnet.
- 상태: SG는 stateful. NACL은 stateless라서 응답을 자동으로 통과시키지 않는다.
- 룰 종류: SG는 allow only. NACL은 allow와 deny 모두.
여기서 언제 SG가 아닌 NACL이 필요한가가 자연스럽게 나온다. 한 CIDR 자체를 Subnet 단위로 통째로 차단하고 싶을 때, 예컨대 알려진 악성 IP 대역을 막아야 할 때, SG는 deny가 없으므로 못한다. NACL 인바운드에 deny 한 줄이 들어가야 그게 가능하다.
이 두 박스가 어떻게 함께 쌓이는지를 다음 편에서 짚는다. 우선 이번 글에서 잡을 것은 이렇다. SG는 ENI에 매달린 stateful, allow-only 방화벽이고, 허용하지 않은 것은 들어오지 않는다. 거기까지가 인스턴스 레벨에서 SG가 책임지는 전부다.

운영에 들어가면 매번 같이 적는 두 줄이 있다. 하나는 새 SG는 만들자마자 outbound 0.0.0.0/0을 좁힌다. 그 한 줄을 그대로 두면 어디든 콜이 새 나간다. 둘은 RDS·내부 서비스 SG의 인바운드는 SG 참조로 적는다, CIDR로 박지 않는다. 이 두 줄이 SG의 운영 비용을 절반으로 깎는다.











