· 9분 분량

안전한 DynamoDB MCP 서버를 만들기까지

에게 데이터베이스를 넘겨주는 일을 아주 간단하게 만듭니다. 바로 그게 문제입니다. 대부분의 DynamoDB MCP 서버는 여러분의 원본 AWS 자격 증명을 쥐고서 쓰기 경로는 있지만 검토 단계는 없는 얇은 프로세스입니다 — 사용자 관점에서 왜 그 방식이 위험하며 어떻게 안전하게 선택하는지에 대해 이미 글을 쓴 바 있습니다.

이 글은 그 반대편 이야기입니다. 안전한 버전을 만드는 데 실제로 무엇이 필요했는가 하는 것이죠. DynoTable은 데스크톱 DynamoDB 클라이언트로, 그 MCP 서버는 게이트가 걸린 AI 도구 모음을 외부 에이전트 — Claude Code, Cursor, Codex — 에 노출하되 에이전트가 AWS 자격 증명에는 결코 손대지 못하게 합니다. 여기에 이르기까지 Electron 앱 내부에 인증 서버를 넣고, 자격 증명 혼동을 표현조차 할 수 없게 만드는 식별자 타입을 설계하고, 한 번 이상, 편리한 기본값이 잘못된 선택임을 판단해야 했습니다.

민감한 무언가를 중심으로 MCP 서버를 만들고 있다면, 여러분도 이런 결정들을 마주하게 될 것입니다.

루프백 HTTP가 저절로 안전한 것은 아니다

서버는 127.0.0.1에 바인딩하는데, 안전하게 들리지만 충분하지는 않습니다. 여러분이 방문하는 어떤 웹 페이지든 localhost로 요청을 시도할 수 있고, DNS 리바인딩은 악성 페이지가 그 요청을 그럴듯한 Host로 꾸밀 수 있게 합니다. 그래서 첫 번째 계층은 오리진 가드입니다. 실제 웹 페이지 Origin을 지닌 요청은 모두 거부하고, 루프백이 아닌 Host 헤더도 거부하되 — Origin이 전혀 없는 요청은 허용합니다. Claude Code 같은 정상적인 CLI 클라이언트는 Origin을 보내지 않기 때문입니다.

그 마지막 조항이 솔직한 대목입니다. 오리진 가드는 브라우저 계열의 공격만 막을 수 있습니다. 누구도 인증할 수는 없습니다. 진짜 관문은 bearer token이며, 이는 토큰이 어디에서 오는가 하는 질문으로 이어집니다.

우리는 앱 안에 OAuth 2.1 인증 서버를 넣어 출시했다

MCP의 원격 서버 인증 방식은 OAuth 2.1이며, 로컬 데스크톱 서버에는 기댈 만한 상위 자격 증명 공급자가 없습니다. 그래서 앱 자체가 인증 서버입니다. 동적 클라이언트 등록, (S256), 인증 코드, 리프레시 토큰, 취소, 그리고 클라이언트가 이 모든 것을 발견할 수 있게 해주는 RFC 8414 / RFC 9728 메타데이터 문서를 갖췄습니다.

우리가 일찍 정하고 지켜온 규칙들:

  • 프로토콜을 손수 구현하지 말 것. MCP SDK는 전체 AS(인증 서버) 표면을 라우터로 제공하며, 우리는 그 아래의 저장소/정책 공급자만 구현합니다. 우리가 받아들인 결과 하나: 우리 MCP 프레임워크는 내부적으로 하나의 HTTP 스택을 사용하고 SDK 라우터는 다른 스택을 사용하므로, AS는 두 번째 루프백 리스너로 동작하고, 리소스 서버는 protected-resource 메타데이터를 통해 클라이언트를 그쪽으로 안내합니다. 리스너 두 개는 범위가 정해진 명시적 비용이고, PKCE와 토큰 문법을 손으로 다시 유도하는 것은 끝이 없는 비용입니다.
  • 리다이렉트는 루프백 전용입니다(네이티브 앱에 대한 RFC 8252에 따라). 그리고 유효하지 않은 리다이렉트는 결코 오류 리다이렉트를 받지 않고 — 곧바로 400을 받습니다. 따라서 공격자의 URI가 목적지가 되는 일은 없습니다.
  • 인증 코드는 일회용입니다 — 재사용된 코드는 유효하지 않은 grant이며, 그걸로 끝입니다.
  • 리프레시는 범위를 좁힐 수는 있어도 넓힐 수는 없으며, 동일한 식별자 payload를 그대로 다시 발급합니다 — 리프레시된 토큰이 슬그머니 다른 리전을 획득할 수는 없습니다.
  • 취소는 연결 시점에 캐시되지 않고 모든 요청마다 다시 확인됩니다 — 설정에서 “취소”를 클릭하면 클라이언트의 다음 호출이 차단됩니다.

개발 빌드는 대신 릴리스 바이너리에는 존재할 수 없도록 만들어진, 더 단순한 인증 경로를 사용합니다 — 출시된 소프트웨어에 인증 없는 전체 범위 토큰이 들어 있다면, Origin 없는 요청을 의도적으로 허용하는 오리진 가드 뒤에 있는 로컬 백도어가 될 것입니다.

연결 하나에 AWS 식별자 하나 — 통째로 지니고, 결코 추측하지 않는다

핵심 안전 속성: 외부 에이전트는 “DynoTable”이 아니라 하나의 프로필에 연결됩니다. 각 연결은 하나의 AWS 프로필과 하나의 리전에 묶이며, 공유되지 않는 자체 DynamoDB 클라이언트로 뒷받침되므로, 서로 다른 프로필의 두 에이전트가 서로 스며들 수 없습니다.

이 바인딩은 처음에는 느슨한 필드들 — awsProfile, region, profileId, 선택적 로컬 포트 — 로 시작해 경로상의 모든 레코드에 거듭 선언됐습니다. 지금은 두 갈래를 가진 하나의 구별 유니온(discriminated-union) 식별자 타입입니다. online(프로필 + 리전 + id)과 offline(로컬 포트 + id, DynamoDB Local 프로필용)입니다. 오프라인 여부가 곧 구별자이므로, 자격 증명 없는 온라인 식별자나 리전을 지닌 오프라인 식별자는 런타임의 뜻밖의 사고가 아니라 타입 오류 입니다. 이 하나의 타입이 동의 → 토큰 → grant → 모든 요청을 거치며 온전하게 이동합니다.

MCP server (tools)User (consent modal)In-app OAuth ASClaude Code / CursorMCP server (tools)User (consent modal)In-app OAuth ASClaude Code / Cursorregister + authorize (PKCE)which profile? which scope?profile P, read-onlycode → token (identity {P, region}pinned)tool call + bearer tokenverify, re-check revocation,clamp scope to license, resolve P's credsresult (agent never sees AWS keys)

우리가 핵심으로 여기는 두 가지 결과:

  • 리전은 동의 시점에 고정됩니다. 사용자가 “리전 Y의 프로필 X”를 승인하면 그 튜플이 토큰에 고정되어 — 리프레시를 견디고, 핫 패스에서 가변 상태로부터 다시 해석되는 일이 없습니다. 프로필의 리전을 편집하려면 다시 동의를 받아야 합니다. 그 대안 — 요청마다 리전을 해석하거나 기본값으로 되돌아가는 것 — 이야말로 에이전트가 슬그머니 us-east-1을 쿼리하게 되는 경로입니다.
  • 형식이 잘못된 토큰은 강제 변환이 아니라 401입니다. 토큰의 식별자 클레임이 유효한 갈래로 파싱되지 않으면 그 요청은 인가되지 않습니다. 'default' 프로필도, 기본 리전도, 빈 문자열도 없습니다. 예전 코드라면 추측했을 모든 지점이 이제는 확실한 실패입니다.

동의는 대화 상자가 아니라 제품 표면이다

첫 동의 프롬프트는 플랫폼 기본 메시지 상자였습니다. 열려 있는 동안 Electron 메인 프로세스를 멈추게 했고, 앱의 디자인 시스템과 어울리지 못했으며, 문서용 스크린샷조차 찍을 수 없었습니다. 지금은 앱 내부 모달입니다. 연결하는 클라이언트의 이름, 사용자가 노출하기로 선택한 바로 그 프로필들에 대한 프로필 선택기, 그리고 가장 좁은 것부터 제시되는 세 가지 범위 — 읽기 전용, 읽기 + 스테이징, 전체 — 를 보여줍니다. 만료되었거나 중복된 동의 요청은 OAuth 흐름을 멈춰 세우는 대신 거부로 처리됩니다.

앱 내부 MCP 동의 모달: 연결하는 클라이언트, 프로필 선택기, 그리고 가장 좁은 것부터 제시되는 세 가지 범위 — 읽기 전용, 읽기 + 스테이징, 전체 접근.
앱 내부 MCP 동의 모달: 연결하는 클라이언트, 프로필 선택기, 그리고 가장 좁은 것부터 제시되는 세 가지 범위 — 읽기 전용, 읽기 + 스테이징, 전체 접근.

이 선택기에는 그 자체의 작은 신뢰 경계가 있습니다. 클라이언트는 authorize URL에 프로필을 힌트로 제시할 수 있지만, grant는 사용자가 실제로 선택한 것으로부터 발급되며, 사용자에게 보여준 스냅샷에 대해 검증됩니다 — 위조된 힌트는 화면에 없던 프로필을 묶을 수 없습니다.

동의가 여러분에게 주지 않는 것에 대해서도 우리는 똑같이 분명히 합니다. 인가는 범위 전용이며, 연결마다 한 번 승인됩니다. 앱 내부 어시스턴트의 호출별 권한 프롬프트는 MCP 트래픽에는 적용되지 않습니다 — 유출된 전체 범위 토큰은 사람이 각 호출을 보지 않아도 (스테이징 영역으로) 쓰기를 할 수 있습니다. 짧은 토큰 수명, 클라이언트별 취소, MCP에서 비롯된 모든 동작을 표시하는 상시 감사 추적, 그리고 기본 제안으로서의 읽기 전용이 그 완화책입니다. 이를 잔여 위험으로 적어 두는 편이 동의 모달이 그것을 막았다고 가장하는 것보다 유용했습니다.

헤드리스 작업을 위한 가드레일

외부 에이전트에는 렌더러도, 확인 팝업도, 지켜보는 사용자도 없습니다. 모든 상호작용 요소에는 헤드리스 대응물이 필요했습니다:

  • 호출마다 실시간 라이선스 클램핑. grant는 사용자가 승인한 범위를 저장하고, 유효한 범위는 모든 도구 호출마다 앱의 현재 라이선스 상태로부터 산출됩니다 — 읽기 전용 라이선스는 전체 grant를 낮춰 클램핑하고, 로그아웃 상태는 어떤 토큰 분기보다 먼저 401로 거부하며(그래서 개발 토큰이 이를 우회할 수 없습니다), 세션 도중의 재활성화는 다시 동의를 받지 않고도 범위를 다시 넓힙니다. 의도적인 비대칭 하나: 앱 내부 내보내기는 읽기 전용 라이선스에서도 계속 사용할 수 있지만(여러분의 데이터는 여러분의 것이니까요), MCP를 통한 헤드리스 대량 내보내기는 클램핑되어 꺼집니다 — 상호작용형 데이터 이동 권리와 에이전트가 주도하는 유출 채널은 서로 다른 신뢰 표면입니다.
  • 확인 팝업 대신 스캔 예산. 앱에서는 테이블 전체 읽기가 먼저 묻습니다. 헤드리스에서는 항목/바이트/시간 예산이 모든 Scan을 제한하고, 테이블에 대한 SQL은 메모리 내 한도가 아니라 디스크 기반 엔진을 통해 스트리밍됩니다.
  • 연결 종료는 정직하게 중단합니다. 클라이언트가 Scan 도중에 끊기면, 서버는 문장 도중이 아니라 다음 페이지 경계에서 중단하고 — 임시 상태를 폐기합니다.
  • 는 연결을 통해 완료됩니다. AWS 자격 증명에 MFA 코드가 필요하고 클라이언트가 MCP elicitation을 지원하면, 서버는 6자리 코드를 에이전트 자체 UI를 통해 요청하고 — Claude Code가 사람에게 프롬프트를 띄웁니다 — 다시 시도합니다. SSO는 이 경로에서 의도적으로 제외됩니다. SSO에는 브라우저 디바이스 흐름이 필요하며, 그렇지 않은 척하는 것은 그저 더 나쁜 오류 메시지가 될 뿐입니다. 같은 자격 증명이 필요한 동시 호출들은 하나의 프롬프트로 합쳐집니다.

애초에 MCP로 데이터베이스를 노출하는 것이 안전하기는 한가?

우리의 솔직한 답: 에이전트가 잘해야 혼란스럽고 최악의 경우 적대적이라는 가정을 중심으로 서버가 설계된 경우에만 그렇습니다. 은 에이전트의 의도 를 신뢰할 수 없게 만들며, 그래서 아키텍처는 에이전트가 얌전히 행동하리라고 결코 기대하지 않습니다. 자격 증명은 앱에 머무르고, 쓰기는 언제나 검토 가능한 스테이징 영역에만 도달하며, 범위는 모델이 무엇을 요청하든 호출이 할 수 있는 일을 클램핑하고, 모든 동작은 감사됩니다. 연결 측 가이드는 각 클라이언트에서 이것이 실제로 무엇을 뜻하는지 다룹니다.

데이터베이스에 접근하는 MCP 서버에 요구해야 할 것들

  • 자격 증명은 어디에 있는가 — 에이전트의 프로세스 안인가, 아니면 서버 뒤인가?
  • 실제 범위를 갖춘 연결별 동의가 있는가, 아니면 설정 파일 하나가 모든 것을 부여하는가?
  • 사람의 검토 단계 없이 쓰기가 실행될 수 있는가?
  • 식별자(프로필, 리전)가 동의 시점에 고정되는가, 아니면 호출 시점에 기본값에서 해석되는가?
  • 형식이 잘못되었거나 취소된 토큰에서는 어떻게 되는가 — 확실한 실패인가, 아니면 폴백인가?
  • 에이전트에서 비롯된 동작을 구분하는 감사 추적이 있는가?
  • 폭주하는 Scan을 무엇이 제한하며, 클라이언트가 작업 도중에 연결을 끊으면 어떻게 되는가?

서버가 이 질문들에 답하지 못한다면, 그 편리한 통합이 곧 위험한 통합입니다. 제한 없는 Scan이 실제로 얼마나 비용을 물릴지 가늠해 보려면, 요금 계산기가 정신이 번쩍 들게 해줍니다 — 그리고 안전한 버전을 읽기보다 실제로 동작하는 걸 보고 싶다면, DynoTable을 다운로드하고, MCP 서버를 켜고, 1분도 안 되어 읽기 전용 범위로 클라이언트를 연결해 보세요.

Console 없이 DynamoDB 작업하기

DynoTable은 DynamoDB를 위한 빠른 데스크톱 클라이언트입니다 — 테이블을 탐색하고, SQL 스타일 쿼리를 실행하고, 항목을 로컬에서 편집하세요.