LLM-flavored authorizers are a trap when the model becomes the gate. The unfair pattern: keep hard allow/deny in IAM, Cedar, or a deterministic policy engine, and use Bedrock only to explain why a decision happened for humans and audit UIs. Prototype explanations inside Lambda authorizers; never let token probabilities decide production access.
⚡ TL;DR: Evaluate Cedar/IAM first; call Bedrock only after a deny (or allow) is final. Stream a grounded explanation citing policy statements and request context. Fail closed if the model times out — the decision already exists. Wire with Bedrock Guardrails and AppSync+Bedrock tool safety.
Split decision from narration
// authorizer.ts — decision is deterministic; Bedrock is optional narration
import { evaluate } from "./cedar-engine.js";
import { explainDeny } from "./bedrock-explain.js";
export async function handler(event: APIGatewayRequestAuthorizerEvent) {
const principal = verifyJwt(event.headers?.authorization);
const decision = evaluate({
principal,
action: event.requestContext.routeKey,
resource: event.path,
context: { tenantId: principal.tenantId },
});
// ✅ Hard gate — model never flips this
const effect = decision.allowed ? "Allow" : "Deny";
let explanation: string | undefined;
if (!decision.allowed && process.env.EXPLAIN_DENIES === "1") {
try {
explanation = await explainDeny({
statements: decision.matchedStatements,
request: { action: event.requestContext.routeKey, path: event.path },
});
} catch {
// ❌ Never: deny because Bedrock failed — decision already Deny
explanation = "Denied by policy; explanation unavailable";
}
}
return {
principalId: principal.sub,
policyDocument: buildIamPolicy(effect, event.methodArn),
context: {
tenantId: principal.tenantId,
explanation: explanation?.slice(0, 1024) ?? "",
policyVersion: decision.policyVersion,
},
};
}
✅ Effect derived solely from Cedar/IAM evaluation.
❌ if (await bedrockSaysAllow(event)) return Allow.
Ground explanations in matched statements
# bedrock_explain.py — illustrative
SYSTEM = """You explain authorization denials.
Only cite the provided policy statements and request fields.
Never invent permissions. Never suggest bypasses.
Output <= 80 words for the API Gateway authorizer context map."""
def explain_deny(statements: list[dict], request: dict) -> str:
user = {
"request": request,
"matchedStatements": statements, # from Cedar residual
}
# Use Converse with guardrails; low temperature
return converse_text(system=SYSTEM, user=json.dumps(user), temp=0.1)
Prompt injection in a path or header must not rewrite the effect — only the narrative. Same discipline as retrieval tenant filters.
Latency and fail-closed UX
Authorizers sit on the hot path. Budget explanation calls separately from the decision:
| Path | Budget | Behavior |
|---|---|---|
| Decision (Cedar) | ≤ 20 ms p95 | Always runs |
| Bedrock explain | ≤ 800 ms, async optional | Skip on timeout |
| Cache | Hash(principal, action, resource, policyVersion) | Reuse narratives |
// ✅ Optional async explain after Deny response already returned to edge
void queueExplanationJob({ requestId, statements }).catch(() => {});
// ❌ Await 5s Bedrock call inside every authorizer invoke
Closing checklist
✅ Dos
– ✅ Deterministic policy engine is the only effect source
– ✅ Bedrock cites matched statements only
– ✅ Authorizer context carries policyVersion for audits
– ✅ Timeouts skip explanation, never flip Allow/Deny
– ✅ Guardrails on explanation prompts
❌ Don’ts
– ❌ Don’t let the model be the final authorization gate
– ❌ Don’t put Allow/Deny tokens in free-form model output you parse
– ❌ Don’t block requests waiting on Bedrock health
– ❌ Don’t log raw JWTs into model prompts
– ❌ Don’t skip idempotent agent tool patterns if explanations trigger side effects
Related reading
- Bedrock Guardrails: Block Prompt Injection Inside Internal Dev Tools
- AppSync With Bedrock: GraphQL Resolvers That Call Tools Safely
- Bedrock Retrieval Filters: Tenant Isolation for Multi-SaaS Code RAG
- AI Coding in VPC: Private Bedrock Endpoints and Secret Hygiene
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Chaos for IAM Boundaries: Prove Apps Fail Closed on Denied Actions - CheatCoders