에이전트가 무엇을 했는지는 트레이스(Trace)로 증명할 수 있지만, 그것이 허용되었는지는 증명할 수 없습니다.
요약
AI 에이전트의 실행 기록(Trace)과 권한 부여 정책 간의 불일치 문제를 다룹니다. OpenTelemetry 스팬은 작업의 성공 여부만 기록할 뿐 권한 허용 여부를 증명하지 못하는 '사각지대'가 존재함을 지적하며, 이를 해결하기 위한 authz_gate.py의 역할을 설명합니다.
핵심 포인트
- OpenTelemetry 스팬은 작업의 실행 여부만 기록하며 권한 허용 여부는 기록하지 않음
- 정책에 의해 거부된 작업이 트레이스상에서 'OK'로 표시되는 사각지대 발생 가능
- authz_gate.py를 통해 텔레메트리 기록과 정책 간의 불일치를 검증 가능
- 에이전트 보안을 위해 실행 기록과 권한 정책의 정합성 확인이 필수적임
AI 에이전트를 위한 호출 전 권한 부여 게이트(pre-call authorization gate)는 도구가 실행되기 전, 선언적 정책(declarative policy)에 따라 각 작업에 대해 허용(ALLOW) 또는 거부(DENY)를 결정합니다. OpenTelemetry 스팬(span)은 이를 수행할 수 없습니다. 스팬은 호출이 실행되었는지 여부를 기록할 뿐, 그것이 허용되었는지 여부는 기록하지 않기 때문입니다. authz_gate.py는 이 두 가지를 조정(reconcile)합니다. 이 포스트의 위반 매니페스트(violating manifest)에서는 정책에 의해 거부된 3개의 작업이 status=OK로 기록되었습니다: 3개의 사각지대(blind spots), 종료 코드(exit) 1.
AI 공개 사항: 저는 AI 어시스턴트와 함께
authz_gate.py를 작성했으며, 게시하기 전에 오프라인에서 직접 실행했습니다. 아래 블록의 모든 숫자는 이 포스트에 포함된 합성 매니페스트(synthetic manifests)를 대상으로, 표준 라이브러리만 사용한 Python 3.13.5 환경의 실제 로컬 실행 결과에서 복사해 왔습니다. 저는 종료 코드(0 / 1 / 2)를 확인했고, STDOUT을 두 번 해싱하여 바이트 단위로 결정론적(deterministic)임을 확인했으며, 모든 줄을 직접 수정했습니다. 외부 인용문(Fiddler의 OpenTelemetry 기술 문서, APort의 권한 부여 가이드)은 저의 것이 아니며 원본 소스를 링크했습니다. 어떤 문장이 그들의 것인지 표시해 두었습니다.
요약하자면:
-
여러분의 OpenTelemetry 스팬은 작업에 대해 한 가지만 인코딩합니다: 호출이 실행되어 오류 없이 반환되었는지(기본값인 UNSET 또는 명시적인 OK 상태), 아니면 오류를 발생시켰는지(ERROR)입니다. 이는 "이 작업이 허용되었는가"를 인코딩하지 않습니다. 이 둘은 서로 다른 질문입니다.
-
따라서 정책에 의해 거부되었음에도 불구하고 실행된 작업은 트레이스(trace) 상에 깨끗하고 녹색인
status=OK로 나타납니다. 기록 전용 스택(record-only stack)은 이를 정당한 성공과 구별할 수 없습니다. 이것이 권한 부여의 사각지대(authorization blind spot)입니다. -
authz_gate.py는 정적 매니페스트(정책, 작업 스트림, 스팬 로그)를 읽고, 정책에 따라 각 작업에 대해 허용(ALLOW) 또는 거부(DENY)를 결정한 다음, 여러분의 스팬 로그가 성공으로 기록한 거부된 작업의 수를 계산합니다. -
중요한 결과: 두 매니페스트는 동일한 세 개의 거부된 작업을 포함하고 있습니다.
violating에서는 스팬이 모두 OK이므로, 3개의 사각지대가 발생하고 종료 코드 1이 반환됩니다.authz_aware에서는 동일한 세 개의 거부 작업이status=ERROR를 포함하므로, 사각지대는 0개이며 종료 코드 0이 반환됩니다. 게이트는 거부 자체에 반응하는 것이 아니라, 텔레메트리(telemetry)와 정책 사이의 불일치에 반응하여 작동합니다. -
표준 라이브러리만 사용 (
json,sys). 네트워크, 모델, 서브프로세스(subprocess), 런타임 인터셉션(runtime interception) 없음. 실행은 바이트 단위로 결정론적(deterministic)임. 도구와 4개의 매니페스트(manifest) 모두 이 포스트에 포함되어 있음.
격차가 발생하는 지점
녹색 스팬(span)은 허가된 동작이 아닙니다. 이 문장이 이 포스트의 전부입니다.
에이전트 스택(agent stacks)에서 제가 계속 목격하는 함정은 다음과 같습니다. 팀은 오케스트레이터(orchestrator)를 통해 OpenTelemetry를 연결합니다. 모든 도구 호출(tool call)은 스팬(span)이 됩니다. 대시보드가 밝게 빛납니다. 이제 에이전트가 무엇을 했는지 볼 수 있기 때문에 모두가 안전하다고 느낍니다. 그러던 어느 날, 오염된 입력(poisoned input)에 의해 유도된 에이전트가 아무도 승인하지 않은 주소로 wallet.transfer를 호출하고, 해당 호출에 대한 트레이스(trace)는 차분한 녹색 status=OK로 나타납니다. 텔레메트리(telemetry)는 제 역할을 다했습니다. 호출이 실행되었고 반환되었다는 것을 기록했습니다. 하지만 호출이 허용되었는지에 대해서는 질문받은 적이 없으며, 그 답변을 기록할 곳도 없습니다.
Fiddler 팀은 그들의 OpenTelemetry 가이드(2026년 5월)에서 경계선을 명확하게 작성했습니다: "OpenTelemetry는 무엇이 일어났는지를 캡처합니다. 일어난 일이 좋았는지 여부는 평가하지 않습니다." 그리고 나중에 사람들이 건너뛰는 부분: "OpenTelemetry는 수동적 계측(passive instrumentation)입니다. 기록은 하지만 인터셉트(intercept), 편집(redact) 또는 차단(block)하지는 않습니다." 이것은 제 말이 아니라 Fiddler의 글에 나온 그들의 표현입니다. 저는 그들의 프레임워크를 빌려와 한 단계 더 나아가고자 합니다. 만약 스팬이 동작의 허용 여부를 말할 수 없다면, 여러분의 트레이스가 성공으로 기록한 거부된 동작의 수는 현재 여러분이 볼 수 없는 숫자라는 것입니다. 이 도구는 정확히 그 숫자를 계산합니다.
스팬이 실제로 인코딩하는 것
정확히 말씀드리겠습니다. 여기서 OpenTelemetry를 비난하기는 쉽지만, 저는 그러고 싶지 않습니다. OTel 스팬 상태 (span status)는 UNSET, OK, 또는 ERROR라는 세 가지 값 중 하나입니다. 이 축은 실행 (execution)을 측정합니다. 즉, 작업이 완료되었는지, 아니면 결함 (fault)이 발생했는지를 측정하는 것입니다. DENIED라고 불리는 네 번째 값은 존재하지 않습니다. 권한 부여 (Authorization)는 기본적으로 스팬 (span)이 운반하는 차원이 아닙니다. 명세 (spec)에 따르면, 계측 (instrumentation)은 성공적인 스팬을 UNSET으로 남겨두도록 되어 있으며, 거의 사용되지 않는 명시적 오버라이드 (explicit override)인 경우에만 OK로 설정하도록 되어 있습니다. 따라서 일상적인 성공 사례는 초록색의 OK보다 더 조용합니다. 그러므로 거부된 동작 (denied action)이 실행되고 정상적으로 반환될 때, 해당 스팬은 OK 또는 UNSET이 되며, 기록 전용 파이프라인 (record-only pipeline)에서는 정당한 성공과 구별할 수 없습니다. 이것은 OpenTelemetry의 실패가 아닙니다. OpenTelemetry가 잘못된 질문에 대해 제 역할을 수행하고 있는 것뿐입니다.
해결책은 "트레이싱 (tracing)을 더 추가하는 것"이 아닙니다. 모든 스팬에 10개의 속성 (attributes)을 더 추가하더라도 "이것이 허용되었는가"라는 질문에는 여전히 답할 수 없습니다. 왜냐하면 허용 여부 (allowed-ness)는 정책 (policy)에 대한 판결이며, 정책은 스팬 외부에 존재하기 때문입니다. 해결책은 선언적 정책 (declarative policy)을 보유하고, 각 동작에 대해 해당 정책에 따른 허용 (ALLOW) 또는 거부 (DENY)를 결정한 다음, 그 판결을 텔레메트리 (telemetry)가 기록한 내용과 대조 (reconcile)하는 것입니다. 두 가지가 일치하지 않을 때, 여러분은 초록색 스팬을 입고 있는 거부된 동작을 찾아낸 것입니다.
60초 안에 실행하기
키 (keys)도 필요 없습니다. 네트워크도 필요 없습니다. Python 외에는 설치할 것도 없습니다. 파일을 저장하고, 매니페스트 (manifest)를 저장하고, 명령어 하나만 실행하면 됩니다.
매니페스트는 세 부분으로 구성된 하나의 JSON 객체입니다:
policy: 기본적으로 거부하는 방식의 허용 목록 (allowlist). 허용된 각 도구는in(값이 목록에 있어야 함),max/min(수치적 경계), 또는equals를 사용하여 인자 (args)를 제한할 수 있습니다.actions: 에이전트가 시도한 호출 스트림으로, 각 호출은seq,tool,args를 포함합니다.spans: 텔레메트리 스택이 기록한 OTel 스타일의 로그로, 각 로그는seq와status를 가지며seq를 통해actions와 결합됩니다.
여기 도구 전체가 있습니다. 표준 라이브러리만 사용하는 단일 파일입니다.
#!/usr/bin/env python3
"""
authz_gate.py -- AI-agent 동작을 위한 호출 전 권한 부여 게이트, 대조됨
...
베이스라인(Baseline): 정책(Policy)과 텔레메트리(Telemetry)의 일치
정상적인 작업을 수행하는 payments-ops-agent로 시작해 보겠습니다. 네 가지 작업이 수행되었으며, 모두 정책(Policy) 범위 내에 있습니다: 잔액 조회, 내부 환율(FX rate) 가져오기, 급여 750 지급(한도인 1000 미만이며 허용된 주소로 전송), 그리고 다시 잔액 조회. 모든 스팬(Span)은 OK 상태입니다. 여기서는 이것이 정직한 결과인데, 왜냐하면 모든 작업이 실제로 허용되었기 때문입니다. (이 피스처(Fixtures)들은 가독성을 위해 깨끗한 스팬에 OK라고 라벨을 붙였습니다. 실제 인스트루멘테이션(Instrumentation)의 경우 이를 UNSET으로 남겨두는 경우가 많으며, 게이트(Gate)는 두 경우 모두 에러가 아닌 것으로 간주하므로 결과는 동일합니다.)
$ python3 authz_gate.py fixtures/clean_manifest.json
AUTHZ-GATE REPORT
policy: default=deny, 3 tool(s) allowed
...
Exit 0. 볼 것이 없습니다. 이것이 핵심입니다. 텔레메트리(Telemetry)와 정책(Policy)이 일치할 때 게이트(Gate)는 침묵을 유지합니다. 일반적으로 거부(Denial) 상황에 대해 소란을 피우지 않습니다. 오직 두 가지가 서로 불일치할 때만 입을 엽니다.
사례를 입증하는 데모
이제 동일한 에이전트와 동일한 정책을 사용하되, 경로를 이탈한 경우를 보겠습니다. 다섯 가지 작업이 수행되었습니다. 그중 세 가지는 권한이 없습니다: 0xATTACKER로 5000을 전송하는 wallet.transfer (금액이 한도인 1000을 초과하고 주소가 허용 목록(Allowlist)에 없으므로 두 번 실패함), 허용되지 않은 도구인 shell.run, 그리고 허용된 호스트 대신 paste.ee로 요청하는 api.fetch. 기록 전용(Record-only) 텔레메트리 스택은 모든 스팬(Span)을 status=OK로 표시했습니다.
$ python3 authz_gate.py fixtures/violating_manifest.json
AUTHZ-GATE REPORT
policy: default=deny, 3 tool(s) allowed
...
Exit 1. blind spots 라인을 다시 읽어보십시오: 정책이 거부했을 세 가지 작업이 스팬 로그에는 모두 성공으로 기록되었습니다. 만약 여러분이 이 스팬들을 기반으로 구축된 대시보드를 보고 있었다면, 세 개의 초록색 행을 보았을 것입니다. amount carried by blind-spot actions: 5000은 피스처(Fixture) 수치일 뿐, 누군가의 실제 트래픽을 측정한 값이 아닙니다. 아무도 이를 실제 운영 수치로 스크린샷을 찍지 않도록 출력 결과 자체에 그렇게 라벨을 붙여두었습니다.
이제 반증 가능성 테스트(falsifiability test)를 진행하겠습니다. 만약 스팬 상태(span status)가 실제로 권한 부여(authorization) 정보를 담고 있다면, 이 논증 전체가 무너집니다. 이에 대한 반대 증거(counter-manifest)는 다음과 같습니다: authz_aware는 **정확히 동일한 세 가지의 권한 없는 작업(unauthorized actions)**을 수행하지만, 이번에는 텔레메트리 스택(telemetry stack)이 스팬(span)에 권한 부여 정보를 연결하여 거부된 호출이 status=ERROR를 포함하게 됩니다. 실제 스택에서는 실행 상태(execution status)를 이런 식으로 과부하(overload)하여 사용하지 않을 것입니다. 대신 거부된 사실을 전용 권한 부여 속성(authorization attribute)이나 이벤트(event)에 기록할 것입니다. 여기서 ERROR는 단순히 리컨실러(reconciler)가 볼 수 있는 가장 단순한 신호이며, "텔레트리가 어딘가에서 거부 사실을 포착했다"는 것을 대신 나타냅니다.
$ python3 authz_gate.py fixtures/authz_aware_manifest.json
AUTHZ-GATE REPORT
policy: default=deny, 3 tool(s) allowed
...
Exit 0. 동일한 세 가지 거부(denials). 사각지대(blind spots)는 제로입니다. DENY: 3 라인은 위반이 발생했던 실행(violating run)과 동일하며, 이 부분이 바로 여러분이 깊이 생각해보아야 할 지점입니다. 게이트(gate)는 거부 횟수를 세는 것이 아닙니다. 텔레트리가 거짓을 말한 거부 횟수를 세는 것입니다. 텔레트리가 진실을 말할 때(거부된 호출에 대해 ERROR를 표시할 때)는 플래그(flag)를 세울 것이 아무것도 없습니다. 따라서 이 지표(metric)는 정책(policy)과 스팬(span) 사이의 불일치이며, 반례(counter-example)가 결론을 뒷받침합니다: 기본적으로 OTel 상태는 권한 부여 정보를 담고 있지 않으며, 그 내부의 거부 사항들은 보이지 않습니다.
호출 전 권한 부여 게이트(pre-call authorization gate)는 각 판결을 어떻게 계산하는가?
로직은 머릿속에 담아둘 수 있을 정도로 간단합니다. 각 작업에 대해 check_action은 다음과 같이 묻습니다: 해당 도구가 허용 목록(allowlist)에 있는가? 없다면, DENY (기본 거부, deny-by-default). 있다면, 각 인자 제약 조건(arg constraint)을 확인합니다. 제약이 걸린 인자가 누락된 경우, 이는 건너뛰는 것이 아니라 DENY입니다. 왜냐하면 페일 클로즈드(fail-closed) 방식은 증거의 부재가 곧 허가를 의미하지 않기 때문입니다. 그다음은 리컨실리에이션(reconciliation) 단계입니다: 판결이 DENY이면서 스팬이 <no-span>이거나 에러가 아닌 집합(ok, unset, completed, success, 빈 문자열)에 속할 때, 해당 작업은 사각지대(blind spot)가 됩니다. 이를 카운트합니다. 만약 카운트가 0보다 크면, exit 1을 수행합니다. 이것이 게이트의 전부입니다.
리뷰어가 지적할 수 있기에 제가 방어하고 싶은 한 가지 설계 선택 사항(design choice)이 있습니다. 게이트(gate)는 누락된 스팬(span)을 깨끗한 스팬(clean span)과 동일하게 취급합니다. 두 경우 모두 "텔레메트리(telemetry)가 문제를 표시하지 않았다"는 것을 의미하며, 트레이스(trace)를 읽는 누구에게나 거부된 동작이 정상적으로 보이게 만듭니다. 만약 누락된 스팬을 별도의 카테고리로 취급하고 싶다면, 그것은 합리적인 변형(variant)이며 span_clean 체크에서 두 줄의 코드 변경만으로 가능합니다. 저는 의도적으로 엄격한 해석을 선택했습니다. 즉, 플래그(flag)의 증거가 없다는 것이 차단(block)의 증거는 아니라는 것입니다.
데이터 평면(Data plane), 제어 평면(Control plane)
이것은 권한 부여(authorization) 측 사람들이 반대편에서 그려온 것과 동일한 선입니다. APort의 실행 전 권한 부여(pre-execution authorization) 가이드를 작성한 Uchi Uchibeke(2026년 4월)는 이를 직설적으로 표현했습니다: _"도구 호출(tool call)이 실행된 후에 로깅하는 것은 관찰(observa
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기