사용자가 OpenAI 또는 Anthropic API 키를 직접 가져오게 하는 방법 (평문으로 저장하지 않고)
요약
멀티 유저 앱에서 OpenAI 또는 Anthropic API 키를 안전하게 관리하는 BYOK(Bring Your Own Key) 구현 방식을 단계별로 설명합니다. 데이터베이스 평문 저장부터 서버 측 암호화 금고까지, 보안 수준에 따른 4가지 구현 단계를 비교 분석합니다.
핵심 포인트
- API 키는 비밀번호와 달리 해싱이 아닌 암호화가 필요한 유효한 결제 자격 증명임
- Level 0(평문 저장)은 보안 유출 시 즉각적인 금전적 피해를 초래하므로 즉시 개선 필요
- Level 2(브라우저 저장)는 보안상 우수하나 서버 측 백그라운드 작업 수행에 한계가 있음
- SaaS 환경에서는 보안과 기능성을 모두 충족하는 서버 측 암호화 방식이 권장됨
가끔 GitHub에서 "API keys" plaintext is:issue를 검색해 보세요. 그러면 실제로 배포되어 운영 중인 멀티 유저(multi-user) 앱의 관리자들이 "키가 현재 데이터베이스에 평문(plaintext)으로 저장되어 있으며, 이는 플랫폼에 부담이 됩니다."와 같은 문장을 작성한 것을 발견할 수 있을 것입니다. 저는 지난 한 달 동안 이 이슈를 작성한 사람들에게 이메일을 보내왔기 때문에, 수십 개의 이러한 이슈를 읽었습니다.
수요 측면은 설명하기 쉽습니다. 사용자들이 BYOK (Bring Your Own Key, 직접 키 가져오기)를 요청하는 이유는 이미 다른 곳에서 AI 비용을 지불하고 있거나, 자신의 프롬프트가 자신의 프로바이더(provider) 계정에서 실행되기를 원하거나, 혹은 귀하의 무료 티어(free tier)의 속도 제한(rate limits)에 불편함을 느끼기 때문입니다. 개발자들이 BYOK를 원하는 이유는 추론(inference) 비용은 사용량에 따라 늘어나지만 수익은 그렇지 않기 때문입니다.
따라서 BYOK 요청은 계속되고, 잘못된 방식으로 구현되는 일도 계속되고 있습니다. 제가 현장에서 보고 있는 네 가지 단계를 최악부터 프로덕션급(production-grade)까지 순서대로 나열해 보겠습니다.
Level 0: 데이터베이스의 평문(plaintext) 컬럼
이는 생각보다 흔하게 발생합니다. 저장할 때 작성되고 모든 요청 시 읽히는 users.openai_api_key 컬럼이 있는 경우입니다.
이해할 수 있는 이유로 이런 일이 발생합니다. 오후 한나절이면 기능이 작동하기 때문입니다. 하지만 프로바이더 키는 비밀번호 해시(password hash)와 다릅니다. 그것은 살아있는, 즉 비용을 지불할 수 있는 자격 증명(credential)입니다. 만약 백업, 잘못 설정된 관리 패널, 또는 하나의 인젝션(injection) 버그를 통해 데이터베이스가 유출된다면, 공격자는 해독해야 할 해시를 얻는 것이 아닙니다. 귀하가 보유한 모든 사용자에 대해 결제가 연결된 작동 가능한 키를 얻게 됩니다.
만약 현재 Level 0 단계라면, 아래의 암호화 컬럼(encrypted-column) 업그레이드는 하루 정도의 작업 분량입니다. 이번 주 내에 완료하십시오.
Level 1: 환경 변수 (environment variables)
배포 환경에 있는 OPENAI_API_KEY입니다. 완벽하게 괜찮지만, 이는 단일 테넌트(single-tenant)를 위한 해답입니다. 운영자가 키를 소유하는 셀프 호스팅(self-hoster)이나 내부 도구에는 적합합니다. 하지만 호스팅 서비스 형태의 멀티 유저 환경이 되는 순간, 하나의 환경 변수는 모든 사용자가 하나의 계정, 하나의 속도 제한, 그리고 하나의 청구서를 공유함을 의미합니다. 그것이 바로 BYOK가 해결해야 하는 상황입니다.
Level 2: 브라우저에 머무는 키
localStorage 또는 IndexedDB를 사용하며, 각 요청에 클라이언트 측에서 키를 첨부하고 서버에는 아무것도 저장하지 않는 방식입니다. 로컬 우선 (Local-first) 도구들은 이 방식을 사용하며, 이들에게는 이것이 올바른 선택입니다. 서버가 본 적이 없는 데이터는 결코 유출될 수 없기 때문입니다.
한계점은 제품이 서버 측 (server-side) 기능을 확장할 때 나타납니다. 백그라운드 작업 (Background jobs), 예약된 실행 (scheduled runs), 웹훅 (webhooks), 팀 워크스페이스 (team workspaces), 모바일 클라이언트 등이 이에 해당합니다. 키가 단 하나의 브라우저 탭에만 존재한다면 이 중 어느 것도 수행할 수 없습니다. Level 2는 로컬 도구들에게는 실제적인 해답이지만, 호스팅형 SaaS (hosted SaaS)에게는 막다른 길입니다.
Level 3: 서버 측 암호화 금고 (server-side encrypted vault)
이것이 호스팅형 멀티 테넌트 (multi-tenant) 제품을 위한 해답이며, 실제 작업의 대부분이 이루어지는 지점입니다. "키를 암호화한다"는 말은 암호학 (crypto) 코드 한 줄이면 될 것처럼 들립니다. 실제 암호화 코드는 진정으로 20줄 정도에 불과합니다. 그 주변의 모든 것이 바로 제품입니다.
제가 프로덕션 환경의 BYOK 구현에 적용할 최소 체크리스트는 다음과 같습니다:
암호화 (Encryption)
- AES-256-GCM, 암호화 시마다 고유한 논스 (nonce) 사용, 동일한 키 아래에서 논스를 재사용하지 말 것.
- 암호화 키는 데이터베이스 외부에 존재해야 함. KMS, 시크릿 매니저 (secrets manager), 또는 최소한 데이터와는 다른 신뢰 경계 (trust boundary)에 있는 환경 변수 (env var).
- 암호문 (ciphertext)을 해당 행 (row)에 바인딩 (AAD를 사용자 또는 연결 ID와 함께 사용)하여, DB 쓰기 권한을 가진 공격자가 행 간에 암호문을 교체할 수 없도록 할 것.
UX 규칙
- 쓰기 전용 (Write-only). 저장 후에는 클라이언트가 키를 다시 받을 수 없음.
sk-...abc4와 같이 표시하고 그 이상은 보여주지 말 것. - 키를 변경한다는 것은 편집하는 것이 아니라 다시 입력하는 것을 의미함.
운영 위생 (Operational hygiene)
- 프로세스 내부에서, 요청 시점에, 프로바이더 (provider)를 호출하는 코드 경로에서만 복호화할 것. 평문 키 (plaintext key)가 로그 라인, 에러 보고서, 또는 분석 이벤트에 절대 닿지 않아야 함. 업스트림 프로바이더의 에러가 때때로 요청 헤더를 에코 (echo)할 수 있으므로, 이 또한 마스킹 (redact)할 것.
생명주기 (Lifecycle) (모두가 과소평가하는 부분)
- 취소 (Revocation)는 즉각적이어야 합니다. 사용자가 연결 해제 (disconnect)를 클릭하면, 진행 중인 (in-flight) 트래픽만이 유일하게 살아남는 트래픽이어야 합니다.
- 사용량 귀속 (Usage attribution). 누군가 "어제 어떤 사용자가 40달러를 썼나요?"라고 물었을 때 답변할 수 있어야 하며, 이는 요청당, 사용자당 토큰 수 (token counts)를 기록 (logging)해야 함을 의미합니다.
- 순환 (Rotation). 사용자는 제공자 키 (provider keys)를 순환시킵니다. 귀하의 제품은 고객 지원 티켓 (support tickets)을 생성하지 않고도 이를 처리할 수 있는 경로를 갖추어야 합니다.
대부분의 잘못된 구현을 잡아낼 수 있는 간단한 자가 테스트: 귀하의 지원 팀이 키를 볼 수 있습니까? 키가 로그 (logs) 어디에라도 나타납니까? 오늘 밤 귀하의 데이터베이스가 유출된다면, 공격자가 정확히 무엇을 손에 넣게 됩니까? 사용자가 클릭 한 번으로 액세스 (access)를 차단할 수 있습니까? 어떤 사용자가 얼마를 썼는지 알 수 있습니까? 만약 이 질문들 중 어느 하나라도 마음을 불편하게 만든다면, 아직 완료된 것이 아닙니다.
구매 옵션 (The buy option)
여기서 제 이해관계에 대해 솔직히 말씀드리자면: 저는 이것 중 하나를 직접 만들었습니다. 따라서 이 섹션의 나머지 내용은 홍보 (pitch)입니다.
Monet은 호스팅된 서비스로서의 레벨-3 금고 (level-3 vault)이며, 사용자들이 이미 신뢰하고 있는 동의 흐름 (consent flow)인 OAuth와 유사한 형태로 설계되었습니다. 귀하의 사용자는 호스팅된 연결 (connect) 페이지에 접속하여, 자신의 ChatGPT Plus 또는 Claude Pro 계정을 연결하거나 직접 제공자 키 (provider key)를 붙여넣습니다. 자격 증명 (credential)은 AES-256-GCM으로 암호화되어 귀하의 앱이 절대 건드리지 않는 금고 (vault)에 저장됩니다. 귀하의 앱은 표준 권한 부여 코드 교환 (authorization-code exchange)을 통해 불투명한 베어러 토큰 (opaque bearer token)을 받으며, 다음과 같이 OpenAI 호환 엔드포인트 (endpoint) 하나를 호출합니다:
from openai import OpenAI
client = OpenAI(
...
프록시 (proxy)는 토큰을 올바른 사용자의 자격 증명으로 해석 (resolve)하고, 응답을 스트리밍 (stream)하여 다시 전달하며, 제공자가 보고한 실제 토큰 수 (token counts)를 포함한 사용 이벤트 (usage event)를 기록합니다. 따라서 미터링 (metering)과 비용 전달 (cost passthrough)이 자동으로 이루어집니다. 연결을 취소 (revoking)하면 금고의 행 (row)이 삭제되고 토큰 해석이 중단됩니다. 서버 측에는 토큰의 SHA-256 해시 (hash) 값만 저장되므로, Monet 자체의 토큰 테이블이 통째로 덤프 (dump)되더라도 사용할 수 없습니다.
베타 기간 동안에는 무료로 이용할 수 있습니다. 전체 흐름을 처음부터 끝까지 확인하고 싶다면 demo.monet.gg에서 실행 중인 라이브 데모 앱을 확인하실 수 있으며, 개발자 대시보드는 monet.gg에 있습니다.
만약 직접 구축하고 싶다면, 위의 체크리스트가 정직한 최소 요구 사항이며, 어떤 방식이든 의견을 나누는 것을 환영합니다. 이번 달에 평문 키 (plaintext-key) 문제에 대해 충분히 읽었기에, 그러한 문제가 더 적게 발생하기를 바랍니다.
관련 글: I'm 15 and I built "OAuth for AI subscriptions". Here's the full architecture는 호스팅 옵션의 전체 설계이며, Stop paying for your's users' AI usage는 BYOK (Bring Your Own Key)에 대한 경제적 논거입니다.
저는 15세의 Shlok이며, 혼자서 Monet을 만들고 있습니다. 모든 답장을 읽습니다: shlokmadhekar88@gmail.com, github.com/shlok-madhekar, @shlokbuilds.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기