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.

update grpc api handling

+429 -93
+2 -2
helm/hsm-secrets-operator/Chart.yaml
··· 2 2 name: hsm-secrets-operator 3 3 description: A Kubernetes operator that bridges Pico HSM binary data storage with Kubernetes Secrets 4 4 type: application 5 - version: 0.5.6 6 - appVersion: v0.5.6 5 + version: 0.5.7 6 + appVersion: v0.5.7 7 7 icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.svg 8 8 home: https://github.com/evanjarrett/hsm-secrets-operator 9 9 sources:
+401
internal/api/proxy_client.go
··· 1 + /* 2 + Copyright 2025. 3 + 4 + Licensed under the Apache License, Version 2.0 (the "License"); 5 + you may not use this file except in compliance with the License. 6 + You may obtain a copy of the License at 7 + 8 + http://www.apache.org/licenses/LICENSE-2.0 9 + 10 + Unless required by applicable law or agreed to in writing, software 11 + distributed under the License is distributed on an "AS IS" BASIS, 12 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + See the License for the specific language governing permissions and 14 + limitations under the License. 15 + */ 16 + 17 + package api 18 + 19 + import ( 20 + "context" 21 + "net/http" 22 + "sync" 23 + 24 + "github.com/gin-gonic/gin" 25 + "github.com/go-logr/logr" 26 + 27 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 28 + ) 29 + 30 + // ProxyClient handles HTTP requests and proxies them to gRPC clients 31 + // It has methods that match the HTTP endpoints and handle the full request/response cycle 32 + type ProxyClient struct { 33 + server *Server 34 + logger logr.Logger 35 + grpcClients map[string]hsm.Client // deviceName -> gRPC client 36 + clientsMutex sync.RWMutex 37 + } 38 + 39 + // NewProxyClient creates a new ProxyClient that handles HTTP routing 40 + func NewProxyClient(server *Server, logger logr.Logger) *ProxyClient { 41 + return &ProxyClient{ 42 + server: server, 43 + logger: logger.WithName("proxy-client"), 44 + grpcClients: make(map[string]hsm.Client), 45 + } 46 + } 47 + 48 + // getOrCreateGRPCClient returns the cached gRPC client for a device or creates a new one 49 + func (p *ProxyClient) getOrCreateGRPCClient(c *gin.Context) (hsm.Client, error) { 50 + // Extract namespace 51 + namespace := c.GetHeader("X-Namespace") 52 + if namespace == "" { 53 + namespace = "secrets" 54 + } 55 + 56 + // Find available agent 57 + deviceName, err := p.server.findAvailableAgent(c.Request.Context(), namespace) 58 + if err != nil { 59 + return nil, err 60 + } 61 + 62 + // Try to get existing client for this device with read lock 63 + p.clientsMutex.RLock() 64 + if client, exists := p.grpcClients[deviceName]; exists && client.IsConnected() { 65 + p.clientsMutex.RUnlock() 66 + return client, nil 67 + } 68 + p.clientsMutex.RUnlock() 69 + 70 + // Need to create/recreate client with write lock 71 + p.clientsMutex.Lock() 72 + defer p.clientsMutex.Unlock() 73 + 74 + // Double-check in case another goroutine created it 75 + if client, exists := p.grpcClients[deviceName]; exists && client.IsConnected() { 76 + return client, nil 77 + } 78 + 79 + // Close existing client for this device if it exists 80 + if oldClient, exists := p.grpcClients[deviceName]; exists { 81 + if closeErr := oldClient.Close(); closeErr != nil { 82 + p.logger.V(1).Info("Error closing old gRPC client", "device", deviceName, "error", closeErr) 83 + } 84 + delete(p.grpcClients, deviceName) 85 + } 86 + 87 + // Create new gRPC client 88 + grpcClient, err := p.server.createGRPCClient(c.Request.Context(), deviceName, namespace) 89 + if err != nil { 90 + return nil, err 91 + } 92 + 93 + // Cache the client for this device 94 + p.grpcClients[deviceName] = grpcClient 95 + p.logger.V(1).Info("Created new gRPC client", "device", deviceName) 96 + return grpcClient, nil 97 + } 98 + 99 + // GetInfo handles GET /hsm/info 100 + func (p *ProxyClient) GetInfo(c *gin.Context) { 101 + grpcClient, err := p.getOrCreateGRPCClient(c) 102 + if err != nil { 103 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 104 + "error": err.Error(), 105 + }) 106 + return 107 + } 108 + 109 + info, err := grpcClient.GetInfo(c.Request.Context()) 110 + if err != nil { 111 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to get HSM info", map[string]any{ 112 + "error": err.Error(), 113 + }) 114 + return 115 + } 116 + 117 + p.server.sendResponse(c, http.StatusOK, "HSM info retrieved successfully", info) 118 + } 119 + 120 + // ListSecrets handles GET /hsm/secrets 121 + func (p *ProxyClient) ListSecrets(c *gin.Context) { 122 + prefix := c.Query("prefix") 123 + 124 + grpcClient, err := p.getOrCreateGRPCClient(c) 125 + if err != nil { 126 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 127 + "error": err.Error(), 128 + }) 129 + return 130 + } 131 + 132 + secrets, err := grpcClient.ListSecrets(c.Request.Context(), prefix) 133 + if err != nil { 134 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to list secrets from HSM", map[string]any{ 135 + "error": err.Error(), 136 + }) 137 + return 138 + } 139 + 140 + response := map[string]any{ 141 + "secrets": secrets, 142 + "count": len(secrets), 143 + "prefix": prefix, 144 + } 145 + p.server.sendResponse(c, http.StatusOK, "Secrets listed successfully", response) 146 + } 147 + 148 + // ReadSecret handles GET /hsm/secrets/:path 149 + func (p *ProxyClient) ReadSecret(c *gin.Context) { 150 + path := c.Param("path") 151 + if path == "" { 152 + p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 153 + return 154 + } 155 + 156 + grpcClient, err := p.getOrCreateGRPCClient(c) 157 + if err != nil { 158 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 159 + "error": err.Error(), 160 + }) 161 + return 162 + } 163 + 164 + data, err := grpcClient.ReadSecret(c.Request.Context(), path) 165 + if err != nil { 166 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to read secret from HSM", map[string]any{ 167 + "error": err.Error(), 168 + "path": path, 169 + }) 170 + return 171 + } 172 + 173 + response := map[string]any{ 174 + "path": path, 175 + "data": data, 176 + } 177 + p.server.sendResponse(c, http.StatusOK, "Secret read successfully", response) 178 + } 179 + 180 + // WriteSecret handles POST/PUT /hsm/secrets/:path 181 + func (p *ProxyClient) WriteSecret(c *gin.Context) { 182 + path := c.Param("path") 183 + if path == "" { 184 + p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 185 + return 186 + } 187 + 188 + // Parse request body 189 + var req struct { 190 + Data map[string]string `json:"data" binding:"required"` 191 + Metadata *hsm.SecretMetadata `json:"metadata,omitempty"` 192 + } 193 + if err := c.ShouldBindJSON(&req); err != nil { 194 + p.server.sendError(c, http.StatusBadRequest, "parse_error", "Failed to parse request body", map[string]any{ 195 + "error": err.Error(), 196 + }) 197 + return 198 + } 199 + 200 + // Convert string data to byte data 201 + data := make(hsm.SecretData) 202 + for key, value := range req.Data { 203 + data[key] = []byte(value) 204 + } 205 + 206 + grpcClient, err := p.getOrCreateGRPCClient(c) 207 + if err != nil { 208 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 209 + "error": err.Error(), 210 + }) 211 + return 212 + } 213 + 214 + if req.Metadata != nil { 215 + err = grpcClient.WriteSecretWithMetadata(c.Request.Context(), path, data, req.Metadata) 216 + } else { 217 + err = grpcClient.WriteSecret(c.Request.Context(), path, data) 218 + } 219 + 220 + if err != nil { 221 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to write secret to HSM", map[string]any{ 222 + "error": err.Error(), 223 + "path": path, 224 + }) 225 + return 226 + } 227 + 228 + response := map[string]any{ 229 + "path": path, 230 + "keys": len(data), 231 + } 232 + if req.Metadata != nil { 233 + response["metadata"] = req.Metadata 234 + } 235 + p.server.sendResponse(c, http.StatusCreated, "Secret written successfully", response) 236 + } 237 + 238 + // DeleteSecret handles DELETE /hsm/secrets/:path 239 + func (p *ProxyClient) DeleteSecret(c *gin.Context) { 240 + path := c.Param("path") 241 + if path == "" { 242 + p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 243 + return 244 + } 245 + 246 + grpcClient, err := p.getOrCreateGRPCClient(c) 247 + if err != nil { 248 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 249 + "error": err.Error(), 250 + }) 251 + return 252 + } 253 + 254 + err = grpcClient.DeleteSecret(c.Request.Context(), path) 255 + if err != nil { 256 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to delete secret from HSM", map[string]any{ 257 + "error": err.Error(), 258 + "path": path, 259 + }) 260 + return 261 + } 262 + 263 + response := map[string]any{ 264 + "path": path, 265 + } 266 + p.server.sendResponse(c, http.StatusOK, "Secret deleted successfully", response) 267 + } 268 + 269 + // ReadMetadata handles GET /hsm/secrets/:path/metadata 270 + func (p *ProxyClient) ReadMetadata(c *gin.Context) { 271 + path := c.Param("path") 272 + if path == "" { 273 + p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 274 + return 275 + } 276 + 277 + grpcClient, err := p.getOrCreateGRPCClient(c) 278 + if err != nil { 279 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 280 + "error": err.Error(), 281 + }) 282 + return 283 + } 284 + 285 + metadata, err := grpcClient.ReadMetadata(c.Request.Context(), path) 286 + if err != nil { 287 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to read metadata from HSM", map[string]any{ 288 + "error": err.Error(), 289 + "path": path, 290 + }) 291 + return 292 + } 293 + 294 + response := map[string]any{ 295 + "path": path, 296 + "metadata": metadata, 297 + } 298 + p.server.sendResponse(c, http.StatusOK, "Metadata read successfully", response) 299 + } 300 + 301 + // GetChecksum handles GET /hsm/secrets/:path/checksum 302 + func (p *ProxyClient) GetChecksum(c *gin.Context) { 303 + path := c.Param("path") 304 + if path == "" { 305 + p.server.sendError(c, http.StatusBadRequest, "missing_path", "Secret path is required", nil) 306 + return 307 + } 308 + 309 + grpcClient, err := p.getOrCreateGRPCClient(c) 310 + if err != nil { 311 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 312 + "error": err.Error(), 313 + }) 314 + return 315 + } 316 + 317 + checksum, err := grpcClient.GetChecksum(c.Request.Context(), path) 318 + if err != nil { 319 + p.server.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to get checksum from HSM", map[string]any{ 320 + "error": err.Error(), 321 + "path": path, 322 + }) 323 + return 324 + } 325 + 326 + response := map[string]any{ 327 + "path": path, 328 + "checksum": checksum, 329 + } 330 + p.server.sendResponse(c, http.StatusOK, "Checksum retrieved successfully", response) 331 + } 332 + 333 + // IsConnected handles GET /hsm/status 334 + func (p *ProxyClient) IsConnected(c *gin.Context) { 335 + grpcClient, err := p.getOrCreateGRPCClient(c) 336 + if err != nil { 337 + p.server.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 338 + "error": err.Error(), 339 + }) 340 + return 341 + } 342 + 343 + connected := grpcClient.IsConnected() 344 + 345 + response := map[string]any{ 346 + "connected": connected, 347 + } 348 + 349 + status := http.StatusOK 350 + message := "HSM connection status retrieved" 351 + if !connected { 352 + status = http.StatusServiceUnavailable 353 + message = "HSM is not connected" 354 + } 355 + 356 + p.server.sendResponse(c, status, message, response) 357 + } 358 + 359 + // Close closes all cached gRPC clients 360 + func (p *ProxyClient) Close() error { 361 + p.clientsMutex.Lock() 362 + defer p.clientsMutex.Unlock() 363 + 364 + var lastErr error 365 + for deviceName, client := range p.grpcClients { 366 + if err := client.Close(); err != nil { 367 + p.logger.Error(err, "Failed to close gRPC client", "device", deviceName) 368 + lastErr = err 369 + } 370 + } 371 + 372 + // Clear the map 373 + p.grpcClients = make(map[string]hsm.Client) 374 + return lastErr 375 + } 376 + 377 + // CleanupDisconnectedClients removes disconnected clients from the cache 378 + func (p *ProxyClient) CleanupDisconnectedClients() { 379 + p.clientsMutex.Lock() 380 + defer p.clientsMutex.Unlock() 381 + 382 + for deviceName, client := range p.grpcClients { 383 + if !client.IsConnected() { 384 + p.logger.V(1).Info("Removing disconnected gRPC client", "device", deviceName) 385 + if closeErr := client.Close(); closeErr != nil { 386 + p.logger.V(1).Info("Error closing disconnected gRPC client", "device", deviceName, "error", closeErr) 387 + } 388 + delete(p.grpcClients, deviceName) 389 + } 390 + } 391 + } 392 + 393 + // GetClientCount returns the number of cached gRPC clients 394 + func (p *ProxyClient) GetClientCount() int { 395 + p.clientsMutex.RLock() 396 + defer p.clientsMutex.RUnlock() 397 + return len(p.grpcClients) 398 + } 399 + 400 + // Interface compliance methods (unused in HTTP mode but required for hsm.Client interface) 401 + func (p *ProxyClient) Initialize(ctx context.Context, config hsm.Config) error { return nil }
+21 -29
internal/api/proxy_handlers.go
··· 22 22 "github.com/gin-gonic/gin" 23 23 ) 24 24 25 - // handleProxyRequest handles all HSM API requests by converting to gRPC calls 26 - func (s *Server) handleProxyRequest(c *gin.Context) { 27 - // Extract namespace from request or use default 28 - namespace := c.GetHeader("X-Namespace") 29 - if namespace == "" { 30 - namespace = "secrets" // Default namespace 31 - } 32 - 33 - // Find available agent (returns device name) 34 - deviceName, err := s.findAvailableAgent(c.Request.Context(), namespace) 35 - if err != nil { 36 - s.sendError(c, http.StatusServiceUnavailable, "no_agent", "No HSM agents available", map[string]any{ 37 - "error": err.Error(), 38 - }) 39 - return 40 - } 41 - 42 - s.logger.V(1).Info("Converting HTTP request to gRPC call", 43 - "method", c.Request.Method, 44 - "path", c.Request.URL.Path, 45 - "device", deviceName) 46 - 47 - // Convert HTTP request to gRPC call 48 - s.proxyToAgent(c, deviceName, c.Request.URL.Path) 49 - } 50 - 51 25 // setupProxyRoutes sets up proxy routes for HSM operations 52 26 func (s *Server) setupProxyRoutes() { 53 27 // Serve web UI static files ··· 59 33 // Create API v1 group 60 34 v1 := s.router.Group("/api/v1") 61 35 { 62 - // HSM operations group - proxy everything to agents 36 + // HSM operations group - use ProxyClient methods directly as handlers 63 37 hsmGroup := v1.Group("/hsm") 64 38 { 65 - // Proxy all HSM operations to agents 66 - hsmGroup.Any("/*path", s.handleProxyRequest) 39 + // HSM device info and status 40 + hsmGroup.GET("/info", s.proxyClient.GetInfo) 41 + hsmGroup.GET("/status", s.proxyClient.IsConnected) 42 + 43 + // Secret operations 44 + secretsGroup := hsmGroup.Group("/secrets") 45 + { 46 + // List secrets 47 + secretsGroup.GET("", s.proxyClient.ListSecrets) 48 + 49 + // Secret-specific operations 50 + secretsGroup.GET("/:path", s.proxyClient.ReadSecret) 51 + secretsGroup.POST("/:path", s.proxyClient.WriteSecret) 52 + secretsGroup.PUT("/:path", s.proxyClient.WriteSecret) 53 + secretsGroup.DELETE("/:path", s.proxyClient.DeleteSecret) 54 + 55 + // Secret metadata and checksum 56 + secretsGroup.GET("/:path/metadata", s.proxyClient.ReadMetadata) 57 + secretsGroup.GET("/:path/checksum", s.proxyClient.GetChecksum) 58 + } 67 59 } 68 60 69 61 // Health and info endpoints can stay local
+4 -61
internal/api/server.go
··· 20 20 "context" 21 21 "fmt" 22 22 "net/http" 23 - "strings" 24 23 "time" 25 24 26 25 "github.com/gin-gonic/gin" ··· 42 41 validator *validator.Validate 43 42 logger logr.Logger 44 43 router *gin.Engine 45 - httpClient *http.Client 44 + proxyClient *ProxyClient 46 45 } 47 46 48 47 // NewServer creates a new API server instance that proxies to agents ··· 53 52 mirroringManager: mirroringManager, 54 53 validator: validator.New(), 55 54 logger: logger.WithName("api-server"), 56 - httpClient: &http.Client{ 57 - Timeout: 30 * time.Second, 58 - }, 59 55 } 56 + 57 + // Create ProxyClient instance 58 + s.proxyClient = NewProxyClient(s, s.logger) 60 59 61 60 s.setupRouter() 62 61 return s ··· 195 194 return "", fmt.Errorf("no available HSM agents found") 196 195 } 197 196 198 - // proxyToAgent forwards the request to an HSM agent via gRPC and returns the HTTP response 199 - func (s *Server) proxyToAgent(c *gin.Context, deviceName, path string) { 200 - // Parse the REST API path and convert to gRPC call 201 - method := c.Request.Method 202 - 203 - // Extract namespace for finding device 204 - namespace := c.GetHeader("X-Namespace") 205 - if namespace == "" { 206 - namespace = "secrets" 207 - } 208 - 209 - // Create gRPC client for this device 210 - grpcClient, err := s.createGRPCClient(c.Request.Context(), deviceName, namespace) 211 - if err != nil { 212 - s.sendError(c, http.StatusServiceUnavailable, "grpc_error", "Failed to connect to HSM agent", map[string]any{ 213 - "error": err.Error(), 214 - }) 215 - return 216 - } 217 - defer func() { 218 - if closeErr := grpcClient.Close(); closeErr != nil { 219 - s.logger.Error(closeErr, "Failed to close gRPC client") 220 - } 221 - }() 222 - 223 - // For now, just implement ListSecrets to test gRPC connection 224 - if method == "GET" && strings.Contains(path, "/secrets") { 225 - s.handleListSecrets(c, grpcClient) 226 - } else { 227 - s.sendError(c, http.StatusNotImplemented, "not_implemented", "gRPC routing not yet implemented for this endpoint", nil) 228 - } 229 - } 230 - 231 197 // createGRPCClient creates a gRPC client for the specified device using AgentManager 232 198 func (s *Server) createGRPCClient(ctx context.Context, deviceName, _ string) (hsm.Client, error) { 233 199 // Use the AgentManager to create a gRPC client directly ··· 243 209 244 210 return grpcClient, nil 245 211 } 246 - 247 - // handleListSecrets handles GET /api/v1/hsm/secrets via gRPC 248 - func (s *Server) handleListSecrets(c *gin.Context, grpcClient hsm.Client) { 249 - // Get query parameters 250 - prefix := c.Query("prefix") 251 - 252 - // Call gRPC ListSecrets 253 - secrets, err := grpcClient.ListSecrets(c.Request.Context(), prefix) 254 - if err != nil { 255 - s.sendError(c, http.StatusInternalServerError, "grpc_error", "Failed to list secrets from HSM agent", map[string]any{ 256 - "error": err.Error(), 257 - }) 258 - return 259 - } 260 - 261 - // Return the secrets in the expected format 262 - response := map[string]any{ 263 - "secrets": secrets, 264 - "count": len(secrets), 265 - } 266 - 267 - s.sendResponse(c, http.StatusOK, "Secrets listed successfully", response) 268 - }
+1 -1
web/app.js
··· 113 113 114 114 try { 115 115 const response = await this.api.listSecrets(); 116 - this.secrets = response.data.paths || []; 116 + this.secrets = response.data.secrets || []; 117 117 118 118 document.getElementById('totalSecrets').textContent = this.secrets.length; 119 119