rdev/docs/architecture/security.md
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

7.3 KiB

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_<random_32_chars>

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:

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

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

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

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:

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:

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

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

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL

RBAC

Service Account Permissions

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