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.

try and create device resource plugin

+208 -20
+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.6.44 6 + VERSION ?= 0.7.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")
+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.6.44 6 - appVersion: v0.6.44 5 + version: 0.7.0 6 + appVersion: v0.7.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:
+66 -14
internal/controller/discovery_daemonset_controller.go
··· 249 249 MountPath: "/run/udev", 250 250 ReadOnly: true, 251 251 }, 252 + // Kubelet device plugin sockets for device plugin registration 253 + { 254 + Name: "device-plugins", 255 + MountPath: "/var/lib/kubelet/device-plugins", 256 + }, 257 + { 258 + Name: "plugins-registry", 259 + MountPath: "/var/lib/kubelet/plugins_registry", 260 + }, 252 261 }, 253 262 Resources: corev1.ResourceRequirements{ 254 263 Requests: corev1.ResourceList{ ··· 274 283 Effect: corev1.TaintEffectNoSchedule, 275 284 }, 276 285 }, 277 - SecurityContext: &corev1.PodSecurityContext{ 278 - RunAsNonRoot: &[]bool{true}[0], 279 - RunAsUser: &[]int64{65534}[0], // nobody user 280 - RunAsGroup: &[]int64{65534}[0], // nobody group 281 - FSGroup: &[]int64{65534}[0], 282 - }, 286 + SecurityContext: r.getPodSecurityContext(isTestEnvironment), 283 287 }, 284 288 }, 285 289 UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ ··· 508 512 }, 509 513 }, 510 514 }, 515 + // Kubelet device plugin sockets for device plugin registration 516 + corev1.Volume{ 517 + Name: "device-plugins", 518 + VolumeSource: corev1.VolumeSource{ 519 + HostPath: &corev1.HostPathVolumeSource{ 520 + Path: "/var/lib/kubelet/device-plugins", 521 + Type: &[]corev1.HostPathType{corev1.HostPathDirectoryOrCreate}[0], 522 + }, 523 + }, 524 + }, 525 + corev1.Volume{ 526 + Name: "plugins-registry", 527 + VolumeSource: corev1.VolumeSource{ 528 + HostPath: &corev1.HostPathVolumeSource{ 529 + Path: "/var/lib/kubelet/plugins_registry", 530 + Type: &[]corev1.HostPathType{corev1.HostPathDirectory}[0], 531 + }, 532 + }, 533 + }, 511 534 ) 512 535 } 513 536 514 537 return volumes 515 538 } 516 539 517 - // getSecurityContext returns the appropriate security context based on environment 540 + // getSecurityContext returns the appropriate container security context based on environment 518 541 func (r *DiscoveryDaemonSetReconciler) getSecurityContext(isTestEnvironment bool) *corev1.SecurityContext { 519 - securityContext := &corev1.SecurityContext{ 520 - RunAsNonRoot: &[]bool{true}[0], 542 + if isTestEnvironment { 543 + // Test environment: restrictive security context 544 + return &corev1.SecurityContext{ 545 + RunAsNonRoot: &[]bool{true}[0], 546 + AllowPrivilegeEscalation: &[]bool{false}[0], 547 + ReadOnlyRootFilesystem: &[]bool{false}[0], 548 + Capabilities: &corev1.Capabilities{ 549 + Drop: []corev1.Capability{"ALL"}, 550 + }, 551 + SeccompProfile: &corev1.SeccompProfile{ 552 + Type: corev1.SeccompProfileTypeRuntimeDefault, 553 + }, 554 + } 555 + } 556 + 557 + // Production environment: need elevated privileges for device plugin 558 + // Device plugins need to create Unix sockets and register with kubelet 559 + return &corev1.SecurityContext{ 560 + RunAsNonRoot: &[]bool{false}[0], 521 561 AllowPrivilegeEscalation: &[]bool{false}[0], 522 - ReadOnlyRootFilesystem: &[]bool{false}[0], // Need write access for termination log 562 + ReadOnlyRootFilesystem: &[]bool{false}[0], 523 563 Capabilities: &corev1.Capabilities{ 524 564 Drop: []corev1.Capability{"ALL"}, 525 565 }, 526 566 } 567 + } 527 568 528 - // Add seccomp profile for test environments to pass restricted pod security policy 569 + // getPodSecurityContext returns the appropriate pod security context based on environment 570 + func (r *DiscoveryDaemonSetReconciler) getPodSecurityContext(isTestEnvironment bool) *corev1.PodSecurityContext { 529 571 if isTestEnvironment { 530 - securityContext.SeccompProfile = &corev1.SeccompProfile{ 531 - Type: corev1.SeccompProfileTypeRuntimeDefault, 572 + // Test environment: non-root user 573 + return &corev1.PodSecurityContext{ 574 + RunAsNonRoot: &[]bool{true}[0], 575 + RunAsUser: &[]int64{65534}[0], // nobody user 576 + RunAsGroup: &[]int64{65534}[0], // nobody group 577 + FSGroup: &[]int64{65534}[0], 532 578 } 533 579 } 534 580 535 - return securityContext 581 + // Production environment: run as root for device plugin socket access 582 + // Device plugins need root to create sockets in /var/lib/kubelet/device-plugins/ 583 + return &corev1.PodSecurityContext{ 584 + RunAsNonRoot: &[]bool{false}[0], 585 + RunAsUser: &[]int64{0}[0], // root user 586 + RunAsGroup: &[]int64{0}[0], // root group 587 + } 536 588 }
+11 -3
internal/controller/discovery_daemonset_controller_test.go
··· 159 159 Expect(podNameEnv).NotTo(BeNil()) 160 160 Expect(podNameEnv.ValueFrom.FieldRef.FieldPath).To(Equal("metadata.name")) 161 161 162 - // Check volumes - now includes /run/udev for event-driven USB discovery 163 - Expect(podSpec.Volumes).To(HaveLen(3)) 164 - var devVolume, sysVolume, udevVolume *corev1.Volume 162 + // Check volumes - includes discovery volumes and kubelet device plugin volumes 163 + Expect(podSpec.Volumes).To(HaveLen(5)) 164 + var devVolume, sysVolume, udevVolume, devicePluginsVolume, pluginsRegistryVolume *corev1.Volume 165 165 for i := range podSpec.Volumes { 166 166 switch podSpec.Volumes[i].Name { 167 167 case "dev": ··· 170 170 sysVolume = &podSpec.Volumes[i] 171 171 case "run-udev": 172 172 udevVolume = &podSpec.Volumes[i] 173 + case "device-plugins": 174 + devicePluginsVolume = &podSpec.Volumes[i] 175 + case "plugins-registry": 176 + pluginsRegistryVolume = &podSpec.Volumes[i] 173 177 } 174 178 } 175 179 176 180 Expect(devVolume).NotTo(BeNil()) 177 181 Expect(sysVolume).NotTo(BeNil()) 178 182 Expect(udevVolume).NotTo(BeNil()) 183 + Expect(devicePluginsVolume).NotTo(BeNil()) 184 + Expect(pluginsRegistryVolume).NotTo(BeNil()) 179 185 180 186 // In CI environments, volumes use EmptyDir; in production they use HostPath 181 187 if devVolume.HostPath != nil { ··· 183 189 Expect(devVolume.HostPath.Path).To(Equal("/dev")) 184 190 Expect(sysVolume.HostPath.Path).To(Equal("/sys")) 185 191 Expect(udevVolume.HostPath.Path).To(Equal("/run/udev")) 192 + Expect(devicePluginsVolume.HostPath.Path).To(Equal("/var/lib/kubelet/device-plugins")) 193 + Expect(pluginsRegistryVolume.HostPath.Path).To(Equal("/var/lib/kubelet/plugins_registry")) 186 194 } else { 187 195 // CI/test environment - expect EmptyDir volumes 188 196 Expect(devVolume.EmptyDir).NotTo(BeNil())
+19
internal/controller/hsmpool_agent_controller.go
··· 519 519 520 520 // createAgentDeployment creates the HSM agent deployment for a specific device 521 521 func (r *HSMPoolAgentReconciler) createAgentDeployment(ctx context.Context, hsmPool *hsmv1alpha1.HSMPool, specificDevice *hsmv1alpha1.DiscoveredDevice, customAgentName string) error { 522 + logger := log.FromContext(ctx) 523 + 522 524 if specificDevice == nil { 523 525 return fmt.Errorf("specificDevice is required") 524 526 } ··· 532 534 533 535 targetNode := specificDevice.NodeName 534 536 deviceName := hsmPool.OwnerReferences[0].Name 537 + 538 + // Fetch the HSMDevice to get the device type for extended resource requests 539 + var hsmDevice hsmv1alpha1.HSMDevice 540 + if err := r.Get(ctx, types.NamespacedName{Name: deviceName, Namespace: hsmPool.Namespace}, &hsmDevice); err != nil { 541 + logger.Error(err, "Failed to get HSMDevice for agent deployment", "device", deviceName) 542 + return fmt.Errorf("failed to get HSMDevice %s: %w", deviceName, err) 543 + } 544 + 545 + // Build extended resource name from device type (e.g., "hsm.j5t.io/picohsm") 546 + extendedResourceName := corev1.ResourceName( 547 + fmt.Sprintf("hsm.j5t.io/%s", strings.ToLower(string(hsmDevice.Spec.DeviceType))), 548 + ) 549 + logger.V(1).Info("Using extended resource for agent", 550 + "agent", agentName, 551 + "resource", extendedResourceName) 535 552 536 553 // Get agent image from config or fallback to auto-detection 537 554 var agentImage string ··· 657 674 Requests: corev1.ResourceList{ 658 675 corev1.ResourceCPU: resource.MustParse("100m"), 659 676 corev1.ResourceMemory: resource.MustParse("128Mi"), 677 + extendedResourceName: resource.MustParse("1"), // Request 1 HSM device 660 678 }, 661 679 Limits: corev1.ResourceList{ 662 680 corev1.ResourceCPU: resource.MustParse("500m"), 663 681 corev1.ResourceMemory: resource.MustParse("256Mi"), 682 + extendedResourceName: resource.MustParse("1"), // Limit to 1 HSM device 664 683 }, 665 684 }, 666 685 SecurityContext: &corev1.SecurityContext{
+109
internal/modes/discovery/discovery.go
··· 22 22 "flag" 23 23 "fmt" 24 24 "maps" 25 + "sync" 25 26 "time" 26 27 27 28 // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) ··· 144 145 eventMonitoringActive bool 145 146 deviceCache map[string][]hsmv1alpha1.DiscoveredDevice // Cache current device state per HSMDevice 146 147 deviceLookup map[string]map[string]int // Fast lookup: HSMDevice -> (serial/path -> index) 148 + deviceTypes map[string]hsmv1alpha1.HSMDeviceType // Cache: HSMDevice name -> device type 149 + 150 + // Device plugin managers (one per device type) 151 + devicePluginManagers map[hsmv1alpha1.HSMDeviceType]*discovery.HSMDeviceManager 152 + devicePluginMutex sync.RWMutex 147 153 } 148 154 149 155 // Run starts the discovery loop with event-driven monitoring ··· 151 157 // Initialize device cache 152 158 d.deviceCache = make(map[string][]hsmv1alpha1.DiscoveredDevice) 153 159 d.deviceLookup = make(map[string]map[string]int) 160 + d.deviceTypes = make(map[string]hsmv1alpha1.HSMDeviceType) 161 + 162 + // Initialize device plugin managers map 163 + d.devicePluginManagers = make(map[hsmv1alpha1.HSMDeviceType]*discovery.HSMDeviceManager) 154 164 155 165 // Step 1: Mandatory initial discovery to detect already-connected devices 156 166 d.logger.Info("Performing initial device discovery scan") ··· 181 191 case <-ctx.Done(): 182 192 d.logger.Info("Discovery agent shutting down") 183 193 d.usbDiscoverer.StopEventMonitoring() 194 + d.stopAllDevicePlugins() 184 195 return nil 185 196 186 197 case event, ok := <-eventChan: ··· 237 248 238 249 d.logger.V(1).Info("Discovering devices", "hsmDevice", hsmDevice.Name, "node", d.nodeName) 239 250 251 + // Cache the device type for use in event handlers 252 + d.deviceTypes[hsmDevice.Name] = hsmDevice.Spec.DeviceType 253 + 240 254 // Perform local discovery 241 255 discoveredDevices, err := d.discoverDevicesForSpec(ctx, hsmDevice) 242 256 if err != nil { ··· 251 265 252 266 // Update device cache 253 267 d.updateDeviceCache(hsmDevice.Name, discoveredDevices) 268 + 269 + // Update device plugin with discovered devices 270 + if err := d.updateDevicePlugin(hsmDevice, discoveredDevices); err != nil { 271 + d.logger.Error(err, "Failed to update device plugin", "device", hsmDevice.Name) 272 + // Continue - don't fail the entire discovery for device plugin issues 273 + } 254 274 255 275 // Update pod annotation with discovery results 256 276 if err := d.updatePodAnnotation(ctx, hsmDevice.Name, discoveredDevices); err != nil { ··· 526 546 if err := d.updatePodAnnotation(ctx, event.HSMDeviceName, currentDevices); err != nil { 527 547 d.logger.Error(err, "Failed to update pod annotation after device reconnection") 528 548 } 549 + // Update device plugin 550 + if err := d.updateDevicePluginByName(event.HSMDeviceName, currentDevices); err != nil { 551 + d.logger.Error(err, "Failed to update device plugin after device reconnection") 552 + } 529 553 return 530 554 } 531 555 ··· 543 567 if err := d.updatePodAnnotation(ctx, event.HSMDeviceName, currentDevices); err != nil { 544 568 d.logger.Error(err, "Failed to update pod annotation after device add") 545 569 } 570 + 571 + // Update device plugin 572 + if err := d.updateDevicePluginByName(event.HSMDeviceName, currentDevices); err != nil { 573 + d.logger.Error(err, "Failed to update device plugin after device add") 574 + } 546 575 } 547 576 548 577 // handleDeviceRemove handles USB device removal events ··· 583 612 // Update pod annotation 584 613 if err := d.updatePodAnnotation(ctx, event.HSMDeviceName, currentDevices); err != nil { 585 614 d.logger.Error(err, "Failed to update pod annotation after device remove") 615 + } 616 + 617 + // Update device plugin 618 + if err := d.updateDevicePluginByName(event.HSMDeviceName, currentDevices); err != nil { 619 + d.logger.Error(err, "Failed to update device plugin after device remove") 586 620 } 587 621 } 588 622 ··· 746 780 747 781 return -1 748 782 } 783 + 784 + // ensureDevicePluginForType creates or returns existing device plugin manager for a device type 785 + func (d *DiscoveryAgent) ensureDevicePluginForType(deviceType hsmv1alpha1.HSMDeviceType) (*discovery.HSMDeviceManager, error) { 786 + d.devicePluginMutex.Lock() 787 + defer d.devicePluginMutex.Unlock() 788 + 789 + if manager, exists := d.devicePluginManagers[deviceType]; exists { 790 + return manager, nil 791 + } 792 + 793 + // Create new device plugin manager 794 + resourceName := string(deviceType) 795 + manager := discovery.NewHSMDeviceManager(deviceType, resourceName) 796 + 797 + // Start the device plugin 798 + if err := manager.Start(); err != nil { 799 + return nil, fmt.Errorf("failed to start device plugin for %s: %w", deviceType, err) 800 + } 801 + 802 + d.devicePluginManagers[deviceType] = manager 803 + d.logger.Info("Started device plugin", "deviceType", deviceType, "resource", manager.GetResourceName()) 804 + 805 + return manager, nil 806 + } 807 + 808 + // updateDevicePlugin updates the device plugin with discovered devices for a specific HSMDevice 809 + func (d *DiscoveryAgent) updateDevicePlugin(hsmDevice *hsmv1alpha1.HSMDevice, devices []hsmv1alpha1.DiscoveredDevice) error { 810 + return d.updateDevicePluginByType(hsmDevice.Spec.DeviceType, devices) 811 + } 812 + 813 + // updateDevicePluginByName updates the device plugin using the cached device type for an HSMDevice name 814 + func (d *DiscoveryAgent) updateDevicePluginByName(hsmDeviceName string, devices []hsmv1alpha1.DiscoveredDevice) error { 815 + deviceType, exists := d.deviceTypes[hsmDeviceName] 816 + if !exists { 817 + d.logger.V(1).Info("No cached device type for HSMDevice, skipping device plugin update", 818 + "hsmDevice", hsmDeviceName) 819 + return nil 820 + } 821 + return d.updateDevicePluginByType(deviceType, devices) 822 + } 823 + 824 + // updateDevicePluginByType updates the device plugin for a specific device type 825 + func (d *DiscoveryAgent) updateDevicePluginByType(deviceType hsmv1alpha1.HSMDeviceType, devices []hsmv1alpha1.DiscoveredDevice) error { 826 + manager, err := d.ensureDevicePluginForType(deviceType) 827 + if err != nil { 828 + return err 829 + } 830 + 831 + // Filter to only devices on this node that are available 832 + nodeDevices := make([]hsmv1alpha1.DiscoveredDevice, 0) 833 + for _, device := range devices { 834 + if device.NodeName == d.nodeName && device.Available { 835 + nodeDevices = append(nodeDevices, device) 836 + } 837 + } 838 + 839 + manager.UpdateDevices(nodeDevices) 840 + d.logger.V(1).Info("Updated device plugin", 841 + "deviceType", deviceType, 842 + "nodeDevices", len(nodeDevices)) 843 + 844 + return nil 845 + } 846 + 847 + // stopAllDevicePlugins stops all running device plugins 848 + func (d *DiscoveryAgent) stopAllDevicePlugins() { 849 + d.devicePluginMutex.Lock() 850 + defer d.devicePluginMutex.Unlock() 851 + 852 + for deviceType, manager := range d.devicePluginManagers { 853 + d.logger.Info("Stopping device plugin", "deviceType", deviceType) 854 + manager.Stop() 855 + } 856 + d.devicePluginManagers = make(map[hsmv1alpha1.HSMDeviceType]*discovery.HSMDeviceManager) 857 + }