목차
- Step 1. 모든 서버에 공인 IP를 줬더니
- Step 2. 2005년 데이터센터: DMZ의 등장
- Step 3. 내부 DB가 패치를 못 받는다
- Step 4. "NAT 있으면 밖에서도 DB 들어올 수 있는 거 아냐?"
- Step 5. 급하다고 DB SSH를 열었더니
- Step 6. 데이터센터를 AWS로 옮기기
- Step 7. Private Subnet인데 Web에서 DB로 접속이 된다
- Step 8. "내부끼리는 전부 허용"으로 SG를 걸었더니
- 정리하며
과제 1에서 Web, App, DB 서버를 나누고 이중화까지 했다. 그런데 이 서버들이 어떤 네트워크에 어떻게 놓이는지는 전혀 다루지 않았다. 과제 2는 그 빈칸을 채우는 과제다. 모든 서버를 인터넷에 그냥 연결한 상태에서 시작해서, 2005년 데이터센터를 설계해 보고, 그 구조를 AWS로 옮긴 뒤, 마지막으로 내부에 들어온 공격자의 이동까지 막는 순서로 진행했다.
Step 1. 모든 서버에 공인 IP를 줬더니

문제 하루 만에 DB 서버 3306 포트로 전 세계에서 로그인 시도가 들어왔다.
Web, App, DB 세 서버 모두 공인 IP를 받아 인터넷에 직접 연결했다. 하루 만에 DB 서버 3306 포트로 전 세계에서 로그인 시도가 들어왔다. DB는 외부 사용자와 통신할 일이 전혀 없는데도 인터넷에 노출되어 있었던 것이다.
처음에는 "DB 비밀번호만 강하게 걸면 괜찮지 않나?"라고 생각했다. 그런데 이건 잘못된 생각이었다. 비밀번호는 들어오는 시도를 막는 마지막 방어선일 뿐이고, 시도 자체가 가능하다는 건 이미 공격면이 열려 있다는 뜻이다. DB 소프트웨어에 인증 우회 취약점이 하나만 나와도 비밀번호는 의미가 없어진다. 애초에 닿을 수 없게 만드는 것이 먼저다.
그래서 서버를 나누는 기준을 "이 서버가 인터넷과 직접 통신해야 하는가"로 잡았다.
- Web — 사용자 요청을 받아야 하므로 외부 노출이 필요하다.
- App — Web에서만 요청을 받는다. 외부와 직접 통신할 이유가 없다.
- DB — App에서만 요청을 받는다. 외부와 통신할 이유가 전혀 없다.
이게 Network Segmentation이고, 그 기준이 최소 권한(Least Privilege)이다. 사람에게 필요한 권한만 주듯이, 서버에도 필요한 통신 경로만 준다.
Step 2. 2005년 데이터센터: DMZ의 등장
문제 전부 사설 IP로 내부망에 넣었더니 안전해졌는데, 외부 고객이 웹 서버에 접속을 못 한다.
AWS가 없던 시절로 돌아가서 생각해 봤다. 웹 서버, 사내 업무 서버, DB 서버, 관리자 PC가 있고, 전부 사설 IP로 내부망에 넣었다. 안전해지긴 했는데 이번엔 외부 고객이 웹 서버에 접속을 못 한다.
여기서 필요한 게 DMZ(Demilitarized Zone)다. 외부에 노출되어야 하는 서버만 따로 두는 완충 영역이다. 이름 그대로 "비무장지대"처럼 외부와 내부 사이에 끼어 있다.

각 요소를 조사하며 정리한 내용은 이렇다.
- Router는 서로 다른 네트워크 사이에서 패킷이 어디로 갈지 경로를 정한다.
- Firewall은 그 경로를 지나가는 트래픽 중 무엇을 허용할지 정책으로 판단한다. Router가 "길"이라면 Firewall은 "검문소"다.
- Private IP는 인터넷에서 라우팅되지 않는 주소 대역(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)이다. 이 주소만 가진 서버는 인터넷에서 직접 찾아올 수 없다.
- VLAN은 물리적으로 같은 스위치에 연결되어 있어도 논리적으로 네트워크를 나누는 기술이다. 업무 서버, DB, 관리자 PC를 서로 다른 VLAN에 두면 같은 내부망이라도 방화벽을 거쳐야만 통신할 수 있다.
방화벽을 두 겹으로 둔 이유도 있다. 외부 방화벽이 뚫려도 DMZ까지만 들어오고, 내부로 가려면 한 번 더 통과해야 한다. DMZ에 있는 웹 서버가 탈취되는 상황을 처음부터 가정한 구조다.
Step 3. 내부 DB가 패치를 못 받는다
문제 DB 서버 OS 패치를 받아야 하는데 내부망이라 인터넷에 못 나간다.
웹 서버는 DMZ에, DB는 내부망에 두었다. 그런데 DB 서버 OS 패치를 받아야 하는데, 내부망이라 인터넷에 나갈 수가 없다.
사설 IP는 인터넷에서 라우팅되지 않으니 요청을 보내도 응답이 돌아올 길이 없다. 그래서 NAT(Network Address Translation)가 필요하다. 내부 서버가 밖으로 나갈 때 출발지 사설 IP를 NAT 장비의 공인 IP로 바꿔서 내보내고, 응답이 오면 다시 원래 사설 IP로 바꿔서 돌려준다.
여기에 Routing 설정이 같이 따라와야 한다. 내부망의 라우팅 테이블에 "내부 대역이 아닌 목적지(0.0.0.0/0)는 NAT로 보내라"는 기본 경로를 넣어야 패킷이 NAT를 거쳐 나간다.
Step 4. "NAT 있으면 밖에서도 DB 들어올 수 있는 거 아냐?"
팀원이 이렇게 물었을 때 바로 답을 못 했다. 조사해 보니 Outbound와 Inbound는 완전히 다른 문제였다.

NAT는 내부에서 먼저 시작한 연결에 대해서만 변환 테이블(매핑)을 만든다. 응답 패킷은 이 매핑을 보고 원래 서버로 돌아간다. 반대로 외부에서 먼저 들어온 패킷은 매핑이 없으니 어느 내부 서버로 보내야 할지 모르고, 그냥 버려진다. 그래서 일반적인 NAT(SNAT)는 Outbound만 해결한다.
그런데 여기서 한 번 더 잘못 생각했다. "그럼 NAT가 방화벽 역할을 하는구나"라고 결론 내렸는데, 이건 틀렸다. NAT가 외부 연결을 막는 건 부수 효과일 뿐이다. 포트 포워딩(DNAT)을 하나만 설정해도 외부에서 내부로 들어오는 길이 생긴다. NAT의 목적은 주소 변환이지 접근 통제가 아니다.
실제 차단은 Stateful Firewall의 역할이다. Stateful 방화벽은 연결 상태를 추적해서 "내가 먼저 나간 연결의 응답"은 허용하고, "외부에서 먼저 시작한 연결"은 정책에 따라 막는다. 결과적으로 NAT와 비슷해 보이지만, 방화벽은 의도를 가지고 막는 장비라는 점이 다르다.
Step 5. 급하다고 DB SSH를 열었더니
문제 며칠 뒤 DB 서버에 알 수 없는 로그인 시도가 수천 건 쌓였다.
관리자가 집에서 내부 서버를 관리해야 해서, 급한 김에 방화벽에서 DB 서버의 SSH 포트를 외부에 열었다. 며칠 뒤 알 수 없는 로그인 시도가 수천 건 쌓였다. Step 1에서 겪은 일을 SSH 포트로 똑같이 반복한 셈이다.
내부 서버에 직접 포트를 열면 안 되는 이유는 이렇게 정리했다.
- 서버마다 포트를 열면 공격면이 서버 수만큼 늘어난다.
- 인터넷에서 가장 중요한 DB까지 한 번에 도달할 수 있게 된다.
- 접속 기록이 서버마다 흩어져서 누가 언제 들어왔는지 추적하기 어렵다.
해결책은 Bastion Host다. DMZ에 관리 접속 전용 서버를 하나 두고, 관리자는 반드시 여기를 거쳐서만 내부 서버로 들어간다.
Bastion은 입구를 한 곳으로 모으는 역할이다. 이 한 곳만 강하게 지키면 된다. 접근 가능한 IP를 ACL로 제한하고, 키 기반 인증과 MFA를 걸고, 모든 접속을 기록한다. 내부 서버는 Bastion에서 오는 SSH만 받으면 된다. 대안으로 VPN으로 사내망에 먼저 들어오는 방식도 있다.
지금까지의 온프레미스 최종 설계는 위 온프레미스 네트워크 설계도와 같다.
Step 6. 데이터센터를 AWS로 옮기기
이제 이 구조를 AWS로 그대로 옮긴다. 온프레미스의 각 요소가 AWS에서 무엇이 되는지 대응시켜 봤다.
온프레미스 | AWS | 비고 |
사내 네트워크 영역 | VPC | 격리된 가상 네트워크 전체 |
Network Segment (VLAN) | Subnet | VPC 안을 나눈 대역, 하나의 AZ에 속함 |
Routing Table | Route Table | 서브넷별로 연결 |
DMZ | Public Subnet | IGW로 가는 경로가 있는 서브넷 |
Internal Network | Private Subnet | IGW로 가는 경로가 없는 서브넷 |
Firewall Rule | Security Group / NACL | SG는 인스턴스 단위·Stateful, NACL은 서브넷 단위·Stateless |
NAT | NAT Gateway | Public Subnet에 두고 Private에서 사용 |
Internet Router | Internet Gateway | VPC와 인터넷을 잇는 출입구 |
조사하면서 가장 헷갈렸던 건 Public Subnet과 Private Subnet의 차이가 무엇으로 결정되는가였다. 처음에는 서브넷을 만들 때 "Public"이라는 옵션을 고르는 줄 알았다. 그런데 AWS에 그런 설정은 없다. 서브넷에 연결된 Route Table에
0.0.0.0/0 → Internet Gateway 경로가 있으면 Public, 없으면 Private이다. 결국 차이는 라우팅 한 줄이다.SG와 NACL의 차이도 정리했다. SG는 Stateful이라 들어온 요청의 응답은 자동으로 나간다. NACL은 Stateless라 응답 트래픽(임시 포트 1024~65535)까지 따로 허용해야 한다. 그래서 기본 통제는 SG로 하고, NACL은 서브넷 단위의 굵은 차단용 2차 방어선으로 쓴다.
Step 7. Private Subnet인데 Web에서 DB로 접속이 된다

문제 공격자가 웹 서버 취약점으로 권한을 얻자, Web에서 DB로 바로 접속이 됐다. Private Subnet인데 왜?
AWS로 옮기면서 Web은 Public, App과 DB는 Private에 넣었다. 그런데 공격자가 웹 서버 취약점으로 Web Server 권한을 얻었더니, Web에서 DB로 바로 접속이 됐다.
여기서 세 번째로 잘못 생각하고 있었다는 걸 알았다. "Private Subnet에 넣었으니 안전하다"고 생각했던 것이다. 조사해 보니 Private Subnet이 막아 주는 것은 인터넷에서 들어오는 트래픽(North-South)뿐이었다. IGW로 가는 경로가 없으니 밖에서 찾아올 수 없다. 딱 거기까지다.
VPC의 모든 Route Table에는 기본으로
VPC 대역 → local 경로가 들어 있다. 즉 같은 VPC 안의 서브넷끼리는 기본적으로 전부 통신할 수 있다. Web이 Public에 있든 DB가 Private에 있든, 둘은 같은 VPC 안에 있으니 라우팅상 막힘이 없다. 이렇게 내부 서버끼리 오가는 트래픽을 East-West Traffic이라고 한다.이미 내부에 들어온 공격자의 이동을 막으려면 라우팅이 아니라 접근 제어가 필요하다. 출발지(Source), 목적지, 포트를 기준으로 누가 누구에게 무엇으로 접근할 수 있는지 정해야 한다. 그게 Security Group이다.
Step 8. "내부끼리는 전부 허용"으로 SG를 걸었더니
문제 SG를 붙였는데도 여전히 Web에서 DB로 접속이 된다.
처음에는 SG를 걸되 "VPC 대역(10.0.0.0/16)에서 오는 건 전부 허용"으로 설정했다. 당연히 여전히 Web에서 DB로 접속이 됐다. Step 7의 문제를 SG로 그대로 다시 만든 것이다.
"내부니까 신뢰한다"는 가정이 깨지는 지점은 명확하다. 외부에 노출된 서버가 탈취되는 순간, 공격자는 이미 "내부"에 있다. 위치(내부 IP 대역)를 기준으로 신뢰하면 탈취된 Web도 신뢰 대상이 된다. 그래서 신뢰 기준을 위치가 아니라 역할로 바꿔야 한다. 이게 Zero Trust의 핵심이라고 이해했다.
AWS에서는 이걸 Security Group Chaining으로 구현한다. Source를 IP 대역 대신 다른 SG의 ID로 지정하는 방식이다. "10.0.0.0/16에서 오는 3306"이 아니라 "App SG를 가진 인스턴스에서 오는 3306만"으로 쓴다. 이렇게 하면 App 서버가 늘거나 IP가 바뀌어도 규칙을 고칠 필요가 없고, Web 서버는 IP가 어디든 App SG가 없으니 DB에 닿을 수 없다.
그리고 과제 1에서 배운 내용을 반영해서 배치도 하나 바꿨다. 처음에는 Web을 Public에 두었는데, 인터넷에서 직접 요청을 받는 건 ALB만으로 충분하다. Web 서버도 Private에 두고 ALB에서만 요청을 받게 하면 공인 IP를 가진 서버가 아예 없어진다.
최종 AWS 네트워크 설계

배치 근거는 이렇다.
- Public Subnet에는 인터넷과 직접 마주해야 하는 것만 둔다. 사용자 요청을 받는 ALB, Private 서버의 외부 통신을 대신하는 NAT Gateway, 관리 접속 입구인 Bastion이다.
- Private-Web / Private-App은 인터넷에서 직접 올 일은 없지만 패치나 외부 API 호출이 필요할 수 있으니 NAT 경로를 둔다.
- Private-DB는 외부로 나갈 일도 없다. RDS는 AWS가 패치를 관리하므로 NAT 경로도 주지 않았다. 계층마다 서브넷을 나눈 이유는 서브넷 단위로 NACL을 걸어 한 번 더 막을 수 있기 때문이다.
- 모든 계층을 두 AZ에 나눠 둔 건 과제 1의 Failure Domain 개념을 네트워크에도 적용한 것이다. NAT Gateway도 AZ마다 하나씩 둬야 한쪽 AZ가 죽었을 때 다른 AZ의 외부 통신까지 끊기지 않는다.
접근 제어 정리

누가 | 누구에게 | Port | Source 지정 | 왜 |
Internet | ALB | 443 (80은 443으로 리다이렉트) | 0.0.0.0/0 | 서비스 진입점, 암호화된 요청만 |
ALB | Web | 80 | sg-alb | ALB를 거친 요청만 받게 하기 위해 |
Web | App (Internal ALB 경유) | 8080 | sg-web | Web이 로직 처리를 요청하는 유일한 경로 |
App | ElastiCache | 6379 | sg-app | 세션 조회는 App만 필요 |
App | DB | 3306 | sg-app | DB에 쿼리할 수 있는 건 App뿐 |
Web | DB | 없음 | — | Web은 DB를 쓸 이유가 없다. 이 경로가 있으면 Web 탈취가 곧 DB 탈취다 |
관리자 | Bastion | 22 | 관리자 공인 IP/32 | 관리 입구를 한 곳으로 모으고 허용 IP 제한 |
Bastion | 내부 서버 | 22 | sg-bastion | Bastion을 거치지 않은 관리 접속 차단 |
Web → DB 규칙이 "없음"이라는 게 이 표에서 가장 중요한 줄이라고 생각한다. 허용 규칙을 쓰는 것보다, 필요 없는 경로를 처음부터 만들지 않는 게 최소 권한이다.
추가로 알게 된 점도 있다. AWS에서는 SSM Session Manager를 쓰면 Bastion 없이, 22번 포트를 아예 열지 않고도 서버에 접속할 수 있다. 에이전트가 AWS 쪽으로 먼저 연결을 맺는 방식이라 Inbound 규칙이 필요 없고, 접속 기록도 CloudTrail에 남는다. Bastion이라는 공격면 자체를 없앨 수 있으니, 실제 구축할 때는 이 방식이 더 나아 보인다.
정리하며
과제 2를 하면서 가장 크게 바뀐 생각은 "네트워크 위치는 신뢰의 근거가 아니다"라는 것이다. Private Subnet도, 내부망도, NAT도 처음에는 전부 "안전하게 해 주는 것"으로 보였다. 그런데 하나씩 따져 보니 각각 막아 주는 범위가 정해져 있었다. Private Subnet은 인터넷에서 오는 것만, NAT는 먼저 들어오는 연결만 막을 뿐이고, 그것도 의도한 게 아니었다. 이미 들어온 공격자에게는 아무 소용이 없다.
잘못 생각했던 부분은 세 가지다. 비밀번호가 강하면 노출돼도 괜찮다고 본 것, NAT를 방화벽으로 착각한 것, Private Subnet이면 내부 이동도 막힌다고 생각한 것이다. 셋 다 "무엇으로부터 보호하는가"를 따지지 않고 이름만 보고 판단한 결과였다.
과제 1과 합쳐 보면 흐름이 이어진다. 과제 1은 장애가 퍼지는 범위를 줄이는 설계였고, 과제 2는 공격이 퍼지는 범위를 줄이는 설계였다. 둘 다 "하나가 무너져도 거기서 멈추게 한다"는 같은 생각이다.
![[Cloud & CERT] 조사 2. 네트워크 배치 (박진혁)](https://www.notion.so/image/https%3A%2F%2Fwww.notion.so%2Fimages%2Fpage-cover%2Fnasa_robert_stewart_spacewalk.jpg?table=block&id=3e4d23c8-25c0-80cf-8ea0-c044c4aaa756&cache=v2&width=1600)

