GitOps + OPA Bundle Server로 분산 MCP 환경의 Policy Drift 없애기: 실전 구축 가이드
MCP 서버 3개를 운영하는 팀이 보안 정책 하나를 패치하고 배포했습니다. 30분 후, 인스턴스 A는 패치를 받았지만 B와 C는 여전히 이전 버전을 실행 중이었습니다. 그 30분 동안 AI 에이전트는 B와 C를 통해 수정된 정책이 막아야 했던 도구를 계속 호출할 수 있었습니다. 특별한 공격 없이, 정책 배포 타이밍의 차이만으로 발생한 상황입니다.
Anthropic의 MCP(Model Context Protocol)를 통해 AI 에이전트가 기업 인프라의 도구와 서비스에 연결되는 구성이 늘어나면서, 분산된 MCP 서버 인스턴스들이 항상 동일한 인가 정책을 실행하도록 보장하는 일이 핵심 과제가 되었습니다. 이를 Policy Drift라고 하며, 인스턴스 수가 2개만 넘어도 현실적인 위험이 됩니다.
이 글에서는 OPA(Open Policy Agent) Bundle Server를 GitOps 파이프라인과 연동해 Rego 정책을 안전하게 배포하고, 분산 MCP 환경 전체에서 Policy Drift를 방지하는 실전 아키텍처를 다룹니다. 이 글을 읽고 나면 로컬 번들 서버를 직접 구동하고, GitHub Actions로 자동 배포 파이프라인을 구성하며, OPA 인스턴스 간 정책 버전 일치 여부를 모니터링하는 방법을 알 수 있게 됩니다.
TL;DR
- OPA Bundle은 Rego 정책을
tar.gz로 패키징해 HTTP로 배포하는 단위입니다.- GitOps 파이프라인(GitHub Actions + S3)으로 "정책 변경 = PR + CI 테스트 + 자동 배포" 흐름을 구성할 수 있습니다.
- 번들 서명 없이 프로덕션에 배포하면 번들 서버 탈취 시 임의의 정책이 전파되는 위험이 생깁니다.
- 30~120초 폴링 지연이 허용되지 않으면 OPAL의 WebSocket Push 방식이 대안입니다.
/v1/status엔드포인트로 각 인스턴스의revision을 수집하면 Policy Drift를 조기에 감지할 수 있습니다.
이 글의 대상 독자: 컨테이너 기반 서비스 배포 경험이 있는 백엔드·인프라 개발자를 대상으로 합니다. Docker Compose, GitHub Actions, AWS S3에 대한 기본 이해가 있으면 예시 코드를 그대로 따라가실 수 있습니다.
핵심 개념
OPA와 Rego: 정책을 코드로
**OPA(Open Policy Agent)**는 CNCF 졸업 프로젝트로, 인가 결정을 애플리케이션 코드로부터 분리해주는 범용 정책 엔진입니다. Kubernetes, API 게이트웨이, 마이크로서비스, CI/CD 파이프라인 등 어디서나 동일한 방식으로 정책을 적용할 수 있습니다.
Rego는 OPA 전용 선언형 정책 언어입니다. "무엇을 허용하고 무엇을 거부할 것인가"를 코드로 표현하며, JSON/YAML 형태의 입력을 받아 allow, deny 같은 결과를 계산합니다.
# policies/mcp/tool_access.rego
# OPA v1.x 환경 (rego_version: 1) — if, in 키워드가 기본 내장되므로 별도 import가 불필요합니다
package mcp.tool_access
default allow := false
# 에이전트 역할에 허용된 도구 목록에 요청 도구가 포함된 경우 허용
allow if {
allowed_tools := data.mcp.roles[input.agent.role].tools
input.request.tool in allowed_tools
}
# 감사 로그용 거부 이유 생성
deny_reason := msg if {
not allow
msg := sprintf(
"role '%v'은 tool '%v'에 대한 접근 권한이 없습니다",
[input.agent.role, input.request.tool]
)
}Rego의 선언형 특성: Rego는 "어떻게 계산할지"가 아니라 "무엇이 참인지"를 기술합니다. 규칙 안의 모든 조건이 참일 때 규칙이 성립합니다. 명령형 언어에 익숙한 개발자라면 처음엔 낯설 수 있지만, 정책 로직을 간결하고 테스트 가능하게 표현하는 데 강점이 있습니다.
OPA Bundle: 정책의 패키징과 배포 단위
번들(Bundle)은 Rego 정책 파일, JSON/YAML 데이터 파일, 메타데이터 .manifest를 tar.gz로 패키징한 배포 단위입니다. OPA 인스턴스는 HTTP(S)를 통해 Bundle Server에서 번들을 주기적으로 내려받아, 재시작 없이 정책을 최신 상태로 유지합니다.
# opa-config.yaml — OPA 번들 폴링 설정
services:
bundle-server:
url: https://bundle.example.com
credentials:
bearer:
token: "${BUNDLE_TOKEN}"
bundles:
main:
service: bundle-server
resource: /bundles/main.tar.gz
polling:
min_delay_seconds: 30
max_delay_seconds: 120
signing:
keyid: "prod-key-2025"번들 매니페스트(.manifest)는 번들의 버전, Rego 문법 버전, 루트 데이터 경로를 정의합니다.
{
"revision": "git-sha-abc1234",
"roots": ["mcp"],
"rego_version": 1,
"metadata": {
"built_at": "2026-04-14T09:00:00Z",
"environment": "production"
}
}
rego_version필드:0은 OPA v0.x 문법,1은 OPA v1.x 문법을 의미합니다.rego_version: 1로 지정하면if,in,every등의 키워드가 기본 활성화되어 별도의import future.keywords선언이 불필요합니다.import future.keywords는 OPA v0.x 환경에서 v1 키워드를 미리 활성화할 때만 사용하는 것으로,rego_version: 1과 함께 쓰면 불필요하거나 deprecated 처리됩니다.
MCP 분산 환경의 Policy Drift
MCP는 AI 에이전트(클라이언트)가 다양한 외부 도구와 서비스(MCP 서버)에 연결할 수 있게 해주는 프로토콜입니다. 기업 환경에서는 MCP 서버가 여러 인스턴스로 분산 배포됩니다. 이때 각 인스턴스가 서로 다른 버전의 정책을 갖고 있다면 — 이를 Policy Drift라고 합니다 — 동일한 요청이 어느 인스턴스에 도달했느냐에 따라 허용되기도 하고 거부되기도 하는 상황이 발생합니다.
Policy Drift는 인스턴스가 2개인 소규모 환경에서도 현실적인 위험입니다. 번들 서버 네트워크 파티셔닝, 롤링 재배포 타이밍, 또는 단순히 폴링 주기 차이만으로도 인스턴스 간 정책 버전 불일치가 일어날 수 있습니다.
실전 적용
예시 1: GitOps 기반 Bundle Server 파이프라인
가장 현장에서 많이 사용되는 패턴은 Git 저장소를 정책의 단일 진실 공급원(Single Source of Truth)으로 삼고, CI/CD 파이프라인을 통해 번들을 빌드·서명·배포하는 방식입니다.
개발자 PR → GitHub Actions 트리거
→ opa test ./policies/... (단위 테스트)
→ opa build -b ./policies (번들 빌드 + 서명)
→ AWS S3 업로드 (Bundle Server 역할)
→ OPA 인스턴스 자동 폴링 반영 (30~120초)먼저 번들 서명에 사용할 EC 키 쌍을 생성합니다.
# EC P-256 서명 키 생성
openssl ecparam -genkey -name prime256v1 -noout -out signing-key.pem
# 공개키 추출 (OPA 인스턴스 검증용)
openssl ec -in signing-key.pem -pubout -out signing-key-pub.pem생성한 signing-key.pem의 내용을 GitHub Secrets의 OPA_SIGNING_KEY에 등록하고, signing-key-pub.pem은 OPA 인스턴스 설정의 signing.keyid에 대응하는 키로 포함시킵니다.
# .github/workflows/policy-deploy.yml
name: Deploy OPA Bundle
on:
push:
branches: [main]
paths:
- 'policies/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: OPA 설치
run: |
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod +x opa && sudo mv opa /usr/local/bin/
- name: 정책 단위 테스트
run: opa test ./policies/... -v
- name: 번들 빌드 및 서명
run: |
opa build -b ./policies \
--signing-key "${{ secrets.OPA_SIGNING_KEY }}" \
--signing-alg ES256 \
-o bundle.tar.gz
- name: S3에 번들 업로드
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 cp bundle.tar.gz \
s3://my-opa-bundles/bundles/main.tar.gz \
--cache-control "max-age=60"번들이 참조하는 역할 데이터는 정책과 함께 번들에 포함됩니다. 파일 경로가 데이터 경로에 매핑되므로, data/mcp/roles.json의 내용은 Rego에서 data.mcp.roles로 접근됩니다.
// data/mcp/roles.json
{
"analyst": { "tools": ["read_database", "run_query"] },
"admin": { "tools": ["read_database", "run_query", "write_database"] }
}정책 파일에 대응하는 단위 테스트는 _test.rego 파일로 작성합니다.
# policies/mcp/tool_access_test.rego
package mcp.tool_access_test
import data.mcp.tool_access
# analyst 역할은 read_database를 호출할 수 있어야 합니다
test_analyst_can_read if {
tool_access.allow with input as {
"agent": {"role": "analyst"},
"request": {"tool": "read_database"}
} with data.mcp.roles as {
"analyst": {"tools": ["read_database", "run_query"]}
}
}
# analyst 역할은 write_database를 호출할 수 없어야 합니다
test_analyst_cannot_write if {
not tool_access.allow with input as {
"agent": {"role": "analyst"},
"request": {"tool": "write_database"}
} with data.mcp.roles as {
"analyst": {"tools": ["read_database", "run_query"]}
}
}opa test ./policies/... -v를 실행하면 두 테스트 케이스가 모두 통과하는 것을 확인할 수 있습니다.
예시 2: 분산 MCP 환경의 다층 정책 게이트웨이
예시 1의 GitOps 파이프라인으로 번들 배포 기반을 갖췄다면, 이제 MCP 게이트웨이에서 다층 인가 검증을 적용할 수 있습니다. 에이전트의 모든 도구 호출 요청이 Envoy External Authorization을 통해 OPA를 거치며, 세 개의 계층에서 순차적으로 검증됩니다.
Envoy External Authorization: Envoy 프록시가 요청을 OPA에 외부 인가 서버로 보내 허용 여부를 결정받는 패턴입니다. 애플리케이션 코드 수정 없이 정책을 적용할 수 있다는 장점이 있습니다.
AI Agent
└─→ MCP Gateway (Envoy External Authorization)
└─→ OPA 정책 결정
├─→ Layer 1: 도구 접근 권한 (agent role × tool)
├─→ Layer 2: 명령 실행 범위 (파라미터 허용 패턴)
└─→ Layer 3: 리소스 레벨 제어 (특정 데이터 접근 제한)
└─→ 승인 시: 대상 MCP 서버 호출Ephemeral Token: 일부 구현에서 게이트웨이가 단기 스코프 토큰을 발급해 대상 MCP 서버에서 재검증하는 패턴이 사용됩니다. 이 토큰은 작업 범위에만 유효한 임시 자격증명으로, 발급·검증 방식은 MCP OAuth 2.1 + RFC 8707 Resource Indicators 주제로 다음 글에서 상세히 다룹니다.
역할 데이터는 예시 1과 동일한 data/mcp/roles.json을 사용합니다.
# policies/mcp/tool_access.rego — Layer 1: 도구 접근 권한
package mcp.tool_access
default allow := false
allow if {
allowed_tools := data.mcp.roles[input.agent.role].tools
input.request.tool in allowed_tools
}
deny_reason := msg if {
not allow
msg := sprintf(
"role '%v'은 tool '%v'에 대한 접근 권한이 없습니다",
[input.agent.role, input.request.tool]
)
}# policies/mcp/command_scope.rego — Layer 2: 명령 실행 범위
package mcp.command_scope
default allow := false
allow if {
allowed_pattern := data.mcp.commands[input.request.tool].param_pattern
# regex.match(pattern, value): 첫 번째 인자가 패턴, 두 번째가 검사할 값입니다
regex.match(allowed_pattern, input.request.params.target)
not is_write_operation
}
is_write_operation if {
input.request.params.operation in {"write", "delete", "update"}
input.agent.scope == "read-only"
}예시 3: OPAL을 통한 실시간 다중 인스턴스 동기화
예시 1의 번들 폴링 방식은 30~120초의 전파 지연이 발생합니다. 이 지연이 허용되지 않는 보안 크리티컬 환경에서는 **OPAL(Open Policy Administration Layer)**이 이 문제를 해결합니다. OPAL은 예시 1과 동일한 Git 저장소를 정책 소스로 사용하면서, 변경 감지 후 폴링 대신 WebSocket Pub/Sub으로 모든 OPA 인스턴스에 즉시 정책을 전파합니다.
# docker-compose.yml — OPAL 서버 + 클라이언트 구성 예시
# 프로덕션에서는 'latest' 대신 특정 버전 태그를 고정하는 것을 권장합니다
services:
opal-server:
image: permitio/opal-server:0.7.2
environment:
- OPAL_POLICY_REPO_URL=https://github.com/my-org/opa-policies
- OPAL_POLICY_REPO_POLLING_INTERVAL=30
- OPAL_AUTH_PRIVATE_KEY=${OPAL_PRIVATE_KEY}
ports:
- "7002:7002"
opal-client:
image: permitio/opal-client:0.7.2
environment:
- OPAL_SERVER_URL=http://opal-server:7002
- OPAL_OPA_SERVER_URL=http://opa:8181
depends_on:
- opal-server
- opa
opa:
image: openpolicyagent/opa:0.68.0-static
command:
- "run"
- "--server"
- "--addr=0.0.0.0:8181"
- "--log-format=json"정책 Git 저장소 변경 감지
└─→ OPAL Server (변경 이벤트 발행)
└─→ WebSocket PubSub
├─→ OPAL Client A → OPA Instance A ← 즉시 반영
├─→ OPAL Client B → OPA Instance B ← 즉시 반영
└─→ OPAL Client N → OPA Instance N ← 즉시 반영OPAL 도입 현황: Tesla, Walmart, NBA, Cisco 등에서 수십 개 OPA 인스턴스에 걸친 정책·데이터 동기화에 OPAL을 사용하고 있습니다.
실무에서 가장 흔한 실수
1. 번들 서명을 생략하는 경우
프로덕션에서 번들 서명 없이 배포하면 번들 서버가 탈취되었을 때 임의의 정책이 모든 인스턴스에 배포될 수 있습니다. opa build --signing-key 옵션과 OPA의 signing.keyid 설정을 함께 사용하는 것을 권장합니다. 개발 초기에는 서명을 생략해도 무방하지만, 스테이징 환경부터는 반드시 적용하는 것이 좋습니다.
2. 정책만 번들에 포함하고 데이터를 외부 API로만 공급하는 경우
정책 로직과 데이터(사용자 역할 테이블, 리소스 목록)의 업데이트 타이밍이 어긋나면 의도치 않은 인가 결과가 발생합니다. 앞서 살펴본 것처럼 참조 데이터를 번들에 함께 패키징하거나 OPAL로 동기화하는 방식을 사용하는 것이 안전합니다.
3. 분산 인스턴스의 정책 버전을 모니터링하지 않는 경우
OPA는 /v1/status 엔드포인트를 통해 현재 활성화된 번들의 revision(번들 매니페스트에 기록된 Git 커밋 해시)과 last_successful_activation 등을 노출합니다. 이 값을 Prometheus 등으로 수집해 모든 인스턴스가 동일한 revision을 가리키는지 확인하지 않으면 Policy Drift를 탐지하기 어렵습니다.
# 특정 OPA 인스턴스의 현재 번들 상태 확인
curl http://opa-instance-a:8181/v1/status | jq '.bundles.main.active_revision'장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 정책-코드 분리 | 정책 변경 시 애플리케이션 재배포가 필요 없어 운영 부담이 줄어듭니다 |
| 버전 관리 | Git에서 정책 이력·코드 리뷰·롤백을 관리할 수 있습니다 |
| 언어 중립성 | 어떤 언어로 만든 서비스든 OPA REST API로 통합이 가능합니다 |
| 번들 서명 | 변조된 정책이 배포되는 것을 암호학적으로 차단합니다 |
| 확장성 | OPA를 각 서비스의 사이드카(메인 컨테이너와 같은 Pod 내에서 실행되는 보조 컨테이너)로 배포해 네트워크 지연 없이 로컬 정책 평가를 지원합니다 |
| 감사 가능성 | .manifest의 revision에 Git 커밋 해시를 담아 정책 이력 추적이 가능합니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 최종적 일관성 | 번들 폴링 방식은 기본 30~120초 지연이 발생합니다 | OPAL 실시간 Push 방식 또는 폴링 간격 단축을 고려할 수 있습니다 |
| Policy Drift | 네트워크 파티셔닝 시 인스턴스마다 다른 버전이 활성화될 수 있습니다 | /v1/status 엔드포인트로 revision을 모니터링하는 것을 권장합니다 |
| Rego 학습 곡선 | 선언형 언어로 명령형에 익숙한 개발자에게 진입 장벽이 있습니다 | OPA Playground와 _test.rego 단위 테스트부터 시작하면 학습 부담을 줄일 수 있습니다 |
| 번들 서버 가용성 | 번들 서버 다운 시 초기 로드 전이라면 정책 없는 상태가 됩니다 | Fail-Close 설정과 번들 서버 이중화를 적용하는 것이 좋습니다 |
| 데이터-정책 불일치 | 정책과 외부 데이터가 별도로 업데이트될 경우 일시적 불일치가 발생할 수 있습니다 | 데이터도 번들에 포함하거나 OPAL로 동기화하는 방식을 사용할 수 있습니다 |
Fail-Close(실패 안전): 번들 로드에 실패했을 때 기본적으로 모든 요청을 거부하는 동작입니다.
default allow := false가 Rego의 기본 동작이므로, 번들이 없는 상태에서는 아무것도 허용되지 않습니다. 반대 개념인 Fail-Open은 보안 환경에서 위험할 수 있습니다.
마치며
OPA Bundle Server와 GitOps를 결합하면 Rego 정책을 안전하고 일관되게 배포할 수 있으며, OPAL이나 능동적 모니터링을 더하면 분산 MCP 환경에서도 Policy Drift 없이 모든 인스턴스가 동일한 인가 기준을 유지할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계입니다.
- OPA와 Rego 기초 체험: OPA Playground에서 브라우저 안에서 바로 Rego 코드를 작성하고 평가 결과를 확인해 볼 수 있습니다.
_test.rego파일을 함께 작성하고opa test ./policies/... -v로 단위 테스트를 실행하는 것부터 시작하면 선언형 사고방식을 가장 빠르게 익힐 수 있습니다. - 로컬 Bundle Server 구성 실습: 간단한 Rego 정책을
opa build -b ./policies -o bundle.tar.gz로 빌드한 후,python3 -m http.server 8888또는 MinIO를 번들 서버로 활용해 OPA가 번들을 폴링하는 흐름을 직접 확인해 볼 수 있습니다. 번들 로드 상태는curl http://localhost:8181/v1/status로 확인할 수 있습니다. - GitHub Actions CI/CD 연결: 앞서 소개한 워크플로우를 기반으로
opa test → opa build → S3 업로드파이프라인을 구성해 볼 수 있습니다. 개발 초기에는 서명 없이 시작하고, 스테이징 환경부터openssl ecparam -genkey -name prime256v1 -noout -out signing-key.pem으로 서명 키를 생성해--signing-key옵션을 단계적으로 적용하는 것을 권장합니다.
번들 로드 오류나 서명 키 형식 문제가 생겼을 때는 opa run --bundle bundle.tar.gz로 번들을 로컬에서 직접 로드해보면 대부분의 문제를 빠르게 진단할 수 있습니다.
다음 글: MCP OAuth 2.1 + RFC 8707 Resource Indicators를 활용해 AI 에이전트에 최소 권한 임시 토큰을 발급하는 Ephemeral Scoped Token 패턴 심층 분석
참고 자료
- OPA 공식 Bundle 문서 | openpolicyagent.org
- CNCF: OPA 보안 배포 모범 사례 (2025.03) | cncf.io
- OPAL 공식 문서 | docs.opal.ac
- OPAL GitHub (Permit.io) | github.com
- OPA CI/CD 파이프라인 공식 가이드 | openpolicyagent.org
- OPA 보안 설정 문서 | openpolicyagent.org
- InfoQ: MCP + OPA + Ephemeral Runner 아키텍처 구현 | infoq.com
- Red Hat: MCP 게이트웨이 고급 인증·인가 | developers.redhat.com
- MCP 공식 인가 스펙 (2025-11-25) | modelcontextprotocol.io
- OPA + GitOps: 플랫폼 팀을 위한 컴플라이언스 자동화 | medium.com
- CodiLime: OPA가 AI 에이전트 가드레일이 되어야 하는 이유 | codilime.com
- Strata: 분산 MCP 서버 보안 거버넌스 | strata.io
- OPA Bundle Server 구현 예제 | github.com