들어가며
RDS는 Private Subnet에 두고, Public Subnet의 EC2 bastion을 SSH로 경유하는 구조는 인프라 구축 시 흔히 쓰이는 패턴이다. 이 회사에 들어와서 배웠던 솔루션도, 자격증 공부를 하면서 이것저것 맛보면서 배운 내용도 바로 이거였다.
문제는 보안 그룹에 SSH(22) + 0.0.0.0/0이 그대로 남아 있었다는 것이었다. 조금 찜찜했지만, 몰아치는 일을 쳐내면서 그렇게 바쁘게 살았더니… 어느 날 모니터링 도구에서 출처 불명의 ip 접속이 찍혀있더라 ㅎ 정황상 데이터 유출 흔적은 없었지만..
급하게 EC2를 내려 접근을 끊었고, 해킹 위협에서 벗어났지만 나 또한 DB에 접근을 할 수 없게 되었다… 그래서 내부 관계자들만 완벽하게 쓸 수 있는! 구조로 DB 접근을 세팅해봤다.
정리한 내용은 다음과 같다.
- 인바운드
0.0.0.0/0전부 제거 - SSM Session Manager 포트 포워딩으로 RDS 접속
- IAM Identity Center(SSO) + AWS Profile 기반 인증
- DB 자격 증명은 Secrets Manager에서 조회
- 인프라는 CloudFormation, 운영 문서·자동화 스크립트 정리
1. 레거시 설정의 문제점
| 항목 | 당시 상태 |
|---|---|
| bastion EC2 SG | SSH(22) ← 0.0.0.0/0 |
| Public IP / Elastic IP | 없음 |
| RDS | Private Subnet, 퍼블릭 액세스 비활성 |
| 접근 인원 | 전체 3명 이하 (본인 포함) |
| 마지막 구조 점검 | 초기 세팅 이후 4~5년 방치 |
Public IP가 없어도 SG에 0.0.0.0/0이 열려 있으면 잠재 공격면은 그대로다. 나중에 Public IP가 붙거나, 다른 리소스와 엮이면 바로 exposure가 된다.
2. 대안 검토와 선택
2.1 외주 QA를 위한 IAM 설정: 제외
현재 한국 팀에는 QA가 한 명 밖에 없는데, 대규모 실시간 아키텍쳐로 돌리는 프로젝트까지 합치면 한 사람 당 최소 3개의 프로젝트를 맡고 있는 우리… 따라서 부족한 QA을 인도쪽에서 외주로 돌리고 있다.
그런데 이 사람들은 aws account 접근 권한이 없다는 사실을 오늘 알게 됨 ㅎ…
원래는 전용 IAM User를 따로 발급하고 스크립트를 공유할까 싶었지만, 생각보면 결국 또 하나의 접근 경로를 여는 것이라 키 로테이션, 퇴사자 회수, 최소 권한 관리까지 운영 부담이 생길 것 같은 느낌이 들었다. 안그래도 내가 회사에서 제일 일 많이하는데 여기서 일이 더 늘어난다? 이건 미친 짓이다. ^^…
무엇보다 외주 인원에게 DB까지 열어줄 이유도 없었다. 따라서 QA용 별도 IAM은 만들지 않기로 결정했다.
2.2 왜 SSM 포트 포워딩 + SSO Profile인가
| 대안 | 검토 결과 |
|---|---|
| SSH + IP 화이트리스트 | IP 고정·변동 관리 부담, 근본적으로 인바운드 포트 필요 |
| AWS Client VPN | 소규모에 VPN 인프라 운영 비용·복잡도 과함 |
| 외주용 별도 IAM | 접근 경로 증가, 키 관리 부담 |
| SSM + SSO Profile | 채택 — 인바운드 불필요, Org 단위 Role 통제, 로컬 Profile로 안정적 운영 |
회사는 AWS Organizations 아래 Account·Role 단위로 권한을 나눠 쓴다. 이미 SSO로 Profile을 쓰고 있었기 때문에, SSM 포트 포워딩을 그 위에 얹는 게 가장 자연스러웠다.
인바운드 포트를 새로 열지 않으면서, Org에서 관리하는 IAM Role을 가진 사람만 RDS에 닿게 하려면 SSM이 맞다고 판단했다.
2.3 bastion의 역할 변경
SSM을 쓰면 EC2는 여전히 필요하다. 다만 SSH 점프박스가 아니라, SSM Agent가 붙은 프록시 노드로 역할이 바뀐다.
- Public IP / Elastic IP: 없음
- SG Inbound: 없음
- Outbound 443: SSM 엔드포인트 통신
- RDS SG: bastion SG에서만 DB 포트 허용
┌─────────────┐ SSO + Profile ┌──────────────────┐ SSM 터널 ┌─────────────────┐ 3306/5432 ┌─────┐
│ 로컬 PC │ ─────────────────> │ AWS SSM (443) │ ───────────> │ EC2 (Private) │ ────────────> │ RDS │
│ AWS CLI │ IAM Role 검증 │ │ 암호화 채널 │ 인바운드 포트 0 │ │ │
└─────────────┘ └──────────────────┘ └─────────────────┘ └─────┘
│ ▲
└──────────────── localhost:13306 포트 포워딩 ─────────────────────────────────────────────────────────┘
│
└── DB 비밀번호: Secrets Manager에서 조회
3. 구축 내용
3.1 인프라 (CloudFormation)
SG, IAM Role, SSM 관련 설정은 CloudFormation으로 관리한다. (환경이 많기 때문에 노가다를 할 수 없으니 IaC가 필수이다)
EC2 Instance Role에는 관리형 정책 AmazonSSMManagedInstanceCore를 붙인다.
개발자 Role(SSO Permission Set)에는 포트 포워딩에 필요한 최소 권한만 준다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:StartSession"],
"Resource": [
"arn:aws:ec2:ap-northeast-2:ACCOUNT_ID:instance/i-xxxxxxxxxxxxxxxxx",
"arn:aws:ssm:ap-northeast-2:ACCOUNT_ID:document/AWS-StartPortForwardingSessionToRemoteHost"
]
},
{
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:ap-northeast-2:ACCOUNT_ID:session/${aws:username}-*"
}
]
}
Resource ARN은 실제 bastion 인스턴스와 Document로 좁혀둔다.
3.2 보안 그룹
# EC2 (Bastion)
Inbound: (없음)
Outbound: 443 → SSM, DB 포트 → RDS SG
# RDS
Inbound: 3306/5432 ← EC2 Bastion SG만
마이그레이션 후 전체 SG를 훑어서 0.0.0.0/0 잔존 규칙이 없는지 확인했다.
3.3 접속 방법
환경별 Profile이 여러 개라, 팀원이 매번 긴 CLI를 치지 않도록 자동화 스크립트를 만들고 운영 문서에 정리했다.
#!/bin/bash
# db-tunnel.sh — 환경별 Profile 지정 후 실행
PROFILE="${1:?Usage: ./db-tunnel.sh <aws-profile>}"
INSTANCE_ID="i-0abc123def456789"
RDS_HOST="mydb.xxxxx.ap-northeast-2.rds.amazonaws.com"
LOCAL_PORT="13306"
aws ssm start-session \
--profile "$PROFILE" \
--target "$INSTANCE_ID" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{
\"host\":[\"$RDS_HOST\"],
\"portNumber\":[\"3306\"],
\"localPortNumber\":[\"$LOCAL_PORT\"]
}"
DB 클라이언트 접속 정보:
Host: localhost
Port: 13306
User: (Secrets Manager에서 확인)
Password: (Secrets Manager에서 확인)
비밀번호는 .env나 개인 메모에 두지 않고, Secrets Manager에서 가져와 쓴다.
3.4 SSO 로그인 흐름
# 1. SSO 로그인 (환경별 Profile)
aws sso login --profile company-dev
# 2. 터널 시작
./db-tunnel.sh company-dev
# 3. 별도 터미널에서 DB 클라이언트 접속
Profile이 여러 개인 환경(dev/staging/prod)에서는 스크립트에 Profile을 인자로 넘기는 방식이 실수를 줄인다.
전환 작업 자체는 AWS 공식 문서 순서대로 진행했고, 딱히 막히는 이슈는 없었다. CloudFormation으로 VPC·IAM이 이미 잡혀 있었고, 로컬에서는 SSO 로그인 → 스크립트 실행 → localhost로 DB 붙이면 끝이다.
4. 추가로 점검한 것들
| 항목 | 내용 |
|---|---|
| SSM 세션 로깅 | S3 또는 CloudWatch에 세션 기록 보관 설정 |
| 세션 타임아웃 | idleSessionTimeout, maxSessionDuration 설정으로 방치 터널 차단 |
| Secrets Manager | DB 자격 증명 중앙 관리, 로컬 저장 금지 |
| SG 전수 조사 | 0.0.0.0/0 잔존 규칙 제거 |
| 운영 문서 | 접속 절차, Profile 목록, 트러블슈팅 정리 |
5. Before & After
| 항목 | Before | After |
|---|---|---|
| SG Inbound | SSH 22 ← 0.0.0.0/0 |
없음 |
| Public IP | 없음 | 없음 |
| 인증 | SSH Key Pair | SSO + IAM Role (Profile) |
| DB 자격 증명 | 분산 관리 | Secrets Manager |
| 인프라 관리 | 초기 세팅 후 방치 | CloudFormation |
| 접근 인원 통제 | SSH 키 공유 암묵적 | Org Role 기반 |
| 외주 QA IAM | 검토했으나 미채택 | 별도 경로 없음 |
| 운영 | ad-hoc | 문서 + 자동화 스크립트 |
6. 정리
이번에 느낀 점을 정리하면 다음과 같다.
- 소규모 팀일수록 레거시 보안 부채가 쌓이기 쉽다. 인프라라 더 방치된 느낌이 들었다…
- 외주·협력사 IAM은 “열어주면 편하다”는 유혹이 있지만, 접근 경로를 늘리는 선택이다.
일만 좀 줄어들면 종종 레거시 코드를 고치겠는데 회사가 너무 잘 나간 나머지.. 이건 축복인지 저주인지 모르겠다. 그냥 좋게 생각하지 뭐 안짤리는게 어디야,,,