사용자의 권한을 Agent 실행에 위임하기
개발중인 AI Agent가 단순한 질의응답을 넘어 실제 업무 데이터를 조회하기 시작하면서 기존 권한 모델에서 생각할 게 하나 더 늘었습니다.
사용 시나리오를 하나 예시로 들면, 사용자가 Agent에게 “구성원들의 평가 결과를 비교해줘”라고 요청하면 Agent는 구성원 정보와 평가 데이터를 읽는 Tool을 선택합니다. 이때 실제 내부 API를 호출하는 주체는 브라우저도 사용자도 아닌 Agent runtime입니다.
그렇다면 이 호출은 누구의 권한으로 실행되어야 할까요?
Agent 서비스 자체에 데이터 조회 역할을 부여할 수도 있고, 로그인한 사용자의 JWT를 Agent까지 전달하여 활용할 수도 있습니다. 사용자에게 허용된 행동 중에서 Agent가 대신 수행할 범위를 별도의 credential로 표현하는 방법도 있습니다.
우리가 선택한 방향은 Agent에게 고유한 데이터 권한을 부여하는 것이 아니라, 기존 사용자의 authority에서 Agent가 실행할 수 있는 범위를 사전에 실행 문맥에 위임하는 방식이었습니다.
userId 전달에서 실행 단위 위임까지#
초기에는 단순하게 Core가 Agent를 호출할 때 내부 shared secret과 tenant, userId를 전달했습니다.
그리고 Agent에서 Core의 데이터를 다시 조회할 때, 전달받은 userId를 기준으로 기존 IAM을 확인했습니다. 평가 결과를 읽는 endpoint의 경우에는 다음과 같은 식의 호출이 있었습니다.
if (!iamPermissionService.hasPermission(
userId,
MEMBER_PERFORMANCE,
READ)) {
return FORBIDDEN;
}Agent 프로세스는 shared secret으로 식별되고 데이터 권한은 함께 전달된 사용자를 기준으로 판단했습니다.
하지만 userId는 신원을 증명할 뿐 Agent 실행이 실제로 그 사용자의 요청에서 시작됐는지 증명하지 못했습니다.
이를 해결하기 위해 사용자의 Bearer JWT를 Agent 호출 경로에 함께 전달했습니다. 이 방식으로 서비스 경계를 지나서도 원래 사용자의 authentication context를 보존했습니다.
하지만 Agent가 background task로 실행되고, 실행 중 Tool을 선택해 다시 Core를 호출하면서 로그인 credential과 Agent 실행 credential의 수명주기가 달라지기 시작했습니다.
사용자의 JWT는 사용자가 누구인지 증명하지만, Agent 실행 시에는 이 사용자를 대신해 특정 실행에서 무엇까지 할 수 있는지를 표현하는 credential이 필요했습니다. 그래서 shared secret + userId, User JWT forwarding을 거쳐 delegated execution credential을 발급하는 구조로 바꾸었습니다.
이 과정이 opaque token으로 바꾸려는 게 목적은 아니었습니다. User Identity와 Agent execution authority를 분리하려는 변경이었습니다.
User Identity와 Agent 실행 권한의 분리#
Agent가 내부 API를 호출하는 경로에는 세 가지 문맥이 겹칩니다.
| 문맥 | 확인하는 질문 | 현재 역할 |
|---|---|---|
| User Identity | 누가 요청을 시작했는가 | OIDC JWT로 원래 사용자와 tenant 식별 |
| Workload Authentication | 지금 호출하는 서비스가 신뢰된 Agent인가 | Core와 Agent 사이의 shared secret 확인 |
| Delegated Authority | 이 실행이 사용자를 대신해 무엇을 할 수 있는가 | 실행에 발급된 capability와 binding 검증 |
신뢰된 workload라고 해서 사용자의 행동까지 허용된 것은 아니고, 사용자가 로그인했다고 모든 Agent 실행이 위임된 것도 아닙니다.
Agent workload 자체에 광범위한 데이터 권한을 두면 “Agent가 신뢰된 서비스인가”와 “이번 요청이 특정 사용자를 대신해 허용된 행동인가”를 구분하기 어렵습니다. 신뢰된 대리인이 자신에게 주어진 권한으로 호출자의 의도와 다른 행동을 수행하는 Confused Deputy 위험도 발생합니다.
Delegation은 이 문제를 모두 해결하는 장치는 아니지만, Agent의 ambient data authority를 줄이고, 각 실행이 누구의 어떤 권한에서 파생됐는지 provenance를 보존하는 경계로 두었습니다.
현재 workload authentication은 shared secret 기반입니다. mTLS나 SPIFFE, 독립적인 OIDC client credential처럼 Agent를 first-class workload principal로 모델링한 상태는 아닙니다.
그래도 세 문맥을 나눠둔 덕분에 이후 workload 인증 수단을 바꾸더라도 데이터 권한의 기준은 사용자 IAM에 남습니다. 이 구분을 실제 credential에 옮기는 발급 지점은 Core입니다.
사용자가 Agent 작업을 시작하면 Core가 인증된 사용자 문맥에서 delegation credential을 발급합니다.
actor는 Core가 인증한 사용자 문맥에서 가져오고, tenant도 Core의 tenant context에서 결정합니다. capability 역시 현재 IAM permission 중 Agent operation으로 노출된 범위만 Core가 계산합니다.
Agent는 delegation을 소비할 수는 있지만 발급 범위를 결정할 수 없습니다. Agent가 임의의 userId나 tenantId를 제출하거나 capability를 추가해 자신의 authority를 넓히는 구조가 아닙니다.
delegation은 최초 Agent thread에 bind됩니다. threadId 자체가 authority를 만들어내지는 않지만, 최초 binding 이후에는 동일한 credential을 다른 thread로 옮겨 재사용할 수 없습니다. 이렇게 발급된 delegation에는 actor, tenant, thread binding, issued capability와 lifetime이 남습니다.
Core는 random opaque token을 발급하고 저장소에는 hash만 남깁니다. 당시 Core는 외부 IdP가 발급한 JWT를 검증하는 resource server였고 자체 JWT issuer는 아니었습니다. 자체 JWT를 새로 발급하면 signing key, rotation, JWKS 같은 운영 책임도 함께 생깁니다.
IdP가 subject와 actor, audience를 포함한 delegated token을 발급한다면 OAuth 2.0 Token Exchange와 같은 표준도 선택지가 됩니다. 지금은 delegated authority의 발급과 revoke 책임을 Core에 두는 편이 기존 운영 모델에 맞았습니다. JWT와 opaque token의 형식보다 누가 authority를 만들고 검증하며 종료할지를 먼저 결정했습니다.
Capability Ceiling과 Current IAM#
Delegation을 도입하면서 Agent 전용 권한 체계를 새로 만들지는 않았습니다. 기존 시스템에는 사용자 역할과 permission을 판정하는 IAM이 이미 있었고, Agent operation을 기존 item × action에 명시적으로 연결했습니다.
| Agent operation | 기존 IAM permission |
|---|---|
agent.organization-member.read | member.identity : READ |
agent.attendance.read | attendance.member : READ |
agent.performance.read | member.performance : READ |
agent.file.write | agent.workspace : CREATE |
agent.file.read | agent.workspace : READ |
agent.file.delete | agent.workspace : DELETE |
Agent-facing operation과 기존 IAM 사이에 mapping layer를 둬서 권한의 최종 원천을 추가하지 않았습니다. 기존 UI와 Agent가 서로 다른 IAM을 바라보는 구조도 피했습니다.
발급 시점을 t0라고 하면 IssuedCapabilities는 당시 사용자가 실행할 수 있는 Agent operation의 부분집합입니다. 여기서 CurrentAuthorizedOperations(t)는 시점 t의 IAM permission을 Agent operation vocabulary로 변환한 집합입니다.
EffectiveOperations(t)
=
if DelegationValid(t)
then IssuedCapabilities
∩ CurrentAuthorizedOperations(t)
else ∅이 식에서 EffectiveOperations(t)는 issued capability와 현재 IAM 중 어느 쪽보다도 넓어지지 않습니다. issued capability는 사용자의 permission cache가 아니라 해당 실행이 넘을 수 없는 authority ceiling인 셈입니다.
발급 당시 사용자에게 A, B operation이 있었다고 해보겠습니다.
발급 시
User = {A, B}
Delegation = {A, B}
이후 User = {A}
→ Effective = {A}
이후 User = {A, B, C}
→ 기존 delegation이 유효하면 Effective = {A, B}
→ C는 새로운 delegation 없이는 사용 불가delegation credential이 계속 유효하다면 ceiling 안의 operation은 IAM 변경에 따라 비활성화됐다가 다시 활성화됩니다. 반면 권한 변경 과정에서 delegation 자체가 revoke됐다면 이후 IAM 권한이 복구돼도 해당 credential은 다시 살아나지 않습니다.
capability를 delegation에 저장하더라도 발급 시점의 값만 믿고 만료까지 사용하지는 않았습니다. 오전 10시에 performance : READ 권한으로 delegation credential을 발급한 뒤 10시 3분에 관리자가 권한을 회수했다고 해보겠습니다. 10시 4분의 Tool 호출에서 발급 당시 capability만 검사하면 요청이 그대로 허용됩니다.
이를 막기 위해 internal API는 issued capability와 현재 IAM을 별도의 조건으로 평가합니다.
Allowed(request, t)
=
TrustedWorkload(request)
AND ValidDelegation(request, t)
AND ContextMatches(request)
AND operation(request) ∈ EffectiveOperations(t)
AND ResourceAllowed(
actor,
operation,
arguments,
target
)ValidDelegation은 delegation의 만료와 revoke 상태를 확인합니다. ContextMatches는 tenant와 thread binding을 검사합니다. operation은 issued capability와 현재 IAM의 교집합에 포함돼야 하며, 실제 대상까지 내려가면 인자와 resource scope도 별도로 허용돼야 합니다.
역할 배정 회수, 역할 권한 감소, 역할 삭제가 발생하면 관련 active delegation을 revoke하는 경로도 두었습니다. 하지만 이 hook만 최종 보장으로 삼지는 않았습니다. hook이 없는 권한 변화나 변경과 Tool invocation 사이의 race가 있어도 다음 internal API 호출에서 현재 IAM을 다시 평가합니다.
delegation이 아직 ACTIVE여도 현재 IAM이 거부하면 실행되지 않습니다. 사용자에게 권한이 남아 있어도 delegation이 REVOKED라면 마찬가지입니다. Credential revoke와 사용자 권한 회수를 별도의 방어선으로 둔 이유입니다.
Operation에서 Resource Authorization까지#
LLM이 Tool을 선택한다고 그 Tool이 다루는 모든 데이터를 볼 권한까지 생기는 것은 아닙니다.
Agent가 performance.read를 선택하면 issued capability와 현재 IAM을 먼저 확인합니다. 같은 operation 안에서도 요청 인자와 대상에 따라 authorization 결과는 달라집니다.
operation = performance.read
memberId = 자기 자신
memberId = 소속 팀원
memberId = 다른 조직 구성원
organizationId = 전체 회사
period = 최근 1개월 또는 전체 기간performance.read가 허용됐다고 임의의 memberId나 organizationId로 호출할 수 있는 것은 아닙니다. Operation authorization은 어떤 함수를 호출할지를 정하고, argument와 resource authorization은 그 함수를 어떤 인자와 대상에 사용할지를 정합니다.
현재 시스템은 Tool operation과 domain permission을 명시적으로 분리했습니다. delegated file에는 object scope도 일부 적용합니다. Agent에게 storage key를 직접 노출하지 않고 Core가 관리하는 fileId를 사용하며, 접근할 때 tenant, owner, thread, file state를 함께 확인합니다.
S3는 파일을 저장하지만 파일 authorization은 Core에 남습니다. 반면 일반 구성원과 조직 데이터에서 본인, 조직장, 하위 조직 범위를 공통으로 판정하는 object-level 정책은 같은 수준으로 완성되지 않았습니다.
Agent operation capability와 실행 시점의 IAM 재평가는 적용했지만, 일반 HR 데이터의 argument와 resource scope는 아직 후속 과제입니다. 앞의 non-escalation invariant도 우선 operation-level authority에 대한 보장이지 모든 HR resource에 대한 보장은 아닙니다.
이 모델이 막는 것과 막지 못하는 것#
이 구조는 상황에 따라 다음과 같이 동작합니다.
| 상황 | 현재 모델의 결과 |
|---|---|
| workload credential만 제출 | delegation이 없어 사용자 데이터 접근 거절 |
| delegation만 제출 | 신뢰된 workload 경계를 만족하지 못하면 거절 |
| 다른 thread에서 delegation credential 재사용 | context binding 실패 |
| 발급 후 사용자 권한 감소 | effective operation 감소 또는 delegation revoke |
| 발급 후 사용자 권한 증가 | 기존 capability ceiling 밖으로 확대되지 않음 |
| 허용되지 않은 Tool 호출 | issued capability 밖이므로 거절 |
| 명시되지 않은 Agent operation | mapping 없음, deny |
| 허용된 Tool을 잘못된 대상에 호출 | argument/resource authorization 필요 |
| 허용 범위 안에서 불필요한 Tool 실행 | 이 모델만으로 완전히 해결하지 못함 |
| Agent runtime 자체 침해 | 이 delegation 모델만으로 완전히 방어하지 못함 |
신규 operation은 이름이나 URL 일부로 권한을 추론하지 않고 명시적인 capability/IAM mapping이 없으면 거부합니다. LLM은 무엇을 호출할지 선택하지만 그 호출을 허용할지는 Core에 있는 권한레이어에서 판단합니다.
prompt injection이나 잘못된 추론이 이미 허용된 capability 안에서 불필요한 Tool을 실행하게 만드는 문제는 request-level scope나 중요한 write operation의 사용자 확인으로 다뤄야 합니다.
workload credential과 delegation을 함께 검사한다고 서로 독립된 2FA가 되는 것도 아닙니다. Agent runtime이 침해되면 같은 프로세스에 있는 두 credential이 함께 노출될 수 있습니다. 이 delegation 모델은 credential 하나의 단독 오용, cross-thread replay, stale authority와 capability 밖의 Tool 호출을 제한하지만 runtime 자체의 compromise까지 막지는 못합니다.
Audit와 아직 남은 권한 경계#
사용자가 직접 API를 호출할 때는 audit log의 actor가 비교적 명확합니다. Agent가 사용자를 대신하면 “Agent가 읽었다”만으로는 누가 어떤 authority로 행동했는지 설명할 수 없습니다.
현재 민감한 구성원 데이터 접근은 Agent 경로에서도 원래 사용자 ID와 대상 구성원, Agent operation domain을 기준으로 감사 기록을 남깁니다. delegation에도 actor, tenant, thread, root delegation, revoke 정보가 남습니다.
이제는 사용자가 Agent를 거쳐 어떤 데이터 영역에 접근했는지 추적할 수 있습니다. 다만 개별 audit row와 delegationId 또는 threadId를 하나의 식별자로 바로 연결하는 correlation model은 아직 부족합니다.
권한의 출처를 보존하면 audit log도 같은 문맥을 따라가야 합니다. 원래 actor와 execution, 위임 범위가 이어져야 개별 audit row와 실제 Agent 실행이 연결됩니다.
현재 delegation에는 자연어 요청 하나에 꼭 필요한 Tool이 아니라 사용자가 Agent에서 행사 가능한 operation 집합이 들어갑니다. 평가와 근태 데이터를 모두 보는 사용자가 “평가 결과를 요약해줘”라고 요청해도 delegation에는 두 operation이 함께 포함될 수 있습니다. 사용자의 전체 시스템 권한보다는 좁지만 task-scoped least privilege는 아닙니다.
first-class workload identity와 사용자에서 Orchestrator, Sub Agent로 이어지는 delegation chain도 아직 다루지 않았습니다. 여러 Agent가 authority를 다시 위임한다면 각 단계에서 범위를 어떻게 줄이고 원래 actor까지 어떻게 추적할지 별도의 모델이 필요합니다.
Durable Execution과 Authority Lifetime#
처음에는 Agent가 내부 API를 호출하도록 인증하는 방법을 찾고 있었습니다. 구조를 만들다 보니 호출 주체, 원래 actor, 위임 범위, 현재 IAM, 대상 resource가 서로 다른 문제였습니다.
실행을 시작할 때는 capability ceiling을 정하고, 실제 Tool을 호출할 때는 사용자에게 그 권한이 지금도 남아 있는지 다시 확인했습니다. Agent에는 Tool 선택을 맡겼지만 권한 판단까지 맡기지는 않았습니다.
여기까지 분리하고 나니 다음 문제가 보였습니다. Agent의 실행은 연결이 끊겨도 background runner는 계속 실행중일 수 있고, 그사이 사용자의 권한이나 계정 상태가 바뀔 수 있습니다.
그렇다면 작업이 계속 살아 있다는 이유만으로 그 작업의 권한도 계속 살아 있어야 할까요?
비동기 Job의 실행과 상태 전이를 분리한 글에서 실행의 durability를 다뤘다면, 다음 글에서는 실행의 수명과 authority의 수명을 분리하는 과정을 이어서 정리하려고 합니다.