Keymate Logo
← Back to Blog

The Limits of RBAC: Why Context Matters

Keymate Team
October 6, 2025
The Limits of RBAC: Why Context Matters

The Limits of RBAC: Why Context Matters

Keycloak was just the beginning. Meet Keymate.

Estimated read: 7–9 minutes

TL;DR

RBAC is necessary but not sufficient at modern scale. Roles multiply, exceptions become the work, and cross-tenant realities strain simple role checks. The path forward is to keep identity and layer fine-grained, contextual, explainable authorization—ABAC/ReBAC/RADAC—without rip-and-replace. Run in parallel, observe decisions, and cut over with confidence.


When roles become the work

So what: At scale, roles become the work; RBAC alone can’t encode context, relationships, risk, or time.

We’ve all seen it: the system grows; so do roles. In one environment—~650 roles across 9 apps and **120 tenants **—role changes grew 35% YoY, while audits slowed to a crawl. RBAC still helped, but the intent behind permissions got blurry.

RBAC isn’t wrong. It’s foundational. But alone, it can’t express who should access what, in which relationship, under which risk, and for how long—without exploding into ad-hoc roles and exceptions.


Why RBAC alone struggles

So what: RBAC answers who—not what/which/when/under which risk/for how long.

RBAC vs. context matrix: RBAC covers who; ABAC/ReBAC/RADAC complete what/which/when/risk/for how long.

Role explosion → exception debt

So what: Role explosion is exception debt; context collapses exceptions into rules.

New use-case? Add a role. Special case? Add another. Before long, “role hygiene” becomes a full-time job. Exceptions accumulate. Audits hurt.

Context blindness

So what: Without attributes, relationships, and risk, roles over-grant or stall behind tickets.

Roles are coarse. They rarely encode attributes (department, region, data class, time), relationships (owner, manager, collaborator), or risk (device posture, anomalous geo, after-hours). Without context, you over-grant or drown in tickets.

Cross-tenant reality

So what: Tenants and org trees must be first-class; scoped admin can’t be faked with roles alone.

B2B/B2B2C/G2C topologies need strict isolation and scoped administration. If your access model doesn’t “know” tenants, org trees, and delegation, you end up with fragile processes. (We’ll dig into this in Part 3.)

Lifecycle opacity

So what: Policy-as-code + explainability turn audits from fire-fights into routine checks.

Policies are often configs, not software artifacts. No versions, diffs, reviews, approvals, or rollbacks. When access is denied, there’s no evidence for “why denied?”—just folklore.

Policy-as-code
Policies should be treated like code—think “GitHub for authorization”: diffs, pull requests, reviews, and* > *rollbacks**.

Policy-as-code lifecycle: diff → PR → review → simulate → approve → deploy → rollback.


“A role is enough”… until it isn’t (real-world edge cases)

So what: Role grants must be gated by conditions—after-hours, untrusted devices, freezes, on-leave states, and scope.

These are things a user’s role may authorize normally, but shouldn’t during higher-risk conditions:

  • After-hours approvals: A Finance Manager can approve payments 09:00–18:00, but after hours + VPN + risk.score ≥ 6deny or require JIT elevation + approval.
  • Change-freeze / SoD: A Prod Admin can deploy, but during freeze or four-eyes periods → require a second approver, or read-only only.
  • PII on unknown device: Support can view PII, but on untrusted device or suspicious IP/geomask fields or deny.
  • On-leave delegation: Approver is on leave → role exists but user.onLeave = truedeny; allow only if * *delegatedApprover = true**.
  • Out-of-scope tenant: Account Manager sees only their customers; opening an unrelated tenant or out-of-region record → deny.

Roles don’t carry these nuances. Context and risk do.


Developer perspective: tiny, concrete examples

So what: Make intent executable and observable—small policies plus decision traces beat sprawling role sets.

Authorization path: app → enforcer (gateway/SDK) → policy engine → decision + trace.

A) From “role only” to “role + attribute”

Naïve check

if user.hasRole("FINANCE_ANALYST"):
  allow("view_report")

Add attribute constraints

allow("view_report") if
  user.role == "FINANCE_ANALYST" &&
  user.region == resource.region &&
  resource.classification in ["internal","confidential"]

B) Relationship (ReBAC-style)

allow("doc:read") if
  relation(user, resource, "owner") ||
  relation(user, resource, "collaborator")

C) Risk-adaptive, time-bounded elevation (RADAC-flavored)

allow("approve_payment") if
  user.role == "FINANCE_MANAGER" &&
  risk(user, device, geo, time) <= "medium" &&
  elevation.active == true &&
  now() < elevation.expires_at

Decision trace (example JSON)

{
  "request_id": "ae8f...c2",
  "subject": "user:1234",
  "action": "approve_payment",
  "resource": "invoice:987",
  "inputs": {
    "role": "FINANCE_MANAGER",
    "region": "EU",
    "device_trust": "low",
    "time": "23:18Z",
    "vpn": true,
    "risk_score": 7
  },
  "evaluation": {
    "strategy": "unanimous",
    "policies": [
      {
        "name": "RBAC:Manager",
        "result": "allow"
      },
      {
        "name": "ABAC:EU-Only",
        "result": "allow"
      },
      {
        "name": "RADAC:LowRisk",
        "result": "deny",
        "reason": "risk_score>=6 || vpn==true"
      }
    ]
  },
  "final": {
    "decision": "deny",
    "why": "RADAC:LowRisk"
  }
}

Decision trace: RADAC failed (risk.score≥6), final=deny. The log shows exactly which rule fired and why.


Complement, don’t replace: RBAC + context

So what: Don’t replace IAM—layer ABAC/ReBAC/RADAC on RBAC and compose them with PBAC strategies.

The fix isn’t to throw RBAC away. It’s to layer context and governance on top:

Model What it adds Typical use Limits
RBAC Role grants Stable job functions Coarse; role sprawl
ABAC Attributes (user/resource/context) Region, data class, time Attribute hygiene
ReBAC Relationships/graph Ownership, team, share Graph modeling
RADAC Risk signals Device, geo, anomaly, hour Signal quality; explainability
PBAC Decision strategies (unanimous/affirmative/consensus) Compose sub-policies Choose strategy wisely

Key idea: RBAC handles who someone is in the org; ABAC/ReBAC/RADAC encode what they’re doing, to which resource, in which relationship, under which risk—and for how long.


Adoption path (without rewrites)

So what: **Ship confidence, not bravado—policy-as-code, explainability, and a parallel run let you cut over safely. **

  1. Model the context you really use
    Start with two attributes (e.g., region, data_class) and one relationship (owner). Keep it practical.

  2. Policy as code
    Put policy in version control. Treat changes like code: diff → PR → review → approve → deploy → rollback.

  3. Explainability & logs
    Emit decision traces: inputs, rule matched, outcome, “why denied/allowed.”

  4. Parallel run
    Run next to your legacy checks. Compare decisions. Fix mismatches. Cut over gradually. Confidence beats bravado.

Parallel run: legacy IAM and Keymate layer run side-by-side; compare decisions, then cut over safely.


What we’re not saying

  • We’re not saying “roles are dead.” RBAC remains a foundation.
  • We’re not saying “rip it all out.” Replacing identity is heavy and risky.
  • We are saying: keep identity, add context and governance—and make decisions observable.

Where this series goes next

In Part 3, we’ll address the elephant we touched on here: Multi-Tenancy & Delegation—how to model tenants, org trees, and scoped admin without spreadsheets. We’ll show concrete patterns and a few anti-patterns to avoid.

Series nav
← Part 1: Why Keymate? How We Got Here
→ Part 3: Multi-Tenancy & Delegation (coming next)

Follow the series join our newsletter

Get the next chapter when it drops, plus insights on modern IAM and authorization patterns.

Stay updated with our latest insights and product updates

Frequently Asked Questions