RDS 목록은 액세스 키 대신 60분 뒤 만료되는 역할로 읽는다

OwlAI SaaS에서 고객의 Amazon RDS·Aurora를 연결하려면 엔드포인트·포트·엔진과 함께 DB가 공개 서브넷에 있는지, 어느 VPC·보안 그룹에 속하는지 같은 망 정보가 필요합니다. Aurora처럼 쓰기·읽기 엔드포인트가 따로 있으면 손으로 옮겨 적다 둘이 뒤섞이기도 합니다. 이 값을 AWS에서 직접 읽으려면 OwlAI가 고객 계정에 들어가야 합니다. 흔한 방법은 읽기 전용 IAM 사용자의 액세스 키를 받는 것인데, 그 키는 고객이 지울 때까지 유효합니다. 목록을 한 번 읽고 나면 쓸 일이 없는 자격 증명이 OwlAI 쪽에 계속 남습니다.
그래서 고객이 자기 계정에 CloudFormation 스택을 직접 만들게 했습니다. 스택은 60분 뒤 만료되는 읽기 전용 역할을 만들어 OwlAI에 알리고, OwlAI는 그 역할을 한 번 맡아(AssumeRole) DB 목록과 망 정보를 읽은 뒤 다시 쓰지 않습니다. 사설 서브넷의 DB라면 같은 방식으로 배스천 스택을 하나 더 만들고, 배스천의 호스트키 지문도 스택이 OwlAI에 알립니다. 이 글은 고객이 고칠 수 있는 링크와 보낸 쪽을 모르는 알림을 어떻게 대조하는지 설명합니다. 고객 역할이 신뢰하는 주체가 OwlAI의 AWS 계정에 있으므로 이 기능은 OwlAI SaaS에만 있습니다.
액세스 키도, 손으로 만든 역할도 고객 계정에 오래 남는다
읽기 전용 IAM 사용자를 만들어 액세스 키를 받으면 구현은 몇 줄로 끝납니다. 대신 그 키는 고객이 지울 때까지 살아 있어서, OwlAI 쪽 저장소가 새면 고객 계정을 읽을 수 있는 키가 함께 샙니다.
교차 계정 역할을 쓰면 비밀이 오가지 않습니다. 고객이 안내 문서를 보며 OwlAI를 신뢰하는 역할을 만들고, 그 ARN을 보내 주는 방식입니다. 문제는 신뢰 정책을 손으로 쓰면 조건 하나를 빠뜨리기 쉽다는 점입니다. 외부 ID 조건이 빠지면 혼동된 대리자 문제(다른 고객이 OwlAI를 시켜 이 역할을 맡게 하는 일)를 막지 못하고, 만료 조건이 없으면 역할은 고객이 지울 때까지 남습니다.
고객이 만든 스택이 만료 시각이 박힌 역할을 만든다
관리자가 Amazon RDS로 연결을 누르면, 템플릿과 파라미터를 미리 채운 CloudFormation 빠른 생성 링크(Quick Create)가 새 탭에서 열립니다. 고객 쪽 담당자는 IAM 리소스 생성 확인란을 체크하고 스택 생성을 누릅니다. 조회 스택이 만드는 리소스는 셋입니다.
- 시작 알림. 사용자 지정 리소스(Custom Resource)이고, 다른 리소스가 모두 이것을 기다린다.
- 조회 역할. 신뢰 주체는 OwlAI 서비스 역할 하나이고, 조건은
sts:ExternalId일치와aws:CurrentTime<ExpiresAt두 가지다. 권한은rds:DescribeDBInstances·rds:DescribeDBClusters·ec2:DescribeRegions·ec2:DescribeSubnets·ec2:DescribeRouteTables다섯 개다. - 등록 알림. 만든 역할의 ARN을 OwlAI에 알린다.
두 알림의 ServiceToken은 OwlAI의 SNS 토픽입니다. CloudFormation이 요청 문서를 토픽에 올리면 OwlAI 쪽 큐로 넘어가고, 워커가 꺼내 처리합니다. 그동안 고객 스택은 OwlAI의 응답을 ServiceTimeout 300초까지 기다립니다. OwlAI가 FAILED로 답하면 스택이 롤백되고 역할도 함께 지워집니다.
링크의 값은 고객이 고칠 수 있어서, 알림마다 다시 대조한다
빠른 생성 링크의 파라미터는 고객 콘솔에 그대로 보이고 고칠 수도 있습니다. 그래서 링크에는 비밀을 싣지 않습니다. ExternalId도 비밀이 아니라, 이 역할이 이 고객의 요청에서 나왔다는 표지입니다. 요청 토큰은 32바이트 난수입니다. OwlAI는 그 해시만 대조용으로 두고, 등록 알림이 오면 한 번만 소비합니다. 토큰의 수명 60분은 신뢰 정책의 ExpiresAt과 같은 시각에 끝납니다.
OwlAI는 고객의 AWS 계정 번호를 미리 받지 않습니다. 토픽은 어느 계정의 CloudFormation이든 알림을 올릴 수 있게 열려 있고, 그래서 알림 하나하나는 누가 보냈는지 모르는 입력입니다. 워커는 다음을 모두 확인한 뒤에만 알림을 받아들입니다.
- 요청 토큰이 맞고, 만료 전이며, 다른 스택이 아직 쓰지 않았다.
- 역할 ARN의 계정, 스택의 계정, 역할을 맡은 뒤 확인한 호출자 계정이 모두 같다.
- 역할이
ExternalId없이는 맡아지지 않는다.
하나라도 어긋나면 FAILED로 답하고 역할을 쓰지 않습니다. 시작 알림도 토큰과 만료를 먼저 대조하므로, 만료됐거나 고친 링크는 역할이 만들어지기 전에 실패합니다.
SQS는 같은 메시지를 두 번 건넬 수 있습니다. 그래서 이미 답한 요청에는 다시 답하지 않습니다. 넓은 역할을 거절해 FAILED로 답한 스택에 두 번째 처리가 SUCCESS를 보내면, 롤백돼야 할 역할이 고객 계정에 남기 때문입니다.
역할은 목록을 한 번 읽는 데만 쓴다
확인을 통과하면 워커는 먼저 SUCCESS로 답해 고객 스택을 끝내고, 그다음 목록을 읽습니다. 역할 세션은 STS가 허용하는 최솟값인 900초라서, 뒤 단계가 쓸 값을 이때 모두 읽어 둡니다.
- 리전: 계정에서 켜진 리전(
DescribeRegions)을 8개씩 함께 읽는다. SCP로 막힌 리전은 실패로 적고 나머지를 계속 읽는다. - 공개 판정:
PubliclyAccessible이 참이고 DB 서브넷에0.0.0.0/0→ 인터넷 게이트웨이 경로가 있을 때만 공개 주소로 붙기를 제안하고, 아니면 SSH 터널을 제안한다. - Aurora: 클러스터 한 줄로 보이고, 공개 여부는 소속 인스턴스로 정한다. 질의가 읽기 전용이라 읽기 엔드포인트를 기본으로 놓는다.
- 엔진: PostgreSQL·MySQL·MariaDB와 각각의 Aurora만 고를 수 있다. Oracle·SQL Server는 목록에 보이지만 고를 수 없다.
이 뒤로 OwlAI는 AWS를 다시 부르지 않습니다. 등록 창의 칸 채우기, 배스천 링크의 파라미터, 등록 대조가 모두 저장한 목록을 씁니다. DB 비밀번호는 AWS에서 읽지 않습니다. 조회 역할에 그 권한이 없어서 관리자가 직접 넣습니다. 조회 스택은 더 쓸 일이 없으므로 화면이 콘솔 링크를 주고 고객이 지웁니다.
등록할 때는 저장한 목록이 요청을 대조합니다. 요청의 엔진·호스트·포트가 목록에 있는 그 DB의 엔드포인트와 다르면 AWS_RDS_TARGET_MISMATCH로 거절합니다. TLS는 배스천을 거쳐도 항상 켜고, 서버 인증서는 그 리전의 RDS 루트 CA로 확인합니다. AWS 전역 번들(174KB)은 연결 하나의 CA 상한 64KB를 넘으므로, 그 리전의 루트 셋(RSA2048·RSA4096·ECC384, 약 5KB)만 골라 넣습니다. 가져온 연결의 주소를 잠그고 다시 가져오기로 고치는 방식은 「OwlDB 연결은 접속 정보 대신 DB 식별자만 든다」에서, 터널 위에서 인증서 이름을 확인하는 방식은 「인증서 오류는 검증을 끄지 않고, 빠진 CA와 틀린 이름을 고쳐서 푼다」에서 다룹니다.
사설 DB의 배스천도 고객 스택이 만들고, 호스트키 지문은 스택이 알린다
DB를 고르면 연결 방식이 셋으로 갈립니다. 공개 주소는 RDS 보안 그룹에 OwlAI의 출구 IP를 DB 포트로 열어 주는 길이고, 기존 배스천은 고객 배스천에 authorized_keys 한 줄을 넣는 「배스천에는 계정 대신 DB 포워딩만 여는 키 한 줄을 넣는다」의 방식입니다. 배스천이 없는 사설 DB에는 새 배스천을 만듭니다.
배스천 스택의 빠른 생성 링크에는 조회 때 읽어 둔 VPC·공개 서브넷·DB 보안 그룹·DB 포트, OwlAI 출구 IP(IPv4 1~3개), 그 DB 하나로만 포워딩하는 authorized_keys 줄, 요청 토큰이 채워집니다. 망 정보는 이미 저장돼 있으므로 조회 역할이 만료된 뒤에도 링크를 만들 수 있습니다. 출구 IP나 공개 서브넷이 없으면 링크를 내지 않고, 링크를 내기 전에 인스턴스 종류와 월 비용 어림을 화면에 보여 줍니다. 고객이 승인하면 스택이 만드는 것은 넷입니다.
- t4g 인스턴스. sshd는 비밀번호 로그인과 root 로그인을 끈다.
- Elastic IP. 인스턴스를 다시 시작해도 주소가 그대로다.
- SSH 보안 그룹. 22번 포트를 OwlAI 출구 IP에서만 받는다.
- DB 보안 그룹 규칙 하나. 배스천에서 DB 포트로 오는 접속을 허용한다. 고객이 직접 열겠다면 끌 수 있다.
첫 접속에서 받은 호스트키를 그대로 믿으면, 그 사이에 끼어든 서버에 DB 계정을 넘길 수 있습니다. 고객이 운영하는 배스천이라면 관리자가 지문을 대조하지만(「배스천에는 계정 대신 DB 포워딩만 여는 키 한 줄을 넣는다」), 방금 만든 배스천의 지문은 아직 아무도 모릅니다. 그래서 배스천이 스스로 알립니다. 부팅한 배스천은 UserData 끝에서 ed25519 호스트키 지문을 계산해 WaitConditionHandle에 올리고, 최대 15분을 기다리는 WaitCondition이 그 값을 받으면 등록 알림이 공개 IP·인스턴스·보안 그룹과 함께 지문을 싣고 옵니다. 지문은 고객 계정 안의 CloudFormation을 거쳐 오므로, OwlAI는 네트워크로 배스천에 붙기 전에 그 값을 압니다.
배스천 알림도 조회 알림처럼 토큰을 대조하고, 스택의 계정·리전이 요청한 DB의 것과 같은지 확인합니다. 워커는 지문의 꼴과 공개 IP가 인터넷에서 닿는 IPv4인지까지 확인한 뒤 지문을 고정합니다. 스택이 실패하거나 취소되면 OwlAI에는 알림이 오지 않으므로, 배스천 요청은 20분이 지나면 끝난 것으로 봅니다. 그 전에는 같은 DB로 두 번째 배스천을 만들지 않고, 화면이 진행 중인 요청을 이어받습니다.
배스천은 부팅할 때마다 스택 파라미터의 authorized_keys 줄을 다시 씁니다. 다른 DB를 열거나 SSH 키를 교체할 때는 고객이 AuthorizedKeysLine(교체 중에는 AuthorizedKeysLine2)을 고쳐 스택을 업데이트하고, 인스턴스는 다시 시작만 하므로 주소와 호스트키가 그대로 남습니다.
배스천은 연결과 따로 살아서, 연결의 비밀번호·DB·계정을 고쳐도 그대로입니다. OwlAI 쪽 기록은 그 배스천을 쓰던 마지막 연결과 함께 지워지고, 스택은 고객이 지웁니다. 비용은 스택을 지울 때까지 고객 계정에 청구됩니다.
설계에서 고른 것
액세스 키를 받는 대신 고객이 스택을 만들게 했습니다. 고객은 콘솔에서 확인란과 버튼을 한 번 더 눌러야 하고, 그 담당자에게는 IAM 역할을 만들 권한이 있어야 합니다. 그 대가로 OwlAI 쪽에는 오래 사는 AWS 자격 증명이 남지 않고, 60분이 지나면 신뢰 정책이 역할을 막습니다.
고객 계정 번호를 미리 등록받는 대신 알림마다 대조했습니다. 등록 단계가 하나 줄지만 알림이 누가 보냈는지 모르는 입력이 되므로, 토큰·계정·외부 ID를 대조하는 코드가 그 자리를 맡습니다. 이 코드는 확인 시점과 중복 전달까지 따져야 해서, 계정 목록을 미리 받는 쪽보다 복잡합니다.
역할을 쥐고 있는 대신 목록을 한 번만 읽습니다. 목록은 그 순간의 사본이라 DB 주소가 바뀌면 다시 가져오기로 스택을 한 번 더 만들어야 합니다.
배스천을 OwlAI가 관리하는 대신 고객 계정의 자원으로 남겼습니다. 정리와 비용은 고객 몫이 되지만, OwlAI가 고객 계정에 인스턴스를 만들거나 지울 권한을 들고 있을 일이 없습니다.
고객 클라우드 계정의 메타데이터를 한 번만 읽으면 되는 SaaS라면 같은 구성을 쓸 수 있습니다. 읽을 권한만 둔 역할에 만료 시각을 신뢰 정책 조건으로 걸고, 고객 쪽에서 오는 알림은 계정과 토큰을 대조해야 하는 입력으로 다룹니다.
어떤 문제를 해결하나요?
DB 연결과 OwlDB의 다른 글

엔지니어링 노트 · DB 연결과 OwlDB
배스천에는 계정 대신 DB 포워딩만 여는 키 한 줄을 넣는다
운영 DB가 사설망 안에 있고 그 앞에 배스천 호스트(사설망으로 들어가는 SSH 서버)가 있으면, OwlAI는 배스천을 거치는 SSH 터널로 DB에 붙습니다.

엔지니어링 노트 · DB 연결과 OwlDB
인증서 오류는 검증을 끄지 않고, 빠진 CA와 틀린 이름을 고쳐서 푼다
기업망에 설치한 OwlAI가 클라우드 LLM이나 고객 DB에 TLS로 붙을 때, 인증서 오류가 나는 경우는 크게 둘입니다.

엔지니어링 노트 · DB 연결과 OwlDB
Supabase 연결 주소는 옮겨 적는 대신 OAuth 승인으로 채운다
Supabase 대시보드에는 같은 DB로 가는 주소가 셋 있습니다.
