A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 318 lines 9.4 kB view raw
1/* 2Copyright 2025. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package security 18 19import ( 20 "context" 21 "fmt" 22 "regexp" 23 "strings" 24 "sync" 25 26 "golang.org/x/time/rate" 27 "google.golang.org/grpc" 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/metadata" 30 "google.golang.org/grpc/peer" 31 "google.golang.org/grpc/status" 32) 33 34const ( 35 // Maximum path length for HSM secret paths 36 MaxSecretPathLength = 256 37 // Maximum secret data size (1MB) 38 MaxSecretDataSize = 1024 * 1024 39 // Maximum metadata field length 40 MaxMetadataFieldLength = 1024 41 // Rate limit: 100 requests per minute per client 42 DefaultRateLimit = rate.Limit(100.0 / 60.0) // per second 43 // Burst allowance 44 DefaultBurst = 20 45) 46 47var ( 48 // Valid secret path pattern: alphanumeric, hyphens, underscores, forward slashes 49 validPathPattern = regexp.MustCompile(`^[a-zA-Z0-9/_-]+$`) 50 // Forbidden path patterns (prevent directory traversal, etc.) 51 forbiddenPatterns = []*regexp.Regexp{ 52 regexp.MustCompile(`\.\.`), // Directory traversal 53 regexp.MustCompile(`//`), // Double slashes 54 regexp.MustCompile(`^/`), // Leading slash 55 regexp.MustCompile(`/$`), // Trailing slash 56 regexp.MustCompile(`_metadata$`), // Reserved metadata suffix 57 } 58) 59 60// InputValidator validates and sanitizes input for HSM operations 61type InputValidator struct{} 62 63// NewInputValidator creates a new input validator 64func NewInputValidator() *InputValidator { 65 return &InputValidator{} 66} 67 68// ValidateSecretPath validates and sanitizes secret paths 69func (v *InputValidator) ValidateSecretPath(path string) error { 70 if path == "" { 71 return fmt.Errorf("secret path cannot be empty") 72 } 73 74 if len(path) > MaxSecretPathLength { 75 return fmt.Errorf("secret path too long: %d > %d", len(path), MaxSecretPathLength) 76 } 77 78 // Check valid pattern 79 if !validPathPattern.MatchString(path) { 80 return fmt.Errorf("secret path contains invalid characters: %s", path) 81 } 82 83 // Check forbidden patterns 84 for _, pattern := range forbiddenPatterns { 85 if pattern.MatchString(path) { 86 return fmt.Errorf("secret path contains forbidden pattern: %s", path) 87 } 88 } 89 90 return nil 91} 92 93// ValidateSecretData validates secret data size and content 94func (v *InputValidator) ValidateSecretData(data map[string][]byte) error { 95 if data == nil { 96 return fmt.Errorf("secret data cannot be nil") 97 } 98 99 if len(data) == 0 { 100 return fmt.Errorf("secret data cannot be empty") 101 } 102 103 totalSize := 0 104 for key, value := range data { 105 if key == "" { 106 return fmt.Errorf("secret data key cannot be empty") 107 } 108 109 if len(key) > MaxMetadataFieldLength { 110 return fmt.Errorf("secret data key too long: %d > %d", len(key), MaxMetadataFieldLength) 111 } 112 113 // Check for metadata key suffix (reserved) 114 if strings.HasSuffix(key, "_metadata") { 115 return fmt.Errorf("secret data key cannot end with '_metadata': %s", key) 116 } 117 118 // Validate key pattern 119 if !validPathPattern.MatchString(key) { 120 return fmt.Errorf("secret data key contains invalid characters: %s", key) 121 } 122 123 totalSize += len(value) 124 if totalSize > MaxSecretDataSize { 125 return fmt.Errorf("secret data too large: %d > %d", totalSize, MaxSecretDataSize) 126 } 127 } 128 129 return nil 130} 131 132// ValidateMetadata validates secret metadata 133func (v *InputValidator) ValidateMetadata(secretMetadata map[string]string) error { 134 if secretMetadata == nil { 135 return nil // Metadata is optional 136 } 137 138 for key, value := range secretMetadata { 139 if len(key) > MaxMetadataFieldLength { 140 return fmt.Errorf("metadata key too long: %d > %d", len(key), MaxMetadataFieldLength) 141 } 142 143 if len(value) > MaxMetadataFieldLength { 144 return fmt.Errorf("metadata value too long: %d > %d", len(value), MaxMetadataFieldLength) 145 } 146 147 // Sanitize metadata fields 148 if strings.ContainsAny(key, "\x00\n\r") { 149 return fmt.Errorf("metadata key contains invalid characters: %s", key) 150 } 151 152 if strings.ContainsAny(value, "\x00") { 153 return fmt.Errorf("metadata value contains null bytes: %s", value) 154 } 155 } 156 157 return nil 158} 159 160// RateLimiter implements per-client rate limiting for gRPC requests 161type RateLimiter struct { 162 limiters map[string]*rate.Limiter 163 mu sync.RWMutex 164 limit rate.Limit 165 burst int 166} 167 168// NewRateLimiter creates a new rate limiter 169func NewRateLimiter() *RateLimiter { 170 return &RateLimiter{ 171 limiters: make(map[string]*rate.Limiter), 172 limit: DefaultRateLimit, 173 burst: DefaultBurst, 174 } 175} 176 177// NewRateLimiterWithConfig creates a rate limiter with custom settings 178func NewRateLimiterWithConfig(limit rate.Limit, burst int) *RateLimiter { 179 return &RateLimiter{ 180 limiters: make(map[string]*rate.Limiter), 181 limit: limit, 182 burst: burst, 183 } 184} 185 186// getLimiter gets or creates a rate limiter for a client 187func (rl *RateLimiter) getLimiter(clientID string) *rate.Limiter { 188 rl.mu.RLock() 189 limiter, exists := rl.limiters[clientID] 190 rl.mu.RUnlock() 191 192 if exists { 193 return limiter 194 } 195 196 rl.mu.Lock() 197 defer rl.mu.Unlock() 198 199 // Double-check in case another goroutine created it 200 if limiter, exists := rl.limiters[clientID]; exists { 201 return limiter 202 } 203 204 // Create new limiter 205 limiter = rate.NewLimiter(rl.limit, rl.burst) 206 rl.limiters[clientID] = limiter 207 return limiter 208} 209 210// Allow checks if a request should be allowed for the given client 211func (rl *RateLimiter) Allow(clientID string) bool { 212 return rl.getLimiter(clientID).Allow() 213} 214 215// getClientID extracts a client identifier from the gRPC context 216func getClientID(ctx context.Context) string { 217 // Try to get peer information 218 if p, ok := peer.FromContext(ctx); ok { 219 return p.Addr.String() 220 } 221 222 // Try to get metadata 223 if md, ok := metadata.FromIncomingContext(ctx); ok { 224 if clientIDs := md.Get("client-id"); len(clientIDs) > 0 { 225 return clientIDs[0] 226 } 227 } 228 229 return "unknown" 230} 231 232// RateLimitInterceptor returns a gRPC unary interceptor for rate limiting 233func (rl *RateLimiter) RateLimitInterceptor() grpc.UnaryServerInterceptor { 234 return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { 235 clientID := getClientID(ctx) 236 237 if !rl.Allow(clientID) { 238 return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded for client %s", clientID) 239 } 240 241 return handler(ctx, req) 242 } 243} 244 245// ValidationInterceptor returns a gRPC unary interceptor for input validation 246func ValidationInterceptor(validator *InputValidator) grpc.UnaryServerInterceptor { 247 return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { 248 // Type-specific validation based on request type 249 switch r := req.(type) { 250 case interface{ GetPath() string }: 251 if err := validator.ValidateSecretPath(r.GetPath()); err != nil { 252 return nil, status.Errorf(codes.InvalidArgument, "invalid path: %v", err) 253 } 254 } 255 256 // Additional validation for write requests 257 switch r := req.(type) { 258 case interface { 259 GetSecretData() interface{ GetData() map[string][]byte } 260 }: 261 if secretData := r.GetSecretData(); secretData != nil { 262 if err := validator.ValidateSecretData(secretData.GetData()); err != nil { 263 return nil, status.Errorf(codes.InvalidArgument, "invalid secret data: %v", err) 264 } 265 } 266 case interface { 267 GetMetadata() interface{ GetLabels() map[string]string } 268 }: 269 if metadata := r.GetMetadata(); metadata != nil { 270 if err := validator.ValidateMetadata(metadata.GetLabels()); err != nil { 271 return nil, status.Errorf(codes.InvalidArgument, "invalid metadata: %v", err) 272 } 273 } 274 } 275 276 return handler(ctx, req) 277 } 278} 279 280// SecurityInterceptor combines multiple security checks 281func SecurityInterceptor(rateLimiter *RateLimiter, validator *InputValidator) grpc.UnaryServerInterceptor { 282 return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { 283 // Rate limiting 284 clientID := getClientID(ctx) 285 if !rateLimiter.Allow(clientID) { 286 return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded for client %s", clientID) 287 } 288 289 // Input validation 290 switch r := req.(type) { 291 case interface{ GetPath() string }: 292 if err := validator.ValidateSecretPath(r.GetPath()); err != nil { 293 return nil, status.Errorf(codes.InvalidArgument, "invalid path: %v", err) 294 } 295 } 296 297 switch r := req.(type) { 298 case interface { 299 GetSecretData() interface{ GetData() map[string][]byte } 300 }: 301 if secretData := r.GetSecretData(); secretData != nil { 302 if err := validator.ValidateSecretData(secretData.GetData()); err != nil { 303 return nil, status.Errorf(codes.InvalidArgument, "invalid secret data: %v", err) 304 } 305 } 306 case interface { 307 GetMetadata() interface{ GetLabels() map[string]string } 308 }: 309 if metadata := r.GetMetadata(); metadata != nil { 310 if err := validator.ValidateMetadata(metadata.GetLabels()); err != nil { 311 return nil, status.Errorf(codes.InvalidArgument, "invalid metadata: %v", err) 312 } 313 } 314 } 315 316 return handler(ctx, req) 317 } 318}