나의 MCP 서버는 두 개의 API 키를 보유하고 있다: 모든 도구 호출이 두 키를 모두 가진 동일한 프로세스에서 실행된다
요약
MCP(Model Context Protocol) 서버 운영 시 단일 프로세스 내에 여러 API 키가 로드됨으로써 발생하는 보안 경계 문제를 다룹니다. 도구별로 권한을 분리했다고 생각하기 쉽지만, 실제로는 프로세스 환경 변수를 공유하여 보안 취약점이 발생할 수 있음을 경고합니다.
핵심 포인트
- MCP 서버의 도구들은 동일한 프로세스 환경 변수를 공유함
- 단일 프로세스 내 모든 도구는 로드된 모든 API 키에 접근 가능함
- 도구 간의 논리적 분리가 실제 프로세스 보안 경계를 보장하지 않음
- 에이전트 세션이 공유 신뢰 경계가 되어 보안 위험이 증폭될 수 있음
이번 주에 어떤 사람이 하나의 에이전트에 세 개의 MCP 서버를 연결하고, 에이전트가 운영 환경(production)에 접근하는 데 필요한 것과 동일한 권한을 아무렇지 않게 요청하는 것을 지켜봤다는 글을 읽었습니다. 댓글창은 "그래, 그게 바로 MCP의 근본적인 문제야"라는 의견들로 가득했고, 저는 하마터면 그냥 지나칠 뻔했습니다. 저는 서버 세 개를 돌리지 않고, 하나만 돌리니까요. 하지만 실제로 server.py를 열어 확인해 보았고, 제 서버 하나도 세 개로 나뉘어 있지 않을 뿐, 단일 파일 안에 접혀 있는 똑같은 형태의 문제를 가지고 있다는 것을 깨달았습니다.
server.py는 두 개의 서로 관련 없는 작업으로 나뉜 8개의 도구(tools)를 가진 FastMCP 서버입니다. 하나는 GitHub 프로필/저장소(repo) 읽기이고, 다른 하나는 DEV.to 기사 읽기 및 쓰기입니다. 두 자격 증명(credentials)은 모두 임포트(import) 시점에 동일한 방식으로 동일한 프로세스 환경(process environment)에 로드됩니다:
def load_env(path=".env"):
try:
with open(path) as f:
...
그리고 두 개의 헬퍼 함수(helper functions)가 이를 다시 읽어옵니다:
def _gh(path, method="GET", data=None):
req = urllib.request.Request(f"https://api.github.com{path}", method=method)
req.add_header("Authorization", f"token {os.environ['GITHUB_TOKEN']}")
...
여기서 "특정 입력에 대해 잘못된 출력"이 나오는 식의 버그는 없습니다. 모든 도구는 말 그대로 동작합니다: get_github_profile은 GitHub를 읽고, create_article은 DEV.to에 씁니다. 문제는 한 단계 위, 즉 프로세스 경계(process boundary)가 실제로 무엇을 보호하느냐에 있습니다. 저는 GITHUB_TOKEN과 DEV_TO_API가 어떤 함수가 읽느냐에 따라 범위(scope)가 지정된, 서로 "다른 도구"에 속한다고 생각했습니다. 하지만 그렇지 않습니다. 그것들은 "프로세스"에 속합니다. 이 8개의 도구 각각은 해당 도구가 하나를 필요로 하든, 다른 하나를 필요로 하든, 혹은 둘 다 필요로 하지 않든 간에, 두 자격 증명이 모두 환경 변수에 놓여 있는 상태에서 실행됩니다. generate_commit_message는 두 API를 모두 건드리지 않습니다. 이 함수는 git diff에 대해 claude -p를 셸 호출(shell out)할 뿐입니다. 하지만 이 함수가 나중에 수정되거나 버그가 발생하여 어딘가에서 문자열을 가져와야 할 때 잘못된 것을 집어 들게 된다면, os.environ["DEV_TO_API"]에 아주 쉽게 접근할 수 있는 프로세스 내에서 실행되고 있는 것입니다.
이것은 세 개의 별도 MCP 서버가 하나의 에이전트에 연결된 경우와 동일한 실패 모드입니다. 에이전트의 세션이 공유 신뢰 경계(shared trust boundary)가 되며, 모든 도구 호출은 이로부터 도달 가능한 모든 것의 합집합을 상속받게 됩니다. 다만 이것이 단일 파일로 압축되어 있어, 가리킬 서버 간 연결선이 없기 때문에 놓치기 더 쉽습니다. 저는 코드를 몇 달 동안 '8개의 도구'로 읽었지, 결코 '쓰기 권한이 있는 2가지 세트의 자격 증명을 보유한 단일 프로세스'로 읽은 적이 없습니다.
실제적으로 이것이 중요한 이유는, 이 서버의 도구 입력들이 모두 신뢰할 수 있는 것은 아니기 때문입니다. update_article(article_id, title=None, body_markdown=None, published=None) 함수는 임의의 정수와 임의의 텍스트를 받는데, 이 텍스트는 때때로 제가 제공한 내용을 요약하는 LLM 호출에서 기원합니다. 초안일 수도 있고, 트렌딩 주제 스크랩일 수도 있으며, 결국에는 댓글 스레드일 수도 있습니다. 만약 그 파이프라인에 외부의 신뢰할 수 없는 텍스트(다른 사람의 dev.to 댓글, 스크랩된 블로그 게시물)에서 기원하는 아티클 콘텐츠가 update_article로 전달되기 전에 단계를 추가하게 된다면, '내 초안 업데이트'와 '내가 의도하지 않은 무언가를 실행'하는 것 사이에 서 있는 유일한 것은 아무도 아직 이 두 가지를 연결하는 코드 경로를 작성하지 않았다는 사실뿐입니다. 프로세스 수준에서 이를 강제하는 벽이 없습니다. 한 통합을 위한 자격 증명이 다른 도구의 표면(tool surface)으로부터 분리되지 않은 것이 아니라, 단지 아무도 아직 그것들을 함께 호출하지 않았기 때문일 뿐입니다.
해결책은 영리하지 않습니다. 그저 지루해서 아무도 하고 싶어 하지 않는 해결책인데, 왜냐하면 이것이 하나가 아닌 두 개의 프로세스를 실행해야 함을 의미하기 때문입니다. 서버를 '하나의 프로젝트처럼 느껴지는' 경계가 아니라, 자격 증명(credential) 경계를 따라 분리해야 합니다:
# github_server.py — 이 프로세스에는 GITHUB_TOKEN만 로드됨
load_env()
mcp = FastMCP(
두 개의 `.env` 파일, 두 개의 프로세스, 그리고 하나의 항목 대신 MCP 클라이언트 설정에 두 개의 항목이 들어갑니다. 로컬에서 실행하기에는 더 번거롭고, 저는 아직 실제로 전환하지는 않았습니다. 이 글은 제가 스스로를 설득하기 전에 그 논거를 미리 적어두는 것입니다. 하지만 결과적으로 얻게 되는 속성이 실제로 중요한 것입니다. 만약 `devto-tools`가 도구 인자 (tool argument)를 통해 악의적인 무언가를 전달받더라도, 최악의 경우 `DEV_TO_API`를 오용하는 것에 그칩니다. `GITHUB_TOKEN`에는 손을 댈 수 없는데, 왜냐하면 그 문자열은 애초에 해당 환경 변수 (environment)에 존재하지 않았기 때문입니다. 이는 현재의 단일 프로세스 (single-process) 버전에서는 제가 각 도구의 구현을 아무리 주의 깊게 검토하더라도 보장할 수 없는 부분입니다. 왜냐하면 제가 실제로 필요로 하는 보장은 Python 내부가 아니라 OS 프로세스 경계 (OS process boundary)에 존재하기 때문입니다.
"설치하기 전에 각 MCP 서버를 검증하라"는 조언 — 제가 얼마 전에 작성했던 체크리스트 — 은 여전히 유효하지만, 이 문제가 제기한 질문과는 다른 질문에 답합니다. 검증 (Vetting)은 단일 서버가 스스로 악의적인 행동을 하지 않는다는 것을 알려줍니다. 하지만 두 개의 서버가, 또는 하나의 서버 내에 있는 두 개의 자격 증명 도메인 (credential domains)이 동일한 에이전트 세션 (agent session)에서 도달 가능한 상태가 되었을 때 어떤 일이 발생하는지에 대해서는 아무것도 말해주지 않습니다. 이러한 구성 위험 (composition risk)은 개별 서버의 소스 코드에서는 나타나지 않습니다. 이는 오직 "지금 이 프로세스의 환경 변수에는 실제로 무엇이 들어있는가, 그리고 나의 8개 도구 중 이론적으로 이 모든 것에 접근할 수 있는 것은 무엇인가"라고 질문할 때만 나타납니다. 저는 다른 사람의 3개 서버 관련 포스트를 보고 확인해보기 전까지는 제 자신의 코드에 대해 이런 질문을 던져본 적이 없었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기