
mcp.json에 API 키를 넣지 마세요: Amazon Cognito와 AWS Lambda를 이용한 사용자별 OAuth 구현
요약
공유 API 키를 사용하는 MCP 환경에서 사용자별 정체성을 보장하기 위한 보안 아키텍처를 제안합니다. Amazon Cognito와 AWS Lambda를 활용하여 공유 키 기반의 업스트림 시스템 앞단에 인증 및 감사 가능한 게이트웨이를 구축하는 방법을 다룹니다.
핵심 포인트
- 공유 API 키 사용 시 사용자별 정체성 상실 문제 해결
- Amazon Cognito와 AWS Lambda를 이용한 OAuth 구현
- 클라이언트 주장 방식이 아닌 암호학적 검증 기반의 신원 확인
- OAuth를 지원하지 않는 레거시 및 내부 시스템 보안 강화
제 AgentCon HK 2026 강연인 "LLM Gateway와 보안 우선 MCP를 통한 팀 전체의 Vibe Coding 강화"의 실행 가능한 동반 자료입니다. 이 강연은 사용자별 OAuth가 공유된 god-token을 안전하고 감사 가능한 에이전트 액세스로 전환하는 핵심이라고 주장했습니다. 이것은 아키텍처 수준에서 설명된 배선(wiring)입니다. 전체 template.yaml, Lambda 및 스크립트는 마지막에 링크된 공개 저장소에 있습니다.
아무도 채우지 않는 간극: 공유 키 API 앞에서의 당신의 정체성
2026년까지 대부분의 대형 SaaS는 공식 MCP 서버를 출시할 것이며, 많은 곳이 OAuth를 지원할 것입니다. 따라서 "내 도구에는 MCP가 없다"와 "내 도구에는 OAuth가 없다"는 문제 모두 점차 사라지고 있습니다. 하나가 부족한 벤더를 쫓아다니는 것은 패배하는 게임입니다. 그 목록은 매주 줄어들고 있습니다.
Tavily — 여기서 제가 사용하는 검색 API — 는 사실 모범적인 시민입니다. 이들의 원격 MCP는 URL에 포함된 공유 키(https://mcp.tavily.com/mcp/?tavilyApiKey=<KEY>)와 OAuth 흐름을 모두 지원합니다. 그렇다면 왜 굳이 이를 래핑(wrap)해야 할까요? 벤더의 OAuth조차 보안 팀이 던지는 질문과는 다른 질문에 답하기 때문입니다:
- 공유 키가 기본값이며, 그 이유는 비용 때문입니다. 이러한 도구 중 상당수는 사용자(seat)당 비용을 책정합니다. 따라서 가장 저렴한 통합 방식 — 즉 팀이 실제로 배포하는 방식 — 은 한 계정의 API 키를 그룹 전체를 위한 단일 공유 MCP에 연결하는 것입니다. 그 키가 공유되는 순간, 사용자별 정체성(per-user identity)은 사라집니다. 모든 호출은 동일한 호출자로 나타나며, "누가 이것을 실행했는가?"라는 질문에는 답할 수 없게 됩니다. 벤더의 OAuth는 모든 사람이 각자의 계정 비용을 지불할 때만 도움이 되는데, 이는 바로 팀들이 피하고자 하는 비용 청구 방식입니다.
- 주장된 것 ≠ 강제된 것. 도구가 사용자별 속성을 부여하더라도, 이는 종종 클라이언트가 제공하는 값에 의존합니다. Tavily의
X-Human-Id헤더가 바로 그런 사례입니다. 클라이언트가 이를 주장(assert)하기 때문에, 호출자가 다른 사람의 ID를 보내는 것을 막을 방법이 없습니다. 즉, 속성 부여(attribution)는 통제가 아닌 예의(courtesy)에 불과합니다. 신뢰할 수 있는 속성 부여는 클라이언트가 입력한 문자열이 아니라, 당신 자신의 게이트웨이가 암호학적으로 검증한 토큰에서 나와야 합니다.
그리고 전혀 줄어들지 않는 핵심적인 부분이 있습니다. 바로 당신에게 가장 중요한 업스트림(Upstream)들은 결코 OAuth를 지원하지 않을 것이라는 점입니다. 내부 결제 API, 아무도 건드리고 싶어 하지 않는 레거시 청구 시스템(Legacy claims system), 단일 팀 API 키를 사용하는 규정 준수용 제3자 도구 등이 그 예입니다. 이들 각각에 대해 유일한 인증 방식은 본질적으로 공유되는 방식인 '공유 키(Shared key)'뿐입니다. 이는 내부 시스템에만 국한되지 않습니다. 수많은 공개 벤더들도 같은 형태를 띠고 있습니다. 예를 들어, Brave Search의 공식 MCP 서버는 단일 BRAVE_API_KEY로 인증하며 OAuth는 전혀 사용하지 않습니다. 키 하나를 팀 전체가 공유하는 방식입니다.
이것이 바로 실질적이고 지속적인 격차입니다. 즉, 공유 키를 사용하는 업스트림으로 넘겨주기 전에, 호출자를 당신의 신원(Identity)으로 인증하고, 권한 범위(Scope)를 지정하며, 감사(Audit)할 수 있는 프런트 도어(Front door)가 없다는 것입니다. 이 포스트는 바로 그것을 구축하는 방법을 다룹니다. Tavily는 실제로 실행해 볼 수 있는 무료 공개 대역(Stand-in)일 뿐이며, 머릿속으로는 이를 당신의 공유 키 API로 치환하여 생각하십시오.
💡 핵심 아이디어: 공유 키 업스트림 앞에, MCP 스펙이 요구하는 PKCE 및 클라이언트 자격 증명(Client-credentials) 흐름을 실행하며 당신의 신원(Amazon Cognito)에 결합된 실제 OAuth 2.0 / OIDC 인증 서버를 단일 Lambda에서 운영하는 것입니다. 각 호출자는 자신만의 권한 범위가 지정되고, 암호학적으로 검증되며, 감사 가능한 신원을 부여받습니다. 공유 키는 서버를 절대 벗어나지 않습니다.
30초 요약 버전이 먼저 필요하신가요? 대화형 데모를 따라가 보세요 — 전체 OAuth 흐름, 권한 범위가 지정된 도구 호출(Scoped tool call), 그리고 호출자가 자신의 권한 범위를 벗어났을 때 발생하는 403 오류를 직접 클릭하며 확인해 보세요.
아키텍처
두 장의 그림이 모든 것을 설명합니다. URL이나 설정 파일에 위치하든 상관없이 모두가 하나의 키를 사용하는 공유 키 경로(Shared-key path)는 다음과 같습니다:
래퍼(Wrapper)를 사용하면, 각 호출자는 자신의 신원(자신의 ID 제공업체를 통해 검증됨)으로서 도착하며, 키는 서버 측에 안전하게 보관됩니다:
세 가지의 관리형 AWS 구성 요소가 각각 명확한 역할을 수행하며 작업을 처리합니다:
Amazon Cognito — 손목 밴드를 발급하는 보안 요원. 이는 OAuth 2.0 / OIDC 인증 서버(OAuth 2.1 및 MCP 스펙이 의존하는 PKCE를 포함)입니다. 호출자는 Amazon Cognito에게 자신의 신원을 증명하며, Amazon Cognito는 특정 범위(scope, 여기서는 tavily-mcp/search)가 찍힌 수명이 짧은 서명된 토큰(signed token)을 반환합니다. 이는 동일한 풀에서 두 종류의 호출자를 모두 처리합니다: 백엔드 에이전트는 머신 간(machine-to-machine) 인증을 수행하고, 사용자는 PKCE가 적용된 호스팅된 로그인 페이지를 통해 로그인합니다. 결정적으로, Amazon Cognito는 여러분의 신원 계층(identity layer) 그 자체입니다. 데모의 사용자 이름/비밀번호를 실제 기업용 SSO(SAML, OIDC 또는 Google, Microsoft, Apple과 같은 소셜 로그인)로 교체하는 것은 재구축이 아닌 설정 변경만으로 가능합니다. 토큰, 범위(scopes), 그리고 그 하위의 모든 요소는 동일하게 유지됩니다.
API Gateway HTTP API + JWT Authorizer — 입구의 손목 밴드 스캐너. MCP 엔드포인트로 향하는 모든 요청 — HTTP POST를 통한 일반적인 JSON-RPC 호출 (Streamable HTTP 전송 방식; 이 래퍼는 전송 계층의 선택 사항인 SSE 스트리밍 채널이 아닌, 요청/응답(request/response) 방식의 POST만 사용합니다) — 은 먼저 API Gateway를 거칩니다. API Gateway의 내장 JWT Authorizer — 이는 HTTP API (v2) 버전의 네이티브 기능이며, 이전의 REST API를 사용한다면 대신 Cognito 또는 커스텀 Lambda Authorizer가 필요합니다 — 는 여러분의 코드가 단 한 줄이라도 실행되기 전에, 에지(edge) 단계에서 토큰의 서명(signature), 발행자(issuer), 만료 시간(expiry)을 확인합니다. 토큰이 없거나, 만료되었거나, 위조된 경우: 그 즉시 401 오류와 함께 거부됩니다. 이것은 순수한 인증(Authentication)입니다: 당신은 토큰이 주장하는 그 사람이 맞는가? 이 검증을 통과하기 전까지는 그 어떤 것도 여러분의 로직에 도달할 수 없습니다. (주의해야 할 실수 하나: Cognito 액세스 토큰에는 aud 클레임(claim)이 포함되어 있지 않으므로, Authorizer의 대상(audience)을 Cognito 앱 클라이언트 ID로 설정해야 합니다. 이 설정이 일치하지 않는 것이 이 스택에서 발생하는 소리 없는 401 오류의 가장 흔한 원인입니다.)
Lambda — 당신이 실제로 들어갈 수 있는 방. 토큰이 유효해지면, Lambda가 *인가(Authorization)*를 수행합니다: 이 신원이 특정 도구를 호출할 수 있는지 확인하기 위해 범위(scope)를 다시 읽고, 해당 동작을 호출자와 연결하는 감사(audit) 로그를 기록한 다음 — 오직 그 시점에만 — Secrets Manager에서 공유된 업스트림 키를 가져와 Tavily를 호출합니다. 키는 오직 이 함수의 실행 역할(execution role) 내부에만 존재하며, 클라이언트로 절대 전송되지 않습니다.
이 분리가 핵심입니다: API Gateway는 "이것이 실제 유효한 토큰인가?"에 답하고, Lambda는 "이 신원이 이 동작을 수행할 권한이 있는가, 그리고 그들이 수행했음을 기록하자"에 답합니다. 에지에서의 인증(Authentication), 코드 내에서의 인가(Authorization), 그리고 그 둘 뒤에 봉인된 비밀(secret).
왜 type: stdio가 아니라 type: http인가
이 래퍼(wrapper)가 로컬 서버가 아닌 원격(remote) MCP 서버인 데에는 이유가 있습니다. 이는 강연에서 OAuth를 전면에 내세운 원격 서버를 아키텍처의 중심으로 둔 이유와 동일합니다. MCP 클라이언트 설정은 세 가지 전송(transport) 옵션을 제공하며, 이 선택이 여러분의 보안 시나리오 전체를 조용히 결정합니다.
stdio— 클라이언트가 로컬 프로세스(npx some-mcp, Python 스크립트 등)를 생성하고 stdin/stdout을 통해 통신합니다. 문제는 이 프로세스가 개발자의 머신에 업스트림 자격 증명(credential)을 가지고 있어야 한다는 점입니다. 따라서 키는mcp.json의env블록에, 셸 히스토리(shell history)에, 혹은 어디로 동기화될지 모를 도트파일(dotfile)에 남게 됩니다. 이제 모든 노트북이 공유 키의 복사본이 됩니다. 이것이 바로 BYOAI / 공유 키 문제의 전형적인 형태입니다. 하나의 자격 증명이 사방에 뿌려져, 깔끔하게 추적하거나 취소하는 것이 불가능해집니다.sse— 기존의 원격 전송 방식입니다. 원격 방식을 택한 직관은 옳았으나, 단순한 SSE는 전송 수단일 뿐 자체적인 인증(auth) 시나리오를 담고 있지 않았습니다. 그래서 실제로는 사람들은 여기에 정적 베어러 토큰(static bearer token)을 덧붙였고, 결국 다시 "하나의 공유 비밀(shared secret)" 문제로 돌아가게 되었습니다.http(Streamable HTTP) — 클라이언트가 일반 HTTPS를 통해 접속하는 원격 엔드포인트이며, 2025년 MCP 사양(spec)에서 표준화된 전송 방식입니다. 결정적으로, 사양은 MCP 서버를 **OAuth 리소스 서버(OAuth resource server)**로 정의합니다. 즉, 인증(authentication)이 사후 고려 사항이 아니라 전송의 일급 시민(first-class part)으로서 포함됩니다. 클라이언트 설정에는 어떠한 비밀(secret)도 들어있지 않습니다. 그저 URL을 가리킬 뿐이며, 나머지는 OAuth가 처리하도록 맡깁니다. (OAuth 네이티브 클라이언트를 사용하면 브라우저 로그인이 자동으로 이루어집니다. 아직 흐름을 직접 제어하지 못하는 클라이언트는 이를 수행하기 위해 작은 로컬 헬퍼(helper)에 의존합니다. 이에 대해서는 아래에서 더 자세히 다룹니다.)
마지막 문장이 이 모든 제안의 핵심입니다. 개발자가 실제로 작성하게 되는 두 가지 설정을 비교해 보십시오:
// stdio — 비밀이 모든 노트북에 존재함
{ "tavily": { "type": "stdio", "command": "npx",
"args": ["tavily-mcp"],
...
http 버전은 유출될 정보가 전혀 없습니다. 자격 증명(credential)은 클라이언트에 절대 도달하지 않습니다. Cognito를 통해 사용자별로 신원이 확인되며, 노트북이 아닌 서버만이 업스트림(upstream) 키에 접근하는 유일한 주체가 됩니다. 업스트림에 원격 OAuth 프론트엔드 http 서버로서 접근할 수 있는 경우에는 반드시 그렇게 해야 합니다. 이 래퍼(wrapper)는 stdio 형태의 공유 키 도구를 http 형태의 도구로 전환하기 위해 정확히 존재합니다.
하나의 엔드포인트, 두 종류의 호출자
백엔드 에이전트와 인간 엔지니어는 완전히 다른 OAuth 흐름을 통해 인증합니다. 머신(machine)은 client_credentials 방식을, 사람은 authorization_code + PKCE 방식을 사용합니다. 하지만 결과적으로 이들은 동일한 스코프(scope)를 가지며 동일한 엔드포인트에 접속하게 됩니다. 서버는 이들을 동일하게 취급하며, 유일한 차이점은 감사 로그(audit log)에 기록되는 내용입니다. 머신은 클라이언트 ID가, 사람은 이메일이 기록됩니다.
대부분의 "MCP + OAuth" 포스트가 생략하는 마지막 단계
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

