Security Model: 5-Layer Defense
Cascade Platform implements security across five layers to protect workflows, data, and users.
Defense in Depth Strategy
┌────────────────────────────────────┐
│ Layer 1: Authentication (Ory) │ WHO are you?
├────────────────────────────────────┤
│ Layer 2: Authorization (SpiceDB) │ WHAT can you do?
├────────────────────────────────────┤
│ Layer 3: Policies (OPA) │ WHAT should you do?
├────────────────────────────────────┤
│ Layer 4: Data Isolation (RLS) │ WHAT data can you see?
├────────────────────────────────────┤
│ Layer 5: Audit Logging (OTEL) │ WHAT did you do?
└────────────────────────────────────┘Layer 1: Authentication - “Who are you?”
Technology: Ory Kratos (open-source identity)
What it does:
- Multi-factor authentication (passwords, TOTP, security keys)
- Social login (Google, GitHub, Microsoft)
- Session management (cookies, tokens)
- Account recovery flows
How it works:
User enters credentials
↓
Ory Kratos validates
↓
Session created (JWT token)
↓
Token included in all requests
↓
Cascade verifies token on each requestImplementation:
// Middleware verifies token
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
// Verify with Ory
claims, err := ory.VerifyToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Add to request context
ctx := context.WithValue(r.Context(), "user_id", claims.Sub)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Layer 2: Authorization - “What can you do?”
Technology: SpiceDB (Zanzibar-style relationship-based access control)
What it does:
- Fine-grained permissions (not just roles)
- Relationship-based access (user X can edit resource Y because…)
- Delegated access (user A grants permission to user B)
- Relationship caching for performance
How it works:
User wants to edit workflow
↓
SpiceDB checks: Is user related to workflow as "editor"?
↓
Relationships exist?
- user:alice → workflow:wf-123 [editor]
- Yes → Allow
- No → DenyExample relationships:
user:alice → organization:acme [member]
user:alice → team:engineering [member]
team:engineering → organization:acme [team_of]
user:alice → workflow:wf-123 [owner]
user:bob → workflow:wf-123 [viewer]Checking permissions:
func CanEditWorkflow(userID, workflowID string) bool {
// SpiceDB query: Does relation exist?
has, err := spicedb.Check(
"user:" + userID,
"edit",
"workflow:" + workflowID,
)
return has && err == nil
}Layer 3: Policies - “What should you do?”
Technology: OPA (Open Policy Agent)
What it does:
- Business rule enforcement
- Context-aware decisions
- Compliance checking
- Workflow constraints
Example policy:
package workflow_policies
# Only engineering team can approve >$10K workflows
allow_approval {
input.user.team == "engineering"
input.amount <= 10000
}
# VP approval required for >$10K
require_vp_approval {
input.amount > 10000
input.user.role == "manager"
}How it works:
func CanApprove(ctx context.Context, user *User, workflow *Workflow) bool {
// Evaluate OPA policy
decision, err := opa.Evaluate(ctx, "workflow_policies", map[string]interface{}{
"user": map[string]interface{}{
"id": user.ID,
"team": user.Team,
"role": user.Role,
},
"amount": workflow.Amount,
})
return decision["allow_approval"].(bool)
}Layer 4: Data Isolation - “What data can you see?”
Technology: PostgreSQL Row-Level Security (RLS)
What it does:
- Multi-tenant isolation
- Row-level filtering
- Transparent to application
- Prevents accidental data leaks
Implementation:
-- Create RLS policy
CREATE POLICY tenant_isolation ON workflows
USING (tenant_id = current_setting('app.current_tenant'));
-- Enable RLS on table
ALTER TABLE workflows ENABLE ROW LEVEL SECURITY;
-- Application sets tenant
SET app.current_tenant = 'tenant-123';
-- Now all queries automatically filtered
SELECT * FROM workflows; -- Only returns tenant-123 workflowsIn application code:
func GetWorkflows(ctx context.Context, tenantID string) ([]Workflow, error) {
// Set tenant in database session
err := db.Exec("SET app.current_tenant = $1", tenantID)
// All subsequent queries auto-filtered by RLS policy
var workflows []Workflow
err = db.QueryContext(ctx, "SELECT * FROM workflows").Scan(&workflows)
return workflows, err
}Layer 5: Audit Logging - “What did you do?”
Technology: OpenTelemetry + PostgreSQL + Loki
What it does:
- Immutable record of all actions
- Who did what, when, where
- Compliance reporting
- Incident investigation
Logged events:
- User authentication (login/logout)
- Permission changes (SpiceDB updates)
- Policy decisions (OPA evaluations)
- Workflow state transitions
- Data access (SELECT, INSERT, UPDATE, DELETE)
- Configuration changes
- Error eventsImplementation:
func LogAction(ctx context.Context, action string, details map[string]interface{}) {
// Get audit information
userID := ctx.Value("user_id").(string)
timestamp := time.Now()
// Record to PostgreSQL audit table
_, err := db.Exec(`
INSERT INTO audit_log (user_id, action, details, timestamp)
VALUES ($1, $2, $3, $4)
`, userID, action, details, timestamp)
// Also emit as trace
span := trace.SpanFromContext(ctx)
span.AddEvent("audit", trace.WithAttributes(
attribute.String("user_id", userID),
attribute.String("action", action),
))
}
// Usage
LogAction(ctx, "workflow_approved", map[string]interface{}{
"workflow_id": "wf-123",
"approved_by": "alice@company.com",
})Query audit log:
-- Who accessed workflow wf-123?
SELECT user_id, action, timestamp FROM audit_log
WHERE details->'workflow_id' = '"wf-123"'
ORDER BY timestamp DESC;
-- What did alice@company.com do today?
SELECT * FROM audit_log
WHERE user_id = 'alice@company.com'
AND timestamp > NOW() - INTERVAL '1 day'
ORDER BY timestamp DESC;Security Features
1. End-to-End Encryption
In Transit:
- TLS 1.3 for all connections
- Certificate pinning for critical services
- mTLS between internal services
At Rest:
- PostgreSQL encrypted columns
- Redis encryption at rest
- Application-level encryption for sensitive data
Implementation:
// Encrypt sensitive data before storage
encrypted, err := encryptSensitive(apiKey)
db.Exec("UPDATE credentials SET api_key = $1", encrypted)
// Decrypt on retrieval
decrypted, err := decryptSensitive(encryptedKey)2. Secrets Management
Using: HashiCorp Vault
What it does:
- Centralized secret storage
- Automatic rotation
- Access logging
- Dynamic credentials
Access pattern:
// Retrieve secret from Vault
secret, err := vault.Read("secret/data/database-credentials")
dbPassword := secret.Data["password"].(string)
// Use in connection string
connStr := fmt.Sprintf("postgres://user:%s@host/db", dbPassword)3. API Key Rotation
Implementation:
- Old key and new key both valid during transition
- 7-day overlap window
- Automatic reminders
- Forceful revocation after expiry
// Create new key
newKey := generateKey()
keyStore.Save(userID, newKey, isActive=false) // Inactive
// Notify user
sendEmail("New API key created. Old key expires in 7 days.")
// After 7 days
scheduleRevocation(oldKey, after=7*days)4. Rate Limiting
Per-user limits:
- 1000 requests/hour (normal users)
- 10000 requests/hour (premium users)
- 100 requests/hour (unauthenticated)
Implementation:
func RateLimitMiddleware(next http.Handler) http.Handler {
limiter := rate.NewLimiter(100/time.Hour, 10) // 100/hour burst of 10
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}5. Request Signing
For webhooks and integrations:
- HMAC-SHA256 signing
- Timestamp validation (prevent replay attacks)
- Request body included in signature
Implementation:
// When sending webhook
signature := hmac.New(sha256.New, secret)
signature.Write([]byte(timestamp + "." + body))
header := "Signature " + hex.EncodeToString(signature.Sum(nil))
// When receiving webhook
verified := verifySignature(signature, timestamp, body, secret)
if !verified {
// Reject webhook
}Multi-Tenancy & Isolation
Tenant Boundary
Each tenant is completely isolated:
- Separate row-level database access (RLS)
- Separate Vault secrets namespace
- Separate audit logs
- Separate observability data
Implementation
type TenantContext struct {
TenantID string
UserID string
Role string
}
func (tc *TenantContext) Query(ctx context.Context, query string, args ...interface{}) ([]map[string]interface{}, error) {
// Set tenant context in database
dbCtx := context.WithValue(ctx, "tenant_id", tc.TenantID)
// PostgreSQL RLS automatically filters based on tenant
// No need to add WHERE clauses
return db.QueryContext(dbCtx, query, args...)
}Security Headers & Best Practices
HTTP Headers
X-Content-Type-Options: nosniff # Prevent MIME sniffing
X-Frame-Options: DENY # Prevent clickjacking
X-XSS-Protection: 1; mode=block # XSS protection
Strict-Transport-Security: max-age=31536000 # Force HTTPS
Content-Security-Policy: default-src 'self' # CSP policyInput Validation
// Validate all inputs
func ValidateWorkflowName(name string) error {
if len(name) == 0 || len(name) > 255 {
return fmt.Errorf("name must be 1-255 characters")
}
// Allow only alphanumeric, dash, underscore
if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(name) {
return fmt.Errorf("name contains invalid characters")
}
return nil
}SQL Injection Prevention
// ✅ SAFE: Using parameterized queries
db.Query("SELECT * FROM workflows WHERE id = $1", id)
// ❌ UNSAFE: String concatenation
db.Query("SELECT * FROM workflows WHERE id = " + id)Compliance & Reporting
Compliance Features
- GDPR: Data export, right to deletion, consent management
- SOC 2: Audit trails, access controls, encryption
- HIPAA: Encryption, access logs, breach notification
- PCI DSS: API key security, no secrets in logs
Audit Report Example
-- Generate compliance report
SELECT
COUNT(*) as total_actions,
COUNT(DISTINCT user_id) as unique_users,
COUNT(CASE WHEN action = 'data_access' THEN 1 END) as data_accesses,
COUNT(CASE WHEN action = 'unauthorized' THEN 1 END) as failed_attempts
FROM audit_log
WHERE timestamp > NOW() - INTERVAL '30 days';Security Checklist
- All communications encrypted (TLS)
- Authentication enabled (Ory)
- Authorization configured (SpiceDB)
- Policies evaluated (OPA)
- Multi-tenant isolation (RLS)
- Audit logging enabled
- Secrets in Vault (not code)
- Rate limiting configured
- API keys rotated
- Regular security reviews
Summary
| Layer | Technology | Purpose | Protection |
|---|---|---|---|
| 1: Auth | Ory Kratos | Verify identity | Against impersonation |
| 2: Authz | SpiceDB | Check permissions | Against unauthorized access |
| 3: Policy | OPA | Business rules | Against policy violations |
| 4: Data | PostgreSQL RLS | Isolate data | Against data leaks |
| 5: Audit | OTEL + Loki | Track actions | Against unaccountable changes |
Next Steps
- Architecture Overview - Full system view
- Getting Started - Deploy securely
- Core Concepts - How it all fits together
Ready to get started? → Getting Started