Skip to main content

Authorization with OPA (Open Policy Agent)

Open Policy Agent (OPA) is an open-source policy engine that enables unified policy-based authorization.

What is OPA

OPA (pronounced "oh-pa") is a Cloud Native Computing Foundation (CNCF) project and a tool for managing and applying policies uniformly across applications.

Key Features

  1. Externalization of Policy

    • Decouples policy decision logic from application code.
    • No code modification required for policy changes.
  2. Versatility

    • Can be used in various environments such as microservices, Kubernetes, API Gateways, etc.
    • Independent of language or framework.
  3. Declarative Policy Language (Rego)

    • Describes policies in a dedicated high-level language.
    • Highly readable and capable of expressing complex rules.
  4. Real-time Decision Making

    • High-speed policy evaluation in milliseconds.
    • Available as an API.

OPA Architecture

Basic OPA Workflow

Rego Policy Language

Rego is the declarative policy language used in OPA.

Basic Syntax

Simple Authorization Policy

package authz

# Deny by default
default allow = false

# Admins are always allowed
allow {
input.user.role == "admin"
}

# General users can only read their own resources
allow {
input.user.role == "user"
input.action == "read"
input.resource.owner == input.user.id
}

Policy Components:

  • package: Policy namespace
  • default: Default value setting
  • allow: Rule for permission decision
  • input: Context information passed from the application

More Complex Example: RBAC (Role-Based Access Control)

package rbac

import future.keywords.if
import future.keywords.in

# Deny by default
default allow = false

# Admins can perform all actions
allow if {
input.user.role == "admin"
}

# Editors can create, read, and update
allow if {
input.user.role == "editor"
input.action in ["create", "read", "update"]
}

# Viewers can only read
allow if {
input.user.role == "viewer"
input.action == "read"
}

# Resource owners have full control over their resources
allow if {
input.resource.owner == input.user.id
input.action in ["read", "update", "delete"]
}

Example of ABAC (Attribute-Based Access Control)

package abac

import future.keywords.if
import future.keywords.in

default allow = false

# Can access resources of the department the user belongs to
allow if {
input.user.department == input.resource.department
input.action == "read"
}

# Project members can edit
allow if {
input.user.id in input.resource.project_members
input.action in ["read", "update"]
}

# Access allowed only during business hours
allow if {
input.user.role == "employee"
is_business_hours
}

is_business_hours if {
hour := time.clock(time.now_ns())[0]
hour >= 9
hour < 18
}

# Confidential data only for users with specific clearance level or higher
allow if {
input.resource.classification == "confidential"
input.user.clearance_level >= 3
input.action == "read"
}

Implementation Examples

Integration with ASP.NET Core

OPA Setup

# Start OPA with Docker
docker run -p 8181:8181 openpolicyagent/opa:latest \
run --server --log-level debug

Placing Policy File (authz.rego)

package authz

import future.keywords.if
import future.keywords.in

default allow = false

# Admins can access all resources
allow if {
input.user.role == "admin"
}

# Users can only access their own data
allow if {
input.user.role == "user"
input.action in ["read", "update"]
input.resource.userId == input.user.id
}

# Public resources are readable by anyone
allow if {
input.action == "read"
input.resource.visibility == "public"
}

Implementation of OPA Client Service

using System.Text;
using System.Text.Json;

public interface IOpaClient
{
Task<bool> EvaluateAsync(object input);
}

public class OpaClient : IOpaClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<OpaClient> _logger;
private const string OpaUrl = "http://localhost:8181/v1/data/authz/allow";

public OpaClient(HttpClient httpClient, ILogger<OpaClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}

public async Task<bool> EvaluateAsync(object input)
{
try
{
var payload = new { input };
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await _httpClient.PostAsync(OpaUrl, content);
response.EnsureSuccessStatusCode();

var responseBody = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<OpaResponse>(responseBody);

return result?.Result ?? false;
}
catch (Exception ex)
{
_logger.LogError(ex, "OPA evaluation error");
throw new InvalidOperationException("Authorization service unavailable", ex);
}
}

private class OpaResponse
{
public bool Result { get; set; }
}
}

Implementation of Authorization Middleware

public class OpaAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly IOpaClient _opaClient;
private readonly ILogger<OpaAuthorizationMiddleware> _logger;

public OpaAuthorizationMiddleware(
RequestDelegate next,
IOpaClient opaClient,
ILogger<OpaAuthorizationMiddleware> logger)
{
_next = next;
_opaClient = opaClient;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
// Get authenticated user info
if (!context.User.Identity?.IsAuthenticated ?? true)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new { error = "Authentication required" });
return;
}

var userId = context.User.FindFirst("sub")?.Value;
var role = context.User.FindFirst("role")?.Value;
var action = GetActionFromMethod(context.Request.Method);

// Construct input data for OPA
var input = new
{
user = new
{
id = userId,
role = role
},
action = action,
resource = new
{
userId = context.Request.RouteValues["userId"]?.ToString(),
visibility = context.Items["ResourceVisibility"]?.ToString() ?? "private"
}
};

try
{
var allowed = await _opaClient.EvaluateAsync(input);

if (allowed)
{
await _next(context);
}
else
{
_logger.LogWarning(
"Access denied for user {UserId} to perform {Action}",
userId, action);

context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new { error = "Access denied" });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Authorization error");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Authorization service error" });
}
}

private static string GetActionFromMethod(string method)
{
return method switch
{
"GET" => "read",
"POST" => "create",
"PUT" => "update",
"PATCH" => "update",
"DELETE" => "delete",
_ => "unknown"
};
}
}

Configuration in Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Register OPA Client
builder.Services.AddHttpClient<IOpaClient, OpaClient>();

// JWT Authentication Configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty)
)
};
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();

// Apply OPA Authorization Middleware
app.UseMiddleware<OpaAuthorizationMiddleware>();

app.UseAuthorization();

// Protected Endpoints
app.MapGet("/api/users/{userId}/profile", (string userId, HttpContext context) =>
{
return Results.Ok(new { message = "User profile data", userId });
})
.RequireAuthorization();

app.MapPut("/api/users/{userId}/profile", (string userId, HttpContext context) =>
{
return Results.Ok(new { message = "Profile updated", userId });
})
.RequireAuthorization();

app.MapGet("/api/resources/{resourceId}", async (
string resourceId,
HttpContext context,
IResourceRepository repository) =>
{
var resource = await repository.GetByIdAsync(resourceId);

if (resource == null)
{
return Results.NotFound(new { error = "Resource not found" });
}

// Add resource info to context (used by OPA middleware)
context.Items["ResourceVisibility"] = resource.Visibility;

return Results.Ok(resource);
})
.RequireAuthorization();

app.Run();

Dynamic Loading of OPA Policies

public class OpaPolicyService
{
private readonly HttpClient _httpClient;
private readonly ILogger<OpaolicyService> _logger;

public OpaolicyService(HttpClient httpClient, ILogger<OpaolicyService> logger)
{
_httpClient = httpClient;
_logger = logger;
}

public async Task UploadPolicyAsync(string policyPath)
{
try
{
var policy = await File.ReadAllTextAsync(policyPath);
var content = new StringContent(policy, Encoding.UTF8, "text/plain");

var response = await _httpClient.PutAsync(
"http://localhost:8181/v1/policies/authz",
content);

response.EnsureSuccessStatusCode();
_logger.LogInformation("Policy uploaded successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to upload policy");
throw;
}
}
}

// Load policy at application startup
public class PolicyLoaderHostedService : IHostedService
{
private readonly OpaolicyService _policyService;

public PolicyLoaderHostedService(OpaolicyService policyService)
{
_policyService = policyService;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
await _policyService.UploadPolicyAsync("./policies/authz.rego");
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Attribute-Based Authorization Implementation

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class OpaAuthorizeAttribute : Attribute, IAsyncAuthorizationFilter
{
private readonly string _action;

public OpaAuthorizeAttribute(string action)
{
_action = action;
}

public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var opaClient = context.HttpContext.RequestServices
.GetRequiredService<IOpaClient>();

var userId = context.HttpContext.User.FindFirst("sub")?.Value;
var role = context.HttpContext.User.FindFirst("role")?.Value;

var input = new
{
user = new { id = userId, role = role },
action = _action,
resource = new
{
userId = context.RouteData.Values["userId"]?.ToString()
}
};

var allowed = await opaClient.EvaluateAsync(input);

if (!allowed)
{
context.Result = new ForbidResult();
}
}
}

// Usage Example
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpGet("{userId}/profile")]
[OpaAuthorize("read")]
public IActionResult GetProfile(string userId)
{
return Ok(new { message = "User profile data", userId });
}

[HttpPut("{userId}/profile")]
[OpaAuthorize("update")]
public IActionResult UpdateProfile(string userId)
{
return Ok(new { message = "Profile updated", userId });
}
}

Usage in Kubernetes

OPA can be used as an admission controller for Kubernetes.

Installing Gatekeeper

# Install Gatekeeper (OPA for Kubernetes)
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.15/deploy/gatekeeper.yaml

Defining Constraint Template

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels

violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}

Applying Constraint

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-labels
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet"]
parameters:
labels: ["app", "environment", "owner"]

Integration with API Gateway

Example with Envoy + OPA

# Envoy Configuration
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: backend_service
http_filters:
- name: envoy.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: opa
timeout: 0.5s
- name: envoy.filters.http.router

clusters:
- name: opa
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: opa
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: opa
port_value: 9191

Best Practices

1. Policy Design Principles

Policy Separation

# Base Policy
package base

default allow = false

# Admin Function Policy
package admin

import data.base

allow {
base.allow
input.user.role == "admin"
}

# User Function Policy
package user

import data.base

allow {
base.allow
input.user.role == "user"
input.resource.owner == input.user.id
}

Testable Policies

package authz_test

import data.authz

# Test for Admin
test_admin_can_access_all {
authz.allow with input as {
"user": {"role": "admin"},
"action": "delete",
"resource": {"owner": "other_user"}
}
}

# Test for General User
test_user_can_access_own_resource {
authz.allow with input as {
"user": {"id": "user123", "role": "user"},
"action": "read",
"resource": {"owner": "user123"}
}
}

test_user_cannot_access_others_resource {
not authz.allow with input as {
"user": {"id": "user123", "role": "user"},
"action": "read",
"resource": {"owner": "other_user"}
}
}

2. Performance Optimization

Policy Caching

using Microsoft.Extensions.Caching.Memory;

public class CachedOpaClient : IOpaClient
{
private readonly IOpaClient _innerClient;
private readonly IMemoryCache _cache;
private readonly ILogger<CachedOpaClient> _logger;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);

public CachedOpaClient(
IOpaClient innerClient,
IMemoryCache cache,
ILogger<CachedOpaClient> logger)
{
_innerClient = innerClient;
_cache = cache;
_logger = logger;
}

public async Task<bool> EvaluateAsync(object input)
{
var cacheKey = GenerateCacheKey(input);

if (_cache.TryGetValue(cacheKey, out bool cachedResult))
{
_logger.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
return cachedResult;
}

var result = await _innerClient.EvaluateAsync(input);

_cache.Set(cacheKey, result, _cacheDuration);
_logger.LogDebug("Cached result for key: {CacheKey}", cacheKey);

return result;
}

private static string GenerateCacheKey(object input)
{
return System.Text.Json.JsonSerializer.Serialize(input);
}
}

// Registration in Program.cs
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<OpaClient>();
builder.Services.AddSingleton<IOpaClient, CachedOpaClient>(sp =>
{
var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
var logger = sp.GetRequiredService<ILogger<OpaClient>>();
var innerClient = new OpaClient(httpClient, logger);

var cache = sp.GetRequiredService<IMemoryCache>();
var cachedLogger = sp.GetRequiredService<ILogger<CachedOpaClient>>();

return new CachedOpaClient(innerClient, cache, cachedLogger);
});

Using Bundles

Use OPA bundles to distribute policies and data efficiently.

# Create Bundle
opa build -b ./policies -o bundle.tar.gz

# Start OPA with Bundle
docker run -p 8181:8181 \
-v $(pwd)/bundle.tar.gz:/bundle.tar.gz \
openpolicyagent/opa:latest \
run --server --bundle /bundle.tar.gz

3. Security Considerations

Input Validation

package authz

import future.keywords.if

# Input Validation
valid_input {
is_string(input.user.id)
is_string(input.user.role)
is_string(input.action)
is_object(input.resource)
}

# Evaluate only if input is valid
allow if {
valid_input
# Actual authorization logic
...
}

Principle of Default Deny

# Always deny by default
default allow = false

# Allow only with explicit allow rules
allow {
# Specific conditions
}

Logging and Monitoring

public class OpaAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly IOpaClient _opaClient;
private readonly ILogger<OpaAuthorizationMiddleware> _logger;

public OpaAuthorizationMiddleware(
RequestDelegate next,
IOpaClient opaClient,
ILogger<OpaAuthorizationMiddleware> logger)
{
_next = next;
_opaClient = opaClient;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();

try
{
var userId = context.User.FindFirst("sub")?.Value;
var role = context.User.FindFirst("role")?.Value;
var action = GetActionFromMethod(context.Request.Method);
var resourceId = context.Request.RouteValues["resourceId"]?.ToString();

var input = BuildOpaInput(context);
var result = await _opaClient.EvaluateAsync(input);

stopwatch.Stop();

// Structured Logging
_logger.LogInformation(
"OPA Authorization - User: {UserId}, Action: {Action}, Resource: {ResourceId}, " +
"Allowed: {Allowed}, Duration: {Duration}ms",
userId, action, resourceId, result, stopwatch.ElapsedMilliseconds);

if (result)
{
await _next(context);
}
else
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new { error = "Access denied" });
}
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex,
"Authorization error after {Duration}ms",
stopwatch.ElapsedMilliseconds);

context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
}
}

private object BuildOpaInput(HttpContext context)
{
var userId = context.User.FindFirst("sub")?.Value;
var role = context.User.FindFirst("role")?.Value;
var action = GetActionFromMethod(context.Request.Method);

return new
{
user = new { id = userId, role = role },
action = action,
resource = new
{
userId = context.Request.RouteValues["userId"]?.ToString(),
visibility = context.Items["ResourceVisibility"]?.ToString() ?? "private"
}
};
}

private static string GetActionFromMethod(string method)
{
return method switch
{
"GET" => "read",
"POST" => "create",
"PUT" => "update",
"PATCH" => "update",
"DELETE" => "delete",
_ => "unknown"
};
}
}

Pros and Cons of OPA

Pros

  1. Centralized Policy Management

    • Apply unified policies across multiple applications.
    • Easy policy modification.
  2. Flexibility

    • Flexibly implement RBAC, ABAC, and other authorization models.
    • Can express complex business rules.
  3. Testability

    • Policies can be tested independently.
    • Unit testing framework is provided.
  4. Language Agnostic

    • Usable from any programming language.
    • Integrated via REST API.
  5. High Performance

    • High-speed in-memory evaluation.
    • Millisecond response times.