アクセス制御
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統合
サポート対象プロバイダー
| プロバイダー | コンストラクター | 発行者 |
|---|---|---|
GoogleProvider::new(client_id) | accounts.google.com | |
| Azure AD | AzureADProvider::new(tenant, client) または AzureADProvider::multi_tenant(client).with_allowed_tenants(["tenant-id"]) | login.microsoftonline.com |
| Okta | OktaProvider::new(domain, client) | {domain}/oauth2/default |
| Auth0 | Auth0Provider::new(domain, audience) | {domain}/ |
| 汎用 | OidcProvider::from_discovery(issuer, client) | 任意の OIDC プロバイダー |
AzureADProvider::multi_tenant() は、with_allowed_tenants(...) で明示的に制限しない限り、設定された audience に対して任意の tenant を受け入れます。
TokenClaims
検証済みの JWTs から抽出された claims:
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 の roles にマッピングします:
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 の場合にのみ email claim を使用します。それ以外の場合は sub にフォールバックします。
SsoAccessControl
SSO の検証と RBAC を 1 回の呼び出しで組み合わせます:
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
adk-auth が再利用可能な JWT ベースのリクエスト抽出器を adk-server に提供する場合は、auth-bridge を有効にします。
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 に転送します。
シークレットプロバイダー
ツールは ToolContext::get_secret と
InvocationContext::get_secret を通じて実行時シークレットにアクセスします。
これらの背後には adk_auth::secrets::SecretProvider があり、
クラウド実装は機能フラグの背後にあります。
| プロバイダー | 機能 |
|---|---|
| AWS Secrets Manager | aws-secrets |
| Azure Key Vault | azure-keyvault |
| GCP Secret Manager | gcp-secrets |
実行に 1 つをアタッチするには、次のように 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_untooled が開く場合を除き拒否 |
すべては許可されるまで拒否され、拒否されると Unauthorized エラーが返されます。拒否された名前が検索されることはないため、プロバイダー側のアクセスログに試行された読み取りとして現れることもありません。
アイデンティティは、ツールが主張するものではありません。LlmAgent は、アプリ、ユーザー、セッション、呼び出しとともに、ディスパッチされたツールの名前をリクエストに記録するため、ツールが別のツールのアイデンティティを提示することはできません。ツールが追加できるのは 目的 だけです。
// inside a tool
let key = ctx.get_secret_for_purpose("weather-api-key", "call the forecast endpoint").await?;
注: ツールとして呼び出されたエージェントは
ToolContextを通過します。これは独自のアイデンティティを持たないため、そのエージェント内部で行われたアクセスには、内部ツールのアイデンティティではなく外側のエージェントのアイデンティティが示されます。それに応じて許可を付与してください。
アクセスの監査
SecretAuditSink は、判定ごとに1つの SecretAccessDecision を受け取り、結果、シークレット名、ツール、ユーザー、呼び出し、理由を含めます。ただし、シークレットの値は決して含めません。許可は info に、拒否は warn にも記録されます。
キャッシュ
CachedSecretProvider は TTL の期間中、値を提供し、その後再取得します。これは制限されており、取り消しも可能です。
| 制御 | 動作 |
|---|---|
with_max_entries(n) | キャッシュされる名前は最大 n 個です。満杯になると、最も長い間使用されていないものが削除されます。デフォルトは 128 です。0 はキャッシュを無効にします |
invalidate(name) | 1 つのシークレットを直ちに削除します。シークレットをローテーションした際に使用すると、残りの TTL の間、古い値が提供されるのを防げます |
invalidate_all() | すべてを破棄 |
purge_expired() | 期限切れのエントリを、再度読み取られるのを待たずに破棄 |
入力からシークレット名が導出される場合、バインドが重要になります。バインドがなければ、キャッシュはプロセスの存続期間中増加し続ける可能性があります。
キャッシュが保証することと保証しないこと
TTL はキャッシュが 返す 内容を制御するものであり、値がプロセスメモリに保持される期間を制御するものではありません。エントリは期限切れ、追い出し、または無効化の際にゼロ化されるため、保持期間はおおよそ TTL まで短縮されます。これは削減であって消去ではありません — String はすでに再割り当てされたり、アロケーターによってコピーされたり、ディスクにスワップされたり、コアダンプに取り込まれたりしている可能性があります。キャッシュのデバッグ出力は秘匿化されているため、診断用の出力によって値が漏洩することはありません。
重要: 単独の
SecretProviderはそれ自体ではポリシーを適用しません — コンテキストを保持するツールは、基盤となる認証情報が読み取れる任意の名前を要求できます。ツールごとの境界を設けるにはAuthorizingSecretServiceでラップし、クラウド認証情報自体についてもスコープを限定してください。つまり、デプロイメントごとに 1 つの IAM ID を使用し、そのデプロイメントに必要なシークレットのみへのアクセスを許可します。 重要: プロバイダーインターフェースが受け取るのはシークレット 名 だけです。ADK レイヤーにはツールごとの許可、名前空間、アクセス監査は存在しないため、コンテキストを保持するツールは、基盤となる認証情報が読み取れる任意の名前を要求できます。クラウド認証情報自体のスコープを限定してください — デプロイメントごとに 1 つの IAM ID を使用し、そのデプロイメントに必要なシークレットのみへのアクセスを許可します — また、プロバイダー側の監査ログをアクセス記録として扱ってください。
抽出器が保護するもの
抽出器を設定すると、すべての非公開ルートで認証が有効になります。
| ルート | extractor が設定されている場合の動作 |
|---|---|
/api/sessions/*, /api/apps/*, 成果物、デバッグ | 有効なトークンがない場合は 401 |
/api/ui/* — ブリッジ、通知、リソース | 有効なトークンがない場合は 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 /a2a | RequestContextExtractor が設定されている場合は必須 |
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を読み取ります。