SSM 포트포워딩으로 배스천 없이 프라이빗 RDS 에 접속하기 — 세션 권한은 네트워크 권한이었습니다
프라이빗 서브넷의 RDS 에 로컬에서 붙으려면 배스천과 SSH 터널이 있어야 한다고 오래 생각했습니다. SSM 포트포워딩은 인바운드 규칙도, 키페어도, 배스천도 없이 그 일을 해 줍니다. 다만 편해진 만큼 세션을 열 권한이 곧 그 인스턴스가 닿는 모든 곳에 대한 접근이 됩니다.
새 DB 앞에 배스천용 인스턴스를 하나 더 띄우려다 Session Manager 문서를 다시 읽은 적이 있습니다. 명령은 생각보다 짧았고, 오래 붙잡고 있었던 것은 오히려 권한 정책이었습니다. 포트포워딩만 허락한 정책이 셸까지 허락하는지를 두고, 제가 먼저 읽은 설명과 현재 공식 문서가 서로 달랐기 때문입니다.
미리 밝혀 두면 이번 주제는 제 계정에서 재현한 출력이 없습니다. 아래의 동작 서술은 전부 AWS 공개 문서(2026년 9월 11일 확인)의 범위이고, 문서에서 찾지 못한 것은 찾지 못했다고 적었습니다.
인바운드 규칙 없이 붙는 이유 — SSM Agent 가 먼저 바깥으로 나갑니다
배스천은 연결이 바깥에서 안으로 들어오는 구조라, 포트를 열고 호스트를 유지하고 SSH 키를 관리해야 합니다. Session Manager 는 방향이 반대입니다. 공식 문서는 SSM Agent 가 Systems Manager 서비스로 가는 연결을 모두 먼저 시작한다고 적습니다. 그래서 대상 인스턴스의 보안 그룹에는 인바운드 규칙이 필요 없고, ssm·ssmmessages·ec2messages 엔드포인트로 가는 HTTPS(443) 아웃바운드만 있으면 됩니다.
내 PC localhost:13306
│ AWS CLI + Session Manager 플러그인 — WebSocket(TLS)
▼
Session Manager 엔드포인트 (ssmmessages)
▲
│ SSM Agent 가 먼저 연 443 아웃바운드 연결
인스턴스 i-0123456789abcdef0
│ 노드에서 원격 호스트로 가는 일반 네트워크 연결
▼
mydb.xxxxxxxxxxxx.ap-northeast-2.rds.amazonaws.com:3306
인스턴스까지는 Session Manager 가 터널이 되어 주지만, 마지막 구간은 평범한 VPC 안의 연결입니다. 이 글의 뒤쪽 이야기는 전부 이 구간에서 나옵니다.
명령 한 줄 — SSM 포트포워딩으로 프라이빗 RDS 에 접속합니다
쓰는 세션 문서는 AWS 가 제공하는 AWS-StartPortForwardingSessionToRemoteHost 이고, 파라미터는 host·portNumber·localPortNumber 세 개입니다.
aws ssm start-session \
--target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.xxxxxxxxxxxx.ap-northeast-2.rds.amazonaws.com"],"portNumber":["3306"],"localPortNumber":["13306"]}'
host 는 원격 호스트의 호스트 이름이나 IP 주소, portNumber 는 원격 포트(생략하면 80), localPortNumber 는 내 PC 에서 트래픽이 시작되는 포트입니다. 세션이 열려 있는 동안 로컬의 DB 도구를 localhost:13306 으로 붙이면, 연결이 인스턴스를 거쳐 RDS 의 3306 포트로 이어집니다.
대상 인스턴스에는 SSM Agent 3.1.1374.0 이상이 설치돼 있어야 합니다. 원격 호스트는 Systems Manager 관리 대상이 아니어도 되지만, 인스턴스에서 원격 호스트로 가는 연결과 이름 해석은 평소처럼 성립해야 합니다. 로컬의 Session Manager 플러그인은 원격 포워딩 전용 최소 버전이 문서에 따로 없고, 공식 최소 지원 버전은 1.2.764.0 입니다.
그런데 무엇이 열렸나 — 세션 권한은 인스턴스의 네트워크 시야입니다
명령에서 목적지를 정하는 것은 --parameters 안의 host 한 칸이고, 그 칸은 세션을 여는 사람이 채웁니다. IAM 으로 그 칸을 조일 수 있는지 AWS 서비스 참조 데이터에서 StartSession 을 찾아봤습니다. 조건 키는 aws:ResourceTag/${TagKey}, ssm:AccessRequestId, ssm:SessionDocumentAccessCheck, ssm:resourceTag/${TagKey} 네 개였고, host 나 portNumber 같은 문서 파라미터를 가리키는 키는 없었습니다.
그러니 IAM 이 답할 수 있는 질문은 «누가, 어느 인스턴스를 거쳐, 어느 문서로»까지이고, «어디로»는 묻지 못합니다. 이 권한의 반경이 정책이 아니라 그 인스턴스가 네트워크에서 닿는 범위로 정해지는 이유입니다. 같은 VPC 의 다른 DB 도 후보가 됩니다. 바깥에서는 인증 프록시를 거쳐야 들어갈 수 있지만 VPC 안에서는 원래 포트가 열려 있는 내부 서비스라면, 포워딩은 그 프록시를 건너뛰고 원래 포트에 곧장 닿을 수 있습니다.
터널 안에서 오간 내용도 남지 않습니다. 공식 문서는 포트포워딩이나 SSH 로 연결한 세션에는 세션 로깅을 제공하지 않는다고 적습니다.
남는 것은 CloudTrail 입니다. Systems Manager 는 제어 영역 작업을 관리 이벤트로 기록하고, StartSession 호출도 그 예로 듭니다. 다만 그 이벤트에 host 값이 함께 남는지는, 공식 문서에 StartSession 이벤트 예시가 실려 있지 않아 확인하지 못했습니다.
IAM 정책에 포트포워딩 문서만 허용하면 셸은 막힐까 — 확인한 것과 못 한 것
반경을 줄일 정책을 떠올렸을 때 제 첫 생각은 단순했습니다. 공식 예시들이 Resource 에 인스턴스 ARN 과 문서 ARN 을 나란히 적으니, 포트포워딩 문서만 적어 두면 셸은 막힌다고 여겼습니다. 그런데 조사하는 중에 «조건 키를 켜지 않으면 기본 셸 문서가 암묵적으로 허용된다»는 설명을 만났고, 하마터면 그 문장을 원문 확인 없이 사실로 옮겨 적을 뻔했습니다.
StartSession 은 문서 이름이 없으면 기본 세션 문서 SSM-SessionManagerRunShell 로 셸을 엽니다. 쟁점은 정책에 이 문서를 적지 않았을 때입니다. 기본 문서가 명시 없이도 쓰인다면, 포트포워딩만 받은 사람이 --document-name 을 빼고 명령을 칠 때 포워딩이 아니라 SSM Agent 가 만든 ssm-user 계정의 셸이 열립니다.
현재 공식 문서가 확인해 주는 쪽은 그 반대입니다. 기본 세션 문서 페이지는 기본 셸 세션을 시작하려면 IAM 정책에 그 문서를 명시해야 한다고 적습니다. 제가 확인한 퀵스타트 샘플 정책들도 태그 조건 예시까지 포함해 SSM-SessionManagerRunShell 문서 ARN 을 별도 문으로 허용합니다. 이 설명대로라면 포트포워딩 문서만 적은 정책으로는 기본 셸이 열리지 않습니다.
확인하지 못한 것도 남습니다. ssm:SessionDocumentAccessCheck 라는 Bool 조건 키는 서비스 참조 데이터에 실제로 있고, 그 안내 페이지 제목은 «AWS CLI 에 세션 문서 권한 확인을 강제한다»입니다.
하지만 그 페이지는 글을 쓰는 동안 주소가 사용자 가이드 첫 화면으로 넘어가 본문을 열 수 없었고, 이 키가 없을 때의 동작도 원문으로 확인하지 못했습니다. 제 계정에서 시험해 보지도 못했습니다. 그래서 어느 해석이 맞아도 셸이 열리지 않도록, 허용 문서를 포트포워딩 문서 하나로 적고 문서 확인 조건을 보험으로 함께 넣었습니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TaggedInstancesWithDocumentCheck",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:ap-northeast-2:111122223333:instance/*",
"Condition": {
"StringLike": { "ssm:resourceTag/tag": ["port-forwarding"] },
"BoolIfExists": { "ssm:SessionDocumentAccessCheck": "true" }
}
},
{
"Sid": "RemoteHostPortForwardingDocumentOnly",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ssm:ap-northeast-2::document/AWS-StartPortForwardingSessionToRemoteHost"
},
{
"Sid": "OwnSessionsOnly",
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
},
{
"Sid": "OwnDataChannelOnly",
"Effect": "Allow",
"Action": "ssmmessages:OpenDataChannel",
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
}
]
}
- 첫 번째 문은
tag태그 값이port-forwarding인 인스턴스로만 세션을 허용하고, 같은 문에 문서 확인 조건을 둡니다. - 두 번째 문은 문서를 원격 호스트 포워딩 문서 하나로 한정합니다. AWS 가 소유한 문서는 ARN 에 계정 ID 를 넣지 않아 리전 뒤에 콜론이 두 개 붙습니다. 공식 예시도 태그 조건 문과 문서 허용 문을 나눠 적습니다.
- 세 번째와 네 번째 문은 세션 종료·재개와 데이터 채널을, 공식 예시의
${aws:userid}기반 세션 ARN 으로 본인 세션에 한정합니다.
연산자 BoolIfExists 는 그 안내 페이지의 설명을 따랐지만, 앞서 적었듯 본문이 열리지 않아 검색에 노출된 문구로만 대조했습니다(키 이름과 Bool 타입은 서비스 참조 데이터로 확인). 태그 조건과 함께 쓴 공식 예시도 찾지 못했으니, 적용한 뒤 --document-name 없이 세션을 열어 거부되는지 한 번 확인해 보는 편이 안전하다고 생각합니다.
조건마다 막히는 것과 여전히 열려 있는 것을 나란히 두면 이렇습니다.
| 정책에 넣은 것 | 막히는 것 | 여전히 열려 있는 것 |
|---|---|---|
| Resource 를 포트포워딩 문서로 한정 | 다른 이름의 문서를 지정한 세션, 그리고 현재 공식 문서 기준으로는 기본 셸 | 허용한 문서로 host 만 바꾼 포워딩 |
+ ssm:SessionDocumentAccessCheck |
기본 문서가 명시 없이 쓰인다는 해석이 맞을 경우의 기본 셸 (원문 미확인 — 보험) | 허용한 문서로 host 만 바꾼 포워딩 |
+ ssm:resourceTag 조건 |
태그가 없는 인스턴스로 여는 세션 | 태그 붙은 인스턴스가 닿는 모든 호스트·포트 |
세션 ARN 을 ${aws:userid} 로 한정 |
다른 사람의 세션 종료·재개 | — |
셋째 줄의 오른쪽 칸이 이 글의 요지입니다. 정책을 끝까지 좁혀도 마지막 반경은 IAM 이 아니라 네트워크가 정합니다.
보안 그룹 인바운드가 여전히 필요한 두 곳
처음에 저는 이 방식을 «인바운드 규칙이 전혀 필요 없다»고 요약했습니다. 절반만 맞는 요약이었습니다. 필요 없는 것은 대상 인스턴스의 보안 그룹이고, 허용이 필요한 곳은 두 군데 남습니다.
하나는 VPC 인터페이스 엔드포인트의 보안 그룹입니다. 인스턴스를 인터넷에 내보내지 않으려고 엔드포인트를 두면, 그 보안 그룹이 인스턴스가 있는 프라이빗 서브넷으로부터 443 인바운드를 허용해야 합니다. 허용하지 않으면 인스턴스가 SSM 엔드포인트에 연결하지 못합니다. Session Manager 의 데이터 채널에는 ssmmessages 엔드포인트가 필요하고, ec2messages 는 SSM Agent 3.3.40.0 부터 가능한 경우 ssmmessages 로 대신 쓰입니다.
다른 하나는 원격 호스트 쪽입니다. DB 가 이 인스턴스로부터 오는 3306 접속을 받아 주지 않으면 포워딩은 인스턴스에서 멈춥니다. 앞 절의 이야기가 여기로 돌아옵니다. IAM 이 «어디로»를 묻지 못하니, 반경을 실제로 줄이는 손잡이는 원격 호스트들이 이 인스턴스를 받아 주는지, 그리고 이 인스턴스가 어디까지 나갈 수 있는지에 남습니다.
유휴 타임아웃 20분과 최대 세션 시간
세션은 기본적으로 20분 동안 활동이 없으면 끝나고, 이 값은 1분에서 60분 사이로 바꿀 수 있습니다. 그런데 유휴 타이머는 키 입력, 창 크기 변경, 재연결(ResumeSession)에서 다시 시작되어 세션이 설정보다 오래 살아 있을 수 있습니다. 그래서 문서도 엄격한 상한이 필요하면 최대 세션 시간을 함께 설정하라고 권합니다. 최대 세션 시간은 기본값이 없어 켜지 않으면 상한이 없고, 켜면 1분에서 1,440분 사이로 정합니다.
여기서도 확인하지 못한 것이 하나 있습니다. 문서가 드는 입력의 예는 터미널과 브라우저 창 기준이라, 포워딩된 DB 트래픽이 유휴 타이머에 어떻게 세어지는지는 공식 문서에서 찾지 못했습니다. 그래서 포워딩을 주로 쓴다면 유휴 타임아웃보다 최대 세션 시간에 기대는 편이 판단의 근거가 분명하다고 생각했습니다.
남는 것 — 배스천이 사라진 자리
배스천을 쓰던 때에는 누가 어디에 닿는지가 보안 그룹과 SSH 키에 적혀 있었습니다. 번거로웠지만 그 번거로움이 곧 경계였습니다. SSM 포트포워딩은 그 경계를 IAM 으로 옮겼고, 옮겨 간 자리의 문법은 목적지가 아니라 경유지를 묻습니다. 정책에는 «어느 인스턴스를 거쳐»를 적고, «어디로»는 그 인스턴스의 네트워크가 대신 답합니다.
그래서 이 방식을 쓰기 전에 확인할 것을 이렇게 추렸습니다.
- 포워딩에 쓸 인스턴스가 닿는 호스트·포트를 먼저 적어 봅니다. 그 목록이 이 권한의 실제 반경입니다.
- 문서는 원격 호스트 포워딩 문서만, 인스턴스는 태그로, 세션 종료·재개는 본인 세션으로 한정합니다.
ssm:StartSession을 허용하는 문에ssm:SessionDocumentAccessCheck조건을 보험으로 함께 둡니다.- 적용한 뒤
--document-name없이 세션을 열어 거부되는지 직접 시험합니다. - 엔드포인트 보안 그룹의 443 인바운드와, 원격 호스트가 이 인스턴스를 허용하는지를 확인합니다.
- 최대 세션 시간을 따로 켭니다.
- 터널 안의 내용은 세션 로그로 남지 않으니, CloudTrail 의
StartSession기록을 감사의 출발점으로 삼습니다.
돌이켜 보면 제가 틀릴 뻔한 두 번은 모양이 같았습니다. «조건 키가 없으면 셸이 열린다»는 설명을 원문 확인 없이 옮기려 한 것도, 인바운드가 필요 없다고 요약한 것도 한 곳의 문장만 보고 권한을 읽은 것이었습니다. 적힌 문장이 무엇을 전제하는지, 다른 문서가 같은 것을 어떻게 적는지까지 대조해야 정책을 읽었다고 할 수 있겠다는 생각이 들었습니다.
참고 문서
- Start a session (port forwarding to remote host)
- AWS Systems Manager Session Manager
- Improve the security of EC2 instances by using VPC endpoints for Systems Manager
- Sample IAM policies for Session Manager
- Additional sample IAM policies for Session Manager
- Start a default shell session by specifying the default session document in IAM policies
- Start a session with a document by specifying the session documents in IAM policies
- Enforce a session document permission check for the AWS CLI — 작성 시점(2026년 9월 11일)에 사용자 가이드 첫 화면으로 이동해 본문을 열지 못한 페이지입니다.
- How AWS Systems Manager works with IAM
- Actions, resources, and condition keys for AWS Systems Manager
- StartSession — AWS Systems Manager API Reference
- Session document schema
- Enabling and disabling session logging
- Logging AWS Systems Manager API calls with AWS CloudTrail
- Specify an idle session timeout value
- Specify maximum session duration
관련 글
macOS에서 OCI HeatWave MySQL 접속하기: Bastion SSH 터널 설정 가이드
Oracle Cloud HeatWave MySQL에 로컬에서 접속하기 위한 OCI CLI 설정과 Bastion SSH 터널 구성 과정을 정리했습니다. API 키 등록부터 터널 스크립트 작성, 트러블슈팅까지 전체 흐름을 다룹니다.
macOS에서 autossh로 SSH 터널 자동화하기: 운영 DB 안전하게 접근하는 방법
로컬 개발 환경에서 운영 MySQL에 접근할 때마다 SSH 터널을 수동으로 여는 건 번거롭습니다. autossh와 macOS LaunchAgent를 활용하면 부팅 시 자동으로 터널이 열리고, 끊어져도 재연결됩니다. 포트 충돌 방지와 보안 고려사항까지 정리했습니다.
OCI MySQL HeatWave, Bastion 없이 Tailscale로 접속하기
Oracle Cloud MySQL HeatWave는 프라이빗 서브넷에만 배치되어 외부 접속이 번거롭습니다. Bastion Service 대신 Tailscale을 활용해 간편하게 접속하는 방법을 정리했습니다.