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
-
Externalization of Policy
- Decouples policy decision logic from application code.
- No code modification required for policy changes.
-
Versatility
- Can be used in various environments such as microservices, Kubernetes, API Gateways, etc.
- Independent of language or framework.
-
Declarative Policy Language (Rego)
- Describes policies in a dedicated high-level language.
- Highly readable and capable of expressing complex rules.
-
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 namespacedefault: Default value settingallow: Rule for permission decisioninput: 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
-
Centralized Policy Management
- Apply unified policies across multiple applications.
- Easy policy modification.
-
Flexibility
- Flexibly implement RBAC, ABAC, and other authorization models.
- Can express complex business rules.
-
Testability
- Policies can be tested independently.
- Unit testing framework is provided.
-
Language Agnostic
- Usable from any programming language.
- Integrated via REST API.
-
High Performance
- High-speed in-memory evaluation.
- Millisecond response times.