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.

implement key deletion

+275 -6
+1 -1
Makefile
··· 3 3 # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 4 # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 5 # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 - VERSION ?= 0.7.0 6 + VERSION ?= 0.8.0 7 7 8 8 # CHANNELS define the bundle channels used in the bundle. 9 9 # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
+11 -2
api/proto/hsm/v1/hsm.pb.go
··· 582 582 type DeleteSecretRequest struct { 583 583 state protoimpl.MessageState `protogen:"open.v1"` 584 584 Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` 585 + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // Optional: delete specific key only, empty means delete entire secret 585 586 unknownFields protoimpl.UnknownFields 586 587 sizeCache protoimpl.SizeCache 587 588 } ··· 619 620 func (x *DeleteSecretRequest) GetPath() string { 620 621 if x != nil { 621 622 return x.Path 623 + } 624 + return "" 625 + } 626 + 627 + func (x *DeleteSecretRequest) GetKey() string { 628 + if x != nil { 629 + return x.Key 622 630 } 623 631 return "" 624 632 } ··· 1136 1144 "\x13ReadMetadataRequest\x12\x12\n" + 1137 1145 "\x04path\x18\x01 \x01(\tR\x04path\"J\n" + 1138 1146 "\x14ReadMetadataResponse\x122\n" + 1139 - "\bmetadata\x18\x01 \x01(\v2\x16.hsm.v1.SecretMetadataR\bmetadata\")\n" + 1147 + "\bmetadata\x18\x01 \x01(\v2\x16.hsm.v1.SecretMetadataR\bmetadata\";\n" + 1140 1148 "\x13DeleteSecretRequest\x12\x12\n" + 1141 - "\x04path\x18\x01 \x01(\tR\x04path\"\x16\n" + 1149 + "\x04path\x18\x01 \x01(\tR\x04path\x12\x10\n" + 1150 + "\x03key\x18\x02 \x01(\tR\x03key\"\x16\n" + 1142 1151 "\x14DeleteSecretResponse\",\n" + 1143 1152 "\x12ListSecretsRequest\x12\x16\n" + 1144 1153 "\x06prefix\x18\x01 \x01(\tR\x06prefix\"+\n" +
+1
api/proto/hsm/v1/hsm.proto
··· 92 92 93 93 message DeleteSecretRequest { 94 94 string path = 1; 95 + string key = 2; // Optional: delete specific key only, empty means delete entire secret 95 96 } 96 97 97 98 message DeleteSecretResponse {}
+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.7.0 6 - appVersion: v0.7.0 5 + version: 0.8.0 6 + appVersion: v0.8.0 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:
+6
helm/hsm-secrets-operator/templates/resources.yaml
··· 6 6 metadata: 7 7 name: {{ .name }} 8 8 namespace: {{ .namespace | default $.Release.Namespace }} 9 + annotations: 10 + "helm.sh/hook": post-install,post-upgrade 11 + "helm.sh/hook-weight": "5" 9 12 labels: 10 13 {{- include "hsm-secrets-operator.labels" $ | nindent 4 }} 11 14 app.kubernetes.io/component: hsmsecret ··· 35 38 metadata: 36 39 name: {{ .name }} 37 40 namespace: {{ .namespace | default $.Release.Namespace }} 41 + annotations: 42 + "helm.sh/hook": post-install,post-upgrade 43 + "helm.sh/hook-weight": "5" 38 44 labels: 39 45 {{- include "hsm-secrets-operator.labels" $ | nindent 4 }} 40 46 app.kubernetes.io/component: hsmdevice
+4
internal/agent/connection_pool.go
··· 48 48 return cw.client.DeleteSecret(ctx, path) 49 49 } 50 50 51 + func (cw *ClientWrapper) DeleteSecretKey(ctx context.Context, path, key string) error { 52 + return cw.client.DeleteSecretKey(ctx, path, key) 53 + } 54 + 51 55 func (cw *ClientWrapper) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 52 56 return cw.client.ListSecrets(ctx, prefix) 53 57 }
+16
internal/agent/grpc_client.go
··· 216 216 return nil 217 217 } 218 218 219 + // DeleteSecretKey removes a specific key from the secret at the given path 220 + func (c *GRPCClient) DeleteSecretKey(ctx context.Context, path, key string) error { 221 + ctx, cancel := context.WithTimeout(ctx, c.timeout) 222 + defer cancel() 223 + 224 + _, err := c.client.DeleteSecret(ctx, &hsmv1.DeleteSecretRequest{ 225 + Path: path, 226 + Key: key, 227 + }) 228 + if err != nil { 229 + return fmt.Errorf("failed to delete secret key: %w", err) 230 + } 231 + 232 + return nil 233 + } 234 + 219 235 // ListSecrets returns a list of secret paths 220 236 func (c *GRPCClient) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 221 237 ctx, cancel := context.WithTimeout(ctx, c.timeout)
+12
internal/agent/grpc_server.go
··· 267 267 } 268 268 269 269 // DeleteSecret removes secret data from the specified HSM path 270 + // If req.Key is specified, only that key is deleted from the secret 270 271 func (s *GRPCServer) DeleteSecret(ctx context.Context, req *hsmv1.DeleteSecretRequest) (*hsmv1.DeleteSecretResponse, error) { 271 272 if req.Path == "" { 272 273 return nil, status.Error(codes.InvalidArgument, "path is required") ··· 276 277 return nil, status.Error(codes.Unavailable, "HSM client not connected") 277 278 } 278 279 280 + // If key is specified, delete only that key 281 + if req.Key != "" { 282 + if err := s.hsmClient.DeleteSecretKey(ctx, req.Path, req.Key); err != nil { 283 + s.logger.Error(err, "Failed to delete secret key", "path", req.Path, "key", req.Key) 284 + return nil, status.Errorf(codes.Internal, "failed to delete secret key: %v", err) 285 + } 286 + s.logger.V(1).Info("Successfully deleted secret key", "path", req.Path, "key", req.Key) 287 + return &hsmv1.DeleteSecretResponse{}, nil 288 + } 289 + 290 + // Delete entire secret 279 291 if err := s.hsmClient.DeleteSecret(ctx, req.Path); err != nil { 280 292 s.logger.Error(err, "Failed to delete secret", "path", req.Path) 281 293 return nil, status.Errorf(codes.Internal, "failed to delete secret: %v", err)
+103
internal/api/proxy_client.go
··· 606 606 } 607 607 } 608 608 609 + // DeleteSecretKey handles DELETE /hsm/secrets/:path/:key with mirroring support 610 + func (p *ProxyClient) DeleteSecretKey(c *gin.Context) { 611 + path, ok := p.validatePathParam(c) 612 + if !ok { 613 + return 614 + } 615 + 616 + key := c.Param("key") 617 + if key == "" { 618 + p.server.sendError(c, http.StatusBadRequest, "missing_key", "Key parameter is required", nil) 619 + return 620 + } 621 + 622 + // Get all available clients for mirroring delete 623 + clients, ok := p.getAllAvailableGRPCClients(c) 624 + if !ok { 625 + return 626 + } 627 + 628 + // Delete key from all devices in parallel 629 + results := p.deleteKeyFromAllDevices(c.Request.Context(), clients, path, key) 630 + 631 + // Check results 632 + successful := 0 633 + var errors []string 634 + deviceResults := make(map[string]any) 635 + 636 + for deviceName, result := range results { 637 + deviceResults[deviceName] = map[string]any{ 638 + "success": result.Error == nil, 639 + "error": func() string { 640 + if result.Error != nil { 641 + return result.Error.Error() 642 + } 643 + return "" 644 + }(), 645 + } 646 + 647 + if result.Error == nil { 648 + successful++ 649 + } else { 650 + errors = append(errors, fmt.Sprintf("%s: %v", deviceName, result.Error)) 651 + p.logger.Error(result.Error, "Failed to delete key from device", "device", deviceName, "path", path, "key", key) 652 + } 653 + } 654 + 655 + // Consider the operation successful if we deleted from at least one device 656 + if successful > 0 { 657 + response := map[string]any{ 658 + "path": path, 659 + "key": key, 660 + "devices": len(clients), 661 + "deviceResults": deviceResults, 662 + } 663 + if len(errors) > 0 { 664 + response["warnings"] = errors 665 + } 666 + 667 + statusCode := http.StatusOK 668 + message := "Key deleted successfully" 669 + if successful < len(clients) { 670 + statusCode = http.StatusPartialContent 671 + message = fmt.Sprintf("Key deleted from %d/%d devices", successful, len(clients)) 672 + } 673 + 674 + p.server.sendResponse(c, statusCode, message, response) 675 + } else { 676 + // All devices failed 677 + p.server.sendError(c, http.StatusInternalServerError, "delete_key_failed", "Failed to delete key from any HSM device", map[string]any{ 678 + "errors": errors, 679 + "deviceResults": deviceResults, 680 + "path": path, 681 + "key": key, 682 + }) 683 + } 684 + } 685 + 686 + // deleteKeyFromAllDevices deletes a specific key from all devices in parallel 687 + func (p *ProxyClient) deleteKeyFromAllDevices(ctx context.Context, clients map[string]hsm.Client, path, key string) map[string]WriteResult { 688 + results := make(map[string]WriteResult) 689 + resultsMutex := sync.Mutex{} 690 + wg := sync.WaitGroup{} 691 + 692 + for deviceName, client := range clients { 693 + wg.Add(1) 694 + go func(deviceName string, client hsm.Client) { 695 + defer wg.Done() 696 + 697 + err := client.DeleteSecretKey(ctx, path, key) 698 + 699 + resultsMutex.Lock() 700 + results[deviceName] = WriteResult{ 701 + DeviceName: deviceName, 702 + Error: err, 703 + } 704 + resultsMutex.Unlock() 705 + }(deviceName, client) 706 + } 707 + 708 + wg.Wait() 709 + return results 710 + } 711 + 609 712 // ReadMetadata handles GET /hsm/secrets/:path/metadata 610 713 func (p *ProxyClient) ReadMetadata(c *gin.Context) { 611 714 path, ok := p.validatePathParam(c)
+1
internal/api/proxy_handlers.go
··· 59 59 secretsGroup.POST("/:path", s.proxyClient.WriteSecret) 60 60 secretsGroup.PUT("/:path", s.proxyClient.WriteSecret) 61 61 secretsGroup.DELETE("/:path", s.proxyClient.DeleteSecret) 62 + secretsGroup.DELETE("/:path/:key", s.proxyClient.DeleteSecretKey) 62 63 63 64 // Secret metadata and checksum 64 65 secretsGroup.GET("/:path/metadata", s.proxyClient.ReadMetadata)
+3
internal/hsm/client.go
··· 68 68 // DeleteSecret removes secret data from the specified HSM path 69 69 DeleteSecret(ctx context.Context, path string) error 70 70 71 + // DeleteSecretKey removes a specific key from the secret at the given path 72 + DeleteSecretKey(ctx context.Context, path, key string) error 73 + 71 74 // ListSecrets returns a list of secret paths 72 75 ListSecrets(ctx context.Context, prefix string) ([]string, error) 73 76
+23
internal/hsm/mock_client.go
··· 186 186 return nil 187 187 } 188 188 189 + // DeleteSecretKey removes a specific key from the secret at the given path 190 + func (m *MockClient) DeleteSecretKey(ctx context.Context, path, key string) error { 191 + m.mutex.Lock() 192 + defer m.mutex.Unlock() 193 + 194 + if !m.connected { 195 + return fmt.Errorf("HSM not connected") 196 + } 197 + 198 + data, exists := m.secrets[path] 199 + if !exists { 200 + return fmt.Errorf("secret not found at path: %s", path) 201 + } 202 + 203 + if _, keyExists := data[key]; !keyExists { 204 + return fmt.Errorf("key %q not found in secret %q", key, path) 205 + } 206 + 207 + delete(data, key) 208 + m.logger.Info("Deleted key from mock HSM secret", "path", path, "key", key) 209 + return nil 210 + } 211 + 189 212 // ListSecrets returns a list of secret paths with the given prefix 190 213 func (m *MockClient) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 191 214 m.mutex.RLock()
+40
internal/hsm/pkcs11_cgo.go
··· 409 409 return nil 410 410 } 411 411 412 + // deleteSecretKeyPKCS11 removes a single data object with the exact label 413 + func deleteSecretKeyPKCS11(session *Session, label string) error { 414 + if session == nil { 415 + return fmt.Errorf("session is nil") 416 + } 417 + 418 + // Find data objects with exact label match 419 + template := []*pkcs11.Attribute{ 420 + pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_DATA), 421 + pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), 422 + } 423 + 424 + if err := session.ctx.FindObjectsInit(session.session, template); err != nil { 425 + return fmt.Errorf("failed to initialize object search: %w", err) 426 + } 427 + 428 + // Find the object 429 + objs, _, err := session.ctx.FindObjects(session.session, 1) 430 + if err != nil { 431 + _ = session.ctx.FindObjectsFinal(session.session) 432 + return fmt.Errorf("failed to find object: %w", err) 433 + } 434 + 435 + // Must finalize search before destroying objects 436 + if err := session.ctx.FindObjectsFinal(session.session); err != nil { 437 + return fmt.Errorf("failed to finalize object search: %w", err) 438 + } 439 + 440 + if len(objs) == 0 { 441 + return fmt.Errorf("key not found: %s", label) 442 + } 443 + 444 + // Destroy the object 445 + if err := session.ctx.DestroyObject(session.session, objs[0]); err != nil { 446 + return fmt.Errorf("failed to destroy object: %w", err) 447 + } 448 + 449 + return nil 450 + } 451 + 412 452 // changePINPKCS11 changes the HSM PIN 413 453 func changePINPKCS11(session *Session, oldPIN, newPIN string) error { 414 454 if session == nil {
+28
internal/hsm/pkcs11_client.go
··· 358 358 return nil 359 359 } 360 360 361 + // DeleteSecretKey removes a specific key from the secret at the given path 362 + func (c *PKCS11Client) DeleteSecretKey(ctx context.Context, path, key string) error { 363 + c.mutex.Lock() 364 + defer c.mutex.Unlock() 365 + 366 + if !c.connected { 367 + return fmt.Errorf("HSM not connected") 368 + } 369 + 370 + // Build the exact label for this key 371 + label := path 372 + if key != "" && key != defaultKeyName { 373 + label = path + "/" + key 374 + } 375 + 376 + c.logger.Info("Deleting secret key from HSM", "path", path, "key", key, "label", label) 377 + 378 + if err := deleteSecretKeyPKCS11(c.session, label); err != nil { 379 + return fmt.Errorf("failed to delete secret key: %w", err) 380 + } 381 + 382 + // Remove from cache 383 + delete(c.dataObjects, label) 384 + 385 + c.logger.Info("Successfully deleted secret key from HSM", "path", path, "key", key) 386 + return nil 387 + } 388 + 361 389 // ListSecrets returns a list of secret paths with the given prefix 362 390 func (c *PKCS11Client) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 363 391 c.mutex.RLock()
+5
internal/hsm/pkcs11_stub.go
··· 56 56 return fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 57 57 } 58 58 59 + // deleteSecretKeyPKCS11 returns an error for non-CGO builds 60 + func deleteSecretKeyPKCS11(session *Session, label string) error { 61 + return fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)") 62 + } 63 + 59 64 // changePINPKCS11 returns an error for non-CGO builds 60 65 func changePINPKCS11(session *Session, oldPIN, newPIN string) error { 61 66 return fmt.Errorf("PKCS#11 support requires CGO (set CGO_ENABLED=1 and rebuild)")
+5
kubectl-hsm/pkg/client/client.go
··· 183 183 return c.doRequest(ctx, "DELETE", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), nil, nil) 184 184 } 185 185 186 + // DeleteSecretKey deletes a specific key from a secret in the HSM 187 + func (c *Client) DeleteSecretKey(ctx context.Context, name, key string) error { 188 + return c.doRequest(ctx, "DELETE", fmt.Sprintf("/api/v1/hsm/secrets/%s/%s", name, key), nil, nil) 189 + } 190 + 186 191 // GetHealth checks the health status of the HSM operator 187 192 func (c *Client) GetHealth(ctx context.Context) (*HealthStatus, error) { 188 193 var result HealthStatus
+14 -1
kubectl-hsm/pkg/commands/delete.go
··· 91 91 } 92 92 93 93 if opts.Key != "" { 94 - return fmt.Errorf("deleting individual keys is not yet supported - please delete the entire secret and recreate it") 94 + // Confirm deletion unless force is specified 95 + if !opts.Force { 96 + if err := opts.confirmDeletion(secretName); err != nil { 97 + return err 98 + } 99 + } 100 + 101 + // Delete the specific key via native API 102 + fmt.Printf("Deleting key '%s' from secret '%s'...\n", opts.Key, secretName) 103 + if err := hsmClient.DeleteSecretKey(ctx, secretName, opts.Key); err != nil { 104 + return fmt.Errorf("failed to delete key: %w", err) 105 + } 106 + fmt.Printf("Key '%s' deleted successfully from secret '%s'.\n", opts.Key, secretName) 107 + return nil 95 108 } 96 109 97 110 // Confirm deletion unless force is specified