[학습 노트][Golang] AI 에이전트 시대의 권한 부여(Authorization) 과제: ID-JAG란 무엇이며 왜 Go로 재구현했는가
요약
AI 에이전트가 사용자를 대신해 리소스에 접근할 때 발생하는 보안 문제를 해결하기 위한 ID-JAG 메커니즘을 소개합니다. OAuth2 및 PKCE의 한계를 넘어 최소 권한 원칙을 구현하는 토큰 교환 흐름을 설명하며, 이를 Go 언어로 재구현한 MCP 서버 실습을 제공합니다.
핵심 포인트
- ID-JAG는 에이전트의 '대리 수행' 시 보안을 강화하는 권한 부여 메커니즘임
- 최소 권한 원칙(PoLP)을 통해 에이전트의 과도한 권한 남용 방지
- Token Exchange 및 JWT Bearer RFC 표준을 기반으로 설계됨
- Go 언어를 사용하여 MCP 서버를 재구현하고 실행/테스트 방법 시연
서문:
지난 6개월 정도 동안, 업무 수행을 돕기 위해 AI 에이전트(AI Agents)를 내부 시스템에 직접 연결하는 것은 더 이상 새로운 뉴스가 아닙니다. 하지만 한 단계 더 나아가 생각해 봅시다. 에이전트는 "누구의 신원(identity)"으로 해당 API를 호출해야 할까요? 만약 에이전트가 받는 권한이 인간의 권한만큼 광범위하다면, 에이전트가 해서는 안 될 작업을 수행하도록 속았을 때 그 결과는 인간의 실수보다 훨씬 더 심각할 수 있습니다.
이것이 바로 ID-JAG (Identity Assertion JWT Authorization Grant)가 해결하고자 하는 문제입니다. 저는 최근 이 메커니즘의 원리를 정리하고, 튜토리얼 리포지토리 athenz-community/id-jag-the-hard-way의 MCP 서버를 Go를 사용하여 kkdai/id-jag-mcp로 재구현했습니다. 이 글은 ID-JAG의 기술적 원리를 명확히 하는 것을 목표로 합니다. 즉, 어떤 RFC 표준을 기반으로 구축되었는지, 이전에 작성했던 OAuth2 / PKCE와 어떻게 다른지, 실제 토큰 교환 흐름(token exchange flow)은 어떻게 되는지, 그리고 마지막으로 이 Go 프로젝트를 실행하고 테스트하는 방법을 시연합니다.
요약 (TL;DR)
이 글은 다음 순서로 내용을 소개합니다:
- ID-JAG란 무엇인가? 왜 필요한가?
- OAuth2 및 PKCE에서 에이전트 시대의 새로운 문제로
- 두 가지 RFC 초석: 토큰 교환 (Token Exchange) 및 JWT Bearer
- 전체 ID-JAG 토큰 교환 흐름
- 모든 단계에서의 권한 축소 (Downscoping permissions): 최소 권한 원칙 (Principle of Least Privilege)이 구현되는 방식
- 왜 이 MCP 서버를 Go로 재구현했는가?
- 실습: 설치, 실행 및 테스트
- 결론
- 참고 문헌
ID-JAG란 무엇인가? 왜 필요한가?
ID-JAG는 AI 에이전트(AI Agents)가 사용자를 대신하여(on behalf of) 보호된 리소스에 접근할 수 있도록 허용하는 권한 부여(Authorization) 메커니즘이며, 핵심 키워드는 "~를 대신하여("on behalf of")"입니다. 현재는 여전히 IETF 인터넷 초안(Internet-Draft) 단계이며 아직 공식 RFC가 되지는 않았지만, LY Corporation(Athenz 권한 부여 시스템 사용) 및 Okta와 같은 조직들은 이미 구현을 시작했으며, MCP(Model Context Protocol) 사양에서도 이미 이 초안을 인용하고 있습니다.
전통적인 서비스 간(service-to-service) 권한 부여는 보통 두 가지 극단적인 방식 중 하나를 따릅니다. 서비스 전체가 범용 마스터 키(API Key, Service Account)를 공유하거나, 사용자의 세션 또는 장기 토큰(long-lived token)을 프로그램에 직접 빌려주는 방식입니다. 전자는 권한이 너무 과도하며, 후자는 감사 추적(audit trail)이 부족합니다. 만약 토큰이 유출되면 공격자가 사용자를 거의 완벽하게 사칭할 수 있으며, 이를 탐지하는 것도 매우 어렵습니다. AI 에이전트가 어떤 도구를 호출할지, 어떤 내부 API에 연결할지를 스스로 결정하기 시작하면 두 방식 모두의 위험성이 증폭됩니다. 즉, 프롬프트 주입(prompt injection)이나 환각(hallucinations)으로 인해 에이전트가 사용자가 전혀 의도하지 않은 작업을 수행할 수 있습니다. 만약 에이전트가 마스터 키를 보유하고 있다면, 그 결과로 회사의 모든 데이터가 에이전트의 처분에 맡겨지게 됩니다. 실제 시나리오에서는 "오케스트레이터 에이전트(Orchestrator Agent)가 서브 에이전트(Sub-Agent)를 호출"하는 다층 구조(multi-layered architecture)가 빈번하며, 이 과정에서 위험이 아래 단계까지 그대로 전달됩니다.
ID-JAG가 달성하고자 하는 목표는 다음과 같습니다. 에이전트(Agent)가 수행하는 모든 동작에 대해, "이것은 특정 시점에 특정 사용자가 나에게 이 특정한 일을 수행하도록 권한을 부여한 것이다"라는 사실을 증명할 수 있어야 하며, 이러한 권한 부여 범위(authorization scope)는 가능한 한 좁아야 하고, 유효 기간(validity period)은 가능한 한 짧아야 합니다. ID-JAG는 OAuth2 토큰 교환 (RFC 8693)을 기반으로 구축되었으며, 여기에 "이 토큰은 인간의 신원 주장(identity assertion)으로부터 파생되었다"라는 증명 계층을 추가합니다. 이것이 ID-JAG가 OAuth.net의 교차 앱 액세스 (Cross-App Access, XAA) 페이지에 등재된 이유이기도 합니다. 이는 정확히 에이전트(Agent) 시대에 새롭게 등장하는 문제입니다.
덧붙여, ID-JAG는 매우 실질적인 사용자 경험(UX) 문제도 해결합니다. 만약 에이전트가 새로운 서비스에 액세스해야 할 때마다 사용자가 수동으로 "동의"를 클릭하도록 브라우저 창을 띄워야 한다면, 사용자 경험은 금방 피로해질 것이고 결국 사용자는 모든 것에 그냥 동의해 버리는 상황에 이를 것입니다. ID-JAG는 권한 부여 동작을 사용자가 SSO를 통해 로그인하는 시점으로 통합합니다. 그 이후 에이전트가 새로운 액세스 권한이 필요할 때, 이미 발급된 신원 주장(identity assertion)을 사용하여 사용자에게 다시 창을 띄워 클릭하게 할 필요 없이 권한 부여 서버(authorization server)와 토큰을 교환합니다.
OAuth2 및 PKCE에서 에이전트 시대의 새로운 문제로
저는 이전에 Golang을 통해 OAuth2 PKCE를 개발하는 방법에 대해 작성한 적이 있으며, 여기에는 PKCE를 채택한 LINE 로그인 구현 경험이 담겨 있었습니다. 해당 글에서 해결했던 문제와 ID-JAG가 해결하는 문제는 사실 서로 다른 두 가지 수준에 있습니다. 이 둘을 비교해 보면 ID-JAG가 무엇이 새로운지 더 명확해집니다.
PKCE는 "클라이언트의 신원을 신뢰할 수 있는가"라는 문제를 해결합니다. 클라이언트 비밀(client secret)을 안전하게 저장할 수 없는 모바일 앱과 같은 퍼블릭 클라이언트(public client)의 경우, 전송 과정에서 동일한 휴대폰에 있는 악의적인 앱에 의해 권한 부여 코드(authorization code)가 가로채질 수 있습니다. PKCE는 code_verifier / code_challenge라는 일회용 쌍을 사용하여, 설령 코드가 도난당하더라도 올바른 검증값(verifier) 없이는 토큰으로 교환될 수 없도록 보장합니다. 이 모든 문제는 사용자의 손에 있는 앱과 사용자 사이의 "단일 홉(single hop)" 단계에서 발생합니다.
ID-JAG는 "이 비인간 서비스 신원(non-human service identity)이 이 작업을 위해 이 사람을 대신하여 행동할 자격이 있는가"라는 문제를 해결하며, 이는 종종 여러 단계의 홉(hops)에 걸쳐 발생합니다: 사용자 IdP 로그인 → AI 클라이언트 게이트웨이(AI Client Gateway) → MCP 서버 → 최종 리소스 서버(Resource Server). 각 홉에서의 호출자는 사용자 본인이 아니지만, 각 단계는 자신이 "권한 하에 행동하고 있음"을 증명해야 합니다. OAuth 2.0의 원래 설계는 "인간 사용자 ↔ 애플리케이션" 시나리오를 위한 것이었으며, 이러한 다층적인 에이전트 체인 위임(Agent chain delegation) 시나리오를 직접적으로 지원하지 않습니다. PKCE는 단일 권한 부여 교환의 무결성을 보호하며, ID-JAG는 전체 권한 부여 체인의 모든 연결 고리에서 필요한 최소한의 권한을 보호합니다. 이 둘은 충돌하지 않습니다. 이들은 동일한 광범위한 아키텍처 하에서 서로 다른 단계의 문제를 해결하는 메커니즘입니다.
두 가지 RFC 초석: 토큰 교환(Token Exchange)과 JWT Bearer
ID-JAG는 무에서 새로운 프로토콜을 발명한 것이 아닙니다. 대신, 기존의 두 가지 IETF 표준을 결합했습니다. 이후에 이어지는 전체 교환 흐름이 무엇을 수행하는지 이해하려면 이 두 가지 초석을 이해하는 것이 필수적입니다.
첫 번째는 RFC 8693 — OAuth 2.0 Token Exchange입니다. 이는 "한 유형의 토큰을 다른 유형으로 교환"하기 위한 일반적인 프로토콜을 정의합니다. 개념적으로는 엔화를 대만 달러로 바꾸기 위해 환전소에 가는 것과 유사하지만, 여기서는 보안 토큰을 교환한다는 점이 다릅니다. 토큰 교환 요청은 대략 다음과 같은 형태를 띱니다:
POST /token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<Identity Assertion JWT>
...
subject_token은 위임된 신원 (delegated identity)을 나타내는 토큰을 포함하며, requested_token_type은 교환하고자 하는 토큰의 유형을 지정하고, scope는 교환 시점에 권한 범위를 좁힐 수 있습니다.
두 번째는 RFC 7523 — JWT Bearer Grant입니다. 이는 인증 코드 교환 (authorization code exchange) 과정을 먼저 거칠 필요 없이, JWT 자체를 OAuth 2.0 권한 자격 증명 (authorization credential)으로 직접 사용할 수 있게 해줍니다:
POST /token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=<Signed Identity Assertion JWT>
이를 수신한 후, 권한 서버 (authorization server)는 발행자 (issuer, IdP)의 공개 키 (public key)를 사용하여 이 JWT의 서명 (signature)을 검증합니다. 만약 audience, scope, exp와 같은 필드들이 적절하다면, IdP가 이미 이 신원 주장 (identity assertion)을 승인했기 때문에 2차적인 사용자 동의 없이도 액세스 토큰 (Access Token)을 직접 발급할 수 있습니다.
교환될
전체 체인(chain)을 거치는 동안 토큰은 한 번 이상 교환됩니다. 토큰은 신뢰 경계(trust boundary)를 통과할 때마다 교환되며, 교환될 때마다 스코프(scope)는 점점 더 좁아집니다. 이러한 설계는 경로의 각 세그먼트에 대한 "자격 증명(credential)"이 서로 다르도록 의도적으로 보장합니다. 즉, AI 클라이언트 게이트웨이(AI Client Gateway)가 보유한 토큰은 MCP 서버(MCP Server) 단계에서 재검증 및 재발급을 강제하기 때문에, 최종 리소스 서버(Resource Server)를 속이는 데 직접 사용할 수 없습니다. 발급된 액세스 토큰(Access Token)은 일반적으로 sub(프록시된 사용자)와 act(실제로 작업을 수행하는 에이전트의 신원)를 모두 기록하므로, 다운스트림(downstream) 서비스는 "Alice가 특정 에이전트를 통해 이 작업을 수행했다"는 것을 명확히 알 수 있으며 감사 추적(audit trail)이 중간에 끊기지 않습니다.
매 홉(hop)마다 권한 축소(Downscoping): 최소 권한 원칙(Principle of Least Privilege)의 구현 방식
이것이 제가 전체 아키텍처 설계에서 가장 아름답다고 생각하는 부분입니다. 최소 권한(least privilege)은 단순히 문서에 적힌 원칙이 아니라, 토큰 교환 메커니즘에 의해 물리적으로 강제됩니다.
제가 재구현한 id-jag-mcp를 예로 들면, 다음과 같은 세 가지 도구를 제공합니다:
| 도구 (Tool) | 대응하는 Athenz 스코프 (Athenz Scope) |
|---|---|
get_k8s_docs | api:role.docs-getter |
| ... |
MCP 서버가 요청을 받으면, AI 클라이언트가 보낸 액세스 토큰을 상위 API(upstream API)로 직접 전달하지 않습니다. 대신, MCP 서버는 항상 자체 mTLS 인증서를 사용하여 "이 도구에 실제로 필요한 스코프"에 대해 Athenz ZTS와 토큰 교환을 수행합니다. 오직 새로 교환된 토큰만이 상위 서비스를 호출하는 데 사용됩니다. 설령 AI 클라이언트 게이트웨이에서의 토큰 스코프가 더 넓더라도(예: 읽기 및 삭제 권한을 모두 보유), MCP 서버는 전달하기 전에 각 도구에 실제로 필요한 구체적이고 작은 권한만을 요청합니다.
다시 말해, 전체 시스템의 어떤 부분도 현재 작업에 필요한 것보다 더 많은 권한을 "우연히" 갖게 되지 않습니다. 이는 코드 리뷰나 내부 규정을 통해 확인되는 것이 아니라, 아키텍처적으로 우회하는 것이 불가능합니다.
왜 이 MCP Server를 Go로 재구현했는가?
id-jag-the-hard-way(api_server/mcp/)에 있는 기존 MCP Server는 TypeScript + Express로 작성되었으며, (공식 SDK 없이) 직접 구현한 JSON-RPC 2.0 프로토콜을 사용했습니다. 저는 두 가지를 확인하고 싶었습니다. 첫째, 이 토큰 교환 아키텍처를 다른 언어와 다른 MCP SDK를 사용하여 구현했을 때 로직이 진정으로 복제될 수 있는지, 둘째, 공식 modelcontextprotocol/go-sdk가 실제로 어떻게 작동하는지입니다.
최종 구현체는 기존의 핵심 로직(동일한 스코프 매핑, 동일한 mTLS 토큰 교환 흐름)을 유지하면서, 프로토콜 계층을 공식 Go SDK로 완전히 교체했습니다. mTLS 클라이언트는 (Athenz의 공식 Go 클라이언트 라이브러리에 의존하지 않고) 커스텀으로 구축되었습니다.
실습: 설치, 실행 및 테스트
코드는 kkdai/id-jag-mcp에 있으며 (Apache 2.0 라이선스), 프로젝트 구조와 책임 분리는 대략 다음과 같습니다:
cmd/id-jag-mcp/ 엔트리 포인트(Entry point): 설정 읽기, 모든 컴포넌트 조립, HTTP 서버 시작
internal/config/ 환경 변수 설정 읽기
internal/athenz/ mTLS 클라이언트 + Athenz ZTS RFC 8693 토큰 교환
...
먼저, 프로젝트를 클론하고 바이너리를 빌드합니다:
git clone https://github.com/kkdai/id-jag-mcp.git
cd id-jag-mcp
go build -o id-jag-mcp ./cmd/id-jag-mcp
실제로 실행하려면 mTLS 인증서, 접근 가능한 Athenz ZTS, 그리고 업스트림 API 서버를 준비해야 합니다. 설정은 전적으로 환경 변수를 통해 이루어집니다:
mkdir -p certs
cp /path/to/api-mcp.crt /path/to/api-mcp.key /path/to/ca.crt certs/
...
시작 후에는 MCP 클라이언트 연결을 위한 /mcp 엔드포인트 외에도, 세 가지 도구(tools)에 대응하는 REST 단축 경로(shortcut routes)를 제공하므로 MCP 클라이언트 없이도 curl로 직접 쉽게 테스트할 수 있습니다:
curl -H "Authorization: Bearer $AT" http://localhost:8101/api/docs
curl -X DELETE -H "Authorization: Bearer $AT" http://localhost:8101/api/docs/5
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기