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>
323 lines
7.3 KiB
Markdown
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
|