Building Policy-Driven Applications: Using Sidecar with OPA in Python
A Comprehensive Guide to Implementing Authorization as a Service
Introduction
As modern applications grow in complexity, managing authorization and access control becomes increasingly challenging. The sidecar pattern combined with Open Policy Agent (OPA) offers an elegant solution by decoupling policy decisions from application code. This architectural approach enables you to centralize policy management, improve security, and maintain flexibility as your authorization requirements evolve.
In this blog post, we'll explore how to implement a Python application that leverages OPA as a sidecar for policy enforcement. We'll cover the architecture, implementation details, and best practices for building production-ready, policy-driven applications.
What is Open Policy Agent (OPA)?
Open Policy Agent is a general-purpose policy engine that enables unified, context-aware policy enforcement across your entire stack. OPA provides a high-level declarative language called Rego for writing policies and REST APIs for querying those policies.
Key benefits of OPA include:
- Decoupling policy decisions from application code
- Policy reusability across multiple services
- Centralized policy management and versioning
- Support for complex authorization scenarios
- Language-agnostic policy enforcement
Understanding the Sidecar Pattern
The sidecar pattern is a container design pattern where a helper container (the sidecar) is deployed alongside your main application container. In the context of OPA, the sidecar runs as a separate process that handles all policy evaluation requests from your application.
This pattern provides several advantages:
- Separation of concerns: Policy logic is isolated from business logic
- Independent scaling: OPA can be scaled separately from your application
- Technology independence: OPA works with any programming language
- Simplified updates: Policies can be updated without redeploying applications
Architecture Overview
Our implementation consists of three main components:
- Python Application: The main service that needs authorization decisions
- OPA Sidecar: A containerized OPA instance running alongside the application
- Rego Policies: Declarative policy definitions that encode authorization rules
The application communicates with OPA through HTTP REST API calls, sending input data and receiving policy decisions in JSON format. This loose coupling ensures that policy changes don't require application code modifications.
Implementation Guide
Step 1: Setting Up the OPA Sidecar
First, we'll set up OPA as a Docker container. Create a docker-compose.yml file:
services:
opa:
image: openpolicyagent/opa:latest
ports:
- "8181:8181"
volumes:
- ./policies:/policies
command:
- "run"
- "--server"
- "--addr=0.0.0.0:8181"
- "--log-level=debug"
- "/policies"
app:
build: .
ports:
- "5001:5001"
environment:
- OPA_URL=http://opa:8181
depends_on:
- opa
Step 2: Writing Rego Policies
Create a policy file (policies/authz.rego) that defines authorization rules. Here's an example for role-based access control:
package authz
import rego.v1
# Default deny
default allow := false
# Allow admins to perform any action
allow if {
input.user.role == "admin"
}
# Allow users to read their own resources
allow if {
input.action == "read"
input.resource.owner == input.user.id
}
# Allow users to update their own resources
allow if {
input.action == "update"
input.resource.owner == input.user.id
input.user.role == "user"
}
# Allow managers to read resources in their department
allow if {
input.action == "read"
input.user.role == "manager"
input.resource.department == input.user.department
}
Step 3: Creating the Python Client
Now, let's create a Python client to interact with OPA. We'll build a reusable class that handles policy queries:
# opa_client.py
import requests
from typing import Dict, Any, Optional
import logging
class OPAClient:
"""Client for interacting with Open Policy Agent."""
def __init__(self, opa_url: str = "http://127.0.0.1:8181", timeout: int = 5):
"""
Initialize the OPA client.
Args:
opa_url: Base URL for the OPA service
timeout: Request timeout in seconds
"""
self.opa_url = opa_url.rstrip('/')
self.timeout = timeout
self.logger = logging.getLogger(__name__)
def check_permission(
self,
policy_path: str,
input_data: Dict[str, Any]
) -> bool:
"""
Check if a permission is allowed based on OPA policy.
Args:
policy_path: Path to the policy (e.g., "authz/allow")
input_data: Input data for policy evaluation
Returns:
True if allowed, False otherwise
"""
try:
url = f"{self.opa_url}/v1/data/{policy_path}"
self.logger.debug(f"Querying OPA at {url} with input: {input_data}")
response = requests.post(
url,
json={"input": input_data},
timeout=self.timeout
)
response.raise_for_status()
result = response.json()
allowed = result.get("result", False)
self.logger.info(
f"Policy decision for {policy_path}: {'ALLOW' if allowed else 'DENY'}"
)
return allowed
except requests.exceptions.Timeout:
self.logger.error(f"OPA request timed out after {self.timeout}s")
return False
except requests.exceptions.RequestException as e:
self.logger.error(f"OPA request failed: {e}")
return False
except Exception as e:
self.logger.error(f"Unexpected error during policy check: {e}")
return False
def query_policy(
self,
policy_path: str,
input_data: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""
Query OPA policy and return full result.
Args:
policy_path: Path to the policy
input_data: Input data for policy evaluation
Returns:
Full policy result or None on error
"""
try:
url = f"{self.opa_url}/v1/data/{policy_path}"
response = requests.post(
url,
json={"input": input_data},
timeout=self.timeout
)
response.raise_for_status()
return response.json().get("result")
except Exception as e:
self.logger.error(f"Policy query failed: {e}")
return None
def health_check(self) -> bool:
"""
Check if OPA is healthy and reachable.
Returns:
True if healthy, False otherwise
"""
try:
url = f"{self.opa_url}/health"
response = requests.get(url, timeout=2)
return response.status_code == 200
except Exception:
return False
Step 4: Integrating with Your Application
Here's an example of how to use the OPA client in a Flask application:
# app.py
import os
from flask import Flask, request, jsonify, g
from functools import wraps
from opa_client import OPAClient
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# Initialize OPA client
opa_url = os.environ.get("OPA_URL", "http://127.0.0.1:8181")
opa = OPAClient(opa_url=opa_url)
def get_current_user(request):
"""Extract user information from request (e.g., from JWT token)."""
# This is a simplified example - implement proper authentication
return {
'id': request.headers.get('X-User-ID', 'anonymous'),
'role': request.headers.get('X-User-Role', 'guest'),
'department': request.headers.get('X-User-Department', 'none')
}
def require_permission(action: str):
"""Decorator to check OPA permissions before executing endpoint."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user = get_current_user(request)
# Extract resource information from the route
resource_id = kwargs.get('resource_id')
if not resource_id:
return jsonify({"error": "Resource ID required"}), 400
# Prepare input for OPA
input_data = {
"user": user,
"action": action,
"resource": {
"id": resource_id,
"owner": get_resource_owner(resource_id),
"department": get_resource_department(resource_id)
}
}
# Check permission
if not opa.check_permission("authz/allow", input_data):
return jsonify({
"error": "Forbidden",
"message": "You don't have permission to perform this action"
}), 403
# Store user in g for use in the endpoint
g.current_user = user
return f(*args, **kwargs)
return decorated_function
return decorator
# Mock database functions
def get_resource_owner(resource_id: str) -> str:
"""Get the owner of a resource."""
# In production, query your database
mock_db = {
"resource_1": "user_123",
"resource_2": "user_456",
"resource_3": "user_123"
}
return mock_db.get(resource_id, "unknown")
def get_resource_department(resource_id: str) -> str:
"""Get the department of a resource."""
mock_db = {
"resource_1": "engineering",
"resource_2": "sales",
"resource_3": "engineering"
}
return mock_db.get(resource_id, "unknown")
def get_resource_data(resource_id: str) -> dict:
"""Get resource data."""
return {
"id": resource_id,
"name": f"Resource {resource_id}",
"owner": get_resource_owner(resource_id),
"department": get_resource_department(resource_id),
"data": "sensitive information"
}
# API Endpoints
@app.route('/api/resources/<resource_id>', methods=['GET'])
@require_permission('read')
def get_resource(resource_id):
"""Get a resource by ID."""
return jsonify(get_resource_data(resource_id))
@app.route('/api/resources/<resource_id>', methods=['PUT'])
@require_permission('update')
def update_resource(resource_id):
"""Update a resource."""
data = request.get_json()
# Perform update logic here
return jsonify({
"message": "Resource updated successfully",
"resource_id": resource_id,
"updated_by": g.current_user['id']
})
@app.route('/api/resources/<resource_id>', methods=['DELETE'])
@require_permission('delete')
def delete_resource(resource_id):
"""Delete a resource."""
# Perform delete logic here
return jsonify({
"message": "Resource deleted successfully",
"resource_id": resource_id
})
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint."""
opa_healthy = opa.health_check()
return jsonify({
"status": "healthy" if opa_healthy else "degraded",
"opa": "up" if opa_healthy else "down"
}), 200 if opa_healthy else 503
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5001))
app.run(debug=True, host='0.0.0.0', port=port)
Step 5: Creating a Dockerfile
Create a Dockerfile for your Python application:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ .
EXPOSE 5001
CMD ["python", "app.py"]
And a requirements.txt:
flask==3.0.0
requests==2.31.0
Best Practices
1. Policy Design
- Use a default-deny approach: Start with
default allow = falseto ensure security by default - Keep policies modular: Organize policies by domain (authz, data_filtering, etc.)
- Write comprehensive tests: Use OPA's built-in testing framework
- Document policy intent: Add comments explaining the business rules
- Version your policies: Use Git to track policy changes
- Use meaningful variable names: Make policies readable and maintainable
2. Performance Optimization
- Implement caching: Cache policy decisions for frequently accessed resources
- Use connection pooling: Reuse HTTP connections to OPA
- Monitor response times: Set up alerts for slow policy evaluations
- Consider bundle-based distribution: For large deployments, use OPA bundles
- Optimize Rego policies: Avoid expensive operations in hot paths
- Use partial evaluation: For data filtering scenarios, leverage OPA's partial evaluation
3. Error Handling
- Fail closed: Always deny access when OPA is unreachable
- Implement proper logging: Log all policy decisions for audit trails
- Set up health checks: Monitor OPA availability continuously
- Use circuit breakers: Prevent cascading failures when OPA is down
- Provide meaningful error messages: Help users understand why access was denied
- Implement retry logic: With exponential backoff for transient failures
4. Security Considerations
- Secure communication: Use TLS for OPA API calls in production
- Validate input data: Sanitize data before sending to OPA
- Implement authentication: Secure OPA API access with tokens or mTLS
- Regularly update OPA: Stay current with security patches
- Audit policy changes: Track who changed what policies and when
- Separate policy environments: Use different OPA instances for dev/staging/prod
5. Testing
- Unit test policies: Write Rego tests for all policy rules
- Integration test the client: Test OPA client error handling
- Load test: Ensure OPA can handle your expected traffic
- Test failure scenarios: Verify behavior when OPA is unavailable
- Use test fixtures: Create reusable test data for policy testing
Advanced Features
Policy Bundles
For production deployments, OPA supports policy bundles that can be served from remote locations. This allows you to update policies without restarting OPA:
# Start OPA with bundle configuration
opa run --server \
--set bundles.authz.service=bundle_service \
--set bundles.authz.resource=bundles/authz.tar.gz \
--set services.bundle_service.url=https://bundle-server.example.com
Create a bundle configuration file (bundle-config.json):
{
"services": {
"bundle_service": {
"url": "https://bundle-server.example.com",
"credentials": {
"bearer": {
"token": "your-auth-token"
}
}
}
},
"bundles": {
"authz": {
"service": "bundle_service",
"resource": "bundles/authz.tar.gz",
"polling": {
"min_delay_seconds": 60,
"max_delay_seconds": 120
}
}
}
}
Decision Logging
Enable decision logging to audit all policy decisions for compliance and debugging:
# Enable console decision logging
opa run --server \
--set decision_logs.console=true
# Or send to a remote service
opa run --server \
--set decision_logs.service=logger \
--set services.logger.url=https://logger.example.com
Decision logs include:
- Input data sent to the policy
- Policy decision result
- Timestamp and duration
- Policy version
Policy Testing
OPA provides a built-in testing framework. Create test files alongside your policies:
# authz_test.rego
package authz
import rego.v1
# Test admin access
test_admin_allowed if {
allow with input as {
"user": {"role": "admin"},
"action": "delete",
"resource": {"owner": "someone_else"}
}
}
# Test user can read own resources
test_user_read_own if {
allow with input as {
"user": {"id": "user_123", "role": "user"},
"action": "read",
"resource": {"owner": "user_123"}
}
}
# Test user cannot read others' resources
test_user_cannot_read_others if {
not allow with input as {
"user": {"id": "user_123", "role": "user"},
"action": "read",
"resource": {"owner": "user_456"}
}
}
# Test user can update own resources
test_user_update_own if {
allow with input as {
"user": {"id": "user_123", "role": "user"},
"action": "update",
"resource": {"owner": "user_123"}
}
}
# Test user cannot update others' resources
test_user_cannot_update_others if {
not allow with input as {
"user": {"id": "user_123", "role": "user"},
"action": "update",
"resource": {"owner": "user_456"}
}
}
# Test manager can read department resources
test_manager_department_access if {
allow with input as {
"user": {
"id": "mgr_123",
"role": "manager",
"department": "engineering"
},
"action": "read",
"resource": {
"owner": "user_456",
"department": "engineering"
}
}
}
# Test manager cannot read resources from other departments
test_manager_cannot_read_other_department if {
not allow with input as {
"user": {
"id": "mgr_123",
"role": "manager",
"department": "engineering"
},
"action": "read",
"resource": {
"owner": "user_456",
"department": "sales"
}
}
}
# Test guest cannot access anything
test_guest_denied if {
not allow with input as {
"user": {"id": "guest_1", "role": "guest"},
"action": "read",
"resource": {"owner": "user_123"}
}
}
Run tests with:
opa test policies/ -v
Data Filtering with Partial Evaluation
OPA can help filter large datasets by generating optimized queries:
# data_filter.rego
package data_filter
import future.keywords.if
import future.keywords.in
# Return all documents the user can access
allowed_documents[doc] {
doc := data.documents[_]
allow_document(doc)
}
# Allow if user owns the document
allow_document(doc) if {
doc.owner == input.user.id
}
# Allow if user is in the same department
allow_document(doc) if {
input.user.role == "manager"
doc.department == input.user.department
}
# Allow if user is admin
allow_document(doc) if {
input.user.role == "admin"
}
Deployment Considerations
Kubernetes Deployment
Deploy OPA as a sidecar container in your pod definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 5000
env:
- name: OPA_URL
value: "http://localhost:8181"
- name: opa
image: openpolicyagent/opa:latest
args:
- "run"
- "--server"
- "--addr=localhost:8181"
- "--set=decision_logs.console=true"
ports:
- name: http
containerPort: 8181
livenessProbe:
httpGet:
path: /health
port: 8181
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health?bundles
port: 8181
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
High Availability
For high-availability scenarios, consider these approaches:
Option 1: Sidecar per instance (Recommended)
- Deploy one OPA sidecar with each application instance
- Provides maximum isolation and performance
- No network latency between app and OPA
- Scales automatically with your application
Option 2: Shared OPA service
- Run OPA as a separate deployment with multiple replicas
- Use a Kubernetes service for load balancing
- Reduces resource overhead
- Requires network calls between app and OPA
Option 3: Hybrid approach
- Use sidecars for critical paths
- Use shared OPA for less critical operations
- Balance performance and resource utilization
Configuration Management
Use ConfigMaps for OPA configuration:
apiVersion: v1
kind: ConfigMap
metadata:
name: opa-config
data:
config.yaml: |
decision_logs:
console: true
bundles:
authz:
service: bundle_service
resource: bundles/authz.tar.gz
services:
bundle_service:
url: https://bundle-server.example.com
Reference in your deployment:
- name: opa
image: openpolicyagent/opa:latest
args:
- "run"
- "--server"
- "--config-file=/config/config.yaml"
volumeMounts:
- name: opa-config
mountPath: /config
volumes:
- name: opa-config
configMap:
name: opa-config
Monitoring and Observability
Prometheus Metrics
OPA exposes Prometheus metrics at the /metrics endpoint. Key metrics to monitor:
# Policy evaluation latency
http_request_duration_seconds{handler="v1/data"}
# Total number of policy decisions
http_request_count_total{handler="v1/data"}
# Bundle loading status
bundle_loaded_timestamp_seconds
# Policy compile time
rego_load_bundles_elapsed_time_seconds
Setting up Prometheus Scraping
Add Prometheus annotations to your pod:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8181"
prometheus.io/path: "/metrics"
Grafana Dashboard
Create a Grafana dashboard to visualize:
- Policy evaluation latency (p50, p95, p99)
- Request rate and error rate
- Bundle update status
- OPA memory and CPU usage
- Policy decision distribution (allow vs deny)
Alerting
Set up alerts for:
# High latency
- alert: OPAPolicyEvaluationSlow
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{handler="v1/data"}[5m])) > 0.5
annotations:
summary: "OPA policy evaluation is slow"
# High error rate
- alert: OPAHighErrorRate
expr: rate(http_request_count_total{handler="v1/data",code=~"5.."}[5m]) > 0.01
annotations:
summary: "OPA is returning errors"
# Bundle not updating
- alert: OPABundleNotUpdating
expr: time() - bundle_loaded_timestamp_seconds > 3600
annotations:
summary: "OPA bundle hasn't updated in over an hour"
Distributed Tracing
Integrate OPA with your distributed tracing system:
from opentelemetry import trace
from opentelemetry.instrumentation.requests import RequestsInstrumentor
# Instrument requests library
RequestsInstrumentor().instrument()
class OPAClient:
def check_permission(self, policy_path: str, input_data: Dict[str, Any]) -> bool:
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("opa.check_permission") as span:
span.set_attribute("policy.path", policy_path)
span.set_attribute("user.id", input_data.get("user", {}).get("id"))
# Make OPA request (automatically traced)
result = self._make_request(policy_path, input_data)
span.set_attribute("policy.result", result)
return result
Example: Complete Project Structure
Here's a recommended project structure:
opa-python-app/
├── app/
│ ├── __init__.py
│ ├── app.py # Flask application
│ ├── opa_client.py # OPA client
│ ├── models.py # Data models
│ └── decorators.py # Auth decorators
├── policies/
│ ├── authz.rego # Authorization policies
│ ├── authz_test.rego # Policy tests
│ └── data_filter.rego # Data filtering policies
├── tests/
│ ├── test_app.py # Application tests
│ ├── test_opa_client.py # OPA client tests
│ └── integration/
│ └── test_authorization.py
├── k8s/
│ ├── deployment.yaml # Kubernetes deployment
│ ├── service.yaml # Kubernetes service
│ └── configmap.yaml # OPA configuration
├── docker-compose.yml # Local development
├── Dockerfile # Application container
├── requirements.txt # Python dependencies
└── README.md # Documentation
Working Example Repository: A complete, working implementation of this tutorial is available at github.com/mshoaibiqbal/opa-python-sidecar. Clone it to get started quickly with OPA sidecars in Python.
Conclusion
Using OPA as a sidecar with Python applications provides a powerful, flexible approach to authorization. By decoupling policy from code, you gain:
- Agility: Update authorization rules without code changes
- Consistency: Enforce the same policies across all services
- Auditability: Clear trail of all policy decisions
- Scalability: Independent scaling of policy evaluation
- Security: Centralized, expert-reviewed policies
The sidecar pattern, combined with OPA's expressive policy language and robust ecosystem, enables you to build secure, compliant applications that evolve with your business requirements. As you scale this architecture, remember to focus on:
- Comprehensive policy testing
- Performance monitoring and optimization
- Security hardening (TLS, authentication, input validation)
- High availability and disaster recovery
- Clear documentation and team training
Whether you're building microservices, APIs, or monolithic applications, OPA sidecars offer a production-ready solution for policy enforcement that integrates seamlessly with Python and any other technology in your stack.
Additional Resources
- Example Repository: https://github.com/mshoaibiqbal/opa-python-sidecar (Complete working implementation)
- Official OPA Documentation: https://www.openpolicyagent.org/docs/
- Rego Playground: https://play.openpolicyagent.org/
- OPA Policy Library: https://github.com/open-policy-agent/library
- Styra Academy: https://academy.styra.com/ (Free OPA training)
- Python requests library: https://requests.readthedocs.io/
- OPA GitHub Repository: https://github.com/open-policy-agent/opa
- OPA Slack Community: https://slack.openpolicyagent.org/
This blog post provides a foundation for implementing OPA sidecars with Python. For production deployments, always consult the official OPA documentation and consider your specific security and compliance requirements.