Skip to content
Rooz Jalali
Go back

Cedar, and the decision point my AI flow was missing

I spent a while building policies for an AI flow that could propose changes to infrastructure. The model reads telemetry, works out what is probably wrong, and suggests a fix. The interesting problem is not the suggesting. It is the gap between a model proposing something and that something being permitted to run.

I tried the obvious approaches first and they were all bad. What eventually worked was Cedar, which I had assumed was an AWS product and which turns out to be neither AWS only nor a product. It is a small, strange, formally verified language that is very good at exactly one thing, and I have not stopped recommending it since.

Table of contents

Open Table of contents

The problem with the obvious approaches

The first instinct is a pile of conditions in the application. Is this environment production, is the severity high enough, has somebody approved it, is the blast radius acceptable. That works for about three weeks. Then the conditions multiply, the precedence between them becomes implicit, and the answer to “why was this allowed” lives in whatever order somebody happened to write the checks.

The second instinct is a general purpose policy engine, which for most people means Rego. Rego is genuinely good and I would still reach for it to validate infrastructure plans or gate admission to a cluster. For this job it was the wrong shape. I did not want expressive power. I wanted to be able to state a rule and then prove that nothing anybody wrote later could undermine it, and expressive power is precisely what makes that impossible.

Because the real requirement was not “decide correctly”. It was “be able to demonstrate, to somebody who does not trust me, that a category of action cannot happen”.

What Cedar is

An authorisation language, open source under the Apache licence, originally from AWS and now with implementations in Rust, Java and Go. The Go one is a plain library with no AWS dependency and no network calls, which is worth saying early because almost everybody assumes otherwise. Nothing about using Cedar requires an AWS account or any AWS service.

A policy looks like this, and the shape barely varies.

permit (
  principal == Agent::"remediator",
  action in [Action::"k8s.deployment.rollback", Action::"k8s.rollout.restart"],
  resource in Env::"production"
) when {
  context.severity >= 2
};

Who is asking, what they want to do, what they want to do it to, and the facts at the time. There are two effects, permit and forbid, and everything is denied unless something permits it.

That is the whole language, more or less. No loops, no function definitions, no recursion, no way to make evaluation take an unbounded amount of time. It is less capable than its competitors on purpose, and the rest of this piece is about why that turned out to be the point.

The property everything else rests on

Forbid beats permit. Always, unconditionally, regardless of the order policies appear in, regardless of how many permits there are and how specific they are.

That sounds like a small ergonomic nicety. It is the single most useful property I have found in any policy system, because it means a denial written once cannot be undone by anything written afterwards, by anyone, including me.

Here is how I used it. There is a baseline that ships with the platform and cannot be edited by the people whose rules run on top of it.

@id("baseline_deny_data_tier")
@note("The data tier is never touched autonomously under the baseline.")
forbid (
  principal,
  action,
  resource in Tier::"data"
);

Above that sit layers: rules for a provider, then rules for one of their clients, then for an environment, then for a single service. Each layer can grant more autonomy than the one above. None of them can touch that forbid. Not through a more specific rule, not by being loaded later, not by accident.

The consequence for the implementation is almost embarrassing. Compiling the effective rules for a client means reading five files and concatenating them into one policy set. That is it. No precedence logic, no conflict resolution, no ordering rules to get wrong at two in the morning, because the language has already guaranteed the only precedence question that matters. I have written conflict resolution for layered configuration before and it is always subtly wrong. Here there was nothing to write.

And this is not a promise in the documentation. The property has been mechanically proven. Cedar’s authoriser and validator are formally modelled in Dafny, more recent work models the semantics in Lean, and the team runs differential testing in the millions to check the Rust implementation agrees with the model. The theorem that forbid always trumps permit regardless of evaluation order is a proven theorem, not an intention.

Containment instead of enumeration

The second thing that made it fit was the entity hierarchy.

Cedar entities can have parents, and the in operator walks that relationship. So I project the estate into the entity store with its real containment, a deployment inside a namespace inside a cluster inside an account inside a region, and then attach grouping parents that have nothing to do with topology: which environment something belongs to, which tier of criticality it sits in.

That is what makes resource in Env::"production" and resource in Tier::"data" work. A rule scopes itself to a whole class of things without naming any of them, and a resource created tomorrow is covered the moment it is projected with the right parents. Nobody maintains a list. The alternative, which I have also built, is a list that is always slightly out of date and whose staleness is invisible until it matters.

One detail I would pass on. Anything in the estate that has no corresponding entity type falls back to a generic resource, and a generic resource cannot be named in a rule. Which means a customer who wants to scope a grant to an account simply cannot, and nothing tells them why. There is a test whose only job is to fail when somebody adds a kind of thing to the estate without giving it a Cedar type. Worth having.

Annotations, which nobody talks about

Cedar lets you annotate a policy with arbitrary key and value pairs. The evaluator hands you back which policies decided the outcome, so you can read the annotations off the winning rule.

This turned out to be the feature that made the whole design work, and I have never seen anybody write about it.

The problem is that allow and deny is not enough. Permitting an action to run unattended at three in the morning is a different thing from permitting it behind an approval, or behind a canary, or with a cap on how much of the fleet it may touch at once. I needed the decision to carry conditions.

@id("prod_k8s_stateless")
@rung("A4")
@max_fleet_percent("20")
@canary_percent("10")
@soak_minutes("5")
@verify_probe("slo.availability")
@verify_window_minutes("10")
@approvers("0")
@notify("slack#sre_alerts")
permit (
  principal == Agent::"remediator",
  action in [Action::"k8s.deployment.rollback", Action::"k8s.rollout.restart"],
  resource in Env::"production"
) when {
  context.severity >= 2
};

So a decision is never a bare yes. It is yes, at this autonomy level, canary a tenth of it first, never expose more than a fifth at once, soak for five minutes, verify with this named probe inside this window, and tell that channel. The part of the system that acts reads those obligations and has no discretion about them.

Cedar decides. The annotations describe the shape of the permission. Keeping those separate meant I never had to extend the language or wrap it in something cleverer.

The other use is duller and matters more. Every policy has an @id, so the audit record cites a rule a human can go and read, by name, rather than an index into an array. When somebody asks why a change ran, the answer is a file, a line and a note written by whoever wrote the rule.

Keeping the model out of the decision

This is the part that made Cedar feel like the right tool rather than a convenient one.

The request evaluated is a typed structure assembled by the platform: an action identifier from a fixed catalogue, a target, an environment, a tier, a severity, a blast radius, and a set of contextual facts. The model’s prose does not appear in it anywhere. Nothing the model writes is ever evaluated as a rule, and there is no string in the request that a prompt could steer.

One of the context fields is worth calling out.

context.untrusted_evidence

It is true when the justification for an action leans on text an attacker could have influenced, a log line for instance, which is exactly the problem I have written about elsewhere. Once it is a fact in the request rather than a caveat in somebody’s head, a rule can act on it, and the platform can refuse to act unattended on evidence that came from a source somebody else can write to.

That is the general trick, and it is not really about Cedar. If you push the question of what a model is allowed to do out into a declarative layer the model cannot reach or rewrite, you get a boundary. If you leave it in the prompt, you have a suggestion.

Why it is an underdog

Three reasons, and the first two are not really about the technology.

Everybody assumes it is an AWS lock in. It carries the AWS name, it appeared alongside a managed AWS service, and people stop reading there. The language is under a permissive licence, the Go implementation is a normal dependency with no cloud in it, and I used it in a service that never called AWS for anything. The assumption costs it a great many evaluations it would win.

It lost the mindshare race to a general purpose tool. Rego covers authorisation, cluster admission control, infrastructure plan validation and compliance checks. Cedar covers authorisation. In a market where teams want one thing to learn, breadth wins, and the comparison articles duly file Cedar as the limited option. It is limited. That is the specification, not a shortfall.

The best thing about it is invisible in a demo. You cannot show formal verification on a slide and have anyone care. There is now an open source analysis toolkit, with its symbolic compiler written in Lean, that will answer questions like whether two versions of a policy set are equivalent, or whether a policy can ever produce a particular outcome, with both soundness and completeness. Confirmed properties hold in every case, and a reported violation comes with a concrete example. No false positives.

Sit with what that gives you. I can change a rulebook and ask a machine whether the new one permits anything the old one did not. Not test it against examples I thought of. Ask, and get a real answer. Nothing else in my toolchain offers anything remotely like that, and hardly anybody using Cedar seems to know the tooling exists.

A note on being early

I built this because I wanted a hard boundary between a model’s suggestion and a change to infrastructure, and I could not find anything that did the job properly.

In March 2026 AWS shipped Cedar as the policy engine inside its agent platform, intercepting agent tool calls at the gateway and authorising each one against a policy set. Which is, allowing for the difference in scale, the same pattern: a declarative layer the model cannot reach, deciding what the model is permitted to do, evaluated outside the loop.

I take that as confirmation rather than as being scooped. The pattern is correct, and the language people keep dismissing as an AWS curiosity is quietly becoming the default way to express it.

When not to use it

For completeness, because a recommendation with no boundary is just enthusiasm.

If you need to validate the shape of an infrastructure plan, evaluate a Kubernetes admission request against structural rules, or compute anything, use Rego. Cedar cannot do those things and is not trying to. If your authorisation model is fundamentally about relationships at scale, who shares which document with whom, look at the systems designed for that instead.

Cedar answers one question. May this principal take this action on this resource, given these facts. If that is your question, I do not think there is anything better, and I think the reasons it keeps getting passed over have almost nothing to do with whether it is any good.


Share this post:

Previous Post
DORA, and the supplier your regulator now supervises
Next Post
Generation got cheap, review did not