AWS Verified Permissions: Cedar Policies That Authorize Agent Tools

0 views

IAM decides whether the Lambda role can call S3 or Bedrock. It does not decide whether tenant Acme’s agent may call shell_exec on repo payments-api at 2am. That is application authorization. Teams bury it in if (ALLOWLIST.includes(tool)) until the list drifts and a prompt injects a tool name you forgot to remove. AWS Verified Permissions (AVP) + Cedar give you deny-by-default policies, IsAuthorized from the agent runtime, and an audit trail that IAM alone cannot provide.

⚡ TL;DR: Authorize each tool call with Cedar via Verified Permissions (IsAuthorized), scoping principal (tenant/user), action (tool name), and resource (repo/env). Keep IAM for AWS API blast radius (IAM Condition Keys for Agent Runtimes); keep AppConfig for emergency kills (AppConfig kill switches); keep schemas strict (tool schemas). Do not replace IAM with Cedar — layer them.

IAM vs Cedar vs allowlists

Layer Question it answers
IAM (role + condition keys) May this runtime call s3:PutObject on tagged buckets?
Cedar / AVP May this principal invoke tool deploy_prod on resource repo:payments?
AppConfig flags Is deploy_prod globally/incident-disabled right now?
Hardcoded allowlist Whatever someone committed last Tuesday
// ❌ Hardcoded allowlist — drifts, no tenant scope, no audit
const TOOLS = new Set(["read_file", "grep", "apply_patch", "shell_exec"]);
export function authorize(tool: string) {
  if (!TOOLS.has(tool)) throw new Error("denied");
}

✅ Express intent in Cedar; evaluate on every tool hop.

Cedar policy shape for agent tools

Model entities explicitly:

  • Principal: User::"{sub}" or Tenant::"{tenantId}"
  • Action: Action::"invokeTool" with context toolName
  • Resource: Repo::"{repoId}" or Environment::"prod"
// permit read-only tools for any authenticated tenant member
permit (
  principal in Tenant::"*",
  action == Action::"invokeTool",
  resource
)
when {
  context.toolName like "read_*" ||
  context.toolName == "grep" ||
  context.toolName == "list_files"
};

// deny shell in production unless role is breakglass
forbid (
  principal,
  action == Action::"invokeTool",
  resource == Environment::"prod"
)
when {
  context.toolName == "shell_exec" &&
  !(principal in Role::"breakglass")
};

// permit apply_patch only on repos the principal owns
permit (
  principal,
  action == Action::"invokeTool",
  resource
)
when {
  context.toolName == "apply_patch" &&
  resource in principal.ownedRepos
};

Store policies in an AVP policy store. Version them in git; deploy via CI. Prefer forbid for irreversible tools so a missing permit cannot accidentally allow deploy_prod.

IsAuthorized from the agent runtime

Call AVP before side effects — after schema validation, before Redis idempotency write.

import {
  VerifiedPermissionsClient,
  IsAuthorizedCommand,
} from "@aws-sdk/client-verifiedpermissions";

const avp = new VerifiedPermissionsClient({});
const POLICY_STORE_ID = process.env.AVP_POLICY_STORE_ID!;

export async function authorizeToolCall(input: {
  tenantId: string;
  userSub: string;
  toolName: string;
  resourceType: "Repo" | "Environment";
  resourceId: string;
  ownedRepos?: string[];
}): Promise<{ allowed: boolean; reasons?: string[] }> {
  const entities = {
    entityList: [
      {
        identifier: { entityType: "User", entityId: input.userSub },
        attributes: {
          tenantId: { string: input.tenantId },
        },
        parents: [{ entityType: "Tenant", entityId: input.tenantId }],
      },
      {
        identifier: {
          entityType: input.resourceType,
          entityId: input.resourceId,
        },
      },
    ],
  };

  const out = await avp.send(
    new IsAuthorizedCommand({
      policyStoreId: POLICY_STORE_ID,
      principal: { entityType: "User", entityId: input.userSub },
      action: { actionType: "Action", actionId: "invokeTool" },
      resource: {
        entityType: input.resourceType,
        entityId: input.resourceId,
      },
      context: {
        contextMap: {
          toolName: { string: input.toolName },
        },
      },
      entities,
    })
  );

  return {
    allowed: out.decision === "ALLOW",
    reasons: (out.determiningPolicies ?? []).map((p) => p.policyId ?? ""),
  };
}
// ✅ Gate in the tool runner
export async function handler(event: {
  tenantId: string;
  userSub: string;
  toolName: string;
  repoId: string;
  args: unknown;
}) {
  const authz = await authorizeToolCall({
    tenantId: event.tenantId,
    userSub: event.userSub,
    toolName: event.toolName,
    resourceType: "Repo",
    resourceId: event.repoId,
  });
  if (!authz.allowed) {
    return { ok: false, error: "cedar_denied", policies: authz.reasons };
  }
  if (!(await isToolEnabled(event.toolName))) {
    return { ok: false, error: "appconfig_disabled" };
  }
  return executeTool(event);
}

❌ Calling AVP only at session start and caching “all tools allowed” for an hour misses mid-session policy updates and role revocation. Cache negative briefly if you must; re-check irreversible tools every time.

Batch authz and latency

IsAuthorized adds a network hop. Mitigations:

  • Use AVP authorization caching where available / short TTL local memo keyed by (principal, action, resource, toolName)
  • Batch independent tool calls only after a single broader check when Cedar permits it (rare — prefer per-tool)
  • Place the policy store in the same region as agent Lambdas
  • Fail closed on AccessDeniedException / throttling — same posture as AppConfig

Combine with Bedrock ApplyGuardrail for content filters: Cedar answers may they call this tool; Guardrails answer is this argument/result safe.

Vs hardcoded allowlists and IAM-only

IAM condition keys (blast radius by tag) constrain what the execution role can touch in AWS. Cedar constrains what the user/tenant requested inside your product. You need both:

  1. IAM: tool Lambda role can only write arn:aws:s3:::agent-artifacts-${env}/* with aws:ResourceTag/tenant=${tenant}
  2. Cedar: user may only apply_patch on repos they own
  3. AppConfig: kill apply_patch globally during an incident

Skipping Cedar and stuffing tenant rules into IAM leads to role explosion. Skipping IAM and trusting Cedar alone leaves a compromised runtime with broad AWS power.

Operational checklist

  • [ ] Define Cedar entity model: User/Tenant, Action::invokeTool, Repo/Environment
  • [ ] Forbid irreversible tools by default; permit explicitly
  • [ ] Call IsAuthorized per tool invoke; fail closed on AVP errors
  • [ ] Layer IAM condition keys + AppConfig kills + Cedar
  • [ ] Version policies in git; review forbids as carefully as IAM
  • [ ] Log determining policy IDs (not secrets) for audit
  • [ ] Load-test IsAuthorized p99 under Map fan-out concurrency

Related reading

Last updated on September 20, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.