비개발 팀에게 RDS 조회 환경을 안전하게 전달하기
1. 배경
HQ 마케터와 기획자가 내부 데이터를 직접 확인해야 하는 상황이 있었다.
다만 이들은 SQL이나 개발 환경에 익숙하지 않았고,
RDS를 직접 노출하거나 관리자 계정을 공유하는 방식은 보안상 적절하지 않았다.
그래서 다음 조건을 만족하는 방식을 구성했다.
- RDS는 외부에 직접 공개하지 않는다.
- Bastion EC2를 통해서만 접근한다.
- HQ 전용 DB 계정은 SELECT 권한만 가진다.
- 비개발자는 복잡한 SSH 명령어를 직접 입력하지 않는다.
- Windows 환경에서도 실행 가능해야 한다.
- Claude Desktop의 MCP Postgres 서버에서 사용할 수 있어야 한다.
2. 최종 아키텍처
HQ Windows PC
↓ PowerShell SSH Tunnel
localhost:15432
↓
EC2 Bastion
↓
RDS PostgreSQL:5432
Claude Desktop에서는 RDS endpoint를 직접 바라보지 않고, 로컬 터널 주소인 localhost:15432로 접속한다.
postgresql://hq_readonly_user:password@localhost:15432/mydb
3. RDS에 readonly 계정 만들기
먼저 HQ 전용 계정을 만들고, 필요한 schema에 대해 권한을 부여했다.
여기서 USAGE ON SCHEMA는 스키마 내부 객체에 접근하기 위한 권한이다.
테이블에 SELECT 권한을 주더라도, 해당 스키마에 대한 USAGE 권한이 없으면 serving.some_table에 접근할 수 없다.
그래서 readonly 계정에는 다음 순서로 권한을 부여했다.
- DB 접속 권한
- 스키마 사용 권한
- 기존 테이블/시퀀스 조회 권한
- 앞으로 생성될 테이블/시퀀스에 대한 기본 조회 권한
CREATE USER hq_readonly_user WITH PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE mydb TO hq_readonly_user;
GRANT USAGE ON SCHEMA serving TO hq_readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA serving TO hq_readonly_user;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA serving TO hq_readonly_user;
그리고 앞으로 생성될 테이블에 대해서도 자동으로 SELECT 권한이 부여되도록 default privileges를 설정했다.
여기서 주의할 점은 FOR ROLE에 들어가는 값은 DB 이름이 아니라,
실제로 테이블을 생성하는 role이어야 한다는 점이다.(ex: postgres)
SELECT
schemaname,
tablename,
tableowner
FROM pg_tables
WHERE schemaname = 'serving'
ORDER BY tablename;
확인한 owner role을 기준으로 다음을 실행했다.
ALTER DEFAULT PRIVILEGES FOR ROLE actual_owner_role IN SCHEMA serving
GRANT SELECT ON TABLES TO hq_readonly_user;
ALTER DEFAULT PRIVILEGES FOR ROLE actual_owner_role IN SCHEMA serving
GRANT SELECT ON SEQUENCES TO hq_readonly_user;
4. Bastion에 터널 전용 OS 계정 만들기
기본 계정인 ec2-user를 그대로 공유하지 않고, SSH 터널 전용 계정을 새로 만들었다.
sudo adduser hq-tunnel
sudo mkdir -p /home/hq-tunnel/.ssh
sudo touch /home/hq-tunnel/.ssh/authorized_keys
sudo chown -R hq-tunnel:hq-tunnel /home/hq-tunnel/.ssh
sudo chmod 700 /home/hq-tunnel/.ssh
sudo chmod 600 /home/hq-tunnel/.ssh/authorized_keys
이 계정은 Bastion에서 작업하기 위한 계정이 아니라, RDS로 가는 SSH 터널을 열기 위한 계정이다.
추가로 /etc/ssh/sshd_config에서 hq-tunnel 계정에만 별도 제한을 걸었다.
이 계정은 Bastion 서버에서 명령을 실행하기 위한 계정이 아니라, RDS로 향하는 SSH 터널만 열기 위한 계정이다.
그래서 TCP forwarding은 허용하되, TTY, X11 forwarding, agent forwarding은 막았다.
PermitOpen을 통해 이 계정이 열 수 있는 터널 목적지도 RDS의 5432 포트로 제한했다.
Match User hq-tunnel
AllowTcpForwarding yes → SSH 터널링 허용
X11Forwarding no → GUI forwarding 차단
AllowAgentForwarding no → SSH agent forwarding 차단
PermitTTY no → 쉘 터미널 할당 차단
ForceCommand /bin/false → 명령 실행 차단
PermitOpen rds-endpoint:5432 → 지정한 RDS:5432로만 포워딩 허용
sudo nano /etc/ssh/sshd_config
sudo systemctl restart sshd
단, ForceCommand /bin/false를 적용한 뒤에는 실제 SSH 터널이 정상적으로 열리는지 반드시 테스트해야 한다.
환경에 따라 설정 조합이 잘못되면 쉘 접속뿐 아니라 터널링까지 실패할 수 있다.
5. SSH key 생성과 public key 등록
로컬에서 HQ용 key를 생성했다.
ssh-keygen -t ed25519 -f ./hq-readonly-user
생성 결과는 다음과 같다.
hq-readonly-user # private key
hq-readonly-user.pub # public key
EC2의 authorized_keys에는 public key만 등록한다.
sudo nano /home/hq-tunnel/.ssh/authorized_keys
단순히 public key를 넣는 대신, 이 key가 특정 RDS endpoint의 5432 포트로만 포워딩할 수 있도록 제한했다.
no-pty,no-agent-forwarding,no-X11-forwarding,permitopen="my-rds-endpoint.ap-northeast-2.rds.amazonaws.com:5432" ssh-ed25519 AAAAC3... hq-readonly-user
이 설정의 의미는 다음과 같다.
no-pty → 터미널 쉘 사용 제한
no-agent-forwarding → SSH agent forwarding 금지
no-X11-forwarding → X11 forwarding 금지
permitopen → 지정한 host:port로만 터널 허용
6. Windows용 PowerShell 스크립트 만들기
HQ 사용자는 Windows 환경이었기 때문에,
SSH 명령어를 직접 입력하게 하지 않고 .bat 파일을 더블클릭하는 방식으로 만들었다.
폴더 구조는 다음과 같다.
hq-rds-access/
├─ start-rds-tunnel.bat
├─ start-rds-tunnel.ps1
└─ hq-readonly-user.pem
PowerShell 스크립트는 다음과 같다.
$KEY_PATH = "$PSScriptRoot\hq-readonly-user.pem"
$LOCAL_PORT = 15432
$RDS_ENDPOINT = "my-rds-endpoint.ap-northeast-2.rds.amazonaws.com"
$RDS_PORT = 5432
$BASTION_USER = "hq-tunnel"
$BASTION_HOST = "bastion-public-ip"
Write-Host "Preparing SSH key permission..."
icacls "$KEY_PATH" /inheritance:r | Out-Null
icacls "$KEY_PATH" /grant:r "$($env:USERNAME):(R)" | Out-Null
Write-Host ""
Write-Host "Starting RDS SSH tunnel..."
Write-Host "Local DB endpoint: localhost:$LOCAL_PORT"
Write-Host "Keep this PowerShell window open while using Claude/Desktop DB tools."
Write-Host ""
ssh -i "$KEY_PATH" `
-N `
-L ${LOCAL_PORT}:${RDS_ENDPOINT}:${RDS_PORT} `
${BASTION_USER}@${BASTION_HOST}
비개발자가 더 쉽게 실행할 수 있도록 .bat 파일도 만들었다.
@echo off
powershell -ExecutionPolicy Bypass -File "%~dp0start-rds-tunnel.ps1"
pause
사용자는 start-rds-tunnel.bat을 더블클릭하고, 열린 PowerShell 창을 유지하면 된다.
7. Claude Desktop MCP 설정
Claude Desktop의 Postgres MCP 서버는 RDS endpoint가 아니라 로컬 터널 주소를 바라보게 설정했다.
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://hq_readonly_user:password@localhost:15432/mydb"
]
}
}
}
중요한 점은 다음과 같았다.
RDS endpoint는 PowerShell SSH 터널 스크립트 안에만 들어간다.
Claude Desktop에는 localhost:15432를 넣는다.
8. 중간에 만난 문제들
Windows에서 icacls 오류
처음에는 다음 오류가 발생했다.
"/grant:r" 매개 변수가 잘못되었습니다.
원인은 PowerShell의 변수 해석 문제였다.
아래처럼 수정해서 해결했다.
icacls "$KEY_PATH" /grant:r "$($env:USERNAME):(R)" | Out-Null
Permission denied publickey 오류
또 다른 문제는 SSH 접속 시 다음 오류였다.
Permission denied (publickey,gssapi-keyex,gssapi-with-mic)
확인해보니 Bastion 계정명이 hq-tunnel인데 스크립트에는 hq_tunnel처럼 다르게 들어간 문제가 있었다.
SSH 계정명, authorized_keys 위치, private/public key 쌍을 다시 확인해서 해결했다.
Ghostty에서 nano 실행 오류
Bastion에서 nano를 실행할 때 다음 오류도 있었다.
ncurses: cannot initialize terminal type ($TERM="xterm-ghostty")
서버가 xterm-ghostty terminfo를 몰라서 생긴 문제였고, 임시로 아래처럼 우회했다.
export TERM=xterm-256color
nano /home/hq-tunnel/.ssh/authorized_keys
9. 최종 사용 방식
HQ 사용자는 다음 순서만 알면 된다.
1. start-rds-tunnel.bat 실행
2. PowerShell 창 유지
3. Claude Desktop 실행
4. DB 관련 질문 또는 조회 작업 수행
5. 작업이 끝나면 PowerShell 창 닫기
비개발자에게 SSH, Bastion, RDS endpoint, 보안 그룹 구조를 설명할 필요 없이, 실행 절차를 단순화할 수 있었다.
10. 회고
이번 구성에서 가장 중요했던 점은 “접근 가능하게 만드는 것”보다 “어디까지 접근 가능하게 할 것인가”였다.
단순히 RDS 접속 정보를 공유하면 빠르게 해결될 수는 있지만, 운영 환경에서는 위험하다.
그래서 다음과 같이 계층별로 권한을 나눴다.
네트워크 레벨: HQ 고정 IP만 Bastion 22번 접근 허용
서버 레벨: hq-tunnel 전용 OS 계정 사용
SSH 레벨: permitopen으로 RDS:5432만 허용
DB 레벨: hq_readonly_user에 SELECT 권한만 부여
클라이언트 레벨: localhost:15432로만 접근하게 구성
결과적으로 HQ는 Claude Desktop을 통해 필요한 데이터를 조회할 수 있게 되었고,
개발팀은 RDS를 직접 공개하지 않으면서도 최소 권한 원칙을 지킬 수 있었다.
이번 경험은 비개발 조직에게 내부 데이터를 열어줄 때,
단순히 “연결되게 하는 것”이 아니라 “안전하게 사용할 수 있는 형태로 포장하는 것”이 중요하다는 걸 보여준 사례였다.
'TIL' 카테고리의 다른 글
| [260531 TIL] 멀티가능 미니 FPS 만들기 1편(기본구현) (0) | 2026.05.31 |
|---|---|
| [260528 TIL] Next.js Vercel → AWS 마이그레이션 (0) | 2026.05.28 |
| [260508 TIL] Personas - 임베딩 검색 구현 - overview (1) | 2026.05.09 |
| [260508 TIL] Personas - 임베딩 검색 구현 - observability (0) | 2026.05.09 |
| [260508 TIL] Personas - 임베딩 검색 구현 - pgvector+HNSW (1) | 2026.05.09 |