Policy
One Cedar policy set decides web access, application sign-in, SSH logins, sudo, provisioning and the directory itself.
Most identity estates have three or four policy languages that cannot see each other: LDAP ACLs, HBAC and sudo rules, and whatever the SSO product calls mappers. None are diffable, none are testable, and none can answer why somebody was refused.
Cardinal has one, Cedar, and it is the decision point everywhere:
| Decision point | The question |
|---|---|
Traefik forwardAuth |
May they reach this URL? |
| OIDC authorization | May they sign in to this application? |
| SSH certificate issuance | May they log into this host, as which local accounts? |
| The assignment an agent fetches | Does their name belong in sudoers there? |
| SCIM | May this provisioner create and change accounts? |
| The admin API | May they change this directory object? |
That last row is the one that surprises people: the directory's own access control is the same engine as everything else. There is no separate, arcane ACL syntax for administering Cardinal itself.
Permit, forbid, and which wins
Cedar is default-deny: nothing is permitted unless a rule permits it. A
forbid beats any permit, which is what makes the guardrails in the shipped
set guarantees rather than conventions. See
administration tiers.
That also makes a particular mistake invisible. A rule naming a group that does not exist never matches, which looks exactly like the rule working. Everything that publishes a policy checks for it and says so.
Groups and policy
Coming from a system where you bind a group to an application when you create it (the application carries an "allowed groups" field, and a user gets in when they are in a bound group), the shape here is different, and the difference is worth being explicit about:
- A group is a set of members in the directory. It grants nothing on its own.
- A policy is the Cedar rules. A rule names a group and a resource:
"principals in group
grafana-usersmay reach applicationgrafana." The rule is the binding, and it lives in the versioned policy, not on the application.
So "group G may use application X" is a rule:
cardinal group create grafana-users
cardinal grant grafana-users alice
cardinal policy rule add web-access -group grafana-users -app grafana
The binding being a rule rather than a field on the application is the point of policy-as-code: it is diffable, testable and reviewable before it governs anything, and it can say things a checkbox cannot. The cost is that it is less immediately obvious than a setting on the application. The rule builder closes most of that gap: it writes exactly this kind of per-application rule, in the console or the CLI, without editing the policy file, so the everyday act is close to binding a group while what lands is a reviewable version.
The default is least-privilege. A fresh install ships no blanket access
rule: no application is reachable until a rule names it, the same default-deny
the rest of the directory has. Grant each application to the group that may use
it. For an application genuinely open to everyone who signs in, put it in the
staff-apps group and enable the bucket rule the default file ships commented
out. Existing installs keep whatever policy they already activated; the default
is what a new install seeds.
What policy does not decide
Cardinal decides whether you may reach a thing. What you may do once inside it is the application's own model, deliberately (ADR 0019). Cardinal has no concept of a role inside somebody else's application, and adding one would mean every application's permission model living here.
What it costs
Cedar is a real language and somebody has to learn it. A rule set is code, with the review, the testing and the blast radius of code. The rule builder narrows that to four common shapes; it does not remove it.