액세스 제어

adk-auth를 사용하는 엔터프라이즈급 AI 에이전트 액세스 제어입니다.

개요

adk-auth는 역할 기반 액세스 제어(RBAC), 범위 기반 권한 부여, 감사 로깅 및 SSO를 지원하여 ADK 에이전트에 대한 액세스를 제공합니다. 이를 통해 어떤 사용자가 어떤 도구에 액세스할 수 있는지 안전하고 세밀하게 제어할 수 있습니다.

아키텍처

┌─────────────────────────────────────────────────────────────────┐
│                        Agent Request                             │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     SSO Token Validation                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │
│  │ Google      │  │ Azure AD    │  │ OIDC Discovery          │  │
│  │ Provider    │  │ Provider    │  │ (Okta, Auth0, etc)     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘  │
│                          │                                       │
│                   ┌──────┴──────┐                                │
│                   │ JWKS Cache  │  ← Auto-refresh keys          │
│                   └─────────────┘                                │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼ TokenClaims
┌─────────────────────────────────────────────────────────────────┐
│                       Claims Mapper                              │
│                                                                  │
│    IdP Groups          →        adk-auth Roles                  │
│    ─────────────────────────────────────────                    │
│    "AdminGroup"        →        "admin"                         │
│    "DataAnalysts"      →        "analyst"                       │
│    (default)           →        "viewer"                        │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼ Roles
┌─────────────────────────────────────────────────────────────────┐
│                      Access Control                              │
│                                                                  │
│    Role: admin                                                   │
│    ├── allow: AllTools                                          │
│    └── allow: AllAgents                                         │
│                                                                  │
│    Role: analyst                                                 │
│    ├── allow: Tool("search")                                    │
│    ├── allow: Tool("summarize")                                 │
│    └── deny:  Tool("code_exec")  ← Deny takes precedence        │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼ Check Result
┌─────────────────────────────────────────────────────────────────┐
│                      Audit Logging                               │
│                                                                  │
│    {"user":"alice","resource":"search","outcome":"allowed"}     │
│    {"user":"bob","resource":"exec","outcome":"denied"}          │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Tool Execution                               │
│               (only if access granted)                          │
└─────────────────────────────────────────────────────────────────┘

설계 원칙

1. 거부 우선

역할에 허용 규칙과 거부 규칙이 모두 있는 경우 항상 거부가 우선합니다:

let role = Role::new("limited")
    .allow(Permission::AllTools)      // Allow everything...
    .deny(Permission::Tool("admin")); // ...except admin

// Result: Can access any tool EXCEPT "admin"

2. 다중 역할 통합

여러 역할을 가진 사용자는 권한의 통합을 갖지만, 어떤 역할의 거부 규칙이든 여전히 적용됩니다:

let ac = AccessControl::builder()
    .role(reader)    // allow: search
    .role(writer)    // allow: write
    .assign("alice", "reader")
    .assign("alice", "writer")
    .build()?;

// Alice can access both "search" AND "write"

3. 암시적 권한보다 명시적 권한 우선

권한은 명시적으로 부여됩니다. 기본적으로 액세스가 허용되지 않습니다:

let role = Role::new("empty");
// This role grants NO permissions

ac.check("user", &Permission::Tool("anything")); // → Denied

4. 인증과 권한 부여의 분리

  • 인증 (SSO): 사용자가 누구인지 확인합니다(JWT를 통해).
  • 권한 부여 (RBAC): 사용자가 무엇에 액세스할 수 있는지 결정합니다.
// Authentication: validate JWT, extract claims
let claims = provider.validate(token).await?;

// Authorization: check specific permission
ac.check(&claims.sub, &Permission::Tool("search"))?;

// Combined: SsoAccessControl does both
sso.check_token(token, &permission).await?;

설치

[dependencies]
adk-auth = "2.1.0"

# For SSO/OAuth support
adk-auth = { version = "2.1.0", features = ["sso"] }

핵심 구성 요소

권한

pub enum Permission {
    Tool(String),     // Specific tool by name
    AllTools,         // Wildcard: all tools
    Agent(String),    // Specific agent by name  
    AllAgents,        // Wildcard: all agents
}

역할

let analyst = Role::new("analyst")
    .allow(Permission::Tool("search".into()))
    .allow(Permission::Tool("summarize".into()))
    .deny(Permission::Tool("code_exec".into()));

AccessControl

let ac = AccessControl::builder()
    .role(admin)
    .role(analyst)
    .assign("alice@company.com", "admin")
    .assign("bob@company.com", "analyst")
    .build()?;

// Check permission
ac.check("bob@company.com", &Permission::Tool("search".into()))?;

ProtectedTool

자동 권한 확인 기능으로 도구를 래핑합니다:

use adk_auth::ToolExt;

let protected = my_tool.with_access_control(Arc::new(ac));

// When executed, checks permission before running
protected.execute(ctx, args).await?;

AuthMiddleware

여러 도구를 일괄적으로 보호합니다:

let middleware = AuthMiddleware::new(ac);
let protected_tools = middleware.protect_all(tools);

ScopeGuard

JWT 클레임 또는 세션 상태에서 비롯되는 요청 수준의 권한 부여에 범위를 사용합니다:

use adk_auth::{ContextScopeResolver, ScopeGuard};

let guard = ScopeGuard::new(ContextScopeResolver);
let protected = guard.protect(my_tool);

RBAC + 범위 결합

RBAC는 "이 사용자가 도구에 액세스할 수 있는가?"에 답합니다. 범위는 "이 특정 요청이 지금 권한이 부여된 요청인가?"에 답합니다.

use std::sync::Arc;
use adk_auth::{AuthMiddleware, ContextScopeResolver, ScopeGuard};

let rbac = AuthMiddleware::new(ac);
let scoped = ScopeGuard::new(ContextScopeResolver);

let protected = scoped.protect(rbac.protect(transfer_tool));

SSO 통합

지원되는 공급자

제공자생성자발급자
GoogleGoogleProvider::new(client_id)accounts.google.com
Azure ADAzureADProvider::new(tenant, client) 또는 AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"])login.microsoftonline.com
OktaOktaProvider::new(domain, client){domain}/oauth2/default
Auth0Auth0Provider::new(domain, audience){domain}/
GenericOidcProvider::from_discovery(issuer, client)모든 OIDC 제공업체

AzureADProvider::multi_tenant()는 with_allowed_tenants(...)를 사용하여 명시적으로 제한하지 않는 한 구성된 대상의 모든 테넌트를 허용합니다.

TokenClaims

검증된 JWTs에서 추출된 클레임:

pub struct TokenClaims {
    pub sub: String,              // Subject (user ID)
    pub email: Option<String>,    // Email
    pub name: Option<String>,     // Display name
    pub groups: Vec<String>,      // IdP groups
    pub roles: Vec<String>,       // IdP roles
    pub hd: Option<String>,       // Google hosted domain
    pub tid: Option<String>,      // Azure tenant ID
    // ... more standard OIDC claims
}

ClaimsMapper

IdP 그룹을 adk-auth 역할에 매핑합니다:

let mapper = ClaimsMapper::builder()
    .map_group("AdminGroup", "admin")
    .map_group("Users", "viewer")
    .default_role("guest")
    .user_id_from_email()
    .build();

user_id_from_email()는 email_verified == true인 경우에만 이메일 클레임을 사용하며, 그렇지 않으면 sub로 대체합니다.

SsoAccessControl

한 번의 호출로 SSO 검증과 RBAC을 결합합니다:

let sso = SsoAccessControl::builder()
    .validator(GoogleProvider::new("client-id"))
    .mapper(mapper)
    .access_control(ac)
    .audit_sink(audit)
    .build()?;

// Validate token + check permission + audit log
let claims = sso.check_token(token, &Permission::Tool("search".into())).await?;

감사 로깅

FileAuditSink

let audit = FileAuditSink::new("/var/log/adk/audit.jsonl")?;
let middleware = AuthMiddleware::with_audit(ac, audit);

출력 형식 (JSONL)

{"timestamp":"2025-01-01T10:30:00Z","user":"bob","session_id":"sess-123","event_type":"tool_access","resource":"search","outcome":"allowed"}
{"timestamp":"2025-01-01T10:30:01Z","user":"bob","session_id":"sess-123","event_type":"tool_access","resource":"code_exec","outcome":"denied"}

사용자 지정 감사 싱크

use adk_auth::{AuditSink, AuditEvent, AuthError};
use async_trait::async_trait;

pub struct DatabaseAuditSink { /* ... */ }

#[async_trait]
impl AuditSink for DatabaseAuditSink {
    async fn log(&self, event: AuditEvent) -> Result<(), AuthError> {
        // Insert into database
        sqlx::query("INSERT INTO audit_log ...")
            .bind(event.user)
            .bind(event.resource)
            .execute(&self.pool)
            .await?;
        Ok(())
    }
}

예제

cargo check -p adk-auth
cargo check -p adk-auth --features sso

보안 모범 사례

실천 방법설명
기본적으로 거부명시적으로 필요한 권한만 부여
명시적 거부위험한 작업에 대한 거부 규칙 추가
모든 항목 감사규정 준수를 위해 로깅 활성화
서버 측에서 검증서버에서 항상 JWTs 검증
HTTPS 사용JWKS 엔드포인트에는 보안 연결 필요
키 순환JWKS 캐시는 매시간 자동으로 새로 고침
토큰 수명 제한수명이 짧은 액세스 토큰 사용
Azure 테넌트 제한다중 테넌트 Azure 앱의 경우 with_allowed_tenants(...) 구성
ID 매핑 전에 이메일 확인이메일이 확인되지 않은 경우 user_id_from_email()은(는) 이제 sub(으)로 대체됨
철회 계획 수립토큰 철회는 기본 제공되지 않으므로 즉시 차단이 필요한 경우 사용자 지정 검증기에서 적용
비용이 많이 드는 범위 조회 캐시ScopeResolver이 외부 시스템을 호출하는 경우 요청/세션별로 결과를 캐시합니다.

Auth Bridge

auth-bridge을 활성화하면 adk-auth이(가) adk-server을(를) 위한 재사용 가능한 JWT 기반 요청 추출기를 제공하도록 할 수 있습니다:

use adk_auth::auth_bridge::JwtRequestContextExtractor;
use adk_auth::sso::{ClaimsMapper, GoogleProvider};

let extractor = JwtRequestContextExtractor::builder()
    .validator(GoogleProvider::new("client-id"))
    .mapper(ClaimsMapper::builder().user_id_from_email().build())
    .build()?;

추출기는 Bearer 토큰을 검증하고, ClaimsMapper을(를) 사용하여 user_id을(를) 매핑한 다음, JWT scope / scp 클레임을 RequestContext.scopes(으)로 전달합니다.

Secret Providers

도구는 ToolContext::get_secret 및 InvocationContext::get_secret을(를) 통해 런타임 시크릿에 접근합니다. 이들 뒤에는 adk_auth::secrets::SecretProvider이(가) 있으며, 클라우드 구현은 기능 플래그 뒤에 있습니다:

공급자기능
AWS Secrets Manageraws-secrets
Azure Key Vaultazure-keyvault
GCP 보안 비밀 관리자gcp-secrets

실행에 하나를 연결하려면 다음과 같이 감싸세요: SecretService

use adk_auth::secrets::{CachedSecretProvider, SecretProvider, SecretServiceAdapter};
use std::sync::Arc;
use std::time::Duration;

// Any SecretProvider — here wrapped in the cache
let cached = Arc::new(CachedSecretProvider::new(provider, Duration::from_secs(300)));
let service = Arc::new(SecretServiceAdapter::new(cached));

도구별 권한 부여

기본적으로 컨텍스트를 보유한 도구는 어떤 비밀 값이든 이름으로 지정할 수 있으며, 제공자는 해당 이름만 확인합니다. 즉, 날씨 도구가 자체 API 키를 요청하는 경우와 동일한 도구가 결제 자격 증명을 요청하는 경우를 구분할 수 없습니다. AuthorizingSecretService는 제공자에게 요청하기 전에 도구별로 이를 결정합니다.

use adk_auth::secrets::authorizing::{AuthorizingSecretService, SecretGrant};
use std::sync::Arc;

let service = Arc::new(
    AuthorizingSecretService::new(inner)
        .grant("weather_lookup", SecretGrant::none().name("weather-api-key"))
        .grant("charge_card", SecretGrant::none().prefix("billing/"))
        .with_audit_sink(audit_sink),
);
규칙동작
도구에 이름을 포함하는 grant가 있음허용됨
도구에 이름을 포함하지 않는 grant가 있음거부됨; provider는 호출되지 않음
도구에 권한 부여가 없음거부됨
요청에 도구 식별자가 없음grant_untooled에서 열지 않는 한 거부됨

모든 항목은 허용될 때까지 거부되며, 거부되면 Unauthorized 오류가 반환됩니다. 거부된 이름은 절대 조회되지 않으므로, 제공자 측 액세스 로그에도 시도된 읽기로 나타나지 않습니다.

ID는 도구가 주장하는 것이 아닙니다. LlmAgent은 앱, 사용자, 세션 및 호출과 함께 디스패치된 도구의 이름을 요청에 기록하므로, 도구가 다른 도구의 ID를 내세울 수 없습니다. 도구가 추가할 수 있는 것은 목적뿐입니다.

// inside a tool
let key = ctx.get_secret_for_purpose("weather-api-key", "call the forecast endpoint").await?;

참고: 도구로 호출된 에이전트는 자체 ID를 전달하지 않는 ToolContext을 통과하므로, 해당 에이전트 내부에서 이루어진 액세스에는 내부 도구의 ID가 아니라 외부 에이전트의 ID가 표시됩니다. 이에 맞게 허용을 부여하세요.

액세스 감사

SecretAuditSink은 결정마다 하나의 SecretAccessDecision을 수신하며, 여기에는 결과, 비밀 이름, 도구, 사용자, 호출 및 이유가 포함되고 비밀 값은 절대 포함되지 않습니다. 허용은 info에, 거부는 warn에 기록됩니다.

캐싱

CachedSecretProvider은 TTL 동안 값을 제공한 다음 다시 가져옵니다. 이는 범위가 제한되고 철회할 수 있습니다:

제어동작
with_max_entries(n)최대 n개의 이름이 캐시되며, 캐시가 가득 차면 가장 오래 사용되지 않은 항목이 삭제됩니다. 기본값은 128이며, 0는 캐싱을 비활성화합니다
invalidate(name)보안 비밀 하나를 즉시 삭제합니다. 보안 비밀이 교체되었을 때 사용하면 TTL의 남은 기간 동안 이전 값이 제공되지 않습니다
invalidate_all()모든 항목 삭제
purge_expired()만료된 항목을 다시 읽을 때까지 기다리지 않고 삭제

입력에서 비밀 이름이 파생되는 경우 바인딩이 중요합니다. 바인딩이 없으면 캐시가 프로세스 수명 동안 계속 커질 수 있습니다.

캐시가 보장하는 것과 보장하지 않는 것

TTL은 캐시가 반환하는 항목을 제어하지만, 값이 프로세스 메모리에 얼마나 오래 남아 있는지는 제어하지 않습니다. 항목은 만료되거나 제거되거나 무효화될 때 제로화되며, 이에 따라 상주 시간이 대략 TTL으로 단축됩니다. 이는 삭제가 아니라 감소입니다. String은 이미 재할당되었거나, 할당자에 의해 복사되었거나, 디스크로 스왑되었거나, 코어 덤프에 포함되었을 수 있습니다. 캐시의 디버그 출력은 값이 진단 출력으로 유출되지 않도록 마스킹됩니다.

중요: 단순한 SecretProvider에는 자체 정책이 적용되지 않습니다. 컨텍스트를 보유한 모든 도구는 백업 자격 증명이 읽을 수 있는 모든 이름을 요청할 수 있습니다. AuthorizingSecretService로 감싸 도구별 경계를 설정하고, 클라우드 자격 증명 자체에도 범위를 지정하십시오. 배포마다 하나의 IAM ID를 사용하고 해당 배포에 필요한 비밀에만 액세스하도록 하십시오. 중요: 제공자 인터페이스는 비밀 이름만 받습니다. ADK 계층에는 도구별 권한 부여, 네임스페이스 또는 액세스 감사 기능이 없으므로, 컨텍스트를 보유한 모든 도구는 백업 자격 증명이 읽을 수 있는 모든 이름을 요청할 수 있습니다. 클라우드 자격 증명 자체에 범위를 지정하십시오. 배포마다 하나의 IAM ID를 사용하고 해당 배포에 필요한 비밀에만 액세스하도록 하며, 제공자 측 감사 로그를 액세스 기록으로 취급하십시오.

추출기가 보호하는 항목

추출기를 구성하면 모든 공개되지 않은 경로에 대해 인증이 활성화됩니다.

경로추출기가 구성된 경우의 동작
/api/sessions/*, /api/apps/*, artifacts, debug유효한 토큰이 없으면 401
/api/ui/* — bridge, notifications, resources유효한 토큰이 없으면 401; 인증된 사용자가 요청 본문에 지정된 사용자를 대체합니다
/api/run*유효한 토큰이 없어 401이 반환됨; 인증된 사용자가 제공된 사용자를 재정의함
/health공개

UI 브리지 상태는 요청 본문에서 가져온 (app_name, user_id, session_id)을 키로 사용하므로, 인증된 사용자가 본문 값을 대신하며 해당 값이 신뢰되지는 않습니다. 등록된 UI 리소스에는 이를 등록한 사용자가 기록됩니다. 해당 사용자만 이를 읽거나 교체할 수 있으며, 다른 사용자의 리소스를 읽으면 404가 반환되므로 URI의 존재가 공개되지 않습니다.

추출기가 구성되지 않은 경우 바인딩할 인증된 ID가 없으므로 라우트는 열려 있고 리소스는 전역에 공개된 상태로 유지됩니다. 인증은 선택 사항이지만, 단일 신뢰 사용자 환경이 아닌 모든 배포에서는 추출기를 구성해야 합니다.

오류 처리

use adk_auth::{AccessDenied, AuthError};
use adk_auth::sso::TokenError;

// RBAC errors
match ac.check("user", &Permission::Tool("admin".into())) {
    Ok(()) => { /* access granted */ }
    Err(AccessDenied { user, permission }) => {
        eprintln!("Denied: {} cannot access {}", user, permission);
    }
}

// SSO errors
match provider.validate(token).await {
    Ok(claims) => { /* token valid */ }
    Err(TokenError::Expired) => { /* token expired */ }
    Err(TokenError::InvalidSignature) => { /* signature invalid */ }
    Err(TokenError::InvalidIssuer { expected, actual }) => { /* wrong issuer */ }
    Err(e) => { /* other error */ }
}

이전: ← 평가 | 다음: 도구 권한 부여 →

A2A 엔드포인트

경로인증
GET /.well-known/agent.json공개 — 피어는 자격 증명을 보유하기 전에 카드를 가져옵니다
POST /a2aRequestContextExtractor가 구성된 경우 필요
POST /a2a/stream필수, RequestContextExtractor이 구성된 경우

JSON-RPC 경로는 에이전트 및 도구 작업을 실행하므로 세션, 아티팩트, 디버그 라우터와 동일한 레이어를 적용받습니다. 구성된 추출기가 없으면 요구할 자격 증명이 없고 경로가 계속 열려 있으므로, 관문을 추가해도 기존 배포가 중단되지 않습니다.

중요: 이러한 경로는 이전에 /api에 적용된 레이어 외부에서 라우터 루트에 병합되어 있었습니다. 다른 모든 변경 작업 인터페이스에 인증을 적용한 배포에서도 포트에 연결할 수 있는 모든 클라이언트가 에이전트를 구동하여 비용을 발생시킬 수 있었습니다. 이는 create_app_with_a2a 및 ServerBuilder::build 모두에 적용되었습니다.

A2aServer::builder()은 기본적으로 127.0.0.1:8080에 바인딩됩니다. 이를 노출하려면 bind_addr를 호출하되, 그 전에 추출기를 구성하십시오. 생성된 a2a-server 스캐폴드도 동일한 규칙을 따르며, 더 넓은 바인딩을 선택하려면 BIND_HOST를 읽습니다.