In modern zero trust architectures, identity-based access control has become the foundation of secure communications. At Riptides, we’ve built a system that automatically issues X.509 certificates (SVIDs) to workloads based on process selectors, allowing services to authenticate each other without hardcoded credentials. Beyond securing workload-to-workload traffic with automatic mTLS, Riptides also federates identities across clouds and injects short-lived credentials directly into workloads. Rooted in SPIFFE, it gives workloads seamless access to third-party APIs without any static or long-lived secrets.
But authentication alone isn’t enough. What if access should only be allowed during certain hours? What if a credential must be usable only once? What if permissions need to vanish right after an emergency deploy? That’s where Riptides Conditional Access comes in, extending our policy engine with time-based, usage-limited, and context-aware controls, all evaluated through Open Policy Agent (OPA).
Riptides uses declarative YAML configuration files to define workload identities, TLS policies, and secrets. Here’s what a typical policy looks like:
# API Gateway service handling external requests
- selectors:
- process:uid: 1000
process:name: api-gateway
destination:port: [8080, 8443]
workloadID: api-gateway
svid:
x509:
dnsNames:
- api.acme.corp
- gateway.acme.corp
ttl: 3600s
allowedSPIFFEIDs:
inbound:
- spiffe://{{.TrustDomain}}/frontend
- spiffe://{{.TrustDomain}}/mobile-app
outbound:
- spiffe://{{.TrustDomain}}/payment-service
- spiffe://{{.TrustDomain}}/user-service
# Payment Service processing transactions
- selectors:
- process:uid: 1001
process:name: payment-svc
destination:port: 9000
workloadID: payment-service
svid:
x509:
dnsNames:
- payment.internal.acme.corp
ttl: 3600s
allowedSPIFFEIDs:
inbound:
- spiffe://{{.TrustDomain}}/api-gateway
- spiffe://{{.TrustDomain}}/order-service
# Payment service backend
- addresses:
- address: payment-service.internal
port: 9000
- address: payment-svc-01.us-west-2.internal
port: 9000
- address: payment-svc-02.us-west-2.internal
port: 9000
labels:
service: payment
tier: backend
region: us-west-2
# User service backend
- addresses:
- address: user-service.internal
port: 8080
- address: users-db-proxy.internal
port: 5432
labels:
service: user-management
tier: backend
Riptides uses TokenEx to dynamically fetch short-lived cloud credentials via workload identity federation—eliminating long-lived secrets entirely. TokenEx is our new open source Go library (short for Token Exchange), to handle fetching and refreshing credentials so everything stays short-lived by default.
# Fetch AWS credentials dynamically using AWS Workload Identity Federation
webserver:
aws-s3-access:
source:
type: tokenex-aws
roleArn: arn:aws:iam::123456789012:role/prod-s3-reader
region: us-east-1
# Fetch GCP access tokens using GCP Workload Identity Federation
accounting:
gcp-bigquery-access:
source:
type: tokenex-gcp
serviceAccount: bigquery-reader@acme-prod.iam.gserviceaccount.com
# Fetch Azure access tokens using Azure Workload Identity Federation
api-gateway:
azure-keyvault-access:
source:
type: tokenex-azure
clientId: 12345678-1234-1234-1234-123456789012
tenantId: 87654321-4321-4321-4321-210987654321
TokenEx exchanges SPIFFE SVIDs for cloud provider credentials (AWS session tokens, GCP/Azure access tokens and OCI UPSTs) and automatically refreshes them before expiration.
To learn more about Tokenex you can check our post: Introducing TokenEx: An Open Source Go Library for Fetching and Refreshing Cloud Credentials
Under the hood, Riptides feeds these YAML policies into an Open Policy Agent (OPA) evaluator (pkg/eval/socket.rego). When a connection is initiated, the agent:
Here’s a simplified flow from the connection evaluation logic:
func (c *evalCommand) HandleCommand(cmd *driver.Command) (*driver.Command, error) {
// Get connection metadata
input := cmd.GetOpaEval().GetConnection()
// Augment with process/system labels
augmentationResp, err := c.augmenter.Augment(taskContext)
if err != nil {
return nil, err
}
// Merge labels
if input.Labels != nil {
maps.Copy(input.Labels, augmentationResp.Labels)
} else {
input.Labels = augmentationResp.Labels
}
// Evaluate against OPA policies
res, err := c.eval.Eval(ctx, input)
if err != nil {
return nil, err
}
// Return policy decision
return buildResponse(res), nil
}
Today’s YAML policies are static, they define who can connect to what, but they don’t capture when, how often, or under what conditions. Real-world security scenarios demand more:
Your on-call engineer needs temporary admin access to production databases during a P0 incident but only for 2 hours, and only once.
AWS credentials should only be valid during a specific deployment window (e.g., 2 AM - 3 AM UTC) to minimize blast radius if leaked.
A service account should have a bearer token that works for exactly 100 API calls, then automatically revokes.
PCI DSS requires that production database access is only permitted during business hours (9 AM - 5 PM EST) for non-emergency personnel.
These examples just scratch the surface, there are countless scenarios where dynamic, context-aware policies are essential for enforcing least-privilege access safely.
Riptides Conditional Access extends the YAML policy format with conditional blocks that leverage OPA’s powerful policy language. Here’s what’s coming:
# Grant database access only during business hours
- selectors:
- process:name: postgresql
destination:port: 5432
workloadID: prod-db-admin
svid:
x509:
dnsNames:
- admin.db.acme.corp
ttl: 3600s
conditionalAccess:
timeWindow:
start: "09:00:00"
end: "17:00:00"
timezone: "America/New_York"
daysOfWeek: [1, 2, 3, 4, 5] # Monday-Friday
allowedSPIFFEIDs:
inbound:
- spiffe://acme.corp/dba-team
# Single-use emergency credentials
- selectors:
- process:name: curl
destination:port: 443
workloadID: emergency-deploy
credentialName: aws-emergency-cred
conditionalAccess:
usageLimit:
maxCount: 1
resetOnExpiry: false
allowedSPIFFEIDs:
outbound:
- spiffe://acme.corp/prod-api
# Restrict access to specific API endpoints and HTTP methods
- selectors:
- process:name: [node, python3]
destination:port: 443
workloadID: api-client
svid:
x509:
dnsNames:
- client.api.acme.corp
ttl: 3600s
conditionalAccess:
allOf:
# Only allow read operations
- httpMethod:
allowed: [GET, HEAD, OPTIONS]
# Restrict to specific paths
- httpPath:
allowed:
- /api/v1/users/*
- /api/v1/orders/read
denied:
- /api/v1/admin/*
- /api/v1/users/*/delete
allowedSPIFFEIDs:
outbound:
- spiffe://acme.corp/api-server
# Emergency access: valid for 2 hours, usable once, only by specific engineer
- selectors:
- process:name: psql
destination:port: 5432
workloadID: break-glass-db-access
svid:
x509:
dnsNames:
- emergency.db.acme.corp
ttl: 7200s # 2 hours
conditionalAccess:
allOf:
- timeWindow:
duration: 7200s # 2 hours from first use
startOnFirstUse: true
- usageLimit:
maxCount: 1
- requiredLabels:
user:oncall: "true"
incident:severity: "P0"
allowedSPIFFEIDs:
inbound:
- spiffe://acme.corp/oncall-engineer
Riptides’ architecture is influenced by the XACML (eXtensible Access Control Markup Language) standard, a widely adopted framework for attribute-based access control (ABAC) that separates policy enforcement, decision-making, administration, and context enrichment into distinct components.
What is XACML?
XACML is an OASIS standard that defines a policy language and architecture for expressing and evaluating access control policies. It was designed to enable fine-grained, attribute-based authorization across diverse systems. The standard emphasizes separation of concerns, and keeping enforcement, decision-making, and policy administration independent, while supporting attribute-based access control where decisions are based on properties of the subject, resource, action, and environment. Its extensibility allows for custom attributes, conditions, and policy combinators, making it adaptable to different security requirements.
While Riptides doesn’t strictly implement the XACML specification (we use OPA/Rego instead of XACML’s XML-based policy language), we adopt its architectural patterns to achieve similar goals. The separation of enforcement from decision making, combined with rich contextual information, gives us both the performance of kernel-level interception and the flexibility of declarative policy evaluation.
Why userspace policy evaluation? Early in Riptides’ development, we explored kernel-based policy evaluation using WASM, but ultimately moved policy decisions to userspace for better flexibility and debuggability. Read more about this architectural decision in our blog post: From Kernel WASM to User-Space Policy Evaluation: Lessons Learned at Riptides.
In Riptides, the Policy Enforcement Point (PEP) is our kernel module and eBPF code that intercepts connection attempts at the network layer and enforces policy decisions. The Policy Decision Point (PDP) is the Riptides agent running in userspace, which evaluates policies using OPA and returns access decisions to the PEP. The Policy Administration Point (PAP) is the Riptides Controlplane, the central hub that loads YAML configuration files and distributes policies to all agents across your infrastructure. Finally, the Policy Information Point (PIP) is our augmentation layer, which enriches connection metadata with process information, labels, and runtime context.
This separation ensures that enforcement happens at wire speed in the kernel without userspace context switches for data plane operations, while policy decisions remain flexible in userspace and can be updated without kernel changes. Policies are expressed declaratively in YAML, defining “what” should happen rather than “how” to enforce it. The PIP provides rich context by augmenting connections with over 50 labels covering process, node, and container metadata.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Riptides Conditional Access Architecture │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Policy Administration Point (PAP) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Riptides Controlplane │ │
│ │ • Central configuration hub for all agents │ │
│ │ • Loads YAML policies: │ │
│ │ - Workload Identities (SVIDs) │ │
│ │ - Service Discovery │ │
│ │ - Credentials (TokenEx) │ │
│ │ - Conditional Access Policies │ │
│ │ • Distributes to all agents │ │
│ └────────────────────────────────────────────────────────┘ │
└────────────────────────────┬─────────────────────────────────┘
│ Pushes policies to agents
▼
┌──────────────────────────────────────────────────────────────┐
│ Policy Decision Point (PDP) - Userspace │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Riptides Agent + Open Policy Agent (OPA) │ │
│ │ • Receives policies from Controlplane │ │
│ │ • Compiles Rego policies │ │
│ │ • Augments connection context (PIP) │ │
│ │ • Evaluates connections against rules │ │
│ │ • Returns ALLOW/DENY + obligations to kernel │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬───────────────────────────────┘
│
Returns │
policy │
decision │
▼
┌──────────────────────────────────────────────────────────────┐
│ Policy Enforcement Point (PEP) - Kernel │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Kernel eBPF Module (lowest level) │ │
│ │ • Intercepts socket operations │ │
│ │ • Captures connection metadata │ │
│ │ • Sends to agent for policy decision │ │
│ │ • Enforces agent decisions (ALLOW/DENY) │ │
│ │ • Issues X.509 SVIDs │ │
│ │ • Injects credentials │ │
│ │ • Manages TLS handshakes │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬───────────────────────────────┘
│
Intercepts │
connection │
attempts │
│
┌──────────────────────────────▼───────────────────────────────┐
│ Workload Process │
│ (e.g., API Gateway connecting to Payment Service) │
└──────────────────────────────────────────────────────────────┘
Example Flow: API Gateway → Payment Service
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. [PAP] Controlplane loads YAML policies and pushes to all agents
2. API Gateway (UID 1000, process: api-gateway) attempts connection to
payment-service.internal:9000
3. [PEP - Kernel] eBPF hooks socket creation at lowest level, extracts:
• PID, UID, process name
• Destination IP, port
• Protocol (TCP)
Sends to agent for decision
4. [PDP - Agent] Augments with runtime context (PIP):
• Labels: {service: api-gateway, region: us-west-2, ...}
• Current time: 2025-11-27T14:30:00Z
• Usage count: 42 connections today
5. [PDP - OPA] Evaluates against compiled policy from Controlplane:
rego: matching_policies contains policy if {
some p in data.policies # From Controlplane
is_subset(input.labels, p.selectors)
# Check conditional access
time_in_window(p.conditionalAccess.timeWindow, input.timestamp)
usage_under_limit(p.conditionalAccess.usageLimit, input.usage)
# Business hours check: 9 AM - 5 PM EST
hour := time.clock(input.timestamp)[0]
hour >= 9; hour < 17 # ✅ PASS (2:30 PM EST)
}
6. [PDP → PEP] Agent returns policy decision to kernel:
{
"decision": "ALLOW",
"svid": {
"dnsNames": ["api.acme.corp"],
"ttl": 3600
},
"allowedPeers": ["spiffe://acme.corp/payment-service"],
"tlsMode": "MUTUAL"
}
7. [PEP - Kernel] Enforces decision at lowest level:
• Issues X.509 certificate (api.acme.corp)
• Intercepts TLS handshake
• Validates peer SPIFFE ID: spiffe://acme.corp/payment-service
• Connection established ✅
At startup, the agent (PDP) reads YAML configuration files (PAP) and compiles the OPA policy once. The Rego policy logic itself is static—what changes is the data fed into it at evaluation time:
func (c *evalCommand) OnDataUpdate(data map[string]any) error {
// Compile OPA policy once with static policies from YAML
// Policy contains the evaluation rules (socket.rego)
// Data contains the YAML configuration (identities, services, credentials)
opa, err := eval.NewOpaEvaluator(context.Background(), c.logger, data)
if err != nil {
return err
}
c.eval = opa // Compiled policy ready for evaluation
return nil
}
The key insight: The Rego policy is compiled once. Only the input data (connection metadata + runtime context) changes per evaluation.
When a process initiates a connection, the kernel eBPF module (PEP) intercepts it at the socket layer and sends metadata to the userspace agent (PDP) for a policy decision.
The agent’s augmentation layer (PIP):
The pre-compiled Rego policy in the agent (PDP) evaluates the dynamic input against static policy rules:
matching_policies contains policy if {
some p in data.policies # Static policies from YAML (loaded at startup from PAP)
some selectorset in p.selectors
is_subset(input.labels, selectorset) # Match process/destination from PIP context
# NEW: Evaluate conditional access using runtime data
evaluate_conditional_access(p.conditionalAccess, input)
policy := prepare_policy_response(p, input)
}
Key Architecture Points:
data.policies: Static YAML configuration from PAP (identities, services, credentials)input: Dynamic per-connection data enriched by PIP (metadata, timestamp, usage counters)If the policy matches and conditions pass, the agent (PDP) returns a decision to the kernel module (PEP):
The kernel module (PEP) enforces the decision:
If the policy matches and conditions pass:
Riptides Conditional Access will be rolled out in stages, reflecting a careful, iterative process. Each phase is informed by internal testing, collaboration with design partners, and ongoing user feedback, ensuring a robust, production-ready feature set that evolves with real-world requirements.
start/end times)startOnFirstUse + duration)incident:severity=P0)Conditional Access extends Riptides from a workload identity platform into a dynamic, context-aware access control system, fully compatible with Zero Trust principles. By combining:
…you get a system that continuously enforces least-privilege access. Access rights can automatically expire, be limited in use, or adapt to time, context, and compliance requirements—reducing the risk of over-privileged credentials, eliminating manual break-glass processes, and closing security gaps.
Here’s a realistic production policy combining all features:
# Production database access with multiple safeguards
- selectors:
- process:name: [psql, pgcli]
destination:port: 5432
workloadID: prod-db-access
svid:
x509:
dnsNames:
- db.prod.acme.corp
ttl: 1800s # 30 minutes
credentialName: postgres-admin
conditionalAccess:
allOf:
# Only during business hours
- timeWindow:
start: "09:00:00"
end: "17:00:00"
timezone: "America/New_York"
daysOfWeek: [1, 2, 3, 4, 5]
# Max 10 connections per hour
- usageLimit:
maxCount: 10
window: 3600s
# Only allow read operations via HTTP
- httpMethod:
allowed: [GET, HEAD]
# Restrict to specific database query endpoints
- httpPath:
allowed:
- /api/v1/query/*
- /api/v1/reports/*
denied:
- /api/v1/admin/*
# Must have DBA role label
- requiredLabels:
user:role: "dba"
access:level: "admin"
allowedSPIFFEIDs:
inbound:
- spiffe://acme.corp/dba-team
connection:
tls:
mode: MUTUAL
This policy ensures:
Riptides Conditional Access brings fine-grained, time-aware security controls to workload identity. By extending our YAML policy format with OPA-powered conditions, you can enforce least-privilege access dynamically, without sacrificing developer velocity.