Authorization decisions shouldn't exist in a vacuum. When your policy engine denies access at 2 AM, users deserve to know it's because of business hours restrictions, not some mysterious permission issue. When sensitive data gets accessed, audit trails need rich context about who, what, and why. This is where Cerbos outputs shine, providing the contextual metadata that transforms binary allow/deny decisions into intelligent, actionable authorization responses.
While traditional authorization standards have long recognized the need for contextual information beyond simple allow/deny decisions, Cerbos takes a pragmatic approach that fits naturally into modern application architectures. Let me walk you through how outputs work and why they matter for building production authorization systems.
Picture this scenario from a customer I worked with last year. Their support team kept getting tickets about "random" access denials that turned out to be time-based restrictions, geographic limitations, or missing training certifications. The authorization system was working perfectly, but users had no visibility into why their requests failed. Support tickets piled up, user frustration mounted, and the team spent countless hours debugging non-issues.
Traditional authorization gives you ALLOW or DENY, full stop. But real applications need context. They need to know whether a denial is temporary or permanent, whether there's an approval process to follow, or what specific requirement the user failed to meet. They need audit trails that capture not just the decision but the reasoning behind it.
Many authorization systems have tried to solve this by scattering contextual logic across application code or building separate services for rate limiting, audit logging, and user guidance. The result is always the same: inconsistent behavior, maintenance nightmares, and compliance headaches when auditors ask where your authorization logic actually lives.
Cerbos outputs are expressions evaluated when policy rules trigger, returning structured data alongside authorization decisions. These outputs are purely informational - your application decides what to do with them. No rigid requirements about mandatory side effects, no authorization failures if something isn't handled perfectly. Just useful context that makes your authorization decisions actionable.
Here's a simple example that demonstrates the pattern:
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: "default"
resource: "document"
rules:
- name: business-hours-only
actions: ['view', 'edit']
effect: EFFECT_DENY
roles: ['employee']
condition:
match:
expr: now().getHours() < 9 || now().getHours() >= 17
output:
when:
ruleActivated: |-
{
"denial_reason": "OUTSIDE_BUSINESS_HOURS",
"message": "Documents can only be accessed between 9 AM and 5 PM",
"next_available": now().getHours() >= 17 ?
timestamp(string(now().getYear()) + "-" + string(now().getMonth()) + "-" + string(now().getDayOfMonth() + 1) + "T09:00:00Z") :
timestamp(string(now().getYear()) + "-" + string(now().getMonth()) + "-" + string(now().getDayOfMonth()) + "T09:00:00Z"),
"contact_for_override": "security@company.com"
}
The output uses CEL (Common Expression Language) to generate dynamic JSON that your application receives in the API response. It's structured data you can use immediately. The expressions have access to all the context: principal attributes, resource properties, request metadata, and built-in functions like now()
for temporal logic.
One pattern I see repeatedly is teams struggling to maintain comprehensive audit logs for compliance. Security teams want to know who accessed what, when, why, and under what circumstances. Traditional approaches scatter this logic across application code, making it inconsistent and hard to maintain.
With Cerbos outputs, you centralize audit requirements in your policies:
rules:
- name: sensitive-data-access
actions: ['view', 'download']
effect: EFFECT_ALLOW
roles: ['analyst', 'admin']
condition:
match:
expr: R.attr.classification == "sensitive"
output:
when:
ruleActivated: |-
{
"audit": {
"required": true,
"level": "HIGH",
"event_type": "SENSITIVE_DATA_ACCESS",
"principal_id": P.id,
"principal_role": P.roles[0],
"resource_id": R.id,
"classification": R.attr.classification,
"department": P.attr.department,
"timestamp": now(),
"session_id": request.aux_data.jwt.claims.session_id,
"ip_address": request.aux_data.ip_address
}
}
Your application code becomes remarkably clean:
const decision = await cerbos.checkResources({
principal: { id: userId, roles: ['analyst'], attr: { department: 'finance' }},
resources: [{ kind: 'document', id: docId, attr: { classification: 'sensitive' }}],
actions: ['view']
});
const result = decision.results[0];
if (result.actions.view === 'EFFECT_ALLOW') {
// Check for audit requirements
const auditOutput = result.outputs?.find(o => o.val.audit?.required);
if (auditOutput) {
await auditService.log(auditOutput.val.audit);
}
return document;
}
No more scattered audit logic. No more forgotten log entries. The policy defines what needs auditing, and the application just follows through.
Healthcare systems have this concept called "break glass" where doctors can override normal access controls in emergencies. I've seen teams try to implement this with complex if-else chains in application code. It never ends well. The logic becomes unmaintainable, testing is a nightmare, and auditors hate the lack of centralized control.
Cerbos outputs make this pattern elegant. You define normal access rules, emergency override rules, and clear audit requirements all in one place:
rules:
# Normal access - assigned patients only
- name: standard-patient-access
actions: ['view_medical_record']
effect: EFFECT_ALLOW
roles: ['doctor']
condition:
match:
expr: P.id in R.attr.assigned_doctors
output:
when:
ruleActivated: |-
{
"access_type": "STANDARD",
"audit": {
"required": false,
"level": "NORMAL"
}
}
# Emergency override with mandatory audit
- name: emergency-override
actions: ['view_medical_record']
effect: EFFECT_ALLOW
roles: ['doctor']
condition:
match:
expr: request.aux_data.emergency_declared == true
output:
when:
ruleActivated: |-
{
"access_type": "EMERGENCY_OVERRIDE",
"audit": {
"required": true,
"level": "CRITICAL",
"notify": ["compliance@hospital.com", "security@hospital.com"],
"justification_required": true,
"review_within_hours": 24
},
"warning": "This access has been logged and will be reviewed by compliance"
}
# Explain denial with override instructions
- name: explain-denial
actions: ['view_medical_record']
effect: EFFECT_DENY
roles: ['doctor']
condition:
match:
expr: P.id not in R.attr.assigned_doctors
output:
when:
ruleActivated: |-
{
"denial_reason": "NOT_ASSIGNED_PATIENT",
"message": "You are not assigned to this patient",
"override_available": true,
"override_instructions": "Click 'Emergency Access' and provide justification to override"
}
The application handles each scenario appropriately based on the outputs. Emergency access gets logged with high priority, compliance gets notified, and the accessing doctor sees a clear warning. Meanwhile, standard denials come with helpful instructions on how to proceed if truly needed.
A fintech customer once told me their biggest authorization challenge wasn't determining who could do what, but enforcing transaction limits and rate limits based on user tiers, risk scores, and temporal patterns. They had built a separate rate-limiting service that constantly fell out of sync with their authorization policies. Different services had different limits, updates required coordinated deployments, and debugging issues meant correlating logs across multiple systems.
Cerbos outputs let you return rate limit guidance directly from your authorization policies:
rules:
- name: transaction-limits
actions: ['create_transaction']
effect: EFFECT_ALLOW
roles: ['customer']
condition:
match:
all:
of:
- expr: R.attr.amount <= P.attr.daily_limit
- expr: R.attr.amount <= P.attr.remaining_daily_quota
output:
when:
ruleActivated: |-
{
"limits": {
"daily_limit": P.attr.daily_limit,
"remaining_today": P.attr.remaining_daily_quota - R.attr.amount,
"reset_time": timestamp(string(now().getYear()) + "-" + string(now().getMonth()) + "-" + string(now().getDayOfMonth() + 1) + "T00:00:00Z"),
"tier": P.attr.account_tier
},
"risk_checks": {
"unusual_amount": R.attr.amount > P.attr.average_transaction * 3,
"new_recipient": R.attr.recipient_id not in P.attr.known_recipients,
"requires_2fa": R.attr.amount > 1000 || R.attr.recipient_id not in P.attr.known_recipients
}
}
conditionNotMet: |-
{
"denial_reason": R.attr.amount > P.attr.daily_limit ? "EXCEEDS_DAILY_LIMIT" : "INSUFFICIENT_QUOTA",
"limit_exceeded": P.attr.daily_limit,
"amount_requested": R.attr.amount,
"upgrade_available": P.attr.account_tier != "premium",
"upgrade_url": "/account/upgrade"
}
Your application gets everything it needs to enforce limits, trigger additional authentication, and provide helpful feedback to users. The rate-limiting logic lives with your authorization policies where it belongs.
One thing that trips up teams moving from code-based authorization to policy-based systems is testing. How do you verify that your policies not only make correct decisions but also return the right contextual information? Cerbos has built-in testing support that includes output validation:
apiVersion: api.cerbos.dev/v1
tests:
name: TransactionAuthorizationTests
tests:
- name: high-value-transaction-triggers-audit
input:
principal:
id: "user-123"
roles: ["customer"]
attr:
account_tier: "premium"
daily_limit: 10000
resource:
kind: "transaction"
attr:
amount: 5000
classification: "high_value"
actions: ["create"]
expected:
- principal: "user-123"
resource: "transaction"
actions:
create: EFFECT_ALLOW
outputs:
- src: "resource.transaction.vdefault#high-value-monitoring"
val:
audit:
required: true
level: "HIGH"
risk_checks:
requires_2fa: true
These tests run during CI/CD, catching policy changes that might break downstream systems expecting specific outputs. I've seen teams dramatically reduce production incidents by testing not just authorization decisions but the full context their applications depend on.
At a recent authorization conference, someone asked about the performance impact of complex outputs. It's a valid concern. Every output expression gets evaluated when its rule matches, so complicated calculations can add latency. But in practice, the overhead is minimal compared to the network round-trip to your policy decision point.
Here's what I've learned from production deployments. Keep output expressions focused on data assembly rather than complex computation. Use CEL's built-in functions efficiently. Cache computed values when possible. Most importantly, only include outputs that your application actually uses.
The biggest performance win comes from deployment architecture. Running Cerbos as a sidecar eliminates network latency. For one e-commerce platform handling flash sales, this pattern lets them process 50,000 authorization decisions per second, with outputs included. The outputs actually improved overall system performance by eliminating separate calls to fetch rate limits and audit requirements.
The teams that get the most value from outputs are those who think beyond basic authorization. They use outputs to build features that would be complex or impossible with traditional authorization systems. Dynamic help text that explains exactly why access was denied and how to get it. Real-time quota displays that update based on usage patterns. Risk scores that adapt to user behavior.
A SaaS platform I worked with uses outputs to implement progressive disclosure of features. Instead of a binary "has feature X" check, their policies return detailed capability maps. The UI dynamically adjusts based on these outputs, showing upgrade prompts, usage limits, and feature previews all driven by centralized policies. Marketing can adjust tier benefits without code changes. Support can see exactly why a customer hit a limit. Product teams can A/B test new permission models by adjusting outputs.
This is the shift from authorization as a gate to authorization as an intelligent assistant. Your policies don't just decide who can do what; they guide users through your application, explain decisions, enforce business rules, and provide the context that makes complex systems understandable.
If you're coming from an XACML-based system with obligations and advice, the migration path to Cerbos is straightforward. XACML obligations that must be enforced become critical outputs that your application checks explicitly. XACML advice becomes optional outputs that enhance user experience but doesn't block operations.
The key difference is philosophical. XACML's obligation model assumes the PEP might not be able to fulfill requirements, leading to authorization failure. Cerbos assumes your application is capable and gives you the flexibility to handle outputs appropriately. This trust in the application layer makes the system more resilient and easier to debug.
For teams starting fresh, outputs let you avoid the distributed authorization anti-pattern where every service implements its own special rules. Centralize the logic in policies, return rich context through outputs, and let applications focus on their core functionality. Your authorization system becomes a source of truth not just for permissions but for business rules, compliance requirements, and operational constraints.
Cerbos outputs transform authorization from a binary gate into an intelligent policy engine that provides rich, contextual decisions. By returning structured data alongside allow/deny decisions, outputs enable sophisticated patterns like audit trails, emergency access, rate limiting, and user guidance without the complexity of traditional obligation handlers.
The approach fits naturally into modern architectures, integrates cleanly with existing application patterns, and scales to handle millions of authorization decisions. More importantly, it puts control back in the hands of developers and security teams, letting them define and evolve authorization logic without touching application code. Whether you're building a new system or modernizing legacy authorization, outputs provide the flexibility and power to handle real-world authorization requirements elegantly.
If you’re interested in implementing externalized authorization - try out Cerbos Hub or book a call with a Cerbos engineer to see how our solution can help streamline access control in your applications.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team
Join thousands of developers | Features and updates | 1x per month | No spam, just goodies.