Introducing Riptides Conditional Access: Fine-Grained, Time-Aware Security Policies
The Evolution of Zero Trust Security
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).
How Riptides Works
Riptides uses declarative YAML configuration files to define workload identities, TLS policies, and secrets. Here's what a typical policy looks like:
Identity Configuration
# 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-serviceService Discovery
# 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: backendDynamic Secret Sources with TokenEx
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-210987654321TokenEx 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
The OPA Engine
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:
- Augments the connection with runtime metadata (process info, destination, labels, etc)
- Evaluates the connection against OPA policies loaded from YAML
- Returns matching policies with certificate issuance, TLS mode, allowed peers, and credentials
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
}The Problem: Static Policies Aren't Enough
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:
Use Case 1: Emergency Break-Glass Access
Your on-call engineer needs temporary admin access to production databases during a P0 incident but only for 2 hours, and only once.
Use Case 2: Time-Window Credential Rotation
AWS credentials should only be valid during a specific deployment window (e.g., 2 AM - 3 AM UTC) to minimize blast radius if leaked.
Use Case 3: Rate-Limited API Keys
A service account should have a bearer token that works for exactly 100 API calls, then automatically revokes.
Use Case 4: Compliance-Driven Time Fencing
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.
Enter: Conditional Access Policies
Riptides Conditional Access extends the YAML policy format with conditional blocks that leverage OPA's powerful policy language. Here's what's coming:
Time-Based Access Control
# 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-teamUsage-Based Access Control
# 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-apiHTTP Path & Method-Based Access Control
# 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
Combined Conditions: Break-Glass Access
# 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-engineerHow It Works: The OPA Integration
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.
XACML-Inspired Architecture Components
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.
How the Components Work Together
1. Policy Loading & Compilation (PAP → PDP)
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.
2. Connection Interception (PEP)
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.
3. Context Enrichment (PIP → PDP)
The agent's augmentation layer (PIP):
- Captures metadata (PID, UID, process name, destination IP/port)
- Augments with labels (hostname, kernel version, Docker tags, custom labels)
- Adds runtime context (current timestamp, usage counters from state store)
- Queries the pre-compiled OPA policy with this enriched input
4. Policy Evaluation (PDP)
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:
- Rego policy: Compiled once at startup, contains evaluation logic (lives in PDP)
data.policies: Static YAML configuration from PAP (identities, services, credentials)input: Dynamic per-connection data enriched by PIP (metadata, timestamp, usage counters)- Evaluation: Fast—no recompilation, just data lookup and rule matching
5. Policy Enforcement (PDP → PEP)
If the policy matches and conditions pass, the agent (PDP) returns a decision to the kernel module (PEP):
- ALLOW - Issue X.509 SVID with specified DNS names and TTL, inject credentials, configure TLS mode
- DENY - Block the connection at the kernel layer
- OBLIGATIONS - Additional actions (e.g., log the connection, increment usage counters)
The kernel module (PEP) enforces the decision:
- Allows or blocks the connection
- Intercepts TLS handshake if needed (for mTLS or credential injection)
- Reports enforcement events back to the agent for audit logging
4. Certificate Issuance & Credential Injection
If the policy matches and conditions pass:
- Issue X.509 SVID with specified DNS names and TTL
- Inject credentials (AWS, GCP, bearer tokens) into the connection
- Configure TLS mode (mTLS, SIMPLE, intercept)
- Enforce allowed SPIFFE IDs for peer validation
Conditional Access: Phased Rollout Plan
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.
Phase 1: Time-Based Access
- Absolute time windows (
start/endtimes) - Day-of-week filtering
- Timezone support
Phase 2: HTTP-Based Access
- HTTP path-based access control (Layer 7 policies)
- HTTP method restrictions (GET/POST/PUT/DELETE)
- Header-based conditions
Phase 3: Stateful Conditions
- Connection count limits
- Request rate limiting
- Token depletion tracking
- Relative durations (
startOnFirstUse+duration) - Integration with Redis/etcd for distributed state
Phase 4: Context-Aware & Audit
- Required label matching (e.g.,
incident:severity=P0) - IP allowlists/denylists
- Geolocation-based access (cloud region constraints)
- Custom OPA policy hooks
- Conditional access event logging
- Compliance reports (SOC 2, PCI DSS)
- Policy violation alerts
Why This Matters
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:
- Process-level selectors (who is making the connection)
- Service discovery (what they're connecting to)
- Credential injection (what secrets they need)
- Conditional policies (when and how they can access)
...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.
Example: Full Conditional Access Policy
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: MUTUALThis policy ensures:
- ✅ Only DBAs can connect
- ✅ Only during business hours
- ✅ Rate-limited to 10 connections/hour
- ✅ Only read operations (GET/HEAD) allowed
- ✅ Restricted to query/report endpoints (no admin access)
- ✅ Automatically revokes after 30 minutes
- ✅ Full mutual TLS authentication
Conclusion
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.
If you enjoyed this post, follow us on LinkedIn and X for more updates. If you’d like to see Riptides in action, get in touch with us for a demo.
Ready to replace secrets
with trusted identities?
Build with trust at the core.
