---
title: "AgentCore Policy — Cedar at the Gateway boundary, with forbid-wins, default-deny, and NL2Cedar"
date: 2026-06-03
service: "Amazon Bedrock AgentCore"
component: "Policy"
tags: [agentcore, policy, cedar, gateway, oauth-user, iam-entity, default-deny, forbid-wins, nl2cedar, policy-engine, automated-reasoning, quotas]
source: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html
verified_on: 2026-06-03
url: https://vanemmerik.ai/aws-ai/2026-06-03.html
---

# AWS Bedrock & AgentCore · Tip of the Day · 2026-06-03

## AgentCore Policy — Cedar at the Gateway boundary, with forbid-wins, default-deny, and NL2Cedar

**AgentCore Policy** is the rule engine yesterday's tip teased — the
last component that fires before a tool call ever reaches Lambda, an
OpenAPI target, an MCP server, or a Browser session. It sits in front
of an AgentCore Gateway, speaks open-source **Cedar**, and enforces
default-deny, forbid-wins authorization on every `tools/call`.

    $ agentcore add policy-engine --name RefundPolicyEngine \
        --attach-to-gateways PolicyGateway \
        --attach-mode ENFORCE

≈ 9 min read · Bedrock AgentCore · Policy

---

## 01 · The problem Policy in AgentCore exists to solve

The agent loop is non-deterministic by design. The model decides which
tool to call, with what arguments, and when. Most of the time that's
exactly what you want — but "most of the time" isn't an audit posture,
and "the model decided" isn't a control. From the [Policy overview
page](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html):

> "AI agents can dynamically adapt to solve complex problems… However,
> this flexibility introduces new security challenges, as agents may
> inadvertently misinterpret business rules, or act outside their
> intended authority."

Three failure modes you've probably seen:

- **Prompt-injection-as-privilege-escalation.** A tool result tells the
  model to call `RefundTool___process_refund` with `amount: 999999`,
  and it does.
- **Identity confusion.** The agent invokes a high-blast-radius tool
  on behalf of a user who shouldn't have been able to reach it
  directly.
- **In-code "guardrails".** The team writes `if user.role == "admin":`
  in the agent prompt or the tool body, and that check is the only
  thing standing between a junior user and an irreversible action.

> **The shift.** Policy moves authorization *out* of the agent's prose
> and *into* a declarative engine. The engine speaks Cedar, runs at the
> Gateway boundary, defaults to deny, and lets `forbid` always win.

---

## 02 · How Cedar is wired into a Gateway

You need three pieces, in order. From the [Core concepts
page](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-core-concepts.html):

| Resource | What it is | Lifecycle |
| --- | --- | --- |
| **Gateway** | The MCP endpoint your agent already calls. Day 5's tip. | Created first; policy is bolted on. |
| **Policy engine** | A namespaced collection of Cedar policies plus an auto-generated **Cedar schema** derived from your gateway's tool definitions. | Created via `CreatePolicyEngine` and attached to one or more gateways. |
| **Policy** | An individual Cedar statement (≤ **10 KB**), validated against the engine's schema at creation time. | Attached to an engine. |

The two attach modes (set on `agentcore add policy-engine
--attach-mode`):

- **`ENFORCE`** — every `tools/call` is intercepted, every policy is
  evaluated, every denial is returned to the agent as an error.
- **`MONITOR`** — same evaluation, but the answer doesn't gate the
  call. Decisions still log to CloudWatch. This is how you ship Cedar
  in shadow before flipping it on.

The minimal CLI to wire it up, from the
[Getting Started](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-getting-started.html):

    agentcore add gateway --name PolicyGateway --authorizer-type NONE \
                          --runtimes PolicyDemo

    agentcore add gateway-target --name RefundTarget \
        --type lambda-function-arn \
        --lambda-arn <YOUR_LAMBDA_ARN> \
        --tool-schema-file refund_tools.json \
        --gateway PolicyGateway

    agentcore add policy-engine --name RefundPolicyEngine \
        --attach-to-gateways PolicyGateway --attach-mode ENFORCE

    agentcore add policy --name RefundLimit \
        --engine RefundPolicyEngine \
        --source refund_policy.cedar

A small bootstrap pitfall worth knowing: policies that reference a
specific gateway ARN in the `resource` field need a **two-phase
deploy** — first create the gateway, grab the ARN from `agentcore
status`, then write the Cedar file. Cedar does not allow wildcards on
the resource.

---

## 03 · Anatomy of a Cedar policy

Every Cedar statement has the same three slots — **scope** (principal,
action, resource), **effect** (`permit` or `forbid`), and an optional
**condition** (`when` or `unless`). The canonical refund policy from
the [Understanding Cedar
docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-understanding-cedar.html):

    permit(
      principal is AgentCore::OAuthUser,
      action == AgentCore::Action::"RefundTool___process_refund",
      resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/refund-gateway"
    )
    when {
      principal.hasTag("username") &&
      principal.getTag("username") == "John" &&
      context.input.amount < 500
    };

Read it left to right: when the principal is an OAuth user named
`"John"`, calling the `RefundTool___process_refund` tool on the
`refund-gateway` Gateway, *and* the refund amount is under $500, the
request is permitted. Any of those clauses fails, the rule doesn't
match. If no rule matches, the gateway returns DENY (see §5).

The two principal types you can name in the scope, from the [Policy
scope page](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-scope.html):

- **`AgentCore::OAuthUser`** — when the gateway uses OAuth/JWT inbound
  auth. The principal ID is the JWT `sub` claim; every other claim
  (`username`, `scope`, `role`, `department`, …) is stamped onto the
  entity as **tags** and read with `principal.getTag("…")`.
- **`AgentCore::IamEntity`** — when the gateway uses `AWS_IAM` inbound
  auth. The principal has an `id` attribute containing the IAM ARN
  (assumed-role form: `arn:aws:sts::<account>:assumed-role/<role>`).

Actions are exact-match. There is **no wildcard on actions** — you
either name the tool (`AgentCore::Action::"ToolName___operation"`) or
you group tools under a **Gateway Target** and write the rule against
that target (`action in AgentCore::Action::"ReadToolsTarget"`).

---

## 04 · The authorization request the gateway builds

For every `tools/call`, the Gateway constructs a Cedar request from
two inputs — the JWT (or IAM identity) and the MCP tool payload —
and hands it to the policy engine. From the [Authorization flow
doc](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-authorization-flow.html):

    {
      "principal": "AgentCore::OAuthUser::\"12345678-1234-1234-1234-123456789012\"",
      "action":    "AgentCore::Action::\"RefundTool___process_refund\"",
      "resource":  "AgentCore::Gateway::\"arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/refund-gateway\"",
      "context": {
        "input": {
          "orderId": "12345",
          "amount":  450,
          "reason":  "Defective product"
        }
      }
    }

Two things to notice. The tool's input parameters land in
`context.input.*` exactly as the agent submitted them — that's the
**only** way a Cedar rule can react to "what is being requested,"
which is why the refund-amount condition above is on
`context.input.amount`. And the JWT claims arrive as **entity tags**,
not as nested object access — you reach them through
`principal.getTag("username")`, not `principal.username`.

---

## 05 · Default-deny, forbid-wins, and why both matter

Cedar's evaluation algorithm, verbatim from the [Understanding Cedar
page](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-understanding-cedar.html):

1. If any `forbid` policy matches, the decision is **DENY**.
2. If no `forbid` matches and at least one `permit` matches, the
   decision is **ALLOW**.
3. If nothing matches, the decision is **DENY** (default deny).

Two non-obvious corollaries:

- **`forbid` is not redundant with default-deny.** Default-deny only
  catches the absence of a `permit`. A `forbid` rule actively beats a
  matching `permit`. That's how you write "all users may view model
  results, *except* high-sensitivity ones" without rewriting the
  `permit`. From the
  [NL2Cedar](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-natural-language.html)
  guide:

      // Broad permit
      permit(principal is AgentCore::OAuthUser,
             action   == AgentCore::Action::"ModelAPI___view_results",
             resource == AgentCore::Gateway::"...:gateway/model");

      // Forbid overrides for the sensitive subset
      forbid(principal is AgentCore::OAuthUser,
             action   == AgentCore::Action::"ModelAPI___view_results",
             resource == AgentCore::Gateway::"...:gateway/model")
      when { context.input.sensitivity == "high" };

- **`unless` on `forbid` is not a grant.** It only narrows the
  `forbid`'s scope. The docs are explicit: "A `forbid` policy can
  never result in an ALLOW decision." If nothing else matches, the
  result is still DENY.

When a request is denied, the gateway returns an MCP error result with
the text `AuthorizeActionException - Tool Execution Denied`, including
the reason — usually "*No policy applies to the request (denied by
default).*"

---

## 06 · Tool listing is a separate, weaker check

The first thing an MCP client does is call `tools/list`. The policy
engine evaluates that call too — but it can't, by definition, know the
input parameters yet. From the [Use a Gateway with
Policy](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/use-gateway-with-policy.html)
doc:

> "A principal is only allowed to see tools in the listing that they
> would be permitted to call by policy. Because the full context of a
> tool call is not available during listing, this means a principal is
> allowed to list a tool **if there exists any set of circumstances
> under which a call to that tool would be permitted**."

So `tools/list` returns the **superset** — every tool the principal
*might* be allowed to call. The real authorization happens on
`tools/call`, where the gateway has the actual input parameters and
can evaluate `context.input.*` conditions. A tool appearing in the
list is not a guarantee that calling it will succeed.

---

## 07 · NL2Cedar — natural-language authoring with automated reasoning

You don't have to write Cedar by hand. The CLI's `--generate` flag
calls the **policy authoring service**, which takes a sentence and
returns a Cedar policy validated against the gateway's auto-generated
schema. Example from the [NL2Cedar
docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-natural-language.html):

> *"Allow principal with username 'refund-agent' to process refunds
> when the refund amount is less than $500."*

…produces exactly the canonical refund policy in §3. Three quiet
features worth knowing:

- **Schema-aware generation.** Because the engine's schema knows the
  tool's argument types, NL2Cedar refuses to write a rule against a
  field that doesn't exist or compares a string to a number.
- **Automated reasoning checks.** Generated policies are inspected for
  being **vacuous** (always allows), **dead** (never allows), or
  **trivially satisfiable** before they go live. This is the same
  Cedar analysis that powers [Cedar
  validation](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-core-concepts.html).
- **Geography-bounded inference.** Inference for NL2Cedar is routed
  within your geography (US, EU, APAC). Your data stays in the
  origination region; only the inference may roam within the geo.

The CLI shape:

    agentcore add policy --name RefundLimit \
        --engine RefundPolicyEngine \
        --generate "Only allow refunds under 1000 dollars" \
        --gateway PolicyGateway

The `--gateway` flag is required for `--generate` because the
service needs the deployed gateway ARN to resolve the schema.

---

## 08 · Limits worth knowing

From the [AgentCore Policy Service
Quotas](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/bedrock-agentcore-limits.html)
table:

- **Maximum policy size: 10 KB** per individual policy. Not
  adjustable. Composite rules belong in multiple policies, not in one
  giant statement.
- **Total policy size per resource: 200 KB.** Not adjustable.
- **Policies per engine: 1,000.** Not adjustable.
- **Policy engines per account per Region: 1,000.** Not adjustable.
- **Cedar schema size: 400 KB** per policy engine. The schema is the
  *combined* schema generated from all tools across all attached
  gateways. If you exceed it, the fix is to split engines — one per
  gateway, not one per agent.
- **Generated policies per engine: 50,000** over a rolling **7-day
  window**. Sized for CI-driven authoring, not for serving runtime
  authorization.
- **Throttling.** `CreatePolicyEngine` / `UpdatePolicyEngine` /
  `DeletePolicyEngine` / `StartPolicyGeneration` are **1 TPS**. Almost
  every other API on the list (`GetPolicy*`, `ListPolicy*`,
  `CreatePolicy`, `UpdatePolicy`, `DeletePolicy`) is **5 TPS**. These
  are control-plane limits — they do **not** cap data-plane
  authorization throughput, which scales with the Gateway.

Two gotchas not in the quota table:

- **Wildcards.** Cedar does not support `action == AgentCore::Action::*`.
  To group tools, define a **Gateway Target** and write the rule as
  `action in AgentCore::Action::"TargetName"`.
- **Two-phase deploy for ARN-scoped policies.** If your Cedar `resource`
  pins a specific gateway ARN (which is the recommended posture in
  production), you can't ship the policy on the very first deploy.
  Create the gateway, read back its ARN with `agentcore status`, then
  add the policy and redeploy.

---

## 09 · Try it in five minutes

Assuming you already have the AgentCore CLI installed and a project
scaffolded:

    # 1. Create a gateway with no inbound auth (tutorial only)
    agentcore add gateway --name PolicyGateway \
        --authorizer-type NONE --runtimes PolicyDemo

    # 2. Register a Lambda tool target
    agentcore add gateway-target --name RefundTarget \
        --type lambda-function-arn \
        --lambda-arn arn:aws:lambda:us-west-2:111122223333:function:refund \
        --tool-schema-file refund_tools.json \
        --gateway PolicyGateway

    # 3. Attach a policy engine in ENFORCE mode
    agentcore add policy-engine --name RefundPolicyEngine \
        --attach-to-gateways PolicyGateway \
        --attach-mode ENFORCE

    # 4. Deploy so the gateway ARN exists
    agentcore deploy

    # 5. Generate a policy from English (schema-aware, ARN auto-resolved)
    agentcore add policy --name RefundLimit \
        --engine RefundPolicyEngine \
        --generate "Only allow refunds under 1000 dollars" \
        --gateway PolicyGateway

    # 6. Test a denied call
    curl -s -X POST $(agentcore status -o gateway-url) \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc":"2.0","id":1,"method":"tools/call",
        "params":{"name":"RefundTarget___process_refund",
                  "arguments":{"orderId":"1","amount":5000}}
      }'

The response on the over-limit refund is an MCP `isError: true` with
`Tool Execution Denied: Tool call not allowed due to policy
enforcement`. Drop the amount under 1,000 and the same call succeeds.

Tomorrow we'll cover **AgentCore Evaluations** — three evaluation
types (online, on-demand, batch), the built-in LLM-judge evaluators
keyed `Builtin.*`, and the Lambda contract for a custom code-based
evaluator that returns `{label, value, explanation}`.

---

**Verified against the official AWS docs on 2026-06-03.**
Sources:
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-core-concepts.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-understanding-cedar.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-scope.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-authorization-flow.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-natural-language.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-getting-started.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/use-gateway-with-policy.html>,
<https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/bedrock-agentcore-limits.html>.

If the docs change, this lesson is a snapshot of that day — check the
sources for current behaviour.

---

> **This page — research, writing, verification, and deployment — was built by
> Claude Cowork.** No human touched the prose, the layout, or the upload
> pipeline. The lesson was generated this morning, cross-checked against the
> official AWS docs by an independent verification pass, and published
> to Cloudflare R2 on a schedule.
>
> A daily experiment by Monty van Emmerik · <https://vanemmerik.ai/>

— AWS Bedrock & AgentCore · Tip of the Day · No. 009 · vanemmerik.ai
