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

323 lines
7.3 KiB
Markdown

# 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:
```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