Fix 8 high-severity audit findings across security, Go, API, and container domains

- Use typed context key for auth context to prevent collisions (auth.go)
- Eliminate nested locking in CheckRateLimit to prevent potential deadlock (auth.go)
- Add 1MB request body size limit middleware to prevent DoS (router.go)
- Return proper gRPC status codes instead of nil errors on failures (handler.go)
- Use key name instead of raw API key in admin URL paths to prevent secret leakage (keys.go, router.go, keystore_db.go, keystore_hybrid.go)
- Enforce RBAC authorization in service Send/SendBatch for both REST and gRPC (service.go)
- Pin runtime Docker image to alpine:3.21 for reproducible builds (Dockerfile)
- Enable readOnlyRootFilesystem with /tmp emptyDir in k8s deployment (deployment.yaml)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 20:17:51 -07:00
parent 71b02758d7
commit 298c960808
9 changed files with 212 additions and 43 deletions
+13 -5
View File
@@ -115,24 +115,29 @@ func (s *APIKeyStore) ValidateKey(keyStr string) (*APIKey, error) {
// CheckRateLimit checks if a key has exceeded its rate limit
func (s *APIKeyStore) CheckRateLimit(keyStr string) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
// Look up key and limiter under the store lock, then release it
// before acquiring the per-key limiter lock to avoid nested locking.
s.mu.RLock()
key, exists := s.keys[keyStr]
if !exists {
s.mu.RUnlock()
return false, fmt.Errorf("invalid API key")
}
// Unlimited rate limit
if key.RateLimit <= 0 {
s.mu.RUnlock()
return true, nil
}
limiter, exists := s.rateLimits[keyStr]
if !exists {
s.mu.RUnlock()
return false, fmt.Errorf("rate limiter not found")
}
s.mu.RUnlock()
// Now lock only the per-key rate limiter
limiter.mu.Lock()
defer limiter.mu.Unlock()
@@ -206,13 +211,16 @@ func (s *APIKeyStore) ListKeys(clientID string) []*APIKey {
return keys
}
// authContextKey is an unexported type for context keys to avoid collisions.
type authContextKey struct{}
// ContextWithAuth adds auth context to a request context
func ContextWithAuth(ctx context.Context, auth *AuthContext) context.Context {
return context.WithValue(ctx, "auth", auth)
return context.WithValue(ctx, authContextKey{}, auth)
}
// GetAuthContext retrieves auth context from a request context
func GetAuthContext(ctx context.Context) (*AuthContext, bool) {
auth, ok := ctx.Value("auth").(*AuthContext)
auth, ok := ctx.Value(authContextKey{}).(*AuthContext)
return auth, ok
}
+83
View File
@@ -347,6 +347,89 @@ func (ks *KeyStoreDB) GetAuditLog(ctx context.Context, keyStr string, limit int)
return logs, rows.Err()
}
// GetKeyByName retrieves an API key by its name
func (ks *KeyStoreDB) GetKeyByName(ctx context.Context, name string) (*APIKey, error) {
query := `
SELECT key, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit
FROM api_keys
WHERE name = $1
`
var key APIKey
var roles []string
err := ks.db.QueryRowContext(ctx, query, name).Scan(
&key.Key,
&key.Name,
&key.ClientID,
pq.Array(&roles),
&key.CreatedAt,
&key.LastUsedAt,
&key.ExpiresAt,
&key.IsActive,
&key.RateLimit,
)
if err == sql.ErrNoRows {
return nil, ErrKeyNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get key by name: %w", err)
}
key.Roles = roles
return &key, nil
}
// DeactivateKeyByName disables an API key by its name
func (ks *KeyStoreDB) DeactivateKeyByName(ctx context.Context, name string, deactivatedBy string) error {
// First get the key to find its raw key for cache invalidation and audit
key, err := ks.GetKeyByName(ctx, name)
if err != nil {
return err
}
return ks.DeactivateKey(ctx, key.Key, deactivatedBy)
}
// GetAuditLogByName retrieves audit log entries for a key identified by name
func (ks *KeyStoreDB) GetAuditLogByName(ctx context.Context, name string, limit int) ([]map[string]interface{}, error) {
query := `
SELECT al.action, al.performed_by, al.performed_at, al.details
FROM api_key_audit_log al
JOIN api_keys ak ON al.key_id = ak.id
WHERE ak.name = $1
ORDER BY al.performed_at DESC
LIMIT $2
`
rows, err := ks.db.QueryContext(ctx, query, name, limit)
if err != nil {
return nil, fmt.Errorf("failed to get audit log: %w", err)
}
defer rows.Close()
var logs []map[string]interface{}
for rows.Next() {
var action, performedBy, details string
var performedAt time.Time
err := rows.Scan(&action, &performedBy, &performedAt, &details)
if err != nil {
return nil, err
}
logs = append(logs, map[string]interface{}{
"action": action,
"performed_by": performedBy,
"performed_at": performedAt,
"details": details,
})
}
return logs, rows.Err()
}
// Custom errors
var (
ErrKeyNotFound = fmt.Errorf("API key not found")
+26
View File
@@ -136,6 +136,32 @@ func (h *HybridKeyStore) GetAuditLog(ctx context.Context, keyStr string, limit i
return h.db.GetAuditLog(ctx, keyStr, limit)
}
// DeactivateKeyByName deactivates a key by its name (avoids exposing raw key in URLs)
func (h *HybridKeyStore) DeactivateKeyByName(ctx context.Context, name string, deactivatedBy string) error {
h.mu.Lock()
defer h.mu.Unlock()
// Look up the key by name in DB to get the raw key for cache invalidation
key, err := h.db.GetKeyByName(ctx, name)
if err != nil {
return err
}
// Remove from cache
h.cache.mu.Lock()
delete(h.cache.keys, key.Key)
delete(h.cache.rateLimits, key.Key)
h.cache.mu.Unlock()
// Deactivate in database
return h.db.DeactivateKey(ctx, key.Key, deactivatedBy)
}
// GetAuditLogByName retrieves audit log for a key identified by name
func (h *HybridKeyStore) GetAuditLogByName(ctx context.Context, name string, limit int) ([]map[string]interface{}, error) {
return h.db.GetAuditLogByName(ctx, name, limit)
}
// Close closes the database connection
func (h *HybridKeyStore) Close() error {
return h.db.Close()