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:
+13
-5
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user