Skip to content

필사 모드: The Evolution of Authorization Models — RBAC, ABAC, ReBAC, and OpenFGA/Zanzibar

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

Introduction — Authorization, the Next Battleground After Authentication

Over the past several years the industry has largely solved authentication. OIDC/SAML-based SSO is table stakes, and passkeys are replacing passwords. But the question that comes after "who are you" — **"is this user allowed to perform this action on this resource?"** — still lives, in most organizations, as piles of if-statements scattered across every application.

The reasons authorization became a hot topic again in 2026 are clear:

1. **Microservices and multi-tenancy**: Permission logic duplicated across dozens of services makes consistency and auditing impossible.

2. **Collaboration features everywhere**: The Google-Docs-style requirement of "share this document with this person" simply cannot be expressed in a traditional role-based model.

3. **The rise of AI agents**: With non-human principals accessing resources, fine-grained and auditable authorization became mandatory.

4. **Regulation**: More and more compliance regimes demand proof of least privilege.

This post traces the evolution from RBAC to ABAC to ReBAC, covers the core concepts of the [Google Zanzibar paper](https://research.google/pubs/pub48190/) and hands-on modeling with its open-source descendant [OpenFGA](https://openfga.dev/docs), and closes with architecture patterns for microservice environments.

First, terminology:

| Term | Meaning |

| --- | --- |

| AuthN (authentication) | Verifying who you are — the domain of OIDC, SAML, passkeys |

| AuthZ (authorization) | Deciding what you may do — the topic of this post |

| PEP | Policy Enforcement Point — where decisions are enforced (API gateway, service code) |

| PDP | Policy Decision Point — where allow/deny is decided (the authorization engine) |

| PAP | Policy Administration Point — where policies are managed |

| PIP | Policy Information Point — where decision attributes are supplied from |

RBAC — and the Curse of Role Explosion

RBAC (Role-Based Access Control) is the most widely used model. You assign roles to users and bundle permissions into roles.

user ──(assigned)──> role ──(holds)──> permission

jane ─────────────> editor ─────────> document:write

john ─────────────> viewer ─────────> document:read

The advantages of RBAC are obvious. It is easy to understand, simple to audit ("print the list of editor role holders"), and rests on a mature theoretical foundation in the NIST RBAC standard (INCITS 359).

The problem is that **real-world permission requirements do not collapse into the single dimension of "role"**:

- "An editor, but only for documents in their own department"

- "A viewer, but only during business hours"

- "A manager, but unable to approve requests they submitted themselves"

Cram those requirements into RBAC and roles multiply with every combination of dimensions. This is the infamous **role explosion**:

editor → add department dim → editor-sales

editor-hr

editor-engineering

→ add region dim → editor-sales-kr

editor-sales-us

editor-hr-kr

... (combinatorial blowup)

In real enterprise IGA audits, finding "more roles than users" is not rare. The moment you have thousands of roles, the one advantage RBAC had — being easy to understand — evaporates.

Yet RBAC is not dead. For **coarse-grained, organization-level distinctions** — admin/member/billing-manager — it remains optimal. Trouble starts when you try to do per-resource fine-grained control with RBAC.

ABAC — Attributes and Policies, and the Lessons of XACML

ABAC (Attribute-Based Access Control) evaluates conditional expressions over the **attributes** of the subject, resource, action, and environment.

Allow when:

subject.department == resource.department

AND action == "edit"

AND environment.time BETWEEN 09:00 AND 18:00

AND subject.clearance >= resource.classification

The role explosion problem dissolves elegantly. With 100 departments, the policy is still one line.

The standardization attempt for ABAC was OASIS [XACML](https://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html). XACML left a major legacy — the architectural vocabulary of PEP/PDP/PAP/PIP — but as a standard it is widely considered a failure, for these reasons:

- **Extreme XML verbosity**: One simple rule becomes dozens of lines of XML.

- **Undebuggability**: Humans struggle to predict the outcome of policy combining algorithms.

- **No developer experience**: Authoring policies was near-impossible without specialized tooling.

The spirit of XACML lives on in the **policy-as-code** camp — most notably [OPA (Open Policy Agent)](https://www.openpolicyagent.org/docs/latest/) and its Rego language — which implements the same ABAC evaluation in a far more tractable form.

ABAC has structural limits of its own, however. **It answers "can jane view this particular document" well, but it is weak at producing "the full list of documents jane can view" (the reverse query).** And scenarios where permissions derive from **chains of relationships** — "document X is in folder Y, and folder Y is shared with team Z" — are unnatural to model with attributes.

ReBAC — The Paradigm Google Zanzibar Introduced

This is where ReBAC (Relationship-Based Access Control) enters. Permissions are modeled not as attribute predicates but as a **graph of relationships between subjects and objects**. Its pinnacle is the Google [Zanzibar paper](https://research.google/pubs/pub48190/), published in 2019. Zanzibar is the single global system handling authorization for Google Docs, Drive, YouTube, and Cloud — storing trillions of ACLs and serving millions of permission checks per second at around 10ms latency.

Core Concept 1 — Relationship Tuples

All permission data in Zanzibar is expressed as tuples of this shape:

object#relation@user

Examples:

doc:budget-2026#owner@user:jane jane is owner of doc budget-2026

doc:budget-2026#viewer@group:finance#member

members of group finance are viewers

folder:q1#parent@doc:budget-2026 the parent of budget-2026 is folder q1

What makes this powerful is that the user slot can hold **a userset of another object**, as in the second example. You can point at the set "all members of the finance group" in a single line, and when group membership changes, the tuple on the document side stays untouched.

Core Concept 2 — Userset Rewrite

If tuples are the data, userset rewrite rules are the **derivation logic of permissions**. Per object type you declare rules like "an owner is automatically also an editor" or "a viewer of the parent folder is a viewer of the child document."

Evaluation rule for the viewer relation (conceptual):

viewer =

users directly assigned as viewer (this)

∪ users who are editors (computed userset)

∪ users who are viewers on the parent folder (tuple-to-userset)

With just these three — direct relations, computed usersets, and relations that hop across other relations (tuple-to-userset) — combined via union/intersection/exclusion, you can express a sharing model on par with Google Drive.

Core Concept 3 — Zookies and Consistency

The classic trap of distributed authorization is the **"new enemy problem."** Suppose you (1) remove jane from a document and then (2) add sensitive content to it. If, due to replication lag, step (1) has not propagated by the time step (2) is read, the removed jane sees the new content. Unordered caching/replication becomes a security incident.

Zanzibar solves this with a consistency token called the **zookie**. When content is modified, you receive and store a zookie; when checking permissions, you send that zookie along, and evaluation is guaranteed against "the ACL state at least as fresh as that point." It is an elegant device that balances external consistency with performance (aggressive caching). OpenFGA exposes similar control through a consistency parameter (e.g., HIGHER_CONSISTENCY).

Hands-on OpenFGA Modeling — A Document Sharing System

[OpenFGA](https://openfga.dev/docs) is the Zanzibar implementation started by Auth0/Okta and donated to the CNCF. Its DSL is the most approachable entry point into ReBAC. Let us model a document sharing system in the spirit of Google Drive.

model

schema 1.1

type user

type group

relations

define member: [user]

type folder

relations

define owner: [user]

define parent: [folder]

define editor: [user, group#member] or owner or editor from parent

define viewer: [user, group#member] or editor or viewer from parent

type doc

relations

define parent: [folder]

define owner: [user]

define editor: [user, group#member] or owner or editor from parent

define viewer: [user, group#member] or editor or viewer from parent

define can_share: owner or editor

define can_delete: owner

What this one model expresses:

- Sharing with users and groups (the group#member type restriction)

- The permission hierarchy owner ⊃ editor ⊃ viewer (computed relations)

- Inheritance of folder permissions downward ("editor from parent" — tuple-to-userset)

- Action-level permissions (can_share, can_delete)

Write some tuples and query:

Write tuple: grant viewer on folder q1 to members of group finance

fga tuple write --store-id "$FGA_STORE_ID" \

"group:finance#member" viewer "folder:q1"

Add jane to the finance group

fga tuple write --store-id "$FGA_STORE_ID" \

"user:jane" member "group:finance"

Place the budget-2026 doc into folder q1

fga tuple write --store-id "$FGA_STORE_ID" \

"folder:q1" parent "doc:budget-2026"

Query: can jane view budget-2026?

fga query check --store-id "$FGA_STORE_ID" \

"user:jane" viewer "doc:budget-2026"

→ allowed: true (derived via the group → folder → doc relationship chain)

In application code, you check via the SDK:

const fga = new OpenFgaClient({

apiUrl: process.env.FGA_API_URL,

storeId: process.env.FGA_STORE_ID,

});

// Single check (called from the PEP)

const { allowed } = await fga.check({

user: 'user:jane',

relation: 'viewer',

object: 'doc:budget-2026',

});

// Reverse query: the list of docs jane can view (for UI filtering)

const { objects } = await fga.listObjects({

user: 'user:jane',

relation: 'viewer',

type: 'doc',

});

Note that the **reverse query (ListObjects) — the thing ABAC struggles with — is a first-class API.** It makes a decisive difference when building "resources I can access" screens.

The Ecosystem — SpiceDB, Ory Keto, and the Division of Labor with OPA

OpenFGA is not the only Zanzibar-lineage implementation.

| Project | Characteristics | Schema language |

| --- | --- | --- |

| OpenFGA | CNCF, Auth0 heritage, friendliest DSL and docs | FGA DSL / JSON |

| SpiceDB (AuthZed) | Most faithful to the Zanzibar paper, supports caveats (conditional relations) | SpiceDB schema |

| Ory Keto | Integrates with the Ory ecosystem (Kratos/Hydra) | Ory Permission Language |

| Permify | Emphasis on multi-tenancy | Permify schema |

So where does OPA/Rego fit? **OPA is a policy evaluation engine; the Zanzibar family is a relationship data store plus graph evaluation engine.** Roughly:

- **What OPA is good at**: Evaluating rules against context supplied as input. Stateless judgments like "does this K8s manifest meet our security bar" or "are the JWT scopes on this request sufficient for this API." Policy versioned as code — policy-as-code.

- **What a ReBAC engine is good at**: Judgments that require traversing **stored relationship data** as a graph — "jane → group → folder → doc." Check/list queries over hundreds of millions of relationships.

request → API Gateway/service (PEP)

├─ coarse decision: JWT scopes, tenant validation → OPA (stateless policy)

└─ fine decision: may this user touch this doc? → OpenFGA (relationship graph)

They are not competitors but tools at different layers, and using them together is common. A Rego example to get a feel:

OPA Rego — coarse-grained API policy (conceptual example)

package httpapi.authz

default allow := false

allow if {

input.method == "GET"

startswith(input.path, "/api/public/")

}

allow if {

input.method == "POST"

input.path == "/api/docs"

"docs:write" in input.token.scopes

input.token.tenant == input.headers["x-tenant-id"]

}

Architecture Patterns — Centralized PDP vs Embedded Library

There are two broad ways to deploy an authorization engine.

Pattern A: centralized PDP Pattern B: sidecar/embedded library

───────────────────── ─────────────────────────────

svc-a ──┐ svc-a ── [PDP sidecar/lib]

svc-b ──┼──> AuthZ Service (PDP) svc-b ── [PDP sidecar/lib]

svc-c ──┘ + single relation DB svc-c ── [PDP sidecar/lib]

(policy/data replicated out)

Pros: single source of truth, Pros: minimal latency (local eval),

easy audit, model consistency no network dependency

Cons: extra network hop, Cons: complex data sync,

availability affects all consistency hard to guarantee

Practical guidelines:

- **Relationship-data-driven decisions (ReBAC) should default to centralized.** Replicating the relationship graph into every service is a consistency nightmare. Latency can be brought down to 1–5ms with same-AZ placement plus caching.

- **Stateless policy (OPA) embeds naturally.** The standard model is OPA as a sidecar/library next to each service, with policy bundles distributed from a central PAP.

- Treat the availability of a central PDP on par with your authentication IdP: redundancy, health-check-based failover, and **fail-closed on PDP outage** as the rule (fail-open is an authorization bypass).

Combining Keycloak Authorization Services with an External Engine

[Keycloak](https://www.keycloak.org/docs/latest/authorization_services/index.html) also ships its own authorization feature set (Authorization Services), based on UMA 2.0: you define resources/scopes/policies/permissions and carry entitlements in the token (RPT). Is Keycloak alone enough?

The realistic division of labor:

| Layer | Responsibility | Tool |

| --- | --- | --- |

| Identity/role issuance | Who the user is and which coarse roles they hold (token claims) | Keycloak |

| API-level policy | Whether this token may call this endpoint | Keycloak AuthZ or OPA |

| Resource-level relations | What this user may do to this doc/project/ticket | OpenFGA/SpiceDB |

The typical combination pattern: use the sub claim of the Keycloak-issued token as the OpenFGA user identifier, and propagate role/group claim changes into FGA tuples via events.

login → Keycloak → access_token (sub: user:jane, groups: [finance])

request → service (PEP) ──┤

├─ token validation (sig, aud, exp) ← Keycloak public key

└─ fga.check(user:jane, viewer, doc:X) ← OpenFGA

A conceptual event listener that syncs Keycloak group membership into FGA tuples:

// Keycloak SPI — event listener mirroring group membership into OpenFGA (conceptual)

public class FgaSyncEventListener implements EventListenerProvider {

@Override

public void onEvent(AdminEvent event, boolean includeRepresentation) {

if (event.getResourceType() == ResourceType.GROUP_MEMBERSHIP) {

String userId = extractUserId(event.getResourcePath());

String groupId = extractGroupId(event.getResourcePath());

if (event.getOperationType() == OperationType.CREATE) {

fgaClient.writeTuple("user:" + userId, "member", "group:" + groupId);

} else if (event.getOperationType() == OperationType.DELETE) {

fgaClient.deleteTuple("user:" + userId, "member", "group:" + groupId);

}

}

}

}

Syncing Authorization Data in Microservices

Adopting a ReBAC engine creates a new operational challenge: **keeping the facts in your application databases (ownership, membership, folder structure) consistent with the tuples in FGA.**

There are three strategies:

1. **Synchronous dual write**: Perform the FGA write inside the resource-creation transaction path. Simple, but partial failure (DB succeeds, FGA fails) opens permission holes. Compensating transactions or a retry queue are mandatory.

2. **Transactional Outbox + CDC**: The recommended pattern. Commit the resource change and an outbox record in one transaction; a CDC tool such as Debezium reads the outbox and applies it to FGA. Delivery is at-least-once, so design tuple writes to be idempotent.

3. **Periodic reconciliation**: Alongside the above, batch-compare and repair differences between the application DB and FGA tuples. Drift in authorization data is a security vulnerability — reconciliation is not optional.

service transaction

┌──────────────────────────────┐

│ INSERT INTO documents (...) │

│ INSERT INTO outbox ( │ CDC (Debezium) OpenFGA

│ event: doc.created, │ ───────────────────────> tuple write

│ owner: user:jane ) │ (at-least-once) (idempotent)

└──────────────────────────────┘

nightly reconciliation ───┘ (drift detection/repair)

Deletion is trickier. When a resource is deleted, all related tuples must be removed (orphan tuples are audit noise plus potential wrong decisions), and for hierarchical objects like folders you must also design the cleanup ordering for child-object tuples.

Selection Guide — What to Use When

| Situation | Recommended model |

| --- | --- |

| Internal admin tool, 5 or fewer stable roles | RBAC (IdP role claims suffice) |

| Attribute conditions (tenant/department/time) are the core | ABAC (OPA policy-as-code) |

| Doc/folder/project sharing, hierarchy inheritance, collaboration | ReBAC (OpenFGA/SpiceDB) |

| K8s admission, infrastructure policy, CI gates | OPA/Rego |

| Regulated industry + centralized audit of access decisions | Central PDP + decision-log pipeline |

| A mix of all of the above (most of reality) | RBAC (coarse) + ReBAC (fine) + OPA (policy) combined |

As questions:

1. **Do you need "the list of people who can see X" or "the list of X I can see"?** → If yes, ReBAC. You will suffer with ABAC.

2. **Do permissions derive from relationships between resources (folder→doc, org→project)?** → ReBAC.

3. **Do permissions depend on request context (time, IP, device posture)?** → ABAC/OPA. That said, mixing conditions into ReBAC via SpiceDB caveats or OpenFGA conditions is also possible.

4. **Is the organization ready to unify its permission model?** → If not, adopt ReBAC in one domain first (e.g., the document service) and expand gradually.

A Collection of Anti-patterns

1. **Stuffing fine-grained permissions into the JWT**: Putting document ID lists in tokens bloats them, and permission revocation is delayed until token expiry. Tokens carry identity and coarse claims; fine-grained decisions go to the PDP in real time.

2. **Fail-open PDP**: "Allow by default" during an authorization service outage is an authorization-bypass backdoor. Fail-closed, optionally with short-TTL cached decisions, is the standard.

3. **Trusting frontend authorization**: Hiding a button in the UI is UX, not security. All enforcement happens at the server-side PEP.

4. **Piling up tuples without a relationship model**: Accumulating tuples without schema review leads to migration hell. Put model changes through code review plus tests (FGA model tests).

5. **Not collecting decision logs**: If you cannot reconstruct "why was this allowed," audits become impossible. Emit check requests/responses as structured logs.

6. **Dual write without reconciliation**: Drift will happen. Undetected drift is a silent permission bug.

Closing Thoughts

The evolution of authorization models has played out in the tension between expressiveness and operability. RBAC was simple but collapsed under role explosion; ABAC (XACML) gained expressiveness but lost developer experience; Zanzibar/ReBAC offers the current equilibrium with the natural model of relationships, first-class reverse queries, and the zookie consistency device.

The practical conclusion is surprisingly conservative: **coarse permissions as IdP (Keycloak) roles, policy judgments in OPA, resource-level relationships in OpenFGA** — combine the right tool per layer, and keep every decision auditable. That is the model answer for authorization architecture in 2026.

In the next post we cover the new principal about to ride on top of this authorization infrastructure — AI agent identity and MCP authentication.

References

- [Zanzibar: Google Consistent, Global Authorization System](https://research.google/pubs/pub48190/) — the original Zanzibar paper

- [OpenFGA Documentation](https://openfga.dev/docs) — official OpenFGA docs

- [OpenFGA Modeling Guide](https://openfga.dev/docs/modeling) — hands-on modeling guide

- [SpiceDB Documentation](https://authzed.com/docs) — SpiceDB/AuthZed docs

- [Ory Keto Documentation](https://www.ory.sh/docs/keto) — Ory Keto docs

- [Open Policy Agent Documentation](https://www.openpolicyagent.org/docs/latest/) — official OPA/Rego docs

- [OASIS XACML 3.0 Specification](https://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html) — the XACML standard

- [NIST RBAC — INCITS 359](https://csrc.nist.gov/projects/role-based-access-control) — the RBAC standard project

- [Keycloak Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/index.html) — Keycloak authorization features

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700) — OAuth Security BCP

현재 단락 (1/238)

Over the past several years the industry has largely solved authentication. OIDC/SAML-based SSO is t...

작성 글자: 0원문 글자: 18,121작성 단락: 0/238