# Security Architecture rdev implements defense in depth with multiple security layers. ## Authentication ### API Keys All API requests (except health checks) require authentication: ``` ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ Client │────▶│ Auth │────▶│ Auth │────▶│ Handler │ │ │ │ Middleware │ │ Service │ │ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ │ │ ▼ │ ┌────────────┐ │ │ Postgres │ │ │ (keys) │ │ └────────────┘ ▼ Check IP Allowlist ``` ### Key Format ``` rdev_ ``` Keys are stored as SHA-256 hashes, never in plaintext. ### Authentication Flow 1. Extract key from `X-API-Key` header or `Authorization: Bearer` header 2. Hash the key with SHA-256 3. Look up hash in database 4. Verify key is not revoked or expired 5. Check IP allowlist (if configured) 6. Add key to request context ### Scopes | Scope | Description | |-------|-------------| | `projects:read` | List and view projects | | `projects:execute` | Execute commands | | `keys:read` | List API keys | | `keys:write` | Create/revoke keys | | `admin` | Full access | ### IP Allowlisting API keys can be restricted to specific IP addresses or CIDR ranges: ```go type APIKey struct { // ... AllowedIPs []string // CIDR notation: ["192.168.1.0/24", "10.0.0.0/8"] } func (k *APIKey) IsIPAllowed(clientIP string) bool { if len(k.AllowedIPs) == 0 { return true // No restriction } for _, cidr := range k.AllowedIPs { _, network, _ := net.ParseCIDR(cidr) if network.Contains(net.ParseIP(clientIP)) { return true } } return false } ``` ## Command Sanitization All commands are sanitized before execution to prevent: ### Shell Injection Protection ```go // internal/sanitize/command.go func ShellCommand(cmd string) error { // Block command chaining dangerous := []string{";", "&&", "||", "|", "`", "$(", "${"} for _, d := range dangerous { if strings.Contains(cmd, d) { return fmt.Errorf("command chaining not allowed") } } // Block redirects if strings.ContainsAny(cmd, "<>") { return fmt.Errorf("redirects not allowed") } // Block destructive commands if isDestructiveRm(cmd) { return fmt.Errorf("destructive rm not allowed") } return nil } ``` ### Blocked Patterns | Category | Examples | |----------|----------| | Command chaining | `; && || \| $() \`\`` | | Redirects | `> >> < <<` | | Destructive | `rm -rf /`, `dd if=` | | Escape sequences | Null bytes, control chars | ### Git Command Restrictions ```go func GitArgs(args []string) error { if len(args) == 0 { return errors.New("no git subcommand") } blocked := map[string]bool{ "config": true, // Could change credentials "remote": true, // Could add malicious remotes } if blocked[args[0]] { return fmt.Errorf("git %s not allowed", args[0]) } // Block force push if args[0] == "push" { for _, arg := range args { if arg == "-f" || arg == "--force" { return errors.New("force push not allowed") } } } return nil } ``` ### Claude Prompt Sanitization ```go func ClaudePrompt(prompt string) error { // Check for null bytes if strings.ContainsRune(prompt, 0) { return errors.New("null bytes not allowed") } // Check for control characters for _, r := range prompt { if r < 32 && r != '\n' && r != '\r' && r != '\t' { return errors.New("control characters not allowed") } } return nil } ``` ## Rate Limiting ### Request Rate Limiting Token bucket algorithm limits requests per API key: ```go type RateLimiter struct { rate rate.Limit // Requests per second burst int // Maximum burst size limiters sync.Map // Per-key limiters } func (l *RateLimiter) Allow(key string) bool { limiter := l.getLimiter(key) return limiter.Allow() } ``` ### Concurrent Command Limiting Limits active commands per project: ```go type CommandLimiter struct { maxConcurrent int active map[string]int mu sync.Mutex } func (l *CommandLimiter) TryAcquire(projectID string) bool { l.mu.Lock() defer l.mu.Unlock() if l.active[projectID] >= l.maxConcurrent { return false } l.active[projectID]++ return true } ``` ### Rate Limit Headers Responses include rate limit information: ``` X-RateLimit-Limit: 10 X-RateLimit-Remaining: 7 X-RateLimit-Reset: 1642089600 ``` ## Network Security ### Kubernetes Network Policy ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: rdev-api-policy spec: podSelector: matchLabels: app: rdev-api policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: databases ports: - protocol: TCP port: 5432 ``` ### Pod Security ```yaml securityContext: runAsNonRoot: true runAsUser: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: - ALL ``` ## RBAC ### Service Account Permissions ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: rdev-api-role rules: # Read pods for project discovery - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] # Execute commands in pods - apiGroups: [""] resources: ["pods/exec"] verbs: ["create"] # Read ConfigMaps for project config - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch"] ``` ## Security Checklist ### Development - [ ] All inputs sanitized before use - [ ] No secrets in code or logs - [ ] SQL injection protection (parameterized queries) - [ ] No command injection vectors ### Deployment - [ ] TLS termination at ingress - [ ] Network policies applied - [ ] Pod security context configured - [ ] RBAC minimized to required permissions ### Operations - [ ] API keys rotated regularly - [ ] Audit logs enabled - [ ] Rate limits configured appropriately - [ ] IP allowlists for sensitive keys ## Incident Response ### Key Compromise 1. Revoke the compromised key immediately 2. Review audit logs for unauthorized access 3. Issue new key to affected user 4. Investigate source of compromise ### Rate Limit Abuse 1. Identify abusing key from metrics 2. Temporarily lower key's rate limit 3. Contact key owner 4. Consider IP-based blocking if severe