Amazon Bedrock AgentCore Policy 实战:用 Cedar 策略语言精准控制 Agent 工具调用¶
Lab 信息
- 难度: ⭐⭐ 中级
- 预估时间: 60-90 分钟
- 预估费用: $5-10(AgentCore Gateway + Lambda 调用)
- Region: us-east-1
- 最后验证: 2026-03-24
背景¶
AI Agent 最大的风险之一是工具滥用——agent 可能误解业务规则,调用不该调用的工具,或传入超出权限范围的参数。传统方案是在 agent 代码里硬编码安全检查,但这有两个致命缺陷:
- 可绕过性:策略在代码内部,agent 可能通过 prompt injection 绕过
- 不可审计:每个 agent 实现自己的安全逻辑,无法统一管控
Amazon Bedrock AgentCore Policy(2026 年 3 月 GA)将安全策略从 agent 代码中剥离,放到 Gateway 边界执行:
策略引擎基于 Cedar——AWS 开源的策略语言(也用于 Amazon Verified Permissions)。每次 agent 调用工具时,Gateway 拦截请求,Cedar 引擎逐条评估策略,决定 allow 或 deny。策略执行在代码外部,不可被 agent 操纵。
前置条件¶
- AWS 账号(需要
bedrock-agentcore:*,lambda:*,iam:*,cognito-idp:*权限) - AWS CLI v2 已配置
- Python 3.10+
bedrock-agentcore-starter-toolkit(pip install bedrock-agentcore-starter-toolkit)
核心概念¶
架构组件¶
| 组件 | 作用 | 类比 |
|---|---|---|
| Policy Engine | 存储和管理 Cedar 策略集合 | IAM Policy Store |
| Cedar Policy | 定义 permit/forbid 规则 | IAM Policy Statement |
| Gateway | 流量拦截点,连接 Policy Engine | API Gateway |
| OAuth Authorizer | 识别调用者身份(principal) | Cognito Auth |
Cedar 授权请求结构¶
当 agent 通过 Gateway 调用工具时,Gateway 构造一个 Cedar 授权请求:
{
"principal": "AgentCore::OAuthUser::\"client-id\"",
"action": "AgentCore::Action::\"RefundTool___process_refund\"",
"resource": "AgentCore::Gateway::\"gateway-arn\"",
"context": {
"input": {
"amount": 500.0,
"customer_id": "C001"
}
}
}
类型映射(关键!)¶
JSON Schema 到 Cedar 的类型映射:
| JSON Schema type | Cedar type | 示例值 |
|---|---|---|
string |
String | "hello" |
integer |
Long | 42 |
number |
Decimal | 42.0 |
boolean |
Bool | true |
踩坑预警:integer vs number
如果工具 schema 声明参数为 number(映射到 Cedar Decimal),那么 JSON 传入的整数值(如 500)会被解析为 Long 类型,与 Decimal 类型不兼容。必须传入浮点值(如 500.0)。这是 Cedar 类型系统的严格性导致的,官方文档未明确记录此行为。
动手实践¶
Step 1: 创建 Lambda 工具(退款处理)¶
# 创建 Lambda 执行角色
cat > /tmp/lambda-trust.json << EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role \
--role-name AgentCorePolicyTestLambdaRole \
--assume-role-policy-document file:///tmp/lambda-trust.json \
--region us-east-1
aws iam attach-role-policy \
--role-name AgentCorePolicyTestLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# refund-tool.py — 退款工具 Lambda
import json
def lambda_handler(event, context):
body = event
if isinstance(event.get("body"), str):
body = json.loads(event["body"])
amount = body.get("amount", 0)
customer_id = body.get("customer_id", "unknown")
reason = body.get("reason", "no reason")
return {
"statusCode": 200,
"body": json.dumps({
"status": "approved",
"refund_id": f"REF-{customer_id}-{int(amount)}",
"amount": amount,
"customer_id": customer_id,
"message": f"Refund of ${amount} processed for {customer_id}"
})
}
# 打包并创建 Lambda
cd /tmp && zip refund-tool.zip refund-tool.py
aws lambda create-function \
--function-name AgentCorePolicyRefundTool \
--runtime python3.12 \
--handler refund-tool.lambda_handler \
--role arn:aws:iam::YOUR_ACCOUNT:role/AgentCorePolicyTestLambdaRole \
--zip-file fileb:///tmp/refund-tool.zip \
--timeout 30 \
--region us-east-1
Step 2: 创建 OAuth 认证¶
Policy Engine 需要 JWT 来识别 principal(不支持 NONE auth)。使用 starter toolkit 创建 Cognito OAuth:
from bedrock_agentcore_starter_toolkit.operations.gateway.client import GatewayClient
client = GatewayClient(region_name="us-east-1")
cognito_response = client.create_oauth_authorizer_with_cognito("PolicyTest")
# 保存 cognito_response,后续创建 Gateway 时使用
这会自动创建:
- Cognito User Pool + Domain
- Resource Server(scope: PolicyTest/invoke)
- App Client(client_credentials flow)
Step 3: 创建 Policy Engine + Gateway¶
# 创建 Policy Engine
aws bedrock-agentcore-control create-policy-engine \
--name PolicyTestEngine \
--description "Policy engine for refund control" \
--region us-east-1
# 记录返回的 policyEngineId
# 创建 Gateway(附加 Policy Engine,ENFORCE 模式)
cat > /tmp/create-gateway.json << EOF
{
"name": "RefundPolicyGateway",
"roleArn": "arn:aws:iam::YOUR_ACCOUNT:role/AgentCoreGatewayExecutionRole",
"protocolType": "MCP",
"protocolConfiguration": {"mcp": {"supportedVersions": ["2025-03-26"]}},
"authorizerType": "CUSTOM_JWT",
"authorizerConfiguration": {
"customJWTAuthorizer": {
"discoveryUrl": "https://cognito-idp.us-east-1.amazonaws.com/YOUR_POOL_ID/.well-known/openid-configuration",
"allowedClients": ["YOUR_CLIENT_ID"],
"allowedScopes": ["PolicyTest/invoke"]
}
},
"policyEngineConfiguration": {
"arn": "arn:aws:bedrock-agentcore:us-east-1:YOUR_ACCOUNT:policy-engine/YOUR_ENGINE_ID",
"mode": "ENFORCE"
},
"exceptionLevel": "DEBUG"
}
EOF
aws bedrock-agentcore-control create-gateway \
--cli-input-json file:///tmp/create-gateway.json \
--region us-east-1
Step 4: 添加 Gateway Target¶
cat > /tmp/create-target.json << EOF
{
"gatewayIdentifier": "YOUR_GATEWAY_ID",
"name": "RefundTool",
"description": "Process customer refunds",
"targetConfiguration": {
"mcp": {
"lambda": {
"lambdaArn": "arn:aws:lambda:us-east-1:YOUR_ACCOUNT:function:AgentCorePolicyRefundTool",
"toolSchema": {
"inlinePayload": [{
"name": "process_refund",
"description": "Process a customer refund",
"inputSchema": {
"type": "object",
"properties": {
"amount": {"type": "number", "description": "Refund amount in USD"},
"customer_id": {"type": "string", "description": "Customer ID"},
"reason": {"type": "string", "description": "Refund reason"}
},
"required": ["amount", "customer_id"]
}
}]
}
}
}
},
"credentialProviderConfigurations": [{"credentialProviderType": "GATEWAY_IAM_ROLE"}]
}
EOF
aws bedrock-agentcore-control create-gateway-target \
--cli-input-json file:///tmp/create-target.json \
--region us-east-1
Step 5: 创建 Cedar 策略¶
方式一:自然语言生成
aws bedrock-agentcore-control start-policy-generation \
--policy-engine-id YOUR_ENGINE_ID \
--resource {arn:YOUR_GATEWAY_ARN} \
--content {rawText:Allow the process_refund tool from RefundTool to be called only when the refund amount is less than 1000} \
--name RefundLimit \
--region us-east-1
约 17 秒后,生成的 Cedar 策略:
permit(
principal,
action == AgentCore::Action::"RefundTool___process_refund",
resource == AgentCore::Gateway::"YOUR_GATEWAY_ARN"
) when {
((context.input).amount).lessThan(decimal("1000.0"))
};
方式二:直接写 Cedar
cat > /tmp/refund-policy.json << EOF
{
"name": "RefundLimitPolicy",
"policyEngineId": "YOUR_ENGINE_ID",
"description": "Permit refunds under 1000 USD",
"definition": {
"cedar": {
"statement": "permit(\n principal,\n action == AgentCore::Action::\"RefundTool___process_refund\",\n resource == AgentCore::Gateway::\"YOUR_GATEWAY_ARN\"\n) when {\n ((context.input).amount).lessThan(decimal(\"1000.0\"))\n};"
}
}
}
EOF
aws bedrock-agentcore-control create-policy \
--cli-input-json file:///tmp/refund-policy.json \
--region us-east-1
Step 6: 测试策略效果¶
import json, requests
GATEWAY_URL = "https://YOUR_GATEWAY_ID.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp"
TOKEN_URL = "https://YOUR_DOMAIN.auth.us-east-1.amazoncognito.com/oauth2/token"
# 获取 OAuth token
token_resp = requests.post(TOKEN_URL,
data={"grant_type": "client_credentials",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"scope": "PolicyTest/invoke"},
headers={"Content-Type": "application/x-www-form-urlencoded"})
access_token = token_resp.json()["access_token"]
def call_refund(amount):
payload = json.dumps({
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "RefundTool___process_refund",
"arguments": {"amount": amount, "customer_id": "C001", "reason": "test"}}
})
resp = requests.post(GATEWAY_URL, data=payload,
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}"})
return resp.json()
# 测试!
print(call_refund(500.0)) # ✅ ALLOWED
print(call_refund(2000.0)) # ❌ DENIED by policy
测试结果¶
基线测试(ENFORCE 模式)¶
| 场景 | 金额 | JSON 类型 | 预期 | 实际 | 延迟 |
|---|---|---|---|---|---|
| 正常允许 | $500.0 | float | ALLOW | ✅ ALLOW | 984ms |
| 正常拒绝 | $2000.0 | float | DENY | ✅ DENY | 907ms |
| 边界允许 | $999.99 | float | ALLOW | ✅ ALLOW | 973ms |
| 边界拒绝 | $1000.0 | float | DENY | ✅ DENY | 918ms |
| 零值 | $0.01 | float | ALLOW | ✅ ALLOW | 973ms |
| 负值 | $-100.0 | float | ALLOW | ✅ ALLOW | 1014ms |
类型不匹配实验¶
| 金额值 | JSON 传入类型 | Cedar 期望 | 结果 |
|---|---|---|---|
500 |
Long (integer) | Decimal | ❌ 评估错误 |
500.0 |
Decimal (float) | Decimal | ✅ 正常允许 |
"500" |
String | Decimal | ❌ 评估错误 |
关键发现:Cedar 类型系统严格,number 类型参数在 JSON 中必须以浮点格式传入(500.0),不能是整数(500)。
ENFORCE vs LOG_ONLY 模式¶
| 模式 | $500.0 | $2000.0 | 行为 |
|---|---|---|---|
| ENFORCE | ✅ ALLOW | ❌ DENY | 策略生效,违规请求被阻断 |
| LOG_ONLY | ✅ ALLOW | ✅ ALLOW | 策略只记录到 CloudWatch,不阻断 |
建议:上线前先用 LOG_ONLY 观察策略效果,确认无误后再切换到 ENFORCE。
多策略组合(permit + forbid)¶
同时存在两条策略:
- permit: 允许 amount < 1000
- forbid: 禁止 amount >= 500
| 金额 | permit 匹配? | forbid 匹配? | 最终结果 |
|---|---|---|---|
| $100.0 | ✅ | ❌ | ALLOWED |
| $499.99 | ✅ | ❌ | ALLOWED |
| $500.0 | ✅ | ✅ | DENIED (forbid wins) |
| $750.0 | ✅ | ✅ | DENIED (forbid wins) |
| $1000.0 | ❌ | ✅ | DENIED |
关键发现:Cedar 的 "explicit deny wins" 模型与 AWS IAM 一致——forbid 永远覆盖 permit。
自然语言策略生成¶
| 指标 | 值 |
|---|---|
| 生成时间 | ~17 秒 |
| 准确性 | 生成的 Cedar 与手写等效 |
| 验证 | 过度宽松/过度限制策略被自动检测 |
踩坑记录¶
踩坑 1: NONE authorizer 不支持 Policy
Gateway 使用 authorizerType: NONE 时,Policy Engine 会返回 "Policy Evaluation Internal Failure"。原因:Cedar 策略的 principal 必须是 AgentCore::OAuthUser,需要 JWT token 提供身份信息。已查文档确认:principal 必须是 OAuthUser。
踩坑 2: Gateway authorizer 创建后不可更改
已创建的 Gateway 不能从 NONE 改为 CUSTOM_JWT(返回 "Authorizer type cannot be updated")。如果一开始选错了 auth 方式,只能删除重建。已查文档确认:这是 API 限制。
踩坑 3: integer vs number 类型严格
JSON 的 500(integer/Long)和 500.0(float/Decimal)在 Cedar 中是不同类型。如果工具 schema 将参数声明为 number(映射到 Decimal),必须确保 JSON 传入浮点值。实测发现,官方未明确记录此行为。
踩坑 4: 必须使用 client_credentials 流
Cognito User Password Auth 返回的 token 会报 "insufficient_scope"。必须使用 OAuth 2.0 client_credentials 流,并在 Cognito Resource Server 中配置自定义 scope。已查文档确认。
踩坑 5: 无条件 permit 被验证器拒绝
创建无任何 when 条件的 permit 策略,会被标记为 "Overly Permissive" 并拒绝创建(除非设置 validationMode: IGNORE_ALL_FINDINGS)。这是一个安全护栏。
费用明细¶
| 资源 | 单价 | 用量 | 费用 |
|---|---|---|---|
| AgentCore Gateway | 按请求计费 | ~50 requests | ~$0.01 |
| Lambda (128MB) | $0.20/1M requests | ~50 invocations | ~$0 (Free Tier) |
| Cognito User Pool | Free Tier (50K MAU) | 1 user | $0 |
| 合计 | < $1 |
清理资源¶
REGION=us-east-1
PROFILE=your-profile
# 1. 删除策略
aws bedrock-agentcore-control delete-policy \
--policy-id YOUR_POLICY_ID \
--policy-engine-id YOUR_ENGINE_ID \
--region $REGION --profile $PROFILE
# 2. 删除 Gateway Target
aws bedrock-agentcore-control delete-gateway-target \
--gateway-identifier YOUR_GATEWAY_ID \
--target-id YOUR_TARGET_ID \
--region $REGION --profile $PROFILE
# 3. 删除 Gateway
aws bedrock-agentcore-control delete-gateway \
--gateway-identifier YOUR_GATEWAY_ID \
--region $REGION --profile $PROFILE
# 4. 删除 Policy Engine
aws bedrock-agentcore-control delete-policy-engine \
--policy-engine-id YOUR_ENGINE_ID \
--region $REGION --profile $PROFILE
# 5. 删除 Lambda
aws lambda delete-function \
--function-name AgentCorePolicyRefundTool \
--region $REGION --profile $PROFILE
# 6. 删除 Cognito User Pool
aws cognito-idp delete-user-pool \
--user-pool-id YOUR_POOL_ID \
--region $REGION --profile $PROFILE
# 7. 删除 IAM Role
aws iam detach-role-policy \
--role-name AgentCorePolicyTestLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name AgentCorePolicyTestLambdaRole
务必清理
Lab 完成后请执行清理步骤,避免产生意外费用。
结论与建议¶
适用场景¶
- 金融合规:限制退款/转账金额上限
- 多租户 Agent:不同用户只能访问自己权限范围的工具
- 安全运维:紧急关闭某个工具(forbid + no condition = emergency shutdown)
- 灰度发布:LOG_ONLY 模式观察新策略效果
生产环境建议¶
- 先 LOG_ONLY 后 ENFORCE:新策略上线先用日志模式观察,确认无误再切换
- 注意 Cedar 类型匹配:JSON Schema 用
number类型时,确保 agent 发送浮点值 - 利用自然语言生成 + 人工审核:自然语言生成策略效果不错,但建议生成后人工审核 Cedar
- 利用 forbid 做安全兜底:permit 定义允许范围,forbid 做硬性上限
- 策略验证器是好东西:过度宽松/限制/不可满足策略都会被检测
与现有方案对比¶
| 维度 | AgentCore Policy | 代码内策略 | AWS IAM |
|---|---|---|---|
| 执行位置 | Gateway 边界 | Agent 内部 | API 层 |
| 可绕过性 | ❌ 不可 | ⚠️ 可被 agent 操纵 | ❌ 不可 |
| 粒度 | 工具 + 参数级 | 自定义 | API 操作级 |
| 审计 | CloudWatch 自动 | 需自行实现 | CloudTrail |
| 动态调整 | 改策略不改代码 | 需改代码重部署 | 改 IAM Policy |