Skip to Content
ArchitectureSecurity Model

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 request

Implementation:

// 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 → Deny

Example 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 workflows

In 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 events

Implementation:

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 policy

Input 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

LayerTechnologyPurposeProtection
1: AuthOry KratosVerify identityAgainst impersonation
2: AuthzSpiceDBCheck permissionsAgainst unauthorized access
3: PolicyOPABusiness rulesAgainst policy violations
4: DataPostgreSQL RLSIsolate dataAgainst data leaks
5: AuditOTEL + LokiTrack actionsAgainst unaccountable changes

Next Steps

Ready to get started? → Getting Started

Last updated on