Danh tính cho AI agent: workload identity, ủy quyền và MCP authorization không cần key dùng chung

Workload identity cho biết pod nào đang gọi. AI agent cần nhiều hơn thế: nó đang hành động thay ai, và ngay lúc này được làm gì. Hướng dẫn thực tế về danh tính cho agent với token exchange RFC 8693, đặc tả MCP authorization, token vault, và các lựa chọn managed trên AWS, Google Cloud, Microsoft Entra.

Read in English →

Mọi bài trong loạt này đến giờ đều giải một bài toán: workload chứng minh nó là ai mà không cần key tĩnh. Đó là EKS Pod Identity và IRSA, workload identity trên GKE và AKS, federation giữa các cloud, và IAM Roles Anywhere cho máy nằm ngoài mọi cloud.

AI agent phá vỡ giả định đằng sau tất cả những thứ đó. Một workload bình thường chạy code bạn viết, nên bạn biết trước nó sẽ gọi API nào và cấp cho role của nó đúng những quyền đó. Agent thì chạy một model tự quyết định lúc runtime sẽ gọi tool nào, với tham số lấy một phần từ văn bản không đáng tin: email, ticket, trang web, output của tool khác. Và nó thường hành động thay cho một người cụ thể. Nếu bạn cho nó một service account đọc được ticket của mọi khách hàng, chỉ cần một prompt injection là nó làm đúng điều đó.

Vậy danh tính cho agent là workload identity cộng thêm hai câu hỏi nữa.

Ba câu hỏi cho mọi lời gọi của agent: agent là ai (workload identity), nó hành động thay ai (ủy quyền với claim sub và act), và ngay lúc này nó được làm gì (token theo từng tool, gắn audience, ít scope, sống ngắn)
Workload identity trả lời câu 1. Ủy quyền và token theo từng tool trả lời câu 2 và 3.

Những anti-pattern cần thay

  • Mỗi tool một API key, nằm trong biến môi trường của agent. Mọi user có quyền như nhau. Key không bao giờ hết hạn, rồi lọt vào log, trace, có khi vào cả context của model.
  • Agent chuyển tiếp token của user đi khắp nơi. Một token cấp cho app của bạn bị dùng lại với hệ thống ticket, CRM, và một MCP server ai đó mới cài tuần trước. Bất kỳ bên nào trong số đó cũng có thể dùng lại nó với các bên còn lại. Đặc tả MCP gọi việc này là token passthrough và cấm hẳn.
  • Một service account “toàn năng” cho cả nền tảng agent. Audit log ghi agent-platform@ làm mọi thứ. Không ai biết user nào yêu cầu, agent nào thực hiện, và vì sao.

Đích đến là: user vẫn là subject, agent được ghi nhận là actor, và mỗi tool chỉ thấy một token đúc riêng cho nó, chỉ với scope mà tác vụ này cần, sống vài phút.

Câu 1: danh tính riêng của agent

Agent là một workload, nên hãy bắt đầu từ mọi thứ đã có ở các bài trước. Trên Kubernetes, chạy mỗi loại agent dưới ServiceAccount riêng và federate lên cloud bằng Pod Identity, IRSA, GKE Workload Identity hoặc Entra Workload ID. Dùng SPIFFE/SPIRE nếu cần ID xuyên nền tảng. Đừng bao giờ dùng chung một danh tính cho các agent khác nhau.

Ba nền tảng lớn giờ cũng có danh tính thiết kế riêng cho agent:

AWS: AgentCore Identity Google Cloud: Agent Identity Microsoft Entra Agent ID
Agent là gì Một workload identity có ARN riêng, tạo tự động cho agent chạy trên AgentCore Runtime Một principal riêng cho mỗi agent với SPIFFE ID, ví dụ spiffe://agents.global.org-ORG_ID.system.id.goog/resources/… Một agent identity: service principal đặc biệt tạo từ agent identity blueprint
Credential Không có trong code. Runtime đưa cho agent một workload access token gắn cả agent lẫn user Chứng chỉ X.509 hiệu lực 24 giờ, tự xoay vòng. Access token Google gắn với chứng chỉ đó (mTLS) Agent identity không có credential riêng. Blueprint giữ credential và lấy token thay nó qua federated credential
Hành động thay user GetWorkloadAccessTokenForJWT nhận JWT của user, kiểm iss, chữ ký, hạn dùng OAuth token cho tool bên ngoài qua auth manager User token với subject là user, actor là agent identity
Token SaaS bên thứ ba Token vault, mã hoá KMS, 2LO (M2M) và 3LO (USER_FEDERATION) Auth manager (credential vault) Quyền trên blueprint, luồng OBO
Điểm quản trị đáng chú ý IAM có thể chặn đường …ForUserId không xác minh Không impersonate được, không có SA key; deploy lại là principal mới Sponsor (người chịu trách nhiệm), Conditional Access và vô hiệu hoá theo blueprint

Vài chi tiết quan trọng khi làm thật:

  • Google: trong IAM binding bạn dùng dạng principal:// của cùng định danh đó. Trust domain là agents.global.org-ORGANIZATION_ID.system.id.goog, hoặc agents.global.proj-PROJECT_NUMBER.system.id.goog với project không thuộc organization. Agent identity không bị xoá cùng agent, và agent deploy lại sẽ nhận principal mới. Hãy tự động hoá IAM binding (Terraform, Config Connector), nếu không mỗi lần deploy lại là mất quyền và để lại grant mồ côi.
  • AWS: ưu tiên GetWorkloadAccessTokenForJWT. Biến thể …ForUserId nhận bất kỳ chuỗi nào làm user, nên hãy giới hạn bằng IAM, hoặc chặn hẳn nếu bạn luôn có JWT:
{
  "Statement": [{
    "Sid": "DenyForUserIdAccess",
    "Effect": "Deny",
    "Action": "bedrock-agentcore:GetWorkloadAccessTokenForUserId",
    "Resource": "arn:aws:bedrock-agentcore:REGION:ACCOUNT_ID:workload-identity-directory/default"
  }]
}
  • Entra: blueprint là đơn vị kiểm soát. Nó là “loại” agent, ví dụ “Sales Assistant”, và mỗi instance được deploy nhận một agent identity riêng từ nó. Một Conditional Access policy hay một lần vô hiệu hoá trên blueprint áp cho mọi instance. Agent identity luôn single-tenant, kể cả khi blueprint là multitenant.

Câu 2: thay mặt ai, bằng token exchange

Với agent tự host, hoặc khi muốn không phụ thuộc cloud, công cụ chuẩn là OAuth 2.0 Token Exchange (RFC 8693). Agent gửi hai token tới STS của bạn (Keycloak, Okta, Entra, hay STS nội bộ):

  • subject token: token của user, chứng minh ai yêu cầu, và
  • actor token: workload token của chính agent, chứng minh agent nào đang xin.

Nó nhận về một token mới cho đúng một đích, với scope hẹp:

curl -s https://sts.example.com/oauth2/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
  -d subject_token="$USER_ACCESS_TOKEN" \
  -d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
  -d actor_token="$(cat /var/run/secrets/tokens/agent-token)" \
  -d actor_token_type=urn:ietf:params:oauth:token-type:jwt \
  -d resource=https://mcp.tickets.example.com \
  -d scope=tickets:read

actor_token ở đây là projected ServiceAccount token với audience là STS, đúng cơ chế mà IRSA và GKE dùng. Token được cấp mang chuỗi ủy quyền trong claim chuẩn act:

{
  "iss": "https://sts.example.com",
  "sub": "user-8f2c",
  "aud": "https://mcp.tickets.example.com",
  "scope": "tickets:read",
  "exp": 1792396800,
  "act": { "sub": "system:serviceaccount:agents:support-agent" }
}
User đăng nhập vào app; agent chạy với workload identity riêng; nó đổi token của user cộng token của chính nó tại STS lấy một token sống ngắn với sub=user, act=agent, aud=MCP server; MCP server kiểm audience và dùng credential riêng hoặc token vault cho API phía sau thay vì chuyển tiếp token
Quyền thực tế là phần giao: những gì user được làm, những gì agent này được làm, và những gì scope của tool này cho phép.

STS là nơi đặt policy. Trước khi cấp token, nó kiểm ba điều: agent này có được hành động thay user này, trên resource này, với các scope này không? Agent hỗ trợ khách hàng được đọc ticket cho user đang đăng nhập, nhưng không bao giờ lấy được billing:refund, dù một email có thuyết phục khéo đến đâu.

Câu 3: ngay lúc này được làm gì, với MCP authorization

Phần lớn tool của agent giờ là MCP server. Đặc tả MCP authorization (bản hiện tại 2026-07-28) là OAuth 2.1 thuần, cộng vài quy tắc đúng thứ agent cần:

Client nhận 401 kèm resource_metadata, đọc protected resource metadata để tìm authorization server, discover metadata của nó, tự định danh bằng URL Client ID Metadata Document, chạy authorization code với PKCE và tham số resource, kiểm iss, rồi gọi MCP server bằng token gắn audience; 403 insufficient_scope kích hoạt step-up
Discovery, định danh client, token gắn audience và step-up. Mỗi bước là một RFC chuẩn.
  1. Discovery. MCP server MUST công bố Protected Resource Metadata (RFC 9728) và trả 401 kèm WWW-Authenticate: Bearer resource_metadata="…". Nó SHOULD kèm gợi ý scope. Client tìm authorization server từ metadata đó.
  2. Định danh client. Client SHOULD dùng Client ID Metadata Document. client_id là một URL HTTPS mà authorization server sẽ tải về. Dynamic Client Registration giờ đã deprecated, chỉ giữ để tương thích ngược.
  3. Gắn audience. Client MUST gửi tham số resource (RFC 8707) là canonical URI của MCP server. Server MUST từ chối token không cấp cho nó.
  4. Không passthrough. MCP server MUST NOT chuyển tiếp token nó nhận. Với API phía sau, nó cần một token riêng cấp cho API đó: workload identity của chính nó, một lần token exchange khác, hoặc token vault.
  5. Step-up. Khi thiếu scope, server trả 403 với error="insufficient_scope" và các scope mà thao tác này cần. Client xin quyền lại với hợp của scope cũ và mới. Với agent có người dùng, đó chính là khoảnh khắc human-in-the-loop cho các hành động rủi ro.
  6. Chống mix-up. Client ghi lại issuer mong đợi và kiểm iss trong authorization response (RFC 9207).

Protected Resource Metadata của server ticket:

{
  "resource": "https://mcp.tickets.example.com",
  "authorization_servers": ["https://sts.example.com"],
  "scopes_supported": ["tickets:read"],
  "bearer_methods_supported": ["header"]
}

scopes_supported là mức tối thiểu cho dùng cơ bản. tickets:write được xin sau qua step-up, chỉ khi agent thực sự định ghi.

Phía server, phần kiểm tra rất ngắn. Đây là phiên bản Python với PyJWT:

import jwt

RESOURCE = "https://mcp.tickets.example.com"
jwks = jwt.PyJWKClient("https://sts.example.com/.well-known/jwks.json")

def authorize(bearer: str, needed_scope: str) -> dict:
    key = jwks.get_signing_key_from_jwt(bearer).key
    claims = jwt.decode(
        bearer, key, algorithms=["RS256", "ES256"],
        audience=RESOURCE,                 # từ chối token đúc cho bên khác
        issuer="https://sts.example.com",
        options={"require": ["exp", "sub", "aud"]},
    )
    if needed_scope not in claims.get("scope", "").split():
        raise InsufficientScope(needed_scope)   # → 403 + WWW-Authenticate
    return {
        "user": claims["sub"],
        "agent": claims.get("act", {}).get("sub"),  # luôn log cả hai
    }

SSO doanh nghiệp cho MCP: ID-JAG

Người dùng trong công ty không nên phải bấm “Allow” cho từng MCP server. Extension Enterprise-Managed Authorization, đã stable trong repo extension auth của MCP, cho IdP của công ty quyết định thay:

  1. Client đổi ID token của user tại IdP (RFC 8693) lấy một ID-JAG (Identity Assertion JWT Authorization Grant). Nó gửi requested_token_type=urn:ietf:params:oauth:token-type:id-jag, với audience là issuer của authorization server phía MCP.
  2. IdP áp policy của admin, tức app này có được tới MCP server này cho user này không, rồi trả về một ID-JAG sống ngắn. Ví dụ trong extension dùng expires_in: 300.
  3. Client đưa nó cho authorization server của MCP qua JWT bearer grant (urn:ietf:params:oauth:grant-type:jwt-bearer, RFC 7523) và nhận access token gắn với đúng MCP server đó.

Hỗ trợ được công bố bằng urn:ietf:params:oauth:grant-profile:id-jag trong authorization_grant_profiles_supported. Với agent hoàn toàn tự động, không có user tham gia, có một extension client credentials riêng, lúc viết bài vẫn đang ở dạng draft.

SaaS bên thứ ba: token vault, không phải biến môi trường

Sớm muộn agent sẽ cần Google Drive, GitHub, Slack hay Jira với tư cách user. Đừng tự xây kho refresh token bên trong agent. Hãy dùng một vault chỉ trả token cho bên gọi chứng minh được agent + user:

from bedrock_agentcore.identity.auth import requires_access_token

@requires_access_token(
    provider_name="google-provider",
    scopes=["https://www.googleapis.com/auth/drive.metadata.readonly"],
    auth_flow="USER_FEDERATION",          # 3-legged: user đồng ý một lần
    on_auth_url=lambda url: stream_to_caller({"authorization_url": url}),
    force_authentication=False,
    callback_url="https://agent.example.com/oauth/callback",
)
async def list_drive_files(*, access_token: str):
    ...

Đó là AgentCore Identity. Auth manager của Google Agent Identity, hay một broker nội bộ dựa trên Vault, đều theo cùng khuôn:

  • Token được inject vào hàm tool, không vào prompt. Model không bao giờ nhìn thấy nó, nên không thể làm lộ nó.
  • Khoá của vault là (agent, user, provider). Agent A không đọc được token của agent B, và sự đồng ý của user X không bao giờ bị dùng cho user Y.
  • Thu hồi đồng ý chỉ cần làm ở một chỗ.

Checklist rào chắn

  • Mỗi loại agent một danh tính, không dùng chung, không bao giờ là tài khoản của người. Trên Entra, mỗi loại agent một blueprint.
  • Không secret tĩnh trong pod agent: workload identity cho cloud, token exchange hoặc vault cho mọi thứ khác.
  • Token nằm ngoài context của model, ngoài log và trace. Inject ở tầng tool và che header Authorization.
  • Mọi token cho tool đều gắn audience (resource / aud) và sống ngắn. Vài phút cho token exchange, không phải vài giờ.
  • Scope tối thiểu trước, step-up và xác nhận rõ ràng của user cho hành động ghi, xoá, thanh toán, gửi email.
  • Audit cả hai danh tính: sub (user) và act (agent) trên mọi lời gọi tool, gắn với trace ID của agent.
  • Kill switch ở ba mức: một instance agent (vô hiệu hoá identity), một loại agent (blueprint hoặc role), một tool (gỡ MCP server khỏi danh sách audience được phép của authorization server).
  • Vòng đời: mỗi agent identity có owner hoặc sponsor, và dọn IAM binding khi agent được deploy lại hoặc ngừng dùng.

Tổng kết

Workload identity đã giúp các service thoát khỏi key tĩnh. Agent cần thêm một bước, vì đường đi của code do model quyết định, còn thẩm quyền đến từ một con người. Hãy cho agent danh tính riêng, giữ user là subject bằng token exchange, và để mọi lời gọi tool mang một token hẹp, gắn audience, sống ngắn. Đặc tả MCP authorization, RFC 8693 và các agent identity managed trên AWS, Google Cloud, Entra đã có đủ mảnh ghép. Phần việc còn lại là nối chúng lại, với policy đặt ở STS và audit trên cả sub lẫn act.

Nguồn

Bình luận