- Published on
클라우드의 bastion 이란 무엇이고, AWS 에서는 어떻게 쓰는 것이 좋은가 — ProxyJump 실측, SSM Session Manager, EC2 Instance Connect Endpoint
- Authors

- Name
- Youngju Kim
- @fjvbn20031
문이 하나뿐인 건물
서버를 사설 서브넷에 두면 인터넷에서 닿을 수 없다. 그것이 목적이다. 그런데 운영자는 들어가야 한다. 이 모순을 푸는 고전적인 답이 bastion host 다. 공개 서브넷에 작은 호스트를 하나 두고, 인터넷에서 열린 문은 그 호스트의 SSH 포트 하나뿐이며, 나머지 서버는 bastion 에서 오는 접속만 받는다.
건물에 비유하면 출입구를 하나로 줄이고 그 문에만 경비를 세우는 것이다. 경비가 한 명이면 출입 기록도 한 곳에 남는다. 대신 그 문이 뚫리면 건물 전체가 뚫린다. 그래서 bastion 은 가장 단단하게 만들고, 가장 자주 패치하고, 가장 꼼꼼히 기록해야 하는 호스트가 된다.
고전적인 bastion 은 SSH 한 줄로 만들어진다
OpenSSH 에는 이 패턴이 내장되어 있다. ssh_config 매뉴얼의 ProxyJump 설명은 이렇다.
Setting this option will cause ssh(1) to connect to the target host by
first making an ssh(1) connection to the specified ProxyJump host and
then establishing a TCP forwarding to the ultimate target from there.
먼저 점프 호스트에 SSH 로 붙고, 거기서 최종 대상까지 TCP 포워딩을 연다. 명령줄에서는 -J 다. 홈랩에서 실제로 해 봤다. 내 Mac(192.168.219.101)에서 cubi01(192.168.219.120)을 거쳐 nuc1(192.168.219.116)으로 들어간다.
ssh -J cubi01 nuc1 'hostname; echo $SSH_CONNECTION'
첫 시도는 실패했다.
channel 0: open failed: connect failed: Temporary failure in name resolution
stdio forwarding failed
-v 로 보면 ssh 가 무엇을 하는지 드러난다.
debug1: Setting implicit ProxyCommand from ProxyJump: ssh -v -W '[%h]:%p' cubi01
debug1: Executing proxy command: exec ssh -v -W '[nuc1]:22' cubi01
Authenticated to cubi01 ([192.168.219.120]:22) using "publickey".
debug1: channel_connect_stdio_fwd: nuc1:22
nuc1 이라는 이름은 내 Mac 의 /etc/hosts 에는 있지만 cubi01 에는 없다(getent hosts nuc1 이 비어 있다). 대상의 이름은 내 노트북이 아니라 점프 호스트가 푼다. bastion 을 쓰는 사람이 가장 먼저 부딪히는 사실이다. IP 로 바꾸고, known_hosts 의 이름 항목을 그대로 쓰도록 HostKeyAlias 를 주면 통과한다.
ssh -o HostKeyAlias=nuc1 -J cubi01 192.168.219.116 'hostname; echo $SSH_CONNECTION'
nuc1
SSH_CONNECTION=192.168.219.120 53994 192.168.219.116 22
점프 없이 직접 붙었을 때와 비교하면 차이가 보인다.
SSH_CONNECTION=192.168.219.101 55330 192.168.219.116 22
대상 서버는 접속이 bastion(…120)에서 왔다고 본다. 내 Mac 의 주소(…101)는 어디에도 없다. 대상 서버의 로그만 보면 "cubi01 이 들어왔다" 는 것밖에 모른다. 누가 들어왔는지는 bastion 의 로그에만 있다. bastion 의 감사 로그를 반드시 남기고 밖으로 내보내야 하는 이유가 이 한 줄에 있다.
이 홈랩은 다섯 노드가 192.168.219.0/24 안에 있고, 인터넷에서 열린 것은 게이트웨이의 80(ACME HTTP-01 인증용)과 443 뿐이다. 운영 명령은 LAN 안에서 ssh cubi01 로 들어가 실행한다. 즉 cubi01 은 LAN 안의 관리 노드이지 인터넷에서 보이는 bastion 은 아니다. 인터넷에 22번을 열어야 하는 순간부터가 진짜 bastion 이고, 그 순간부터 위의 걱정이 전부 현실이 된다.
bastion 을 직접 운영하면 떠안는 것
- 인바운드 22번 포트. 인터넷에 열린 SSH 는 하루 종일 두드려진다. 열린 문 자체가 공격면이다.
- 키 관리. 누가 나가면 bastion 과 모든 대상의
authorized_keys를 손봐야 한다. 실제로는 잘 안 한다. - 패치. bastion 은 공개 서브넷에 있는 일반 EC2 다. 커널·OpenSSH 취약점이 나오면 가장 먼저 패치해야 한다.
- 로그의 사각. 위에서 봤듯이 대상은 bastion 의 주소만 본다. bastion 에서 세션 기록을 남기지 않으면 "누가 무엇을 했는지" 가 사라진다.
- 단일 장애점. bastion 이 죽으면 아무도 못 들어간다. 그렇다고 두 대를 두면 관리 대상이 둘이 된다.
AWS 는 이 목록을 그대로 문서에 적어 두고, 두 가지 대안을 내놓았다.
AWS 의 답 1: SSM Session Manager
Systems Manager 문서는 Session Manager 를 "인바운드 포트를 열거나, bastion 호스트를 유지하거나, SSH 키를 관리할 필요 없이" 노드를 관리하는 기능이라고 소개한다. 인스턴스 안의 SSM Agent 가 밖으로 Systems Manager 엔드포인트에 연결하고, 운영자는 그 연결을 타고 들어간다. 인바운드 규칙은 하나도 필요 없다.
aws ssm start-session --target i-0123456789abcdef0
권한은 전부 IAM 이다. 어느 인스턴스에 누가 들어갈 수 있는지를 태그나 리소스 ARN 으로 정하고, 세션 내용은 S3·CloudWatch Logs 로, API 호출은 CloudTrail 로 남긴다. 키 파일이 없으니 퇴사자의 키를 지울 일도 없다.
포트 포워딩도 된다. 인스턴스의 80번을 내 노트북의 56789 로 끌어오거나,
aws ssm start-session --target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["80"], "localPortNumber":["56789"]}'
인스턴스를 발판 삼아 그 뒤의 RDS 까지 끌어올 수 있다. 원격 호스트는 Systems Manager 에 등록되어 있을 필요가 없다.
aws ssm start-session --target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.example.us-east-2.rds.amazonaws.com"],"portNumber":["3306"], "localPortNumber":["3306"]}'
기존 ssh·scp 를 그대로 쓰고 싶으면 ~/.ssh/config 에 ProxyCommand 를 두면 된다. 노드의 22번은 닫아 둔 채로다(문서 표현으로 "You can close inbound ports on the node").
# SSH over Session Manager
Host i-* mi-*
ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
User ec2-user
여기에 한 가지 함정이 문서에 명시되어 있다. SSH 나 포트 포워딩으로 맺은 세션은 내용이 기록되지 않는다. SSH 가 TLS 터널 안에서 다시 암호화하기 때문에 Session Manager 는 터널 역할만 한다. "누가 언제 어느 인스턴스에 붙었는지" 는 CloudTrail 에 남지만, "무슨 명령을 쳤는지" 는 일반 세션(start-session 만 쓴 경우)에서만 남는다. 감사가 목적이라면 SSH 경유를 IAM 으로 막을 수 있고, 문서가 그 정책을 준다.
{
"Effect": "Deny",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ssm:*:*:document/AWS-StartSSHSession"
}
AWS 의 답 2: EC2 Instance Connect Endpoint
Session Manager 는 인스턴스 안에 에이전트가 있어야 한다. 에이전트를 못 넣는 인스턴스, 혹은 그냥 평범한 SSH·RDP 를 쓰고 싶을 때를 위해 AWS 는 EC2 Instance Connect Endpoint(EICE) 를 두었다. 문서는 이것을 "identity-aware TCP proxy" 라고 부른다. VPC 의 서브넷 하나에 엔드포인트를 만들면, IAM 자격 증명으로 인증된 터널이 내 컴퓨터에서 그 엔드포인트까지 열리고, 거기서 VPC 안의 인스턴스로 TCP 가 나간다.
문서에서 확인한 성질은 이렇다.
- 인스턴스에 공인 IP 가 필요 없고, VPC 에 인터넷 게이트웨이도 필요 없다.
- 성공·실패를 가리지 않고 모든 접속 시도가 CloudTrail 에 남는다.
- 추가 비용이 없다(다른 가용 영역의 인스턴스에 붙을 때 AZ 간 전송 요금만 든다).
- VPC 당, 서브넷 당 하나 만 만들 수 있고, 엔드포인트 하나가 동시 접속 20개 까지 받는다.
- 터널은 최대 1시간 이며, IAM 정책의
maxTunnelDuration조건으로 더 짧게 강제할 수 있다. IAM 자격 증명이 먼저 만료돼도 터널은 상한까지 유지된다. - 관리 트래픽용이다. 대량 전송은 스로틀된다.
- 기본으로는 인스턴스가 엔드포인트 ENI 의 주소를 클라이언트로 본다. 클라이언트 IP 보존을 켜면 실제 주소가 보이지만, IPv4 전용이고 같은 VPC 여야 한다.
쓰는 법은 SSH 의 ProxyCommand 를 바꾸는 것뿐이다.
Host i-*
ProxyCommand aws ec2-instance-connect open-tunnel --instance-id %h
또는 CLI 가 대신 해 준다.
aws ec2-instance-connect ssh --instance-id i-0123456789abcdef0 --connection-type eice
Windows 인스턴스는 --remote-port 3389 로 터널을 열고 로컬 포트에 RDP 클라이언트를 붙인다.
셋을 나란히
| EC2 bastion (직접 운영) | SSM Session Manager | EC2 Instance Connect Endpoint | |
|---|---|---|---|
| 인터넷에 여는 포트 | 22 | 없음 (에이전트가 밖으로 연결) | 없음 (엔드포인트가 VPC 안에) |
| 인스턴스에 필요한 것 | 공인 IP 있는 bastion + 대상의 SG 규칙 | SSM Agent + IAM 역할 | 없음 (키 페어만) |
| 인증 | SSH 키 | IAM | IAM + SSH 키 |
| 접속 기록 | bastion 의 sshd 로그 (직접 관리) | CloudTrail + 세션 내용(S3/CloudWatch) | CloudTrail (시도 전부) |
| 세션 내용 기록 | 별도 도구 필요 | 일반 세션만, SSH/포워딩 경유는 안 됨 | 안 됨 (TCP 프록시) |
| 상한 | 없음 | — | 서브넷당 1개, 동시 20, 1시간 |
| 비용 | EC2 인스턴스 요금 | 없음 | 없음 (AZ 간 전송 제외) |
| 맞는 곳 | 에이전트도 엔드포인트도 못 쓰는 경우 | 감사·명령 기록이 필요한 운영 | 평범한 SSH/RDP, 에이전트 없는 AMI |
AWS 에서는 이렇게 쓰는 것이 좋다
첫 번째 선택은 Session Manager 다. 인바운드 포트가 없고, 키가 없고, 명령까지 기록된다. bastion 이 떠안던 다섯 가지 부담이 전부 사라진다. 인스턴스에 SSM Agent 와 AmazonSSMManagedInstanceCore 권한을 가진 역할만 붙이면 된다. 사설 서브넷에 인터넷 경로가 없다면 Systems Manager 용 VPC 엔드포인트(PrivateLink)를 두면 에이전트가 VPC 안에서 연결한다.
SSH 그 자체가 필요하면 EICE 다. rsync, scp, IDE 의 원격 개발, RDP 처럼 진짜 TCP 연결이 필요한 작업은 EICE 로 터널을 열고 늘 쓰던 도구를 쓴다. 서브넷당 하나, 동시 20개, 1시간이라는 상한을 기억하고, maxTunnelDuration 으로 더 짧게 묶는다.
bastion EC2 는 마지막 선택이다. 그래도 두어야 한다면 실측이 알려 준 두 가지를 지킨다. 대상 서버는 bastion 의 주소만 보므로 bastion 의 sshd 로그를 바깥(CloudWatch Logs 등)으로 내보내고, 대상의 보안 그룹은 bastion 의 보안 그룹에서 오는 22번만 허용한다. 그리고 그 bastion 에도 SSM Agent 를 넣어 두면, 어느 날 22번을 닫아도 들어갈 길이 남는다.
한 줄로
bastion 은 "문을 하나로 줄인다" 는 생각이고, AWS 에서 그 생각을 가장 잘 실현하는 것은 이제 bastion 호스트가 아니라 인바운드 포트 없이 IAM 으로 들어가는 Session Manager 다. 진짜 SSH 가 필요하면 EICE 를 쓰고, 직접 운영하는 bastion 은 대상이 bastion 의 주소만 본다는 사실을 잊지 않은 채 마지막에 고른다.