배스천에는 계정 대신 DB 포워딩만 여는 키 한 줄을 넣는다

운영 DB가 사설망 안에 있고 그 앞에 배스천 호스트(사설망으로 들어가는 SSH 서버)가 있으면, OwlAI는 배스천을 거치는 SSH 터널로 DB에 붙습니다. 흔한 방법은 고객이 배스천 계정을 만들어 비밀번호나 개인키를 넘겨주는 것입니다. 그런데 그 자격 증명으로는 DB 포트만 열리지 않습니다. 셸 로그인과 명령 실행, 배스천에서 닿는 모든 호스트로의 포워딩까지 열립니다. OwlAI가 보관한 자격 증명이 새면 고객 사설망으로 들어가는 셸도 함께 샙니다.
그래서 OwlAI가 조직마다 SSH 키를 직접 만들고, 고객은 배스천의 authorized_keys에 화면이 주는 한 줄만 넣습니다. 그 줄은 셸과 명령 실행을 막고, 적힌 DB 주소로 가는 포워딩만 엽니다. 이 글은 그 줄이 무엇을 허용하는지, 줄을 배스천마다 하나로 모으는 이유, 키를 바꾸는 동안 줄이 두 줄이 되는 방식, 쿼리마다 터널을 여는 비용과 그에 맞춘 배스천 설정을 다룹니다.
줄 하나가 배스천에서 할 수 있는 일을 정한다
화면이 보여 주는 줄은 이런 꼴입니다.
restrict,port-forwarding,command="/bin/false",permitopen="<db>:<port>" ssh-ed25519 AAAA… owlai
restrict는 이 키로 들어온 세션의 명령 실행, 터미널 할당, 에이전트·X11 포워딩, 포트 포워딩, 사용자 rc 파일 실행을 한꺼번에 끕니다. 뒤이은 port-forwarding이 포트 포워딩만 다시 켜고, permitopen이 그 대상을 적힌 DB의 host:port로 좁힙니다. command="/bin/false"는 세션이 열려도 아무 명령도 돌지 않게 하는 강제 명령입니다. restrict는 OpenSSH 7.2부터 있는 옵션이라, 그보다 오래된 sshd는 이 줄을 받지 않습니다.
DB 호스트 칸에는 배스천에서 본 DB 주소를 넣으므로 사설 주소도 됩니다. 그 주소는 permitopen에 그대로 실리기 때문에, 공백이나 따옴표·쉼표처럼 줄의 문법을 바꿀 수 있는 글자는 등록할 때 거절합니다.
키는 OwlAI가 조직에서 처음 필요할 때 ed25519로 만들고, 개인키는 암호화해 보관합니다. 고객에게는 공개키와 줄만 보입니다. SSH 키 창에서 공개키 원문도 복사할 수 있지만, 원문만 배스천에 넣으면 앞의 제한이 하나도 걸리지 않습니다. 배스천에 넣는 것은 언제나 화면이 준 줄입니다.
같은 배스천을 쓰는 DB는 한 줄에 모은다
sshd는 같은 공개키가 든 줄이 여럿이면 첫 줄의 옵션만 씁니다. 연결마다 줄을 따로 주면 고객은 줄을 차례로 덧붙이고, 둘째 DB부터 포워딩이 거절됩니다. 그래서 줄은 배스천 하나에 키마다 하나이고, 그 배스천을 거치는 연결들의 DB 주소를 permitopen으로 이어 적습니다. 새 연결을 등록할 때 화면은 합친 줄과 이 줄로 열리는 DB 목록을 함께 보여 주고, 고객은 배스천에 있던 OwlAI 줄을 새 줄로 바꿉니다.
까다로운 것은 같은 배스천을 알아보는 일입니다. 관리자는 같은 sshd를 한 연결에서는 이름으로, 다른 연결에서는 IP로 등록할 수 있습니다. 이름만 비교하면 배스천이 둘로 보여 줄을 두 개 주게 되는데, 서버의 authorized_keys 파일은 하나라 다시 첫 줄만 통합니다. 그래서 정규화한 이름(호스트 소문자·포트·계정)이 같거나, 확인한 호스트키 지문과 계정이 같은 연결을 한 배스천으로 묶습니다. 두 기준 가운데 하나로라도 이어지면 같은 묶음입니다.
포워딩할 포트가 DB 포트와 다른 경우도 줄에 들어갑니다. Tibero는 TLS를 켜면 리스너 포트 + 2의 TLS 리스너로 붙으므로, 터널이 포워딩할 포트도 그 포트가 됩니다. 줄은 엔진과 TLS 설정에서 실제로 포워딩할 포트를 계산해 적습니다. 그래서 터널을 쓰는 Tibero 연결에서 TLS를 켜거나 끄면 줄을 다시 받아 배스천에 넣어야 합니다. 터널 위에서 인증서를 어떤 이름으로 확인하는지는 「인증서 오류는 검증을 끄지 않고, 빠진 CA와 틀린 이름을 고쳐서 푼다」에서 다룹니다.
배스천은 대조한 호스트키로만 받는다
줄이 OwlAI 키로 열리는 범위를 좁힌다면, 호스트키 확인은 OwlAI가 엉뚱한 서버에 DB 계정을 넘기지 않게 합니다. 등록 창에서 호스트키 확인을 누르면 OwlAI가 배스천에 붙어 호스트키 지문만 받고 끊습니다. 관리자는 그 지문을 배스천 관리자에게 받은 지문과 대조해 체크한 뒤 등록합니다. 그 뒤로는 저장한 지문과 정확히 같은 호스트키일 때만 접속을 이어 갑니다.
받는 호스트키는 ed25519, ECDSA, RSA SHA-2이고, SHA-1 ssh-rsa만 내는 배스천에는 붙지 않습니다. 지문 조회와 실제 접속은 같은 알고리즘 목록을 같은 순서로 씁니다. 목록이 다르면 관리자가 ed25519 지문을 확인했는데 접속 때는 RSA 키가 와서 불일치로 끊깁니다. 접속에 쓰는 것은 등록된 연결 정보뿐이고, 서버의 known_hosts, ~/.ssh/config, SSH 에이전트, 기본 키 파일은 읽지 않습니다. 배스천 주소는 등록·수정 때와 실행기를 만들 때마다 접속을 허용할 주소인지 판정하고, 판정한 IP로 바로 다이얼합니다.
터널의 OwlAI 쪽 끝도 OwlAI 프로세스만 씁니다. 드라이버는 루프백의 임시 포트로 터널에 붙는데, 같은 호스트의 다른 프로세스가 그 포트를 찾아 붙으면 배스천 너머 DB로 가는 길을 얻습니다. 그래서 터널은 붙은 상대의 소켓이 이 프로세스의 것인지 확인하고, 아니면 끊습니다. 드라이버가 별도 컨테이너(사이드카)에서 도는 Tibero는 공유 볼륨의 Unix 소켓을 터널 끝으로 쓰고, 상대가 사이드카일 때만 포워딩합니다.
배스천이 permitopen으로 포워딩을 거절하면 드라이버는 원인 모를 연결 끊김만 봅니다. 그래서 터널은 SSH 로그인 직후 포워딩을 하나 미리 열어 봅니다. 거절되면 연결 확인의 배스천 단계가 「배스천에서 DB로 연결하지 못했습니다. DB 주소와 permitopen 을 확인하세요」로 실패해, 배스천의 줄이 낡았다는 것을 바로 알 수 있습니다.
키를 바꾸는 동안에는 두 키를 함께 쓴다
조직 키를 정기적으로, 또는 유출이 의심될 때 바꾸려면 배스천마다 줄을 바꿔야 합니다. 배스천이 여럿이면 고객이 모두 바꾸는 데 시간이 걸리고, 그 사이 연결이 끊겨서는 안 됩니다. 그래서 키 교체는 네 단계로 나눴습니다.
- 키 회전 시작을 누르면 새 키가 생깁니다. 이때부터 터널은 지금 키와 새 키를 차례로 내밉니다.
- 고객이 배스천마다 OwlAI 줄을 화면이 보여 주는 두 줄(지금 키·새 키)로 바꿉니다.
- 회전 완료를 누르면 OwlAI가 터널 연결마다 새 키 하나로 로그인해 봅니다. 모두 되면 옛 개인키를 폐기합니다.
- 고객이 배스천에서 옛 키의 줄을 지웁니다.
3단계에서 로그인하지 못한 연결이 있으면 교체를 끝내지 않고 그 연결을 보여 줍니다. 새 줄을 아직 넣지 않은 배스천이 남았다는 뜻입니다. 연동을 해제해 둔 연결도 확인합니다. 다시 켰을 때 끊겨 있으면 안 되기 때문입니다. 확인하는 사이 다른 관리자가 터널 연결을 등록하면 두 작업은 같은 키 행을 잠가 차례로 처리되고, 늦게 온 쪽이 확인을 다시 합니다.
터널은 쿼리마다 열리므로 배스천의 동시 접속 한도를 넉넉히 둔다
OwlAI의 SQL 실행기는 쿼리마다 드라이버 연결을 새로 열고 닫습니다. 터널도 그 연결과 함께 열고 닫아서, 쿼리가 끝나면 남는 터널이 없습니다. 대가는 쿼리마다 붙는 SSH 접속·키 교환·공개키 인증 시간입니다. 아래는 로컬 도커 환경(같은 도커 망의 OpenSSH 배스천, 연결을 포함한 SELECT 1 40회, 2026-10-08)에서 잰 값입니다.
| 경로 | p50 | p90 |
|---|---|---|
| PostgreSQL 직접(평문) | 51.5ms | 88.8ms |
| PostgreSQL 터널(평문) | 199.6ms | 283.1ms |
| PostgreSQL 터널 + TLS | 178.7ms | 219.1ms |
| MariaDB 직접(평문) | 21.2ms | 40.6ms |
| MariaDB 터널(평문) | 152.9ms | 220.0ms |
| MariaDB 터널 + TLS | 161.6ms | 254.1ms |
터널을 열고 닫는 데만 약 100ms가 들고, 그중 SSH 접속과 인증이 약 78ms입니다. 배스천까지 왕복 지연이 있으면 핸드셰이크 왕복 수만큼 더 붙습니다. 채팅의 SQL 도구는 explain과 실행으로 두 번 붙으므로, 답을 기다리는 시간에서는 이 비용이 거의 드러나지 않습니다. 테이블마다 카탈로그·통계·샘플을 여러 번 조회하는 테이블 분석에서는 쌓입니다. 쿼리당 130~150ms가 늘면 쿼리 1,000번에 2분 남짓이 더 듭니다.
배스천 쪽에서는 sshd의 MaxStartups가 걸립니다. 쿼리마다 새 SSH 세션이 열리므로, 인증을 마치기 전의 세션이 동시 쿼리 수만큼 겹칩니다. OpenSSH 기본값 10:30:100은 인증 전 연결이 10개를 넘으면 새 접속 가운데 일부를 무작위로 끊습니다. 한도는 DB 하나의 동시 실행 상한(dbConnection.perDbConcurrency, 기본 4) × 같은 배스천을 쓰는 연결 수 × 백엔드·워커 프로세스 수보다 크게 잡습니다. 예를 들면 MaxStartups 50:30:200입니다.
OpenSSH 9.8 이상의 PerSourcePenalties는 인증 없이 끊는 접속에 벌점을 줍니다. 호스트키 확인은 키만 받고 끊으므로, 짧은 시간에 여러 번 누르면 출구 IP가 잠시 거절될 수 있습니다. 그때는 잠시 기다리거나 출구 IP를 PerSourcePenaltyExemptList에 넣습니다.
설계에서 고른 것
고객의 배스천 계정과 개인키를 받는 대신, OwlAI가 조직 키를 만들고 줄 하나를 주는 쪽을 골랐습니다. 계정을 받으면 고객이 줄을 다룰 일이 없어 등록이 한 단계 줄어듭니다. 하지만 그 계정은 셸과 모든 포워딩을 열어서, 유출되면 피해가 배스천 너머 망 전체로 번집니다. 줄 방식에서 OwlAI 키로 열리는 곳은 줄에 적힌 DB 포트뿐입니다. 대가는 DB를 더하거나 Tibero 연결의 TLS를 바꿀 때마다 고객이 줄을 다시 넣어야 한다는 점입니다.
줄은 연결마다 주는 대신 배스천마다 하나로 모았습니다. sshd가 같은 키의 첫 줄만 읽기 때문입니다. 대가는 호스트키를 일부러 공유하는 HA 배스천 두 대도 한 배스천으로 묶인다는 점입니다. 화면에는 대표 이름 하나만 보이므로 고객은 두 대 모두에 줄을 넣어야 하고, 키 교체라면 회전 완료 확인에서 줄이 빠진 쪽 연결이 드러납니다.
터널은 오래 열어 두고 재사용하는 대신 쿼리마다 엽니다. 재사용하면 쿼리당 100ms 안팎이 줄지만, 끊긴 터널을 알아채고 다시 여는 상태 관리가 붙고 배스천에 세션이 계속 남습니다. 쿼리 단위로 열면 쿼리가 끝날 때 배스천에 남는 세션이 없고, 대가는 위 측정만큼의 시간과 MaxStartups 조정입니다.
두 키를 함께 쓰는 기간에는 기한 대신 로그인 확인을 걸었습니다. 기한을 두면 노출 기간이 짧아 보이지만, 줄을 늦게 바꾼 배스천의 연결이 기한에 맞춰 끊깁니다. 두 개인키는 모두 OwlAI만 갖고 있어, 함께 쓰는 기간이 길어도 키가 닿는 곳은 늘지 않습니다.
DB가 배스천 뒤에 있다면 고객 쪽 일은 셋입니다. 셸 로그인을 막은 전용 계정의 authorized_keys에 화면이 준 줄을 넣고, 배스천 SSH 포트를 OwlAI 출구 IP에만 열고, MaxStartups를 동시 쿼리 수에 맞춥니다. DB 계정에는 조회 권한만 주면, 배스천을 거쳐 닿는 DB에서도 OwlAI가 할 수 있는 일은 읽기로 묶입니다.
어떤 문제를 해결하나요?
DB 연결과 OwlDB의 다른 글

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

엔지니어링 노트 · DB 연결과 OwlDB
RDS 목록은 액세스 키 대신 60분 뒤 만료되는 역할로 읽는다
OwlAI SaaS에서 고객의 Amazon RDS·Aurora를 연결하려면 엔드포인트·포트·엔진과 함께 DB가 공개 서브넷에 있는지, 어느 VPC·보안 그룹에 속하는지 같은 망 정보가 필요합니다.

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