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.

initial commit

Evan Jarrett 6267e518

+13988
+25
.devcontainer/devcontainer.json
··· 1 + { 2 + "name": "Kubebuilder DevContainer", 3 + "image": "golang:1.24", 4 + "features": { 5 + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 6 + "ghcr.io/devcontainers/features/git:1": {} 7 + }, 8 + 9 + "runArgs": ["--network=host"], 10 + 11 + "customizations": { 12 + "vscode": { 13 + "settings": { 14 + "terminal.integrated.shell.linux": "/bin/bash" 15 + }, 16 + "extensions": [ 17 + "ms-kubernetes-tools.vscode-kubernetes-tools", 18 + "ms-azuretools.vscode-docker" 19 + ] 20 + } 21 + }, 22 + 23 + "onCreateCommand": "bash .devcontainer/post-install.sh" 24 + } 25 +
+23
.devcontainer/post-install.sh
··· 1 + #!/bin/bash 2 + set -x 3 + 4 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 5 + chmod +x ./kind 6 + mv ./kind /usr/local/bin/kind 7 + 8 + curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64 9 + chmod +x kubebuilder 10 + mv kubebuilder /usr/local/bin/ 11 + 12 + KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) 13 + curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" 14 + chmod +x kubectl 15 + mv kubectl /usr/local/bin/kubectl 16 + 17 + docker network create -d=bridge --subnet=172.19.0.0/24 kind 18 + 19 + kind version 20 + kubebuilder version 21 + docker --version 22 + go version 23 + kubectl version --client
+3
.dockerignore
··· 1 + # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 + # Ignore build and test binaries. 3 + bin/
+93
.github/workflows/docker-publish.yml
··· 1 + name: Docker 2 + 3 + # This workflow uses actions that are not certified by GitHub. 4 + # They are provided by a third-party and are governed by 5 + # separate terms of service, privacy policy, and support 6 + # documentation. 7 + 8 + on: 9 + push: 10 + branches: [ "*" ] 11 + # Publish semver tags as releases. 12 + tags: [ 'v*.*.*' ] 13 + env: 14 + # Use docker.io for Docker Hub if empty 15 + REGISTRY: ghcr.io 16 + # github.repository as <account>/<repo> 17 + IMAGE_NAME: ${{ github.repository }} 18 + 19 + 20 + jobs: 21 + build: 22 + 23 + runs-on: ubuntu-latest 24 + permissions: 25 + contents: read 26 + packages: write 27 + # This is used to complete the identity challenge 28 + # with sigstore/fulcio when running outside of PRs. 29 + id-token: write 30 + 31 + steps: 32 + - name: Checkout repository 33 + uses: actions/checkout@v4 34 + 35 + # Install the cosign tool except on PR 36 + # https://github.com/sigstore/cosign-installer 37 + - name: Install cosign 38 + if: github.event_name != 'pull_request' 39 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 40 + with: 41 + cosign-release: 'v2.2.4' 42 + 43 + # Set up BuildKit Docker container builder to be able to build 44 + # multi-platform images and export cache 45 + # https://github.com/docker/setup-buildx-action 46 + - name: Set up Docker Buildx 47 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 48 + 49 + # Login against a Docker registry except on PR 50 + # https://github.com/docker/login-action 51 + - name: Log into registry ${{ env.REGISTRY }} 52 + if: github.event_name != 'pull_request' 53 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 54 + with: 55 + registry: ${{ env.REGISTRY }} 56 + username: ${{ github.actor }} 57 + password: ${{ secrets.GITHUB_TOKEN }} 58 + 59 + # Extract metadata (tags, labels) for Docker 60 + # https://github.com/docker/metadata-action 61 + - name: Extract Docker metadata 62 + id: meta 63 + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 64 + with: 65 + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 66 + 67 + # Build and push Docker image with Buildx (don't push on PR) 68 + # https://github.com/docker/build-push-action 69 + - name: Build and push Docker image 70 + id: build-and-push 71 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 72 + with: 73 + context: . 74 + push: ${{ github.event_name != 'pull_request' }} 75 + tags: ${{ steps.meta.outputs.tags }} 76 + labels: ${{ steps.meta.outputs.labels }} 77 + cache-from: type=gha 78 + cache-to: type=gha,mode=max 79 + 80 + # Sign the resulting Docker image digest except on PRs. 81 + # This will only write to the public Rekor transparency log when the Docker 82 + # repository is public to avoid leaking data. If you would like to publish 83 + # transparency data even for private images, pass --force to cosign below. 84 + # https://github.com/sigstore/cosign 85 + - name: Sign the published Docker image 86 + if: ${{ github.event_name != 'pull_request' }} 87 + env: 88 + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable 89 + TAGS: ${{ steps.meta.outputs.tags }} 90 + DIGEST: ${{ steps.build-and-push.outputs.digest }} 91 + # This step uses the identity token to provision an ephemeral certificate 92 + # against the sigstore community Fulcio instance. 93 + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
+23
.github/workflows/lint.yml
··· 1 + name: Lint 2 + 3 + on: 4 + push: 5 + pull_request: 6 + 7 + jobs: 8 + lint: 9 + name: Run on Ubuntu 10 + runs-on: ubuntu-latest 11 + steps: 12 + - name: Clone the code 13 + uses: actions/checkout@v4 14 + 15 + - name: Setup Go 16 + uses: actions/setup-go@v5 17 + with: 18 + go-version-file: go.mod 19 + 20 + - name: Run linter 21 + uses: golangci/golangci-lint-action@v8 22 + with: 23 + version: v2.1.0
+32
.github/workflows/test-e2e.yml
··· 1 + name: E2E Tests 2 + 3 + on: 4 + push: 5 + pull_request: 6 + 7 + jobs: 8 + test-e2e: 9 + name: Run on Ubuntu 10 + runs-on: ubuntu-latest 11 + steps: 12 + - name: Clone the code 13 + uses: actions/checkout@v4 14 + 15 + - name: Setup Go 16 + uses: actions/setup-go@v5 17 + with: 18 + go-version-file: go.mod 19 + 20 + - name: Install the latest version of kind 21 + run: | 22 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 23 + chmod +x ./kind 24 + sudo mv ./kind /usr/local/bin/kind 25 + 26 + - name: Verify kind installation 27 + run: kind version 28 + 29 + - name: Running Test e2e 30 + run: | 31 + go mod tidy 32 + make test-e2e
+23
.github/workflows/test.yml
··· 1 + name: Tests 2 + 3 + on: 4 + push: 5 + pull_request: 6 + 7 + jobs: 8 + test: 9 + name: Run on Ubuntu 10 + runs-on: ubuntu-latest 11 + steps: 12 + - name: Clone the code 13 + uses: actions/checkout@v4 14 + 15 + - name: Setup Go 16 + uses: actions/setup-go@v5 17 + with: 18 + go-version-file: go.mod 19 + 20 + - name: Running Tests 21 + run: | 22 + go mod tidy 23 + make test
+27
.gitignore
··· 1 + # Binaries for programs and plugins 2 + *.exe 3 + *.exe~ 4 + *.dll 5 + *.so 6 + *.dylib 7 + bin/* 8 + Dockerfile.cross 9 + 10 + # Test binary, built with `go test -c` 11 + *.test 12 + 13 + # Output of the go coverage tool, specifically when used with LiteIDE 14 + *.out 15 + 16 + # Go workspace file 17 + go.work 18 + 19 + # Kubernetes Generated files - skip generated files, except for vendored files 20 + !vendor/**/zz_generated.* 21 + 22 + # editor and IDE paraphernalia 23 + .idea 24 + .vscode 25 + *.swp 26 + *.swo 27 + *~
+52
.golangci.yml
··· 1 + version: "2" 2 + run: 3 + allow-parallel-runners: true 4 + linters: 5 + default: none 6 + enable: 7 + - copyloopvar 8 + - dupl 9 + - errcheck 10 + - ginkgolinter 11 + - goconst 12 + - gocyclo 13 + - govet 14 + - ineffassign 15 + - lll 16 + - misspell 17 + - nakedret 18 + - prealloc 19 + - revive 20 + - staticcheck 21 + - unconvert 22 + - unparam 23 + - unused 24 + settings: 25 + revive: 26 + rules: 27 + - name: comment-spacings 28 + - name: import-shadowing 29 + exclusions: 30 + generated: lax 31 + rules: 32 + - linters: 33 + - lll 34 + path: api/* 35 + - linters: 36 + - dupl 37 + - lll 38 + path: internal/* 39 + paths: 40 + - third_party$ 41 + - builtin$ 42 + - examples$ 43 + formatters: 44 + enable: 45 + - gofmt 46 + - goimports 47 + exclusions: 48 + generated: lax 49 + paths: 50 + - third_party$ 51 + - builtin$ 52 + - examples$
+423
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Project Overview 6 + 7 + A Kubernetes operator that bridges Pico HSM binary data storage with Kubernetes Secrets, providing true secret portability through hardware-based storage. The operator implements a controller pattern that watches HSMSecret Custom Resource Definitions (CRDs) and maintains bidirectional synchronization between HSM binary data files and Kubernetes Secret objects. 8 + 9 + ## Development Commands 10 + 11 + ### Operator SDK Commands 12 + ```bash 13 + # Initialize operator project (if not already done) 14 + operator-sdk init --domain=j5t.io --repo=github.com/evanjarrett/hsm-secrets-operator 15 + 16 + # Create new APIs/CRDs 17 + operator-sdk create api --group=hsm --version=v1alpha1 --kind=HSMSecret 18 + 19 + # Generate manifests and code 20 + make generate 21 + make manifests 22 + 23 + # Build and test 24 + make build 25 + make test 26 + 27 + # Docker operations 28 + make docker-build IMG=<registry>/hsm-secrets-operator:latest 29 + make docker-push IMG=<registry>/hsm-secrets-operator:latest 30 + 31 + # Deploy to cluster 32 + make deploy IMG=<registry>/hsm-secrets-operator:latest 33 + make undeploy 34 + ``` 35 + 36 + ### Development Workflow 37 + ```bash 38 + # Run operator locally for development 39 + make install # Install CRDs 40 + make run # Run controller locally 41 + 42 + # Test with sample resources 43 + kubectl apply -f config/samples/ 44 + 45 + # View logs 46 + kubectl logs -f deployment/hsm-secrets-operator-controller-manager -n hsm-secrets-operator-system 47 + ``` 48 + 49 + ## Architecture 50 + 51 + ### Core Components 52 + 53 + 1. **HSMSecret CRD**: Custom resource definition that represents a secret stored on the Pico HSM 54 + 2. **HSMSecret Controller**: Watches HSMSecret resources and manages synchronization 55 + 3. **HSM Client**: PKCS#11 interface wrapper for Pico HSM communication 56 + 4. **Secret Manager**: Handles Kubernetes Secret object lifecycle 57 + 58 + ### Data Flow 59 + 60 + ``` 61 + Pico HSM (binary files) <-> HSMSecret CRD <-> Kubernetes Secret 62 + secrets/appnamespace/appname-secret -> appnamespace/appname-secret 63 + ``` 64 + 65 + ### Controller Pattern 66 + 67 + The operator follows the standard Kubernetes controller pattern: 68 + - **Watch**: Monitor HSMSecret CRDs and HSM file changes 69 + - **Reconcile**: Ensure desired state matches actual state 70 + - **Update**: Sync changes bidirectionally between HSM and K8s Secrets 71 + 72 + ## Goals 73 + 74 + ### Primary Objectives 75 + - **Simple KV Secrets**: Map HSM files to Kubernetes Secret objects (1:1 mapping) 76 + - **Bidirectional Sync**: Changes in HSM automatically update Secret objects 77 + - **Hardware Security**: Leverage Pico HSM's hardware-based protection 78 + - **Secret Portability**: Enable moving secrets between clusters via HSM 79 + 80 + ### Key Features 81 + - **Import Secrets**: Load existing secrets from HSM into Kubernetes 82 + - **Edit Secrets**: Modify secrets without using cumbersome pkcs11-tool 83 + - **Delete Secrets**: Remove secrets from both HSM and Kubernetes 84 + - **Auto-Sync**: Detect HSM changes and update corresponding Secret objects 85 + 86 + ## HSMSecret CRD Structure 87 + 88 + ```yaml 89 + apiVersion: hsm.j5t.io/v1alpha1 90 + kind: HSMSecret 91 + metadata: 92 + name: appname-secret 93 + namespace: appnamespace 94 + spec: 95 + hsmPath: "secrets/appnamespace/appname-secret" # Path on Pico HSM 96 + secretName: "appname-secret" # Target K8s Secret name (optional) 97 + autoSync: true # Enable bidirectional sync (default: true) 98 + secretType: "Opaque" # Kubernetes Secret type (default: Opaque) 99 + syncInterval: 300 # Sync interval in seconds (default: 300) 100 + status: 101 + lastSyncTime: "2024-01-15T10:30:00Z" 102 + hsmChecksum: "sha256:abc123..." 103 + secretChecksum: "sha256:def456..." 104 + syncStatus: "InSync" | "OutOfSync" | "Error" | "Pending" 105 + lastError: "Error message if any" 106 + conditions: [] # Standard Kubernetes conditions 107 + secretRef: # Reference to created Secret 108 + name: "appname-secret" 109 + namespace: "appnamespace" 110 + ``` 111 + 112 + ## Implementation Strategy 113 + 114 + ### Phase 1: Basic Infrastructure ✅ COMPLETED 115 + - [x] Initialize operator-sdk project structure 116 + - [x] Define HSMSecret CRD with complete spec and status 117 + - [x] Implement HSM client wrapper interface with PKCS#11 and Mock implementations 118 + - [x] Create controller skeleton with full reconciliation logic 119 + 120 + ### Phase 2: Core Functionality ✅ COMPLETED 121 + - [x] Implement HSM file reading/writing via client interface 122 + - [x] Add Kubernetes Secret creation/update logic with owner references 123 + - [x] Build complete reconciliation loop with finalizers 124 + - [x] Add comprehensive error handling and logging 125 + 126 + ### Phase 3: Bidirectional Sync ✅ COMPLETED 127 + - [x] Implement HSM sync via configurable polling intervals 128 + - [x] Add SHA256 checksum-based change detection 129 + - [x] Handle conflict resolution through status reporting 130 + - [x] Add detailed status reporting with conditions and timestamps 131 + 132 + ### Phase 4: Secret Management Operations 🚧 IN PROGRESS 133 + - [x] Import existing HSM secrets through HSMSecret CRDs 134 + - [ ] Secret editing interface (kubectl plugin or annotations) 135 + - [x] Secret deletion with proper cleanup via finalizers 136 + - [ ] Bulk operations support 137 + 138 + ### Phase 5: USB Device Discovery ✅ COMPLETED 139 + - [x] HSMDevice CRD for representing discovered HSM hardware 140 + - [x] USB device discovery logic with sysfs scanning 141 + - [x] Path-based device discovery with glob patterns 142 + - [x] DaemonSet controller for node-level device scanning 143 + - [x] Device plugin integration for Kubernetes resource allocation 144 + - [x] Well-known HSM device specifications (Pico HSM, SmartCard-HSM) 145 + - [x] Auto-discovery based on device types 146 + 147 + ## ✅ Current Implementation Status 148 + 149 + ### Completed Components 150 + 151 + 1. **HSMSecret CRD** (`api/v1alpha1/hsmsecret_types.go`) 152 + - Complete API definition with all fields and validation 153 + - Custom printer columns for `kubectl get hsmsecret` 154 + - Short name support (`hsmsec`) 155 + - Comprehensive status tracking with checksums and conditions 156 + 157 + 2. **HSM Client Architecture** (`internal/hsm/`) 158 + - **Client Interface**: Flexible interface supporting multiple HSM implementations 159 + - **Mock Client**: Full testing implementation with pre-populated test secrets 160 + - **PKCS#11 Client**: Production-ready skeleton for real Pico HSM integration 161 + - **Checksum System**: SHA256 checksums for data integrity verification 162 + 163 + 3. **Controller Implementation** (`internal/controller/hsmsecret_controller.go`) 164 + - Complete reconciliation loop with error handling 165 + - Bidirectional sync between HSM and Kubernetes Secrets 166 + - Finalizer-based cleanup on HSMSecret deletion 167 + - Auto-sync with configurable intervals (default: 300s) 168 + - Owner references for proper garbage collection 169 + - Status updates with detailed condition reporting 170 + 171 + 4. **USB Device Discovery & Mirroring** (`internal/discovery/`) 172 + - **USB Discoverer**: Scans sysfs for USB devices matching vendor/product IDs 173 + - **Path Discoverer**: Glob-based device path discovery (e.g., /dev/ttyUSB*) 174 + - **Device Manager**: Kubernetes resource allocation and device management 175 + - **Mirroring Manager**: Cross-node HSM device synchronization and failover 176 + - **Well-known Specs**: Built-in USB specifications for Pico HSM and SmartCard-HSM 177 + - **Topology Manager**: Primary/mirror device role assignment and health monitoring 178 + 179 + 5. **HSMDevice CRD** (`api/v1alpha1/hsmdevice_types.go`) 180 + - Complete device discovery specification with USB and path-based options 181 + - Auto-discovery based on device types with well-known specifications 182 + - Mirroring policies for cross-node high availability 183 + - Node selector support for targeted discovery 184 + - Comprehensive status tracking with discovered device details 185 + - Custom printer columns for `kubectl get hsmdevice` 186 + 187 + 6. **REST API Server** (`internal/api/`) 188 + - **Gin HTTP Server**: Complete REST API with all CRUD operations 189 + - **Secret Management**: Create, read, update, delete HSM secrets via HTTP 190 + - **Bulk Operations**: Import/export multiple secrets with JSON payloads 191 + - **Health & Metrics**: System health checks and operational metrics 192 + - **Error Handling**: Comprehensive error responses with detailed messages 193 + 194 + 6. **Production Features** 195 + - ✅ All unit tests passing 196 + - ✅ Docker image builds successfully 197 + - ✅ CRDs and RBAC manifests auto-generated 198 + - ✅ Sample HSMSecret and HSMDevice configurations provided 199 + - ✅ DaemonSet configuration for node-level device discovery 200 + - ✅ Proper RBAC permissions for Secrets, Events, and Device Discovery 201 + - ✅ Comprehensive logging and error handling 202 + 203 + ### Ready for Deployment 204 + 205 + The operator can be immediately deployed and tested: 206 + 207 + ```bash 208 + # Build and deploy 209 + make docker-build IMG=hsm-secrets-operator:latest 210 + make deploy IMG=hsm-secrets-operator:latest 211 + 212 + # Deploy device discovery (optional - for USB HSM auto-discovery) 213 + kubectl apply -f config/samples/daemonset.yaml 214 + 215 + # Test with sample resources 216 + kubectl apply -f config/samples/hsm_v1alpha1_hsmsecret.yaml 217 + kubectl apply -f config/samples/hsm_v1alpha1_hsmdevice.yaml 218 + 219 + # Monitor status 220 + kubectl get hsmsecret -w 221 + kubectl get hsmdevice -w 222 + kubectl get secrets 223 + ``` 224 + 225 + ### Complete Files Structure 226 + ``` 227 + ├── api/v1alpha1/ 228 + │ ├── hsmsecret_types.go # HSMSecret CRD with mirroring support 229 + │ ├── hsmdevice_types.go # HSMDevice CRD with USB discovery 230 + │ └── groupversion_info.go # API group metadata 231 + ├── internal/ 232 + │ ├── controller/ 233 + │ │ ├── hsmsecret_controller.go # Secret reconciliation with fallback 234 + │ │ └── hsmdevice_controller.go # Device discovery and mirroring 235 + │ ├── discovery/ 236 + │ │ ├── usb.go # USB device discovery 237 + │ │ ├── mirroring.go # Cross-node device mirroring 238 + │ │ └── deviceplugin.go # Kubernetes device management 239 + │ ├── hsm/ 240 + │ │ ├── client.go # HSM client interface 241 + │ │ ├── mock_client.go # Full test implementation 242 + │ │ └── pkcs11_client.go # Production PKCS#11 client 243 + │ └── api/ 244 + │ ├── server.go # REST API server with Gin 245 + │ ├── handlers.go # HTTP request handlers 246 + │ └── middleware.go # API middleware 247 + ├── examples/ 248 + │ ├── basic/ # Basic usage examples 249 + │ ├── advanced/ # Advanced configurations 250 + │ │ ├── talos-deployment.yaml # Talos Linux deployment 251 + │ │ ├── talos-build-guide.md # Talos setup guide 252 + │ │ └── custom-library-guide.md # PKCS#11 library integration 253 + │ └── api/ # API usage examples 254 + │ ├── bulk-operations.sh # Basic bulk operations 255 + │ ├── advanced-bulk-import.sh # Advanced bulk import 256 + │ ├── direct-import-examples.sh # Direct API examples 257 + │ ├── production-import.json # Sample production config 258 + │ └── bulk-secrets.json # Sample bulk config 259 + ├── scripts/ 260 + │ └── build-talos.sh # Talos Linux build automation 261 + ├── deploy/ 262 + │ └── talos/ # Talos-specific manifests 263 + ├── config/ 264 + │ ├── crd/bases/ # Generated CRD manifests 265 + │ ├── rbac/ # Generated RBAC rules 266 + │ └── samples/ # Sample resources 267 + ├── Dockerfile # Standard operator image 268 + ├── Dockerfile.talos # Talos-optimized image 269 + └── cmd/main.go # Main operator entry point 270 + ``` 271 + 272 + ## Technical Requirements 273 + 274 + ### Dependencies 275 + - **operator-sdk**: For scaffolding and building the operator 276 + - **controller-runtime**: Kubernetes controller framework 277 + - **PKCS#11 library**: For HSM communication (sc-hsm-embedded) 278 + - **OpenSC**: PKCS#11 middleware for smart cards/HSMs 279 + 280 + ### HSM Integration 281 + - Use PKCS#11 interface for Pico HSM communication 282 + - Handle HSM authentication and session management 283 + - Implement secure key storage and retrieval 284 + - Support HSM-specific error handling 285 + 286 + ### Kubernetes Integration 287 + - Standard Secret object management 288 + - RBAC for Secret read/write operations 289 + - Event generation for audit trails 290 + - Finalizers for cleanup on deletion 291 + 292 + ## Development Environment 293 + 294 + ### Container Setup 295 + The Dockerfile builds an Alpine-based environment with: 296 + - OpenSC development libraries 297 + - PCSC-Lite for smart card communication 298 + - sc-hsm-embedded library compilation 299 + - USB device support 300 + 301 + ### Testing Strategy 302 + - Unit tests for HSM client wrapper 303 + - Integration tests with mock HSM 304 + - End-to-end tests with real Pico HSM device 305 + - Chaos testing for sync reliability 306 + 307 + ## Security Considerations 308 + 309 + ### HSM Security 310 + - Private keys never leave the HSM 311 + - All cryptographic operations performed on-device 312 + - Hardware-based random number generation 313 + - Tamper resistance and secure storage 314 + 315 + ### Kubernetes Security 316 + - Principle of least privilege RBAC 317 + - Secret encryption at rest (etcd) 318 + - Network policies for HSM access 319 + - Audit logging for all operations 320 + 321 + ### Operational Security 322 + - HSM authentication management 323 + - Certificate lifecycle management 324 + - Backup and recovery procedures 325 + - Key rotation strategies 326 + 327 + ## Monitoring and Observability 328 + 329 + ### Metrics 330 + - Secret sync success/failure rates 331 + - HSM operation latencies 332 + - Secret object count and status 333 + - Error rates by type 334 + 335 + ### Logging 336 + - Structured logging with correlation IDs 337 + - HSM operation audit trail 338 + - Secret lifecycle events 339 + - Performance metrics 340 + 341 + ### Alerting 342 + - HSM connectivity issues 343 + - Sync failures or conflicts 344 + - Authentication failures 345 + - Hardware errors 346 + 347 + ## Future Enhancements 348 + 349 + ### Advanced Features 350 + - Multi-HSM support for high availability 351 + - Cross-cluster secret replication 352 + - Secret versioning and rollback 353 + - Automated key rotation 354 + 355 + ### Integration Opportunities 356 + - ArgoCD/GitOps integration 357 + - Vault operator compatibility 358 + - Service mesh certificate management 359 + - CI/CD pipeline integration 360 + 361 + ## Getting Started 362 + 363 + ### Prerequisites 364 + - Kubernetes cluster (v1.20+) 365 + - Pico HSM device with configured partitions 366 + - operator-sdk CLI tool 367 + - kubectl access with appropriate RBAC 368 + 369 + ### Quick Start 370 + ```bash 371 + # Initialize operator project 372 + operator-sdk init --domain=security --repo=github.com/evanjarrett/hsm-secrets-operator 373 + 374 + # Create CRD 375 + operator-sdk create api --group=hsm --version=v1alpha1 --kind=HSMSecret 376 + 377 + # Build and deploy 378 + make docker-build docker-push IMG=<registry>/hsm-secrets-operator:latest 379 + make deploy IMG=<registry>/hsm-secrets-operator:latest 380 + ``` 381 + 382 + ### Example Usage 383 + ```yaml 384 + # Create HSMSecret resource 385 + apiVersion: hsm.j5t.io/v1alpha1 386 + kind: HSMSecret 387 + metadata: 388 + name: database-credentials 389 + namespace: production 390 + spec: 391 + hsmPath: "secrets/production/database-credentials" 392 + secretName: "database-credentials" 393 + autoSync: true 394 + syncInterval: 60 395 + secretType: Opaque 396 + ``` 397 + 398 + ### Monitoring Operations 399 + ```bash 400 + # View HSMSecret status with custom columns 401 + kubectl get hsmsecret 402 + kubectl get hsmsec # Using short name 403 + 404 + # View HSMDevice status with custom columns 405 + kubectl get hsmdevice 406 + kubectl get hsmdev # Using short name 407 + 408 + # Describe for detailed information 409 + kubectl describe hsmsecret database-credentials 410 + kubectl describe hsmdevice pico-hsm-discovery 411 + 412 + # Check created secrets 413 + kubectl get secrets -l managed-by=hsm-secrets-operator 414 + 415 + # Monitor sync and discovery status 416 + kubectl get hsmsecret database-credentials -o jsonpath='{.status.syncStatus}' 417 + kubectl get hsmdevice pico-hsm-discovery -o jsonpath='{.status.phase}' 418 + 419 + # View discovered devices 420 + kubectl get hsmdevice pico-hsm-discovery -o jsonpath='{.status.discoveredDevices[*].devicePath}' 421 + ``` 422 + 423 + This operator design provides a secure, hardware-backed secret management solution that integrates seamlessly with Kubernetes while maintaining the security benefits of HSM-based storage.
+62
Dockerfile
··· 1 + # Build the manager binary 2 + FROM golang:1.24 AS builder 3 + ARG TARGETOS 4 + ARG TARGETARCH 5 + 6 + WORKDIR /workspace 7 + # Copy the Go Modules manifests 8 + COPY go.mod go.mod 9 + COPY go.sum go.sum 10 + # cache deps before building and copying source so that we don't need to re-download as much 11 + # and so that source changes don't invalidate our downloaded layer 12 + RUN go mod download 13 + 14 + # Copy the go source 15 + COPY cmd/main.go cmd/main.go 16 + COPY api/ api/ 17 + COPY internal/ internal/ 18 + 19 + # Build 20 + # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 + # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 + # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 + # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 + RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 + 26 + FROM alpine:3.22 AS base 27 + 28 + # Update Alpine packages 29 + RUN apk update 30 + 31 + # Install compilation tools 32 + RUN apk add --no-cache \ 33 + git \ 34 + gcc \ 35 + g++ \ 36 + make \ 37 + cmake \ 38 + pkgconfig \ 39 + openssl-dev \ 40 + pcsc-lite-dev \ 41 + libusb-dev \ 42 + autoconf \ 43 + automake \ 44 + libtool 45 + 46 + RUN cd / && git clone https://github.com/CardContact/sc-hsm-embedded.git 47 + WORKDIR /sc-hsm-embedded 48 + RUN autoreconf -fi && ./configure 49 + RUN make && make install 50 + 51 + FROM alpine:3.22 52 + RUN apk add --no-cache opensc-dev ccid pcsc-lite openssl libtool libusb 53 + 54 + COPY --from=base /usr/lib/libssl.so* /usr/lib/ 55 + COPY --from=base /usr/lib/libcrypto.so* /usr/lib/ 56 + COPY --from=base /usr/local/ /usr/local/ 57 + 58 + WORKDIR / 59 + COPY --from=builder /workspace/manager . 60 + USER 65532:65532 61 + 62 + ENTRYPOINT ["/manager"]
+128
Dockerfile.talos
··· 1 + # Multi-stage Dockerfile for HSM Secrets Operator with PKCS#11 libraries 2 + # Optimized for Talos Linux deployments 3 + 4 + # Stage 1: Build PKCS#11 libraries 5 + FROM alpine:3.18 AS pkcs11-builder 6 + 7 + # Install build dependencies 8 + RUN apk add --no-cache \ 9 + wget \ 10 + tar \ 11 + gzip \ 12 + build-base \ 13 + autoconf \ 14 + automake \ 15 + libtool \ 16 + pkgconfig \ 17 + openssl-dev \ 18 + libusb-dev \ 19 + pcsc-lite-dev \ 20 + flex \ 21 + help2man 22 + 23 + # Build OpenSC (most common PKCS#11 library) 24 + ENV OPENSC_VERSION=0.24.0 25 + RUN wget https://github.com/OpenSC/OpenSC/releases/download/${OPENSC_VERSION}/opensc-${OPENSC_VERSION}.tar.gz && \ 26 + tar -xzf opensc-${OPENSC_VERSION}.tar.gz && \ 27 + cd opensc-${OPENSC_VERSION} && \ 28 + ./configure \ 29 + --prefix=/usr/local \ 30 + --enable-pcsc \ 31 + --enable-openssl \ 32 + --disable-static \ 33 + --enable-shared && \ 34 + make -j$(nproc) && \ 35 + make install 36 + 37 + # Build YubiKey PKCS#11 library (optional but common) 38 + ENV YUBICO_PIV_VERSION=2.4.0 39 + RUN wget https://developers.yubico.com/yubico-piv-tool/Releases/yubico-piv-tool-${YUBICO_PIV_VERSION}.tar.gz && \ 40 + tar -xzf yubico-piv-tool-${YUBICO_PIV_VERSION}.tar.gz && \ 41 + cd yubico-piv-tool-${YUBICO_PIV_VERSION} && \ 42 + ./configure --prefix=/usr/local && \ 43 + make -j$(nproc) && \ 44 + make install 45 + 46 + # Build SoftHSM (useful for testing and dev environments) 47 + ENV SOFTHSM_VERSION=2.6.1 48 + RUN wget https://dist.opendnssec.org/source/softhsm-${SOFTHSM_VERSION}.tar.gz && \ 49 + tar -xzf softhsm-${SOFTHSM_VERSION}.tar.gz && \ 50 + cd softhsm-${SOFTHSM_VERSION} && \ 51 + ./configure --prefix=/usr/local && \ 52 + make -j$(nproc) && \ 53 + make install 54 + 55 + # Organize libraries for runtime stage 56 + RUN mkdir -p /pkcs11-libs && \ 57 + cp /usr/local/lib/pkcs11/*.so /pkcs11-libs/ && \ 58 + cp /usr/local/lib/libykcs11*.so /pkcs11-libs/ 2>/dev/null || true && \ 59 + cp /usr/local/lib/libsofthsm2.so /pkcs11-libs/ 2>/dev/null || true && \ 60 + chmod 755 /pkcs11-libs/*.so 61 + 62 + # Create library configuration files 63 + RUN mkdir -p /pkcs11-config 64 + COPY <<EOF /pkcs11-config/opensc.conf 65 + # OpenSC Configuration for HSM Secrets Operator 66 + app default { 67 + card_drivers = piv, openpgp, sc-hsm; 68 + reader_drivers = pcsc, openct; 69 + } 70 + EOF 71 + 72 + # Stage 2: Build Go application 73 + FROM golang:1.21-alpine AS go-builder 74 + 75 + ARG TARGETOS 76 + ARG TARGETARCH 77 + 78 + WORKDIR /workspace 79 + 80 + # Copy the Go Modules manifests 81 + COPY go.mod go.mod 82 + COPY go.sum go.sum 83 + 84 + # Cache deps before building and copying source so that we don't need to re-download as much 85 + # and so that source changes don't invalidate our downloaded layer 86 + RUN go mod download 87 + 88 + # Copy the go source 89 + COPY cmd/main.go cmd/main.go 90 + COPY api/ api/ 91 + COPY internal/ internal/ 92 + 93 + # Build 94 + # the GOARCH has not a default value to allow the binary be built according to the host where the command 95 + # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 96 + # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 97 + # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 98 + RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 99 + 100 + # Stage 3: Runtime image optimized for Talos 101 + FROM gcr.io/distroless/static:nonroot 102 + 103 + # Labels for image metadata 104 + LABEL org.opencontainers.image.title="HSM Secrets Operator for Talos" 105 + LABEL org.opencontainers.image.description="Kubernetes operator for managing HSM-backed secrets on Talos Linux" 106 + LABEL org.opencontainers.image.vendor="HSM Secrets Operator" 107 + LABEL org.opencontainers.image.licenses="Apache-2.0" 108 + 109 + # Copy PKCS#11 libraries from builder stage 110 + COPY --from=pkcs11-builder /pkcs11-libs/* /usr/local/lib/pkcs11/ 111 + COPY --from=pkcs11-builder /pkcs11-config/* /etc/pkcs11/ 112 + 113 + # Copy runtime dependencies (minimal) 114 + COPY --from=pkcs11-builder /usr/local/lib/libopensc.so* /usr/local/lib/ 115 + COPY --from=pkcs11-builder /usr/local/lib/libykcs11.so* /usr/local/lib/ 116 + COPY --from=pkcs11-builder /usr/local/lib/libsofthsm2.so* /usr/local/lib/ 117 + 118 + # Copy the manager binary 119 + COPY --from=go-builder /workspace/manager . 120 + 121 + # Set library path for PKCS#11 libraries 122 + ENV LD_LIBRARY_PATH="/usr/local/lib/pkcs11:/usr/local/lib" 123 + ENV PKCS11_MODULE_PATH="/usr/local/lib/pkcs11" 124 + ENV OPENSC_CONF="/etc/pkcs11/opensc.conf" 125 + 126 + USER 65532:65532 127 + 128 + ENTRYPOINT ["/manager"]
+362
Makefile
··· 1 + # VERSION defines the project version for the bundle. 2 + # Update this value when you upgrade the version of your project. 3 + # To re-generate a bundle for another specific version without changing the standard setup, you can: 4 + # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) 5 + # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) 6 + VERSION ?= 0.0.1 7 + 8 + # CHANNELS define the bundle channels used in the bundle. 9 + # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") 10 + # To re-generate a bundle for other specific channels without changing the standard setup, you can: 11 + # - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) 12 + # - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") 13 + ifneq ($(origin CHANNELS), undefined) 14 + BUNDLE_CHANNELS := --channels=$(CHANNELS) 15 + endif 16 + 17 + # DEFAULT_CHANNEL defines the default channel used in the bundle. 18 + # Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") 19 + # To re-generate a bundle for any other default channel without changing the default setup, you can: 20 + # - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) 21 + # - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") 22 + ifneq ($(origin DEFAULT_CHANNEL), undefined) 23 + BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 24 + endif 25 + BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 26 + 27 + # IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. 28 + # This variable is used to construct full image tags for bundle and catalog images. 29 + # 30 + # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both 31 + # j5t.io/hsm-secrets-operator-bundle:$VERSION and j5t.io/hsm-secrets-operator-catalog:$VERSION. 32 + IMAGE_TAG_BASE ?= j5t.io/hsm-secrets-operator 33 + 34 + # BUNDLE_IMG defines the image:tag used for the bundle. 35 + # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>) 36 + BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) 37 + 38 + # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command 39 + BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 40 + 41 + # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests 42 + # You can enable this value if you would like to use SHA Based Digests 43 + # To enable set flag to true 44 + USE_IMAGE_DIGESTS ?= false 45 + ifeq ($(USE_IMAGE_DIGESTS), true) 46 + BUNDLE_GEN_FLAGS += --use-image-digests 47 + endif 48 + 49 + # Set the Operator SDK version to use. By default, what is installed on the system is used. 50 + # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. 51 + OPERATOR_SDK_VERSION ?= v1.41.1 52 + # Image URL to use all building/pushing image targets 53 + IMG ?= controller:latest 54 + 55 + # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 56 + ifeq (,$(shell go env GOBIN)) 57 + GOBIN=$(shell go env GOPATH)/bin 58 + else 59 + GOBIN=$(shell go env GOBIN) 60 + endif 61 + 62 + # CONTAINER_TOOL defines the container tool to be used for building images. 63 + # Be aware that the target commands are only tested with Docker which is 64 + # scaffolded by default. However, you might want to replace it to use other 65 + # tools. (i.e. podman) 66 + CONTAINER_TOOL ?= docker 67 + 68 + # Setting SHELL to bash allows bash commands to be executed by recipes. 69 + # Options are set to exit when a recipe line exits non-zero or a piped command fails. 70 + SHELL = /usr/bin/env bash -o pipefail 71 + .SHELLFLAGS = -ec 72 + 73 + .PHONY: all 74 + all: build 75 + 76 + ##@ General 77 + 78 + # The help target prints out all targets with their descriptions organized 79 + # beneath their categories. The categories are represented by '##@' and the 80 + # target descriptions by '##'. The awk command is responsible for reading the 81 + # entire set of makefiles included in this invocation, looking for lines of the 82 + # file as xyz: ## something, and then pretty-format the target and help. Then, 83 + # if there's a line with ##@ something, that gets pretty-printed as a category. 84 + # More info on the usage of ANSI control characters for terminal formatting: 85 + # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 86 + # More info on the awk command: 87 + # http://linuxcommand.org/lc3_adv_awk.php 88 + 89 + .PHONY: help 90 + help: ## Display this help. 91 + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 92 + 93 + ##@ Development 94 + 95 + .PHONY: manifests 96 + manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 97 + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 98 + 99 + .PHONY: generate 100 + generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 101 + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 102 + 103 + .PHONY: fmt 104 + fmt: ## Run go fmt against code. 105 + go fmt ./... 106 + 107 + .PHONY: vet 108 + vet: ## Run go vet against code. 109 + go vet ./... 110 + 111 + .PHONY: test 112 + test: manifests generate fmt vet setup-envtest ## Run tests. 113 + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out 114 + 115 + # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. 116 + # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. 117 + # CertManager is installed by default; skip with: 118 + # - CERT_MANAGER_INSTALL_SKIP=true 119 + KIND_CLUSTER ?= hsm-secrets-operator-test-e2e 120 + 121 + .PHONY: setup-test-e2e 122 + setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist 123 + @command -v $(KIND) >/dev/null 2>&1 || { \ 124 + echo "Kind is not installed. Please install Kind manually."; \ 125 + exit 1; \ 126 + } 127 + @case "$$($(KIND) get clusters)" in \ 128 + *"$(KIND_CLUSTER)"*) \ 129 + echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ 130 + *) \ 131 + echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ 132 + $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ 133 + esac 134 + 135 + .PHONY: test-e2e 136 + test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. 137 + KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v 138 + $(MAKE) cleanup-test-e2e 139 + 140 + .PHONY: cleanup-test-e2e 141 + cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests 142 + @$(KIND) delete cluster --name $(KIND_CLUSTER) 143 + 144 + .PHONY: lint 145 + lint: golangci-lint ## Run golangci-lint linter 146 + $(GOLANGCI_LINT) run 147 + 148 + .PHONY: lint-fix 149 + lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes 150 + $(GOLANGCI_LINT) run --fix 151 + 152 + .PHONY: lint-config 153 + lint-config: golangci-lint ## Verify golangci-lint linter configuration 154 + $(GOLANGCI_LINT) config verify 155 + 156 + ##@ Build 157 + 158 + .PHONY: build 159 + build: manifests generate fmt vet ## Build manager binary. 160 + go build -o bin/manager cmd/main.go 161 + 162 + .PHONY: run 163 + run: manifests generate fmt vet ## Run a controller from your host. 164 + go run ./cmd/main.go 165 + 166 + # If you wish to build the manager image targeting other platforms you can use the --platform flag. 167 + # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. 168 + # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 169 + .PHONY: docker-build 170 + docker-build: ## Build docker image with the manager. 171 + $(CONTAINER_TOOL) build -t ${IMG} . 172 + 173 + .PHONY: docker-push 174 + docker-push: ## Push docker image with the manager. 175 + $(CONTAINER_TOOL) push ${IMG} 176 + 177 + # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple 178 + # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 179 + # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ 180 + # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 181 + # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail) 182 + # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. 183 + PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 184 + .PHONY: docker-buildx 185 + docker-buildx: ## Build and push docker image for the manager for cross-platform support 186 + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 187 + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 188 + - $(CONTAINER_TOOL) buildx create --name hsm-secrets-operator-builder 189 + $(CONTAINER_TOOL) buildx use hsm-secrets-operator-builder 190 + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 191 + - $(CONTAINER_TOOL) buildx rm hsm-secrets-operator-builder 192 + rm Dockerfile.cross 193 + 194 + .PHONY: build-installer 195 + build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. 196 + mkdir -p dist 197 + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 198 + $(KUSTOMIZE) build config/default > dist/install.yaml 199 + 200 + ##@ Deployment 201 + 202 + ifndef ignore-not-found 203 + ignore-not-found = false 204 + endif 205 + 206 + .PHONY: install 207 + install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 208 + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 209 + 210 + .PHONY: uninstall 211 + uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 212 + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 213 + 214 + .PHONY: deploy 215 + deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 216 + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 217 + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 218 + 219 + .PHONY: undeploy 220 + undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 221 + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 222 + 223 + ##@ Dependencies 224 + 225 + ## Location to install dependencies to 226 + LOCALBIN ?= $(shell pwd)/bin 227 + $(LOCALBIN): 228 + mkdir -p $(LOCALBIN) 229 + 230 + ## Tool Binaries 231 + KUBECTL ?= kubectl 232 + KIND ?= kind 233 + KUSTOMIZE ?= $(LOCALBIN)/kustomize 234 + CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 235 + ENVTEST ?= $(LOCALBIN)/setup-envtest 236 + GOLANGCI_LINT = $(LOCALBIN)/golangci-lint 237 + 238 + ## Tool Versions 239 + KUSTOMIZE_VERSION ?= v5.6.0 240 + CONTROLLER_TOOLS_VERSION ?= v0.18.0 241 + #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) 242 + ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') 243 + #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) 244 + ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') 245 + GOLANGCI_LINT_VERSION ?= v2.1.0 246 + 247 + .PHONY: kustomize 248 + kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 249 + $(KUSTOMIZE): $(LOCALBIN) 250 + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 251 + 252 + .PHONY: controller-gen 253 + controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 254 + $(CONTROLLER_GEN): $(LOCALBIN) 255 + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 256 + 257 + .PHONY: setup-envtest 258 + setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. 259 + @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." 260 + @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ 261 + echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ 262 + exit 1; \ 263 + } 264 + 265 + .PHONY: envtest 266 + envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 267 + $(ENVTEST): $(LOCALBIN) 268 + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 269 + 270 + .PHONY: golangci-lint 271 + golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 272 + $(GOLANGCI_LINT): $(LOCALBIN) 273 + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) 274 + 275 + # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 276 + # $1 - target path with name of binary 277 + # $2 - package url which can be installed 278 + # $3 - specific version of package 279 + define go-install-tool 280 + @[ -f "$(1)-$(3)" ] || { \ 281 + set -e; \ 282 + package=$(2)@$(3) ;\ 283 + echo "Downloading $${package}" ;\ 284 + rm -f $(1) || true ;\ 285 + GOBIN=$(LOCALBIN) go install $${package} ;\ 286 + mv $(1) $(1)-$(3) ;\ 287 + } ;\ 288 + ln -sf $(1)-$(3) $(1) 289 + endef 290 + 291 + .PHONY: operator-sdk 292 + OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk 293 + operator-sdk: ## Download operator-sdk locally if necessary. 294 + ifeq (,$(wildcard $(OPERATOR_SDK))) 295 + ifeq (, $(shell which operator-sdk 2>/dev/null)) 296 + @{ \ 297 + set -e ;\ 298 + mkdir -p $(dir $(OPERATOR_SDK)) ;\ 299 + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 300 + curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ 301 + chmod +x $(OPERATOR_SDK) ;\ 302 + } 303 + else 304 + OPERATOR_SDK = $(shell which operator-sdk) 305 + endif 306 + endif 307 + 308 + .PHONY: bundle 309 + bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. 310 + $(OPERATOR_SDK) generate kustomize manifests -q 311 + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 312 + $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) 313 + $(OPERATOR_SDK) bundle validate ./bundle 314 + 315 + .PHONY: bundle-build 316 + bundle-build: ## Build the bundle image. 317 + $(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 318 + 319 + .PHONY: bundle-push 320 + bundle-push: ## Push the bundle image. 321 + $(MAKE) docker-push IMG=$(BUNDLE_IMG) 322 + 323 + .PHONY: opm 324 + OPM = $(LOCALBIN)/opm 325 + opm: ## Download opm locally if necessary. 326 + ifeq (,$(wildcard $(OPM))) 327 + ifeq (,$(shell which opm 2>/dev/null)) 328 + @{ \ 329 + set -e ;\ 330 + mkdir -p $(dir $(OPM)) ;\ 331 + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ 332 + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.55.0/$${OS}-$${ARCH}-opm ;\ 333 + chmod +x $(OPM) ;\ 334 + } 335 + else 336 + OPM = $(shell which opm) 337 + endif 338 + endif 339 + 340 + # A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). 341 + # These images MUST exist in a registry and be pull-able. 342 + BUNDLE_IMGS ?= $(BUNDLE_IMG) 343 + 344 + # The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). 345 + CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) 346 + 347 + # Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. 348 + ifneq ($(origin CATALOG_BASE_IMG), undefined) 349 + FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) 350 + endif 351 + 352 + # Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. 353 + # This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: 354 + # https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator 355 + .PHONY: catalog-build 356 + catalog-build: opm ## Build a catalog image. 357 + $(OPM) index add --container-tool $(CONTAINER_TOOL) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) 358 + 359 + # Push the catalog image. 360 + .PHONY: catalog-push 361 + catalog-push: ## Push a catalog image. 362 + $(MAKE) docker-push IMG=$(CATALOG_IMG)
+32
PROJECT
··· 1 + # Code generated by tool. DO NOT EDIT. 2 + # This file is used to track the info used to scaffold your project 3 + # and allow the plugins properly work. 4 + # More info: https://book.kubebuilder.io/reference/project-config.html 5 + domain: j5t.io 6 + layout: 7 + - go.kubebuilder.io/v4 8 + plugins: 9 + manifests.sdk.operatorframework.io/v2: {} 10 + scorecard.sdk.operatorframework.io/v2: {} 11 + projectName: hsm-secrets-operator 12 + repo: github.com/evanjarrett/hsm-secrets-operator 13 + resources: 14 + - api: 15 + crdVersion: v1 16 + namespaced: true 17 + controller: true 18 + domain: j5t.io 19 + group: hsm 20 + kind: HSMSecret 21 + path: github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1 22 + version: v1alpha1 23 + - api: 24 + crdVersion: v1 25 + namespaced: true 26 + controller: true 27 + domain: j5t.io 28 + group: hsm 29 + kind: HSMDevice 30 + path: github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1 31 + version: v1alpha1 32 + version: "3"
+135
README.md
··· 1 + # hsm-secrets-operator 2 + // TODO(user): Add simple overview of use/purpose 3 + 4 + ## Description 5 + // TODO(user): An in-depth paragraph about your project and overview of use 6 + 7 + ## Getting Started 8 + 9 + ### Prerequisites 10 + - go version v1.24.0+ 11 + - docker version 17.03+. 12 + - kubectl version v1.11.3+. 13 + - Access to a Kubernetes v1.11.3+ cluster. 14 + 15 + ### To Deploy on the cluster 16 + **Build and push your image to the location specified by `IMG`:** 17 + 18 + ```sh 19 + make docker-build docker-push IMG=<some-registry>/hsm-secrets-operator:tag 20 + ``` 21 + 22 + **NOTE:** This image ought to be published in the personal registry you specified. 23 + And it is required to have access to pull the image from the working environment. 24 + Make sure you have the proper permission to the registry if the above commands don’t work. 25 + 26 + **Install the CRDs into the cluster:** 27 + 28 + ```sh 29 + make install 30 + ``` 31 + 32 + **Deploy the Manager to the cluster with the image specified by `IMG`:** 33 + 34 + ```sh 35 + make deploy IMG=<some-registry>/hsm-secrets-operator:tag 36 + ``` 37 + 38 + > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin 39 + privileges or be logged in as admin. 40 + 41 + **Create instances of your solution** 42 + You can apply the samples (examples) from the config/sample: 43 + 44 + ```sh 45 + kubectl apply -k config/samples/ 46 + ``` 47 + 48 + >**NOTE**: Ensure that the samples has default values to test it out. 49 + 50 + ### To Uninstall 51 + **Delete the instances (CRs) from the cluster:** 52 + 53 + ```sh 54 + kubectl delete -k config/samples/ 55 + ``` 56 + 57 + **Delete the APIs(CRDs) from the cluster:** 58 + 59 + ```sh 60 + make uninstall 61 + ``` 62 + 63 + **UnDeploy the controller from the cluster:** 64 + 65 + ```sh 66 + make undeploy 67 + ``` 68 + 69 + ## Project Distribution 70 + 71 + Following the options to release and provide this solution to the users. 72 + 73 + ### By providing a bundle with all YAML files 74 + 75 + 1. Build the installer for the image built and published in the registry: 76 + 77 + ```sh 78 + make build-installer IMG=<some-registry>/hsm-secrets-operator:tag 79 + ``` 80 + 81 + **NOTE:** The makefile target mentioned above generates an 'install.yaml' 82 + file in the dist directory. This file contains all the resources built 83 + with Kustomize, which are necessary to install this project without its 84 + dependencies. 85 + 86 + 2. Using the installer 87 + 88 + Users can just run 'kubectl apply -f <URL for YAML BUNDLE>' to install 89 + the project, i.e.: 90 + 91 + ```sh 92 + kubectl apply -f https://raw.githubusercontent.com/<org>/hsm-secrets-operator/<tag or branch>/dist/install.yaml 93 + ``` 94 + 95 + ### By providing a Helm Chart 96 + 97 + 1. Build the chart using the optional helm plugin 98 + 99 + ```sh 100 + operator-sdk edit --plugins=helm/v1-alpha 101 + ``` 102 + 103 + 2. See that a chart was generated under 'dist/chart', and users 104 + can obtain this solution from there. 105 + 106 + **NOTE:** If you change the project, you need to update the Helm Chart 107 + using the same command above to sync the latest changes. Furthermore, 108 + if you create webhooks, you need to use the above command with 109 + the '--force' flag and manually ensure that any custom configuration 110 + previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' 111 + is manually re-applied afterwards. 112 + 113 + ## Contributing 114 + // TODO(user): Add detailed information on how you would like others to contribute to this project 115 + 116 + **NOTE:** Run `make help` for more information on all potential `make` targets 117 + 118 + More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 119 + 120 + ## License 121 + 122 + Copyright 2025. 123 + 124 + Licensed under the Apache License, Version 2.0 (the "License"); 125 + you may not use this file except in compliance with the License. 126 + You may obtain a copy of the License at 127 + 128 + http://www.apache.org/licenses/LICENSE-2.0 129 + 130 + Unless required by applicable law or agreed to in writing, software 131 + distributed under the License is distributed on an "AS IS" BASIS, 132 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 133 + See the License for the specific language governing permissions and 134 + limitations under the License. 135 +
+36
api/v1alpha1/groupversion_info.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 v1alpha1 contains API Schema definitions for the hsm v1alpha1 API group. 18 + // +kubebuilder:object:generate=true 19 + // +groupName=hsm.j5t.io 20 + package v1alpha1 21 + 22 + import ( 23 + "k8s.io/apimachinery/pkg/runtime/schema" 24 + "sigs.k8s.io/controller-runtime/pkg/scheme" 25 + ) 26 + 27 + var ( 28 + // GroupVersion is group version used to register these objects. 29 + GroupVersion = schema.GroupVersion{Group: "hsm.j5t.io", Version: "v1alpha1"} 30 + 31 + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 32 + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 + 34 + // AddToScheme adds the types in this group-version to the given scheme. 35 + AddToScheme = SchemeBuilder.AddToScheme 36 + )
+284
api/v1alpha1/hsmdevice_types.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 v1alpha1 18 + 19 + import ( 20 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 + ) 22 + 23 + // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 + // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 + 26 + // USBDeviceSpec defines USB device identification criteria 27 + type USBDeviceSpec struct { 28 + // VendorID is the USB vendor ID (e.g., "20a0" for Pico HSM) 29 + VendorID string `json:"vendorId"` 30 + 31 + // ProductID is the USB product ID (e.g., "4230" for Pico HSM) 32 + ProductID string `json:"productId"` 33 + 34 + // SerialNumber optionally matches a specific device serial number 35 + // +optional 36 + SerialNumber string `json:"serialNumber,omitempty"` 37 + } 38 + 39 + // DevicePathSpec defines device path-based identification 40 + type DevicePathSpec struct { 41 + // Path is the device path pattern (e.g., "/dev/ttyUSB*", "/dev/sc-hsm*") 42 + Path string `json:"path"` 43 + 44 + // Permissions are the required permissions for device access 45 + // +optional 46 + Permissions string `json:"permissions,omitempty"` 47 + } 48 + 49 + // HSMDeviceType represents the type of HSM device 50 + type HSMDeviceType string 51 + 52 + const ( 53 + // HSMDeviceTypePicoHSM represents a Pico HSM device 54 + HSMDeviceTypePicoHSM HSMDeviceType = "PicoHSM" 55 + // HSMDeviceTypeSmartCardHSM represents a SmartCard-HSM 56 + HSMDeviceTypeSmartCardHSM HSMDeviceType = "SmartCardHSM" 57 + // HSMDeviceTypeGeneric represents a generic PKCS#11 device 58 + HSMDeviceTypeGeneric HSMDeviceType = "Generic" 59 + ) 60 + 61 + // MirroringPolicy defines how devices should be mirrored across nodes 62 + type MirroringPolicy string 63 + 64 + const ( 65 + // MirroringPolicyNone disables device mirroring 66 + MirroringPolicyNone MirroringPolicy = "None" 67 + // MirroringPolicyReadOnly enables readonly mirroring across nodes 68 + MirroringPolicyReadOnly MirroringPolicy = "ReadOnly" 69 + // MirroringPolicyActive enables active-active mirroring (future) 70 + MirroringPolicyActive MirroringPolicy = "Active" 71 + ) 72 + 73 + // MirroringSpec defines device mirroring configuration 74 + type MirroringSpec struct { 75 + // Policy specifies the mirroring strategy 76 + // +kubebuilder:default="None" 77 + // +optional 78 + Policy MirroringPolicy `json:"policy,omitempty"` 79 + 80 + // SyncInterval defines how often to sync device data across nodes (in seconds) 81 + // +kubebuilder:default=60 82 + // +optional 83 + SyncInterval int32 `json:"syncInterval,omitempty"` 84 + 85 + // TargetNodes specifies nodes that should have mirrored access 86 + // If empty, mirrors to all nodes with the device 87 + // +optional 88 + TargetNodes []string `json:"targetNodes,omitempty"` 89 + 90 + // PrimaryNode specifies the preferred primary node for write operations 91 + // +optional 92 + PrimaryNode string `json:"primaryNode,omitempty"` 93 + 94 + // AutoFailover enables automatic failover to healthy nodes 95 + // +kubebuilder:default=true 96 + // +optional 97 + AutoFailover bool `json:"autoFailover,omitempty"` 98 + } 99 + 100 + // HSMDeviceSpec defines the desired state of HSMDevice. 101 + type HSMDeviceSpec struct { 102 + // DeviceType specifies the type of HSM device 103 + DeviceType HSMDeviceType `json:"deviceType"` 104 + 105 + // USB defines USB-based device discovery criteria 106 + // +optional 107 + USB *USBDeviceSpec `json:"usb,omitempty"` 108 + 109 + // DevicePath defines path-based device discovery criteria 110 + // +optional 111 + DevicePath *DevicePathSpec `json:"devicePath,omitempty"` 112 + 113 + // NodeSelector specifies which nodes should be scanned for this device 114 + // +optional 115 + NodeSelector map[string]string `json:"nodeSelector,omitempty"` 116 + 117 + // PKCS11LibraryPath is the path to the PKCS#11 library for this device 118 + // +optional 119 + PKCS11LibraryPath string `json:"pkcs11LibraryPath,omitempty"` 120 + 121 + // MaxDevices limits how many instances of this device can be discovered 122 + // +kubebuilder:default=10 123 + // +optional 124 + MaxDevices int32 `json:"maxDevices,omitempty"` 125 + 126 + // Mirroring configures cross-node device mirroring for high availability 127 + // +optional 128 + Mirroring *MirroringSpec `json:"mirroring,omitempty"` 129 + } 130 + 131 + // DeviceRole defines the role of a device in a mirrored setup 132 + type DeviceRole string 133 + 134 + const ( 135 + // DeviceRolePrimary indicates the device is the primary (read-write) 136 + DeviceRolePrimary DeviceRole = "Primary" 137 + // DeviceRoleReadOnly indicates the device is a readonly mirror 138 + DeviceRoleReadOnly DeviceRole = "ReadOnly" 139 + // DeviceRoleStandby indicates the device is available for failover 140 + DeviceRoleStandby DeviceRole = "Standby" 141 + ) 142 + 143 + // DiscoveredDevice represents a discovered HSM device instance 144 + type DiscoveredDevice struct { 145 + // DevicePath is the system path to the discovered device 146 + DevicePath string `json:"devicePath"` 147 + 148 + // SerialNumber is the serial number of the device (if available) 149 + // +optional 150 + SerialNumber string `json:"serialNumber,omitempty"` 151 + 152 + // NodeName is the name of the node where the device was discovered 153 + NodeName string `json:"nodeName"` 154 + 155 + // LastSeen is the timestamp when the device was last detected 156 + LastSeen metav1.Time `json:"lastSeen"` 157 + 158 + // DeviceInfo contains additional device information 159 + // +optional 160 + DeviceInfo map[string]string `json:"deviceInfo,omitempty"` 161 + 162 + // Available indicates if the device is currently available for use 163 + Available bool `json:"available"` 164 + 165 + // ResourceName is the Kubernetes resource name for this device 166 + // +optional 167 + ResourceName string `json:"resourceName,omitempty"` 168 + 169 + // Role indicates the role of this device in a mirrored setup 170 + // +optional 171 + Role DeviceRole `json:"role,omitempty"` 172 + 173 + // MirroredFrom indicates the primary device this is mirrored from 174 + // +optional 175 + MirroredFrom string `json:"mirroredFrom,omitempty"` 176 + 177 + // LastSyncTime is when this device was last synchronized 178 + // +optional 179 + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` 180 + 181 + // Health represents the health status of the device 182 + // +optional 183 + Health string `json:"health,omitempty"` 184 + } 185 + 186 + // MirroringStatus represents the status of device mirroring 187 + type MirroringStatus struct { 188 + // Enabled indicates if mirroring is currently active 189 + Enabled bool `json:"enabled"` 190 + 191 + // PrimaryNode is the current primary node 192 + // +optional 193 + PrimaryNode string `json:"primaryNode,omitempty"` 194 + 195 + // MirroredNodes lists nodes with mirrored access 196 + // +optional 197 + MirroredNodes []string `json:"mirroredNodes,omitempty"` 198 + 199 + // LastSyncTime is when devices were last synchronized 200 + // +optional 201 + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` 202 + 203 + // FailoverCount tracks the number of failovers that have occurred 204 + FailoverCount int32 `json:"failoverCount"` 205 + 206 + // SyncErrors tracks synchronization errors 207 + // +optional 208 + SyncErrors []string `json:"syncErrors,omitempty"` 209 + } 210 + 211 + // HSMDeviceStatus defines the observed state of HSMDevice. 212 + type HSMDeviceStatus struct { 213 + // DiscoveredDevices lists all discovered devices matching the spec 214 + // +optional 215 + DiscoveredDevices []DiscoveredDevice `json:"discoveredDevices,omitempty"` 216 + 217 + // TotalDevices is the total number of discovered devices 218 + TotalDevices int32 `json:"totalDevices"` 219 + 220 + // AvailableDevices is the number of currently available devices 221 + AvailableDevices int32 `json:"availableDevices"` 222 + 223 + // LastDiscoveryTime is the timestamp of the last discovery scan 224 + // +optional 225 + LastDiscoveryTime *metav1.Time `json:"lastDiscoveryTime,omitempty"` 226 + 227 + // Conditions represent the latest available observations of the device state 228 + // +optional 229 + Conditions []metav1.Condition `json:"conditions,omitempty"` 230 + 231 + // Phase represents the current phase of device discovery 232 + // +optional 233 + Phase HSMDevicePhase `json:"phase,omitempty"` 234 + 235 + // Mirroring represents the status of device mirroring 236 + // +optional 237 + Mirroring *MirroringStatus `json:"mirroring,omitempty"` 238 + } 239 + 240 + // HSMDevicePhase represents the current phase of device discovery 241 + type HSMDevicePhase string 242 + 243 + const ( 244 + // HSMDevicePhasePending indicates discovery is not yet started 245 + HSMDevicePhasePending HSMDevicePhase = "Pending" 246 + // HSMDevicePhaseDiscovering indicates discovery is in progress 247 + HSMDevicePhaseDiscovering HSMDevicePhase = "Discovering" 248 + // HSMDevicePhaseReady indicates devices have been discovered and are ready 249 + HSMDevicePhaseReady HSMDevicePhase = "Ready" 250 + // HSMDevicePhaseError indicates an error occurred during discovery 251 + HSMDevicePhaseError HSMDevicePhase = "Error" 252 + ) 253 + 254 + // +kubebuilder:object:root=true 255 + // +kubebuilder:subresource:status 256 + // +kubebuilder:resource:shortName=hsmdev 257 + // +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.deviceType` 258 + // +kubebuilder:printcolumn:name="Total",type=integer,JSONPath=`.status.totalDevices` 259 + // +kubebuilder:printcolumn:name="Available",type=integer,JSONPath=`.status.availableDevices` 260 + // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` 261 + // +kubebuilder:printcolumn:name="Last Discovery",type=date,JSONPath=`.status.lastDiscoveryTime` 262 + // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` 263 + 264 + // HSMDevice is the Schema for the hsmdevices API. 265 + type HSMDevice struct { 266 + metav1.TypeMeta `json:",inline"` 267 + metav1.ObjectMeta `json:"metadata,omitempty"` 268 + 269 + Spec HSMDeviceSpec `json:"spec,omitempty"` 270 + Status HSMDeviceStatus `json:"status,omitempty"` 271 + } 272 + 273 + // +kubebuilder:object:root=true 274 + 275 + // HSMDeviceList contains a list of HSMDevice. 276 + type HSMDeviceList struct { 277 + metav1.TypeMeta `json:",inline"` 278 + metav1.ListMeta `json:"metadata,omitempty"` 279 + Items []HSMDevice `json:"items"` 280 + } 281 + 282 + func init() { 283 + SchemeBuilder.Register(&HSMDevice{}, &HSMDeviceList{}) 284 + }
+129
api/v1alpha1/hsmsecret_types.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 v1alpha1 18 + 19 + import ( 20 + corev1 "k8s.io/api/core/v1" 21 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 + ) 23 + 24 + // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 + // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 + 27 + // HSMSecretSpec defines the desired state of HSMSecret. 28 + type HSMSecretSpec struct { 29 + // HSMPath is the path on the Pico HSM where the secret data is stored 30 + // Example: "secrets/appnamespace/appname-secret" 31 + HSMPath string `json:"hsmPath"` 32 + 33 + // SecretName is the name of the Kubernetes Secret object to create/update 34 + // Defaults to the HSMSecret name if not specified 35 + // +optional 36 + SecretName string `json:"secretName,omitempty"` 37 + 38 + // AutoSync enables bidirectional synchronization between HSM and Kubernetes Secret 39 + // +kubebuilder:default=true 40 + // +optional 41 + AutoSync bool `json:"autoSync,omitempty"` 42 + 43 + // SecretType specifies the type of Kubernetes Secret to create 44 + // +kubebuilder:default="Opaque" 45 + // +optional 46 + SecretType corev1.SecretType `json:"secretType,omitempty"` 47 + 48 + // SyncInterval defines how often to check for HSM changes (in seconds) 49 + // Only applies when AutoSync is true 50 + // +kubebuilder:default=300 51 + // +optional 52 + SyncInterval int32 `json:"syncInterval,omitempty"` 53 + } 54 + 55 + // SyncStatus represents the synchronization state 56 + type SyncStatus string 57 + 58 + const ( 59 + // SyncStatusInSync indicates HSM and K8s Secret are synchronized 60 + SyncStatusInSync SyncStatus = "InSync" 61 + // SyncStatusOutOfSync indicates HSM and K8s Secret differ 62 + SyncStatusOutOfSync SyncStatus = "OutOfSync" 63 + // SyncStatusError indicates an error occurred during synchronization 64 + SyncStatusError SyncStatus = "Error" 65 + // SyncStatusPending indicates synchronization is in progress 66 + SyncStatusPending SyncStatus = "Pending" 67 + ) 68 + 69 + // HSMSecretStatus defines the observed state of HSMSecret. 70 + type HSMSecretStatus struct { 71 + // LastSyncTime is the timestamp of the last successful synchronization 72 + // +optional 73 + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` 74 + 75 + // HSMChecksum is the SHA256 checksum of the HSM data 76 + // +optional 77 + HSMChecksum string `json:"hsmChecksum,omitempty"` 78 + 79 + // SecretChecksum is the SHA256 checksum of the Kubernetes Secret data 80 + // +optional 81 + SecretChecksum string `json:"secretChecksum,omitempty"` 82 + 83 + // SyncStatus indicates the current synchronization status 84 + // +optional 85 + SyncStatus SyncStatus `json:"syncStatus,omitempty"` 86 + 87 + // LastError contains the last error message if SyncStatus is Error 88 + // +optional 89 + LastError string `json:"lastError,omitempty"` 90 + 91 + // Conditions represent the latest available observations of the HSMSecret's current state 92 + // +optional 93 + Conditions []metav1.Condition `json:"conditions,omitempty"` 94 + 95 + // SecretRef references the created Kubernetes Secret 96 + // +optional 97 + SecretRef *corev1.ObjectReference `json:"secretRef,omitempty"` 98 + } 99 + 100 + // +kubebuilder:object:root=true 101 + // +kubebuilder:subresource:status 102 + // +kubebuilder:resource:shortName=hsmsec 103 + // +kubebuilder:printcolumn:name="HSM Path",type=string,JSONPath=`.spec.hsmPath` 104 + // +kubebuilder:printcolumn:name="Secret Name",type=string,JSONPath=`.spec.secretName` 105 + // +kubebuilder:printcolumn:name="Sync Status",type=string,JSONPath=`.status.syncStatus` 106 + // +kubebuilder:printcolumn:name="Last Sync",type=date,JSONPath=`.status.lastSyncTime` 107 + // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` 108 + 109 + // HSMSecret is the Schema for the hsmsecrets API. 110 + type HSMSecret struct { 111 + metav1.TypeMeta `json:",inline"` 112 + metav1.ObjectMeta `json:"metadata,omitempty"` 113 + 114 + Spec HSMSecretSpec `json:"spec,omitempty"` 115 + Status HSMSecretStatus `json:"status,omitempty"` 116 + } 117 + 118 + // +kubebuilder:object:root=true 119 + 120 + // HSMSecretList contains a list of HSMSecret. 121 + type HSMSecretList struct { 122 + metav1.TypeMeta `json:",inline"` 123 + metav1.ListMeta `json:"metadata,omitempty"` 124 + Items []HSMSecret `json:"items"` 125 + } 126 + 127 + func init() { 128 + SchemeBuilder.Register(&HSMSecret{}, &HSMSecretList{}) 129 + }
+372
api/v1alpha1/zz_generated.deepcopy.go
··· 1 + //go:build !ignore_autogenerated 2 + 3 + /* 4 + Copyright 2025. 5 + 6 + Licensed under the Apache License, Version 2.0 (the "License"); 7 + you may not use this file except in compliance with the License. 8 + You may obtain a copy of the License at 9 + 10 + http://www.apache.org/licenses/LICENSE-2.0 11 + 12 + Unless required by applicable law or agreed to in writing, software 13 + distributed under the License is distributed on an "AS IS" BASIS, 14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + See the License for the specific language governing permissions and 16 + limitations under the License. 17 + */ 18 + 19 + // Code generated by controller-gen. DO NOT EDIT. 20 + 21 + package v1alpha1 22 + 23 + import ( 24 + corev1 "k8s.io/api/core/v1" 25 + "k8s.io/apimachinery/pkg/apis/meta/v1" 26 + runtime "k8s.io/apimachinery/pkg/runtime" 27 + ) 28 + 29 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 + func (in *DevicePathSpec) DeepCopyInto(out *DevicePathSpec) { 31 + *out = *in 32 + } 33 + 34 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevicePathSpec. 35 + func (in *DevicePathSpec) DeepCopy() *DevicePathSpec { 36 + if in == nil { 37 + return nil 38 + } 39 + out := new(DevicePathSpec) 40 + in.DeepCopyInto(out) 41 + return out 42 + } 43 + 44 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 45 + func (in *DiscoveredDevice) DeepCopyInto(out *DiscoveredDevice) { 46 + *out = *in 47 + in.LastSeen.DeepCopyInto(&out.LastSeen) 48 + if in.DeviceInfo != nil { 49 + in, out := &in.DeviceInfo, &out.DeviceInfo 50 + *out = make(map[string]string, len(*in)) 51 + for key, val := range *in { 52 + (*out)[key] = val 53 + } 54 + } 55 + if in.LastSyncTime != nil { 56 + in, out := &in.LastSyncTime, &out.LastSyncTime 57 + *out = (*in).DeepCopy() 58 + } 59 + } 60 + 61 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiscoveredDevice. 62 + func (in *DiscoveredDevice) DeepCopy() *DiscoveredDevice { 63 + if in == nil { 64 + return nil 65 + } 66 + out := new(DiscoveredDevice) 67 + in.DeepCopyInto(out) 68 + return out 69 + } 70 + 71 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 72 + func (in *HSMDevice) DeepCopyInto(out *HSMDevice) { 73 + *out = *in 74 + out.TypeMeta = in.TypeMeta 75 + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 76 + in.Spec.DeepCopyInto(&out.Spec) 77 + in.Status.DeepCopyInto(&out.Status) 78 + } 79 + 80 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMDevice. 81 + func (in *HSMDevice) DeepCopy() *HSMDevice { 82 + if in == nil { 83 + return nil 84 + } 85 + out := new(HSMDevice) 86 + in.DeepCopyInto(out) 87 + return out 88 + } 89 + 90 + // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 91 + func (in *HSMDevice) DeepCopyObject() runtime.Object { 92 + if c := in.DeepCopy(); c != nil { 93 + return c 94 + } 95 + return nil 96 + } 97 + 98 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 99 + func (in *HSMDeviceList) DeepCopyInto(out *HSMDeviceList) { 100 + *out = *in 101 + out.TypeMeta = in.TypeMeta 102 + in.ListMeta.DeepCopyInto(&out.ListMeta) 103 + if in.Items != nil { 104 + in, out := &in.Items, &out.Items 105 + *out = make([]HSMDevice, len(*in)) 106 + for i := range *in { 107 + (*in)[i].DeepCopyInto(&(*out)[i]) 108 + } 109 + } 110 + } 111 + 112 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMDeviceList. 113 + func (in *HSMDeviceList) DeepCopy() *HSMDeviceList { 114 + if in == nil { 115 + return nil 116 + } 117 + out := new(HSMDeviceList) 118 + in.DeepCopyInto(out) 119 + return out 120 + } 121 + 122 + // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 123 + func (in *HSMDeviceList) DeepCopyObject() runtime.Object { 124 + if c := in.DeepCopy(); c != nil { 125 + return c 126 + } 127 + return nil 128 + } 129 + 130 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 131 + func (in *HSMDeviceSpec) DeepCopyInto(out *HSMDeviceSpec) { 132 + *out = *in 133 + if in.USB != nil { 134 + in, out := &in.USB, &out.USB 135 + *out = new(USBDeviceSpec) 136 + **out = **in 137 + } 138 + if in.DevicePath != nil { 139 + in, out := &in.DevicePath, &out.DevicePath 140 + *out = new(DevicePathSpec) 141 + **out = **in 142 + } 143 + if in.NodeSelector != nil { 144 + in, out := &in.NodeSelector, &out.NodeSelector 145 + *out = make(map[string]string, len(*in)) 146 + for key, val := range *in { 147 + (*out)[key] = val 148 + } 149 + } 150 + if in.Mirroring != nil { 151 + in, out := &in.Mirroring, &out.Mirroring 152 + *out = new(MirroringSpec) 153 + (*in).DeepCopyInto(*out) 154 + } 155 + } 156 + 157 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMDeviceSpec. 158 + func (in *HSMDeviceSpec) DeepCopy() *HSMDeviceSpec { 159 + if in == nil { 160 + return nil 161 + } 162 + out := new(HSMDeviceSpec) 163 + in.DeepCopyInto(out) 164 + return out 165 + } 166 + 167 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 168 + func (in *HSMDeviceStatus) DeepCopyInto(out *HSMDeviceStatus) { 169 + *out = *in 170 + if in.DiscoveredDevices != nil { 171 + in, out := &in.DiscoveredDevices, &out.DiscoveredDevices 172 + *out = make([]DiscoveredDevice, len(*in)) 173 + for i := range *in { 174 + (*in)[i].DeepCopyInto(&(*out)[i]) 175 + } 176 + } 177 + if in.LastDiscoveryTime != nil { 178 + in, out := &in.LastDiscoveryTime, &out.LastDiscoveryTime 179 + *out = (*in).DeepCopy() 180 + } 181 + if in.Conditions != nil { 182 + in, out := &in.Conditions, &out.Conditions 183 + *out = make([]v1.Condition, len(*in)) 184 + for i := range *in { 185 + (*in)[i].DeepCopyInto(&(*out)[i]) 186 + } 187 + } 188 + if in.Mirroring != nil { 189 + in, out := &in.Mirroring, &out.Mirroring 190 + *out = new(MirroringStatus) 191 + (*in).DeepCopyInto(*out) 192 + } 193 + } 194 + 195 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMDeviceStatus. 196 + func (in *HSMDeviceStatus) DeepCopy() *HSMDeviceStatus { 197 + if in == nil { 198 + return nil 199 + } 200 + out := new(HSMDeviceStatus) 201 + in.DeepCopyInto(out) 202 + return out 203 + } 204 + 205 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 206 + func (in *HSMSecret) DeepCopyInto(out *HSMSecret) { 207 + *out = *in 208 + out.TypeMeta = in.TypeMeta 209 + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 210 + out.Spec = in.Spec 211 + in.Status.DeepCopyInto(&out.Status) 212 + } 213 + 214 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMSecret. 215 + func (in *HSMSecret) DeepCopy() *HSMSecret { 216 + if in == nil { 217 + return nil 218 + } 219 + out := new(HSMSecret) 220 + in.DeepCopyInto(out) 221 + return out 222 + } 223 + 224 + // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 225 + func (in *HSMSecret) DeepCopyObject() runtime.Object { 226 + if c := in.DeepCopy(); c != nil { 227 + return c 228 + } 229 + return nil 230 + } 231 + 232 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 233 + func (in *HSMSecretList) DeepCopyInto(out *HSMSecretList) { 234 + *out = *in 235 + out.TypeMeta = in.TypeMeta 236 + in.ListMeta.DeepCopyInto(&out.ListMeta) 237 + if in.Items != nil { 238 + in, out := &in.Items, &out.Items 239 + *out = make([]HSMSecret, len(*in)) 240 + for i := range *in { 241 + (*in)[i].DeepCopyInto(&(*out)[i]) 242 + } 243 + } 244 + } 245 + 246 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMSecretList. 247 + func (in *HSMSecretList) DeepCopy() *HSMSecretList { 248 + if in == nil { 249 + return nil 250 + } 251 + out := new(HSMSecretList) 252 + in.DeepCopyInto(out) 253 + return out 254 + } 255 + 256 + // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 257 + func (in *HSMSecretList) DeepCopyObject() runtime.Object { 258 + if c := in.DeepCopy(); c != nil { 259 + return c 260 + } 261 + return nil 262 + } 263 + 264 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 265 + func (in *HSMSecretSpec) DeepCopyInto(out *HSMSecretSpec) { 266 + *out = *in 267 + } 268 + 269 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMSecretSpec. 270 + func (in *HSMSecretSpec) DeepCopy() *HSMSecretSpec { 271 + if in == nil { 272 + return nil 273 + } 274 + out := new(HSMSecretSpec) 275 + in.DeepCopyInto(out) 276 + return out 277 + } 278 + 279 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 280 + func (in *HSMSecretStatus) DeepCopyInto(out *HSMSecretStatus) { 281 + *out = *in 282 + if in.LastSyncTime != nil { 283 + in, out := &in.LastSyncTime, &out.LastSyncTime 284 + *out = (*in).DeepCopy() 285 + } 286 + if in.Conditions != nil { 287 + in, out := &in.Conditions, &out.Conditions 288 + *out = make([]v1.Condition, len(*in)) 289 + for i := range *in { 290 + (*in)[i].DeepCopyInto(&(*out)[i]) 291 + } 292 + } 293 + if in.SecretRef != nil { 294 + in, out := &in.SecretRef, &out.SecretRef 295 + *out = new(corev1.ObjectReference) 296 + **out = **in 297 + } 298 + } 299 + 300 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HSMSecretStatus. 301 + func (in *HSMSecretStatus) DeepCopy() *HSMSecretStatus { 302 + if in == nil { 303 + return nil 304 + } 305 + out := new(HSMSecretStatus) 306 + in.DeepCopyInto(out) 307 + return out 308 + } 309 + 310 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 311 + func (in *MirroringSpec) DeepCopyInto(out *MirroringSpec) { 312 + *out = *in 313 + if in.TargetNodes != nil { 314 + in, out := &in.TargetNodes, &out.TargetNodes 315 + *out = make([]string, len(*in)) 316 + copy(*out, *in) 317 + } 318 + } 319 + 320 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MirroringSpec. 321 + func (in *MirroringSpec) DeepCopy() *MirroringSpec { 322 + if in == nil { 323 + return nil 324 + } 325 + out := new(MirroringSpec) 326 + in.DeepCopyInto(out) 327 + return out 328 + } 329 + 330 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 331 + func (in *MirroringStatus) DeepCopyInto(out *MirroringStatus) { 332 + *out = *in 333 + if in.MirroredNodes != nil { 334 + in, out := &in.MirroredNodes, &out.MirroredNodes 335 + *out = make([]string, len(*in)) 336 + copy(*out, *in) 337 + } 338 + if in.LastSyncTime != nil { 339 + in, out := &in.LastSyncTime, &out.LastSyncTime 340 + *out = (*in).DeepCopy() 341 + } 342 + if in.SyncErrors != nil { 343 + in, out := &in.SyncErrors, &out.SyncErrors 344 + *out = make([]string, len(*in)) 345 + copy(*out, *in) 346 + } 347 + } 348 + 349 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MirroringStatus. 350 + func (in *MirroringStatus) DeepCopy() *MirroringStatus { 351 + if in == nil { 352 + return nil 353 + } 354 + out := new(MirroringStatus) 355 + in.DeepCopyInto(out) 356 + return out 357 + } 358 + 359 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 360 + func (in *USBDeviceSpec) DeepCopyInto(out *USBDeviceSpec) { 361 + *out = *in 362 + } 363 + 364 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new USBDeviceSpec. 365 + func (in *USBDeviceSpec) DeepCopy() *USBDeviceSpec { 366 + if in == nil { 367 + return nil 368 + } 369 + out := new(USBDeviceSpec) 370 + in.DeepCopyInto(out) 371 + return out 372 + }
+310
cmd/main.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 main 18 + 19 + import ( 20 + "context" 21 + "crypto/tls" 22 + "flag" 23 + "os" 24 + "path/filepath" 25 + 26 + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 27 + // to ensure that exec-entrypoint and run can make use of them. 28 + _ "k8s.io/client-go/plugin/pkg/client/auth" 29 + 30 + "k8s.io/apimachinery/pkg/runtime" 31 + utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 + clientgoscheme "k8s.io/client-go/kubernetes/scheme" 33 + ctrl "sigs.k8s.io/controller-runtime" 34 + "sigs.k8s.io/controller-runtime/pkg/certwatcher" 35 + "sigs.k8s.io/controller-runtime/pkg/healthz" 36 + "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 38 + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 39 + "sigs.k8s.io/controller-runtime/pkg/webhook" 40 + 41 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 42 + "github.com/evanjarrett/hsm-secrets-operator/internal/api" 43 + "github.com/evanjarrett/hsm-secrets-operator/internal/controller" 44 + "github.com/evanjarrett/hsm-secrets-operator/internal/discovery" 45 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 46 + // +kubebuilder:scaffold:imports 47 + ) 48 + 49 + var ( 50 + scheme = runtime.NewScheme() 51 + setupLog = ctrl.Log.WithName("setup") 52 + ) 53 + 54 + func init() { 55 + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 56 + 57 + utilruntime.Must(hsmv1alpha1.AddToScheme(scheme)) 58 + // +kubebuilder:scaffold:scheme 59 + } 60 + 61 + // nolint:gocyclo 62 + func main() { 63 + var metricsAddr string 64 + var metricsCertPath, metricsCertName, metricsCertKey string 65 + var webhookCertPath, webhookCertName, webhookCertKey string 66 + var enableLeaderElection bool 67 + var probeAddr string 68 + var secureMetrics bool 69 + var enableHTTP2 bool 70 + var enableAPI bool 71 + var apiPort int 72 + var tlsOpts []func(*tls.Config) 73 + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ 74 + "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") 75 + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 76 + flag.BoolVar(&enableLeaderElection, "leader-elect", false, 77 + "Enable leader election for controller manager. "+ 78 + "Enabling this will ensure there is only one active controller manager.") 79 + flag.BoolVar(&secureMetrics, "metrics-secure", true, 80 + "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") 81 + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") 82 + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") 83 + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") 84 + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", 85 + "The directory that contains the metrics server certificate.") 86 + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") 87 + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") 88 + flag.BoolVar(&enableHTTP2, "enable-http2", false, 89 + "If set, HTTP/2 will be enabled for the metrics and webhook servers") 90 + flag.BoolVar(&enableAPI, "enable-api", true, 91 + "Enable the REST API server for HSM secret management") 92 + flag.IntVar(&apiPort, "api-port", 8090, 93 + "Port for the REST API server") 94 + opts := zap.Options{ 95 + Development: true, 96 + } 97 + opts.BindFlags(flag.CommandLine) 98 + flag.Parse() 99 + 100 + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 101 + 102 + // if the enable-http2 flag is false (the default), http/2 should be disabled 103 + // due to its vulnerabilities. More specifically, disabling http/2 will 104 + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and 105 + // Rapid Reset CVEs. For more information see: 106 + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 107 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 108 + disableHTTP2 := func(c *tls.Config) { 109 + setupLog.Info("disabling http/2") 110 + c.NextProtos = []string{"http/1.1"} 111 + } 112 + 113 + if !enableHTTP2 { 114 + tlsOpts = append(tlsOpts, disableHTTP2) 115 + } 116 + 117 + // Create watchers for metrics and webhooks certificates 118 + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher 119 + 120 + // Initial webhook TLS options 121 + webhookTLSOpts := tlsOpts 122 + 123 + if len(webhookCertPath) > 0 { 124 + setupLog.Info("Initializing webhook certificate watcher using provided certificates", 125 + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) 126 + 127 + var err error 128 + webhookCertWatcher, err = certwatcher.New( 129 + filepath.Join(webhookCertPath, webhookCertName), 130 + filepath.Join(webhookCertPath, webhookCertKey), 131 + ) 132 + if err != nil { 133 + setupLog.Error(err, "Failed to initialize webhook certificate watcher") 134 + os.Exit(1) 135 + } 136 + 137 + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { 138 + config.GetCertificate = webhookCertWatcher.GetCertificate 139 + }) 140 + } 141 + 142 + webhookServer := webhook.NewServer(webhook.Options{ 143 + TLSOpts: webhookTLSOpts, 144 + }) 145 + 146 + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. 147 + // More info: 148 + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server 149 + // - https://book.kubebuilder.io/reference/metrics.html 150 + metricsServerOptions := metricsserver.Options{ 151 + BindAddress: metricsAddr, 152 + SecureServing: secureMetrics, 153 + TLSOpts: tlsOpts, 154 + } 155 + 156 + if secureMetrics { 157 + // FilterProvider is used to protect the metrics endpoint with authn/authz. 158 + // These configurations ensure that only authorized users and service accounts 159 + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: 160 + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization 161 + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization 162 + } 163 + 164 + // If the certificate is not specified, controller-runtime will automatically 165 + // generate self-signed certificates for the metrics server. While convenient for development and testing, 166 + // this setup is not recommended for production. 167 + // 168 + // TODO(user): If you enable certManager, uncomment the following lines: 169 + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates 170 + // managed by cert-manager for the metrics server. 171 + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. 172 + if len(metricsCertPath) > 0 { 173 + setupLog.Info("Initializing metrics certificate watcher using provided certificates", 174 + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) 175 + 176 + var err error 177 + metricsCertWatcher, err = certwatcher.New( 178 + filepath.Join(metricsCertPath, metricsCertName), 179 + filepath.Join(metricsCertPath, metricsCertKey), 180 + ) 181 + if err != nil { 182 + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) 183 + os.Exit(1) 184 + } 185 + 186 + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { 187 + config.GetCertificate = metricsCertWatcher.GetCertificate 188 + }) 189 + } 190 + 191 + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 192 + Scheme: scheme, 193 + Metrics: metricsServerOptions, 194 + WebhookServer: webhookServer, 195 + HealthProbeBindAddress: probeAddr, 196 + LeaderElection: enableLeaderElection, 197 + LeaderElectionID: "64b68d60.j5t.io", 198 + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 199 + // when the Manager ends. This requires the binary to immediately end when the 200 + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 201 + // speeds up voluntary leader transitions as the new leader don't have to wait 202 + // LeaseDuration time first. 203 + // 204 + // In the default scaffold provided, the program ends immediately after 205 + // the manager stops, so would be fine to enable this option. However, 206 + // if you are doing or is intended to do any operation such as perform cleanups 207 + // after the manager stops then its usage might be unsafe. 208 + // LeaderElectionReleaseOnCancel: true, 209 + }) 210 + if err != nil { 211 + setupLog.Error(err, "unable to start manager") 212 + os.Exit(1) 213 + } 214 + 215 + // Initialize HSM client 216 + hsmClient := hsm.NewMockClient() // Use mock client for now 217 + hsmConfig := hsm.DefaultConfig() 218 + // TODO: Load HSM config from environment variables or config file 219 + 220 + ctx := context.Background() 221 + if err := hsmClient.Initialize(ctx, hsmConfig); err != nil { 222 + setupLog.Error(err, "unable to initialize HSM client") 223 + os.Exit(1) 224 + } 225 + 226 + // Initialize USB discoverer 227 + usbDiscoverer := discovery.NewUSBDiscoverer() 228 + 229 + // Get node name for device discovery 230 + nodeName := os.Getenv("NODE_NAME") 231 + if nodeName == "" { 232 + if hostname, err := os.Hostname(); err == nil { 233 + nodeName = hostname 234 + } else { 235 + nodeName = "unknown" 236 + } 237 + } 238 + 239 + // Initialize mirroring manager for cross-node HSM device synchronization 240 + mirroringManager := discovery.NewMirroringManager(mgr.GetClient(), nodeName) 241 + 242 + // Register the HSM client with the mirroring manager for this node 243 + mirroringManager.RegisterHSMClient(nodeName, hsmClient) 244 + 245 + if err := (&controller.HSMSecretReconciler{ 246 + Client: mgr.GetClient(), 247 + Scheme: mgr.GetScheme(), 248 + HSMClient: hsmClient, 249 + MirroringManager: mirroringManager, 250 + }).SetupWithManager(mgr); err != nil { 251 + setupLog.Error(err, "unable to create controller", "controller", "HSMSecret") 252 + os.Exit(1) 253 + } 254 + 255 + if err := (&controller.HSMDeviceReconciler{ 256 + Client: mgr.GetClient(), 257 + Scheme: mgr.GetScheme(), 258 + NodeName: nodeName, 259 + USBDiscoverer: usbDiscoverer, 260 + MirroringManager: mirroringManager, 261 + }).SetupWithManager(mgr); err != nil { 262 + setupLog.Error(err, "unable to create controller", "controller", "HSMDevice") 263 + os.Exit(1) 264 + } 265 + // +kubebuilder:scaffold:builder 266 + 267 + if metricsCertWatcher != nil { 268 + setupLog.Info("Adding metrics certificate watcher to manager") 269 + if err := mgr.Add(metricsCertWatcher); err != nil { 270 + setupLog.Error(err, "unable to add metrics certificate watcher to manager") 271 + os.Exit(1) 272 + } 273 + } 274 + 275 + if webhookCertWatcher != nil { 276 + setupLog.Info("Adding webhook certificate watcher to manager") 277 + if err := mgr.Add(webhookCertWatcher); err != nil { 278 + setupLog.Error(err, "unable to add webhook certificate watcher to manager") 279 + os.Exit(1) 280 + } 281 + } 282 + 283 + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 284 + setupLog.Error(err, "unable to set up health check") 285 + os.Exit(1) 286 + } 287 + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 288 + setupLog.Error(err, "unable to set up ready check") 289 + os.Exit(1) 290 + } 291 + 292 + // Start API server if enabled 293 + if enableAPI { 294 + apiServer := api.NewServer(mgr.GetClient(), hsmClient, mirroringManager, ctrl.Log.WithName("api")) 295 + 296 + // Start API server in a separate goroutine 297 + go func() { 298 + setupLog.Info("starting API server", "port", apiPort) 299 + if err := apiServer.Start(apiPort); err != nil { 300 + setupLog.Error(err, "problem running API server") 301 + } 302 + }() 303 + } 304 + 305 + setupLog.Info("starting manager") 306 + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 307 + setupLog.Error(err, "problem running manager") 308 + os.Exit(1) 309 + } 310 + }
+326
config/crd/bases/hsm.j5t.io_hsmdevices.yaml
··· 1 + --- 2 + apiVersion: apiextensions.k8s.io/v1 3 + kind: CustomResourceDefinition 4 + metadata: 5 + annotations: 6 + controller-gen.kubebuilder.io/version: v0.18.0 7 + name: hsmdevices.hsm.j5t.io 8 + spec: 9 + group: hsm.j5t.io 10 + names: 11 + kind: HSMDevice 12 + listKind: HSMDeviceList 13 + plural: hsmdevices 14 + shortNames: 15 + - hsmdev 16 + singular: hsmdevice 17 + scope: Namespaced 18 + versions: 19 + - additionalPrinterColumns: 20 + - jsonPath: .spec.deviceType 21 + name: Type 22 + type: string 23 + - jsonPath: .status.totalDevices 24 + name: Total 25 + type: integer 26 + - jsonPath: .status.availableDevices 27 + name: Available 28 + type: integer 29 + - jsonPath: .status.phase 30 + name: Phase 31 + type: string 32 + - jsonPath: .status.lastDiscoveryTime 33 + name: Last Discovery 34 + type: date 35 + - jsonPath: .metadata.creationTimestamp 36 + name: Age 37 + type: date 38 + name: v1alpha1 39 + schema: 40 + openAPIV3Schema: 41 + description: HSMDevice is the Schema for the hsmdevices API. 42 + properties: 43 + apiVersion: 44 + description: |- 45 + APIVersion defines the versioned schema of this representation of an object. 46 + Servers should convert recognized schemas to the latest internal value, and 47 + may reject unrecognized values. 48 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 49 + type: string 50 + kind: 51 + description: |- 52 + Kind is a string value representing the REST resource this object represents. 53 + Servers may infer this from the endpoint the client submits requests to. 54 + Cannot be updated. 55 + In CamelCase. 56 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 57 + type: string 58 + metadata: 59 + type: object 60 + spec: 61 + description: HSMDeviceSpec defines the desired state of HSMDevice. 62 + properties: 63 + devicePath: 64 + description: DevicePath defines path-based device discovery criteria 65 + properties: 66 + path: 67 + description: Path is the device path pattern (e.g., "/dev/ttyUSB*", 68 + "/dev/sc-hsm*") 69 + type: string 70 + permissions: 71 + description: Permissions are the required permissions for device 72 + access 73 + type: string 74 + required: 75 + - path 76 + type: object 77 + deviceType: 78 + description: DeviceType specifies the type of HSM device 79 + type: string 80 + maxDevices: 81 + default: 10 82 + description: MaxDevices limits how many instances of this device can 83 + be discovered 84 + format: int32 85 + type: integer 86 + mirroring: 87 + description: Mirroring configures cross-node device mirroring for 88 + high availability 89 + properties: 90 + autoFailover: 91 + default: true 92 + description: AutoFailover enables automatic failover to healthy 93 + nodes 94 + type: boolean 95 + policy: 96 + default: None 97 + description: Policy specifies the mirroring strategy 98 + type: string 99 + primaryNode: 100 + description: PrimaryNode specifies the preferred primary node 101 + for write operations 102 + type: string 103 + syncInterval: 104 + default: 60 105 + description: SyncInterval defines how often to sync device data 106 + across nodes (in seconds) 107 + format: int32 108 + type: integer 109 + targetNodes: 110 + description: |- 111 + TargetNodes specifies nodes that should have mirrored access 112 + If empty, mirrors to all nodes with the device 113 + items: 114 + type: string 115 + type: array 116 + type: object 117 + nodeSelector: 118 + additionalProperties: 119 + type: string 120 + description: NodeSelector specifies which nodes should be scanned 121 + for this device 122 + type: object 123 + pkcs11LibraryPath: 124 + description: PKCS11LibraryPath is the path to the PKCS#11 library 125 + for this device 126 + type: string 127 + usb: 128 + description: USB defines USB-based device discovery criteria 129 + properties: 130 + productId: 131 + description: ProductID is the USB product ID (e.g., "4230" for 132 + Pico HSM) 133 + type: string 134 + serialNumber: 135 + description: SerialNumber optionally matches a specific device 136 + serial number 137 + type: string 138 + vendorId: 139 + description: VendorID is the USB vendor ID (e.g., "20a0" for Pico 140 + HSM) 141 + type: string 142 + required: 143 + - productId 144 + - vendorId 145 + type: object 146 + required: 147 + - deviceType 148 + type: object 149 + status: 150 + description: HSMDeviceStatus defines the observed state of HSMDevice. 151 + properties: 152 + availableDevices: 153 + description: AvailableDevices is the number of currently available 154 + devices 155 + format: int32 156 + type: integer 157 + conditions: 158 + description: Conditions represent the latest available observations 159 + of the device state 160 + items: 161 + description: Condition contains details for one aspect of the current 162 + state of this API Resource. 163 + properties: 164 + lastTransitionTime: 165 + description: |- 166 + lastTransitionTime is the last time the condition transitioned from one status to another. 167 + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 168 + format: date-time 169 + type: string 170 + message: 171 + description: |- 172 + message is a human readable message indicating details about the transition. 173 + This may be an empty string. 174 + maxLength: 32768 175 + type: string 176 + observedGeneration: 177 + description: |- 178 + observedGeneration represents the .metadata.generation that the condition was set based upon. 179 + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 180 + with respect to the current state of the instance. 181 + format: int64 182 + minimum: 0 183 + type: integer 184 + reason: 185 + description: |- 186 + reason contains a programmatic identifier indicating the reason for the condition's last transition. 187 + Producers of specific condition types may define expected values and meanings for this field, 188 + and whether the values are considered a guaranteed API. 189 + The value should be a CamelCase string. 190 + This field may not be empty. 191 + maxLength: 1024 192 + minLength: 1 193 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 194 + type: string 195 + status: 196 + description: status of the condition, one of True, False, Unknown. 197 + enum: 198 + - "True" 199 + - "False" 200 + - Unknown 201 + type: string 202 + type: 203 + description: type of condition in CamelCase or in foo.example.com/CamelCase. 204 + maxLength: 316 205 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 206 + type: string 207 + required: 208 + - lastTransitionTime 209 + - message 210 + - reason 211 + - status 212 + - type 213 + type: object 214 + type: array 215 + discoveredDevices: 216 + description: DiscoveredDevices lists all discovered devices matching 217 + the spec 218 + items: 219 + description: DiscoveredDevice represents a discovered HSM device 220 + instance 221 + properties: 222 + available: 223 + description: Available indicates if the device is currently 224 + available for use 225 + type: boolean 226 + deviceInfo: 227 + additionalProperties: 228 + type: string 229 + description: DeviceInfo contains additional device information 230 + type: object 231 + devicePath: 232 + description: DevicePath is the system path to the discovered 233 + device 234 + type: string 235 + health: 236 + description: Health represents the health status of the device 237 + type: string 238 + lastSeen: 239 + description: LastSeen is the timestamp when the device was last 240 + detected 241 + format: date-time 242 + type: string 243 + lastSyncTime: 244 + description: LastSyncTime is when this device was last synchronized 245 + format: date-time 246 + type: string 247 + mirroredFrom: 248 + description: MirroredFrom indicates the primary device this 249 + is mirrored from 250 + type: string 251 + nodeName: 252 + description: NodeName is the name of the node where the device 253 + was discovered 254 + type: string 255 + resourceName: 256 + description: ResourceName is the Kubernetes resource name for 257 + this device 258 + type: string 259 + role: 260 + description: Role indicates the role of this device in a mirrored 261 + setup 262 + type: string 263 + serialNumber: 264 + description: SerialNumber is the serial number of the device 265 + (if available) 266 + type: string 267 + required: 268 + - available 269 + - devicePath 270 + - lastSeen 271 + - nodeName 272 + type: object 273 + type: array 274 + lastDiscoveryTime: 275 + description: LastDiscoveryTime is the timestamp of the last discovery 276 + scan 277 + format: date-time 278 + type: string 279 + mirroring: 280 + description: Mirroring represents the status of device mirroring 281 + properties: 282 + enabled: 283 + description: Enabled indicates if mirroring is currently active 284 + type: boolean 285 + failoverCount: 286 + description: FailoverCount tracks the number of failovers that 287 + have occurred 288 + format: int32 289 + type: integer 290 + lastSyncTime: 291 + description: LastSyncTime is when devices were last synchronized 292 + format: date-time 293 + type: string 294 + mirroredNodes: 295 + description: MirroredNodes lists nodes with mirrored access 296 + items: 297 + type: string 298 + type: array 299 + primaryNode: 300 + description: PrimaryNode is the current primary node 301 + type: string 302 + syncErrors: 303 + description: SyncErrors tracks synchronization errors 304 + items: 305 + type: string 306 + type: array 307 + required: 308 + - enabled 309 + - failoverCount 310 + type: object 311 + phase: 312 + description: Phase represents the current phase of device discovery 313 + type: string 314 + totalDevices: 315 + description: TotalDevices is the total number of discovered devices 316 + format: int32 317 + type: integer 318 + required: 319 + - availableDevices 320 + - totalDevices 321 + type: object 322 + type: object 323 + served: true 324 + storage: true 325 + subresources: 326 + status: {}
+218
config/crd/bases/hsm.j5t.io_hsmsecrets.yaml
··· 1 + --- 2 + apiVersion: apiextensions.k8s.io/v1 3 + kind: CustomResourceDefinition 4 + metadata: 5 + annotations: 6 + controller-gen.kubebuilder.io/version: v0.18.0 7 + name: hsmsecrets.hsm.j5t.io 8 + spec: 9 + group: hsm.j5t.io 10 + names: 11 + kind: HSMSecret 12 + listKind: HSMSecretList 13 + plural: hsmsecrets 14 + shortNames: 15 + - hsmsec 16 + singular: hsmsecret 17 + scope: Namespaced 18 + versions: 19 + - additionalPrinterColumns: 20 + - jsonPath: .spec.hsmPath 21 + name: HSM Path 22 + type: string 23 + - jsonPath: .spec.secretName 24 + name: Secret Name 25 + type: string 26 + - jsonPath: .status.syncStatus 27 + name: Sync Status 28 + type: string 29 + - jsonPath: .status.lastSyncTime 30 + name: Last Sync 31 + type: date 32 + - jsonPath: .metadata.creationTimestamp 33 + name: Age 34 + type: date 35 + name: v1alpha1 36 + schema: 37 + openAPIV3Schema: 38 + description: HSMSecret is the Schema for the hsmsecrets API. 39 + properties: 40 + apiVersion: 41 + description: |- 42 + APIVersion defines the versioned schema of this representation of an object. 43 + Servers should convert recognized schemas to the latest internal value, and 44 + may reject unrecognized values. 45 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 46 + type: string 47 + kind: 48 + description: |- 49 + Kind is a string value representing the REST resource this object represents. 50 + Servers may infer this from the endpoint the client submits requests to. 51 + Cannot be updated. 52 + In CamelCase. 53 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 54 + type: string 55 + metadata: 56 + type: object 57 + spec: 58 + description: HSMSecretSpec defines the desired state of HSMSecret. 59 + properties: 60 + autoSync: 61 + default: true 62 + description: AutoSync enables bidirectional synchronization between 63 + HSM and Kubernetes Secret 64 + type: boolean 65 + hsmPath: 66 + description: |- 67 + HSMPath is the path on the Pico HSM where the secret data is stored 68 + Example: "secrets/appnamespace/appname-secret" 69 + type: string 70 + secretName: 71 + description: |- 72 + SecretName is the name of the Kubernetes Secret object to create/update 73 + Defaults to the HSMSecret name if not specified 74 + type: string 75 + secretType: 76 + default: Opaque 77 + description: SecretType specifies the type of Kubernetes Secret to 78 + create 79 + type: string 80 + syncInterval: 81 + default: 300 82 + description: |- 83 + SyncInterval defines how often to check for HSM changes (in seconds) 84 + Only applies when AutoSync is true 85 + format: int32 86 + type: integer 87 + required: 88 + - hsmPath 89 + type: object 90 + status: 91 + description: HSMSecretStatus defines the observed state of HSMSecret. 92 + properties: 93 + conditions: 94 + description: Conditions represent the latest available observations 95 + of the HSMSecret's current state 96 + items: 97 + description: Condition contains details for one aspect of the current 98 + state of this API Resource. 99 + properties: 100 + lastTransitionTime: 101 + description: |- 102 + lastTransitionTime is the last time the condition transitioned from one status to another. 103 + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 104 + format: date-time 105 + type: string 106 + message: 107 + description: |- 108 + message is a human readable message indicating details about the transition. 109 + This may be an empty string. 110 + maxLength: 32768 111 + type: string 112 + observedGeneration: 113 + description: |- 114 + observedGeneration represents the .metadata.generation that the condition was set based upon. 115 + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 116 + with respect to the current state of the instance. 117 + format: int64 118 + minimum: 0 119 + type: integer 120 + reason: 121 + description: |- 122 + reason contains a programmatic identifier indicating the reason for the condition's last transition. 123 + Producers of specific condition types may define expected values and meanings for this field, 124 + and whether the values are considered a guaranteed API. 125 + The value should be a CamelCase string. 126 + This field may not be empty. 127 + maxLength: 1024 128 + minLength: 1 129 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 130 + type: string 131 + status: 132 + description: status of the condition, one of True, False, Unknown. 133 + enum: 134 + - "True" 135 + - "False" 136 + - Unknown 137 + type: string 138 + type: 139 + description: type of condition in CamelCase or in foo.example.com/CamelCase. 140 + maxLength: 316 141 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 142 + type: string 143 + required: 144 + - lastTransitionTime 145 + - message 146 + - reason 147 + - status 148 + - type 149 + type: object 150 + type: array 151 + hsmChecksum: 152 + description: HSMChecksum is the SHA256 checksum of the HSM data 153 + type: string 154 + lastError: 155 + description: LastError contains the last error message if SyncStatus 156 + is Error 157 + type: string 158 + lastSyncTime: 159 + description: LastSyncTime is the timestamp of the last successful 160 + synchronization 161 + format: date-time 162 + type: string 163 + secretChecksum: 164 + description: SecretChecksum is the SHA256 checksum of the Kubernetes 165 + Secret data 166 + type: string 167 + secretRef: 168 + description: SecretRef references the created Kubernetes Secret 169 + properties: 170 + apiVersion: 171 + description: API version of the referent. 172 + type: string 173 + fieldPath: 174 + description: |- 175 + If referring to a piece of an object instead of an entire object, this string 176 + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. 177 + For example, if the object reference is to a container within a pod, this would take on a value like: 178 + "spec.containers{name}" (where "name" refers to the name of the container that triggered 179 + the event) or if no container name is specified "spec.containers[2]" (container with 180 + index 2 in this pod). This syntax is chosen only to have some well-defined way of 181 + referencing a part of an object. 182 + type: string 183 + kind: 184 + description: |- 185 + Kind of the referent. 186 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 187 + type: string 188 + name: 189 + description: |- 190 + Name of the referent. 191 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 192 + type: string 193 + namespace: 194 + description: |- 195 + Namespace of the referent. 196 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ 197 + type: string 198 + resourceVersion: 199 + description: |- 200 + Specific resourceVersion to which this reference is made, if any. 201 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency 202 + type: string 203 + uid: 204 + description: |- 205 + UID of the referent. 206 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids 207 + type: string 208 + type: object 209 + x-kubernetes-map-type: atomic 210 + syncStatus: 211 + description: SyncStatus indicates the current synchronization status 212 + type: string 213 + type: object 214 + type: object 215 + served: true 216 + storage: true 217 + subresources: 218 + status: {}
+17
config/crd/kustomization.yaml
··· 1 + # This kustomization.yaml is not intended to be run by itself, 2 + # since it depends on service name and namespace that are out of this kustomize package. 3 + # It should be run by config/default 4 + resources: 5 + - bases/hsm.j5t.io_hsmsecrets.yaml 6 + - bases/hsm.j5t.io_hsmdevices.yaml 7 + # +kubebuilder:scaffold:crdkustomizeresource 8 + 9 + patches: 10 + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 + # patches here are for enabling the conversion webhook for each CRD 12 + # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 + 14 + # [WEBHOOK] To enable webhook, uncomment the following section 15 + # the following config is for teaching kustomize how to do kustomization for CRDs. 16 + #configurations: 17 + #- kustomizeconfig.yaml
+19
config/crd/kustomizeconfig.yaml
··· 1 + # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 + nameReference: 3 + - kind: Service 4 + version: v1 5 + fieldSpecs: 6 + - kind: CustomResourceDefinition 7 + version: v1 8 + group: apiextensions.k8s.io 9 + path: spec/conversion/webhook/clientConfig/service/name 10 + 11 + namespace: 12 + - kind: CustomResourceDefinition 13 + version: v1 14 + group: apiextensions.k8s.io 15 + path: spec/conversion/webhook/clientConfig/service/namespace 16 + create: false 17 + 18 + varReference: 19 + - path: metadata/annotations
+30
config/default/cert_metrics_manager_patch.yaml
··· 1 + # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. 2 + 3 + # Add the volumeMount for the metrics-server certs 4 + - op: add 5 + path: /spec/template/spec/containers/0/volumeMounts/- 6 + value: 7 + mountPath: /tmp/k8s-metrics-server/metrics-certs 8 + name: metrics-certs 9 + readOnly: true 10 + 11 + # Add the --metrics-cert-path argument for the metrics server 12 + - op: add 13 + path: /spec/template/spec/containers/0/args/- 14 + value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs 15 + 16 + # Add the metrics-server certs volume configuration 17 + - op: add 18 + path: /spec/template/spec/volumes/- 19 + value: 20 + name: metrics-certs 21 + secret: 22 + secretName: metrics-server-cert 23 + optional: false 24 + items: 25 + - key: ca.crt 26 + path: ca.crt 27 + - key: tls.crt 28 + path: tls.crt 29 + - key: tls.key 30 + path: tls.key
+234
config/default/kustomization.yaml
··· 1 + # Adds namespace to all resources. 2 + namespace: hsm-secrets-operator-system 3 + 4 + # Value of this field is prepended to the 5 + # names of all resources, e.g. a deployment named 6 + # "wordpress" becomes "alices-wordpress". 7 + # Note that it should also match with the prefix (text before '-') of the namespace 8 + # field above. 9 + namePrefix: hsm-secrets-operator- 10 + 11 + # Labels to add to all resources and selectors. 12 + #labels: 13 + #- includeSelectors: true 14 + # pairs: 15 + # someName: someValue 16 + 17 + resources: 18 + - ../crd 19 + - ../rbac 20 + - ../manager 21 + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 + # crd/kustomization.yaml 23 + #- ../webhook 24 + # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 + #- ../certmanager 26 + # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 + #- ../prometheus 28 + # [METRICS] Expose the controller manager metrics service. 29 + - metrics_service.yaml 30 + # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. 31 + # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. 32 + # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will 33 + # be able to communicate with the Webhook Server. 34 + #- ../network-policy 35 + 36 + # Uncomment the patches line if you enable Metrics 37 + patches: 38 + # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. 39 + # More info: https://book.kubebuilder.io/reference/metrics 40 + - path: manager_metrics_patch.yaml 41 + target: 42 + kind: Deployment 43 + 44 + # Uncomment the patches line if you enable Metrics and CertManager 45 + # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. 46 + # This patch will protect the metrics with certManager self-signed certs. 47 + #- path: cert_metrics_manager_patch.yaml 48 + # target: 49 + # kind: Deployment 50 + 51 + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 52 + # crd/kustomization.yaml 53 + #- path: manager_webhook_patch.yaml 54 + # target: 55 + # kind: Deployment 56 + 57 + # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 58 + # Uncomment the following replacements to add the cert-manager CA injection annotations 59 + #replacements: 60 + # - source: # Uncomment the following block to enable certificates for metrics 61 + # kind: Service 62 + # version: v1 63 + # name: controller-manager-metrics-service 64 + # fieldPath: metadata.name 65 + # targets: 66 + # - select: 67 + # kind: Certificate 68 + # group: cert-manager.io 69 + # version: v1 70 + # name: metrics-certs 71 + # fieldPaths: 72 + # - spec.dnsNames.0 73 + # - spec.dnsNames.1 74 + # options: 75 + # delimiter: '.' 76 + # index: 0 77 + # create: true 78 + # - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor 79 + # kind: ServiceMonitor 80 + # group: monitoring.coreos.com 81 + # version: v1 82 + # name: controller-manager-metrics-monitor 83 + # fieldPaths: 84 + # - spec.endpoints.0.tlsConfig.serverName 85 + # options: 86 + # delimiter: '.' 87 + # index: 0 88 + # create: true 89 + # 90 + # - source: 91 + # kind: Service 92 + # version: v1 93 + # name: controller-manager-metrics-service 94 + # fieldPath: metadata.namespace 95 + # targets: 96 + # - select: 97 + # kind: Certificate 98 + # group: cert-manager.io 99 + # version: v1 100 + # name: metrics-certs 101 + # fieldPaths: 102 + # - spec.dnsNames.0 103 + # - spec.dnsNames.1 104 + # options: 105 + # delimiter: '.' 106 + # index: 1 107 + # create: true 108 + # - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor 109 + # kind: ServiceMonitor 110 + # group: monitoring.coreos.com 111 + # version: v1 112 + # name: controller-manager-metrics-monitor 113 + # fieldPaths: 114 + # - spec.endpoints.0.tlsConfig.serverName 115 + # options: 116 + # delimiter: '.' 117 + # index: 1 118 + # create: true 119 + # 120 + # - source: # Uncomment the following block if you have any webhook 121 + # kind: Service 122 + # version: v1 123 + # name: webhook-service 124 + # fieldPath: .metadata.name # Name of the service 125 + # targets: 126 + # - select: 127 + # kind: Certificate 128 + # group: cert-manager.io 129 + # version: v1 130 + # name: serving-cert 131 + # fieldPaths: 132 + # - .spec.dnsNames.0 133 + # - .spec.dnsNames.1 134 + # options: 135 + # delimiter: '.' 136 + # index: 0 137 + # create: true 138 + # - source: 139 + # kind: Service 140 + # version: v1 141 + # name: webhook-service 142 + # fieldPath: .metadata.namespace # Namespace of the service 143 + # targets: 144 + # - select: 145 + # kind: Certificate 146 + # group: cert-manager.io 147 + # version: v1 148 + # name: serving-cert 149 + # fieldPaths: 150 + # - .spec.dnsNames.0 151 + # - .spec.dnsNames.1 152 + # options: 153 + # delimiter: '.' 154 + # index: 1 155 + # create: true 156 + # 157 + # - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) 158 + # kind: Certificate 159 + # group: cert-manager.io 160 + # version: v1 161 + # name: serving-cert # This name should match the one in certificate.yaml 162 + # fieldPath: .metadata.namespace # Namespace of the certificate CR 163 + # targets: 164 + # - select: 165 + # kind: ValidatingWebhookConfiguration 166 + # fieldPaths: 167 + # - .metadata.annotations.[cert-manager.io/inject-ca-from] 168 + # options: 169 + # delimiter: '/' 170 + # index: 0 171 + # create: true 172 + # - source: 173 + # kind: Certificate 174 + # group: cert-manager.io 175 + # version: v1 176 + # name: serving-cert 177 + # fieldPath: .metadata.name 178 + # targets: 179 + # - select: 180 + # kind: ValidatingWebhookConfiguration 181 + # fieldPaths: 182 + # - .metadata.annotations.[cert-manager.io/inject-ca-from] 183 + # options: 184 + # delimiter: '/' 185 + # index: 1 186 + # create: true 187 + # 188 + # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) 189 + # kind: Certificate 190 + # group: cert-manager.io 191 + # version: v1 192 + # name: serving-cert 193 + # fieldPath: .metadata.namespace # Namespace of the certificate CR 194 + # targets: 195 + # - select: 196 + # kind: MutatingWebhookConfiguration 197 + # fieldPaths: 198 + # - .metadata.annotations.[cert-manager.io/inject-ca-from] 199 + # options: 200 + # delimiter: '/' 201 + # index: 0 202 + # create: true 203 + # - source: 204 + # kind: Certificate 205 + # group: cert-manager.io 206 + # version: v1 207 + # name: serving-cert 208 + # fieldPath: .metadata.name 209 + # targets: 210 + # - select: 211 + # kind: MutatingWebhookConfiguration 212 + # fieldPaths: 213 + # - .metadata.annotations.[cert-manager.io/inject-ca-from] 214 + # options: 215 + # delimiter: '/' 216 + # index: 1 217 + # create: true 218 + # 219 + # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) 220 + # kind: Certificate 221 + # group: cert-manager.io 222 + # version: v1 223 + # name: serving-cert 224 + # fieldPath: .metadata.namespace # Namespace of the certificate CR 225 + # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 226 + # +kubebuilder:scaffold:crdkustomizecainjectionns 227 + # - source: 228 + # kind: Certificate 229 + # group: cert-manager.io 230 + # version: v1 231 + # name: serving-cert 232 + # fieldPath: .metadata.name 233 + # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 234 + # +kubebuilder:scaffold:crdkustomizecainjectionname
+4
config/default/manager_metrics_patch.yaml
··· 1 + # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 + - op: add 3 + path: /spec/template/spec/containers/0/args/0 4 + value: --metrics-bind-address=:8443
+18
config/default/metrics_service.yaml
··· 1 + apiVersion: v1 2 + kind: Service 3 + metadata: 4 + labels: 5 + control-plane: controller-manager 6 + app.kubernetes.io/name: hsm-secrets-operator 7 + app.kubernetes.io/managed-by: kustomize 8 + name: controller-manager-metrics-service 9 + namespace: system 10 + spec: 11 + ports: 12 + - name: https 13 + port: 8443 14 + protocol: TCP 15 + targetPort: 8443 16 + selector: 17 + control-plane: controller-manager 18 + app.kubernetes.io/name: hsm-secrets-operator
+2
config/manager/kustomization.yaml
··· 1 + resources: 2 + - manager.yaml
+98
config/manager/manager.yaml
··· 1 + apiVersion: v1 2 + kind: Namespace 3 + metadata: 4 + labels: 5 + control-plane: controller-manager 6 + app.kubernetes.io/name: hsm-secrets-operator 7 + app.kubernetes.io/managed-by: kustomize 8 + name: system 9 + --- 10 + apiVersion: apps/v1 11 + kind: Deployment 12 + metadata: 13 + name: controller-manager 14 + namespace: system 15 + labels: 16 + control-plane: controller-manager 17 + app.kubernetes.io/name: hsm-secrets-operator 18 + app.kubernetes.io/managed-by: kustomize 19 + spec: 20 + selector: 21 + matchLabels: 22 + control-plane: controller-manager 23 + app.kubernetes.io/name: hsm-secrets-operator 24 + replicas: 1 25 + template: 26 + metadata: 27 + annotations: 28 + kubectl.kubernetes.io/default-container: manager 29 + labels: 30 + control-plane: controller-manager 31 + app.kubernetes.io/name: hsm-secrets-operator 32 + spec: 33 + # TODO(user): Uncomment the following code to configure the nodeAffinity expression 34 + # according to the platforms which are supported by your solution. 35 + # It is considered best practice to support multiple architectures. You can 36 + # build your manager image using the makefile target docker-buildx. 37 + # affinity: 38 + # nodeAffinity: 39 + # requiredDuringSchedulingIgnoredDuringExecution: 40 + # nodeSelectorTerms: 41 + # - matchExpressions: 42 + # - key: kubernetes.io/arch 43 + # operator: In 44 + # values: 45 + # - amd64 46 + # - arm64 47 + # - ppc64le 48 + # - s390x 49 + # - key: kubernetes.io/os 50 + # operator: In 51 + # values: 52 + # - linux 53 + securityContext: 54 + # Projects are configured by default to adhere to the "restricted" Pod Security Standards. 55 + # This ensures that deployments meet the highest security requirements for Kubernetes. 56 + # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 57 + runAsNonRoot: true 58 + seccompProfile: 59 + type: RuntimeDefault 60 + containers: 61 + - command: 62 + - /manager 63 + args: 64 + - --leader-elect 65 + - --health-probe-bind-address=:8081 66 + image: controller:latest 67 + name: manager 68 + ports: [] 69 + securityContext: 70 + allowPrivilegeEscalation: false 71 + capabilities: 72 + drop: 73 + - "ALL" 74 + livenessProbe: 75 + httpGet: 76 + path: /healthz 77 + port: 8081 78 + initialDelaySeconds: 15 79 + periodSeconds: 20 80 + readinessProbe: 81 + httpGet: 82 + path: /readyz 83 + port: 8081 84 + initialDelaySeconds: 5 85 + periodSeconds: 10 86 + # TODO(user): Configure the resources accordingly based on the project requirements. 87 + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 88 + resources: 89 + limits: 90 + cpu: 500m 91 + memory: 128Mi 92 + requests: 93 + cpu: 10m 94 + memory: 64Mi 95 + volumeMounts: [] 96 + volumes: [] 97 + serviceAccountName: controller-manager 98 + terminationGracePeriodSeconds: 10
+28
config/manifests/kustomization.yaml
··· 1 + # These resources constitute the fully configured set of manifests 2 + # used to generate the 'manifests/' directory in a bundle. 3 + resources: 4 + - bases/hsm-secrets-operator.clusterserviceversion.yaml 5 + - ../default 6 + - ../samples 7 + - ../scorecard 8 + 9 + # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 + # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 + # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 + #patches: 13 + #- target: 14 + # group: apps 15 + # version: v1 16 + # kind: Deployment 17 + # name: controller-manager 18 + # namespace: system 19 + # patch: |- 20 + # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 + # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 + # - op: remove 23 + 24 + # path: /spec/template/spec/containers/0/volumeMounts/0 25 + # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 + # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 + # - op: remove 28 + # path: /spec/template/spec/volumes/0
+27
config/network-policy/allow-metrics-traffic.yaml
··· 1 + # This NetworkPolicy allows ingress traffic 2 + # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those 3 + # namespaces are able to gather data from the metrics endpoint. 4 + apiVersion: networking.k8s.io/v1 5 + kind: NetworkPolicy 6 + metadata: 7 + labels: 8 + app.kubernetes.io/name: hsm-secrets-operator 9 + app.kubernetes.io/managed-by: kustomize 10 + name: allow-metrics-traffic 11 + namespace: system 12 + spec: 13 + podSelector: 14 + matchLabels: 15 + control-plane: controller-manager 16 + app.kubernetes.io/name: hsm-secrets-operator 17 + policyTypes: 18 + - Ingress 19 + ingress: 20 + # This allows ingress traffic from any namespace with the label metrics: enabled 21 + - from: 22 + - namespaceSelector: 23 + matchLabels: 24 + metrics: enabled # Only from namespaces with this label 25 + ports: 26 + - port: 8443 27 + protocol: TCP
+2
config/network-policy/kustomization.yaml
··· 1 + resources: 2 + - allow-metrics-traffic.yaml
+11
config/prometheus/kustomization.yaml
··· 1 + resources: 2 + - monitor.yaml 3 + 4 + # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus 5 + # to securely reference certificates created and managed by cert-manager. 6 + # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml 7 + # to mount the "metrics-server-cert" secret in the Manager Deployment. 8 + #patches: 9 + # - path: monitor_tls_patch.yaml 10 + # target: 11 + # kind: ServiceMonitor
+27
config/prometheus/monitor.yaml
··· 1 + # Prometheus Monitor Service (Metrics) 2 + apiVersion: monitoring.coreos.com/v1 3 + kind: ServiceMonitor 4 + metadata: 5 + labels: 6 + control-plane: controller-manager 7 + app.kubernetes.io/name: hsm-secrets-operator 8 + app.kubernetes.io/managed-by: kustomize 9 + name: controller-manager-metrics-monitor 10 + namespace: system 11 + spec: 12 + endpoints: 13 + - path: /metrics 14 + port: https # Ensure this is the name of the port that exposes HTTPS metrics 15 + scheme: https 16 + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 + tlsConfig: 18 + # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables 19 + # certificate verification, exposing the system to potential man-in-the-middle attacks. 20 + # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. 21 + # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, 22 + # which securely references the certificate from the 'metrics-server-cert' secret. 23 + insecureSkipVerify: true 24 + selector: 25 + matchLabels: 26 + control-plane: controller-manager 27 + app.kubernetes.io/name: hsm-secrets-operator
+19
config/prometheus/monitor_tls_patch.yaml
··· 1 + # Patch for Prometheus ServiceMonitor to enable secure TLS configuration 2 + # using certificates managed by cert-manager 3 + - op: replace 4 + path: /spec/endpoints/0/tlsConfig 5 + value: 6 + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize 7 + serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc 8 + insecureSkipVerify: false 9 + ca: 10 + secret: 11 + name: metrics-server-cert 12 + key: ca.crt 13 + cert: 14 + secret: 15 + name: metrics-server-cert 16 + key: tls.crt 17 + keySecret: 18 + name: metrics-server-cert 19 + key: tls.key
+27
config/rbac/hsmdevice_admin_role.yaml
··· 1 + # This rule is not used by the project hsm-secrets-operator itself. 2 + # It is provided to allow the cluster admin to help manage permissions for users. 3 + # 4 + # Grants full permissions ('*') over hsm.j5t.io. 5 + # This role is intended for users authorized to modify roles and bindings within the cluster, 6 + # enabling them to delegate specific permissions to other users or groups as needed. 7 + 8 + apiVersion: rbac.authorization.k8s.io/v1 9 + kind: ClusterRole 10 + metadata: 11 + labels: 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/managed-by: kustomize 14 + name: hsmdevice-admin-role 15 + rules: 16 + - apiGroups: 17 + - hsm.j5t.io 18 + resources: 19 + - hsmdevices 20 + verbs: 21 + - '*' 22 + - apiGroups: 23 + - hsm.j5t.io 24 + resources: 25 + - hsmdevices/status 26 + verbs: 27 + - get
+33
config/rbac/hsmdevice_editor_role.yaml
··· 1 + # This rule is not used by the project hsm-secrets-operator itself. 2 + # It is provided to allow the cluster admin to help manage permissions for users. 3 + # 4 + # Grants permissions to create, update, and delete resources within the hsm.j5t.io. 5 + # This role is intended for users who need to manage these resources 6 + # but should not control RBAC or manage permissions for others. 7 + 8 + apiVersion: rbac.authorization.k8s.io/v1 9 + kind: ClusterRole 10 + metadata: 11 + labels: 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/managed-by: kustomize 14 + name: hsmdevice-editor-role 15 + rules: 16 + - apiGroups: 17 + - hsm.j5t.io 18 + resources: 19 + - hsmdevices 20 + verbs: 21 + - create 22 + - delete 23 + - get 24 + - list 25 + - patch 26 + - update 27 + - watch 28 + - apiGroups: 29 + - hsm.j5t.io 30 + resources: 31 + - hsmdevices/status 32 + verbs: 33 + - get
+29
config/rbac/hsmdevice_viewer_role.yaml
··· 1 + # This rule is not used by the project hsm-secrets-operator itself. 2 + # It is provided to allow the cluster admin to help manage permissions for users. 3 + # 4 + # Grants read-only access to hsm.j5t.io resources. 5 + # This role is intended for users who need visibility into these resources 6 + # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. 7 + 8 + apiVersion: rbac.authorization.k8s.io/v1 9 + kind: ClusterRole 10 + metadata: 11 + labels: 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/managed-by: kustomize 14 + name: hsmdevice-viewer-role 15 + rules: 16 + - apiGroups: 17 + - hsm.j5t.io 18 + resources: 19 + - hsmdevices 20 + verbs: 21 + - get 22 + - list 23 + - watch 24 + - apiGroups: 25 + - hsm.j5t.io 26 + resources: 27 + - hsmdevices/status 28 + verbs: 29 + - get
+27
config/rbac/hsmsecret_admin_role.yaml
··· 1 + # This rule is not used by the project hsm-secrets-operator itself. 2 + # It is provided to allow the cluster admin to help manage permissions for users. 3 + # 4 + # Grants full permissions ('*') over hsm.j5t.io. 5 + # This role is intended for users authorized to modify roles and bindings within the cluster, 6 + # enabling them to delegate specific permissions to other users or groups as needed. 7 + 8 + apiVersion: rbac.authorization.k8s.io/v1 9 + kind: ClusterRole 10 + metadata: 11 + labels: 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/managed-by: kustomize 14 + name: hsmsecret-admin-role 15 + rules: 16 + - apiGroups: 17 + - hsm.j5t.io 18 + resources: 19 + - hsmsecrets 20 + verbs: 21 + - '*' 22 + - apiGroups: 23 + - hsm.j5t.io 24 + resources: 25 + - hsmsecrets/status 26 + verbs: 27 + - get
+33
config/rbac/hsmsecret_editor_role.yaml
··· 1 + # This rule is not used by the project hsm-secrets-operator itself. 2 + # It is provided to allow the cluster admin to help manage permissions for users. 3 + # 4 + # Grants permissions to create, update, and delete resources within the hsm.j5t.io. 5 + # This role is intended for users who need to manage these resources 6 + # but should not control RBAC or manage permissions for others. 7 + 8 + apiVersion: rbac.authorization.k8s.io/v1 9 + kind: ClusterRole 10 + metadata: 11 + labels: 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/managed-by: kustomize 14 + name: hsmsecret-editor-role 15 + rules: 16 + - apiGroups: 17 + - hsm.j5t.io 18 + resources: 19 + - hsmsecrets 20 + verbs: 21 + - create 22 + - delete 23 + - get 24 + - list 25 + - patch 26 + - update 27 + - watch 28 + - apiGroups: 29 + - hsm.j5t.io 30 + resources: 31 + - hsmsecrets/status 32 + verbs: 33 + - get
+29
config/rbac/hsmsecret_viewer_role.yaml
··· 1 + # This rule is not used by the project hsm-secrets-operator itself. 2 + # It is provided to allow the cluster admin to help manage permissions for users. 3 + # 4 + # Grants read-only access to hsm.j5t.io resources. 5 + # This role is intended for users who need visibility into these resources 6 + # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. 7 + 8 + apiVersion: rbac.authorization.k8s.io/v1 9 + kind: ClusterRole 10 + metadata: 11 + labels: 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/managed-by: kustomize 14 + name: hsmsecret-viewer-role 15 + rules: 16 + - apiGroups: 17 + - hsm.j5t.io 18 + resources: 19 + - hsmsecrets 20 + verbs: 21 + - get 22 + - list 23 + - watch 24 + - apiGroups: 25 + - hsm.j5t.io 26 + resources: 27 + - hsmsecrets/status 28 + verbs: 29 + - get
+31
config/rbac/kustomization.yaml
··· 1 + resources: 2 + # All RBAC will be applied under this service account in 3 + # the deployment namespace. You may comment out this resource 4 + # if your manager will use a service account that exists at 5 + # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 + # subjects if changing service account names. 7 + - service_account.yaml 8 + - role.yaml 9 + - role_binding.yaml 10 + - leader_election_role.yaml 11 + - leader_election_role_binding.yaml 12 + # The following RBAC configurations are used to protect 13 + # the metrics endpoint with authn/authz. These configurations 14 + # ensure that only authorized users and service accounts 15 + # can access the metrics endpoint. Comment the following 16 + # permissions if you want to disable this protection. 17 + # More info: https://book.kubebuilder.io/reference/metrics.html 18 + - metrics_auth_role.yaml 19 + - metrics_auth_role_binding.yaml 20 + - metrics_reader_role.yaml 21 + # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by 22 + # default, aiding admins in cluster management. Those roles are 23 + # not used by the hsm-secrets-operator itself. You can comment the following lines 24 + # if you do not want those helpers be installed with your Project. 25 + - hsmdevice_admin_role.yaml 26 + - hsmdevice_editor_role.yaml 27 + - hsmdevice_viewer_role.yaml 28 + - hsmsecret_admin_role.yaml 29 + - hsmsecret_editor_role.yaml 30 + - hsmsecret_viewer_role.yaml 31 +
+40
config/rbac/leader_election_role.yaml
··· 1 + # permissions to do leader election. 2 + apiVersion: rbac.authorization.k8s.io/v1 3 + kind: Role 4 + metadata: 5 + labels: 6 + app.kubernetes.io/name: hsm-secrets-operator 7 + app.kubernetes.io/managed-by: kustomize 8 + name: leader-election-role 9 + rules: 10 + - apiGroups: 11 + - "" 12 + resources: 13 + - configmaps 14 + verbs: 15 + - get 16 + - list 17 + - watch 18 + - create 19 + - update 20 + - patch 21 + - delete 22 + - apiGroups: 23 + - coordination.k8s.io 24 + resources: 25 + - leases 26 + verbs: 27 + - get 28 + - list 29 + - watch 30 + - create 31 + - update 32 + - patch 33 + - delete 34 + - apiGroups: 35 + - "" 36 + resources: 37 + - events 38 + verbs: 39 + - create 40 + - patch
+15
config/rbac/leader_election_role_binding.yaml
··· 1 + apiVersion: rbac.authorization.k8s.io/v1 2 + kind: RoleBinding 3 + metadata: 4 + labels: 5 + app.kubernetes.io/name: hsm-secrets-operator 6 + app.kubernetes.io/managed-by: kustomize 7 + name: leader-election-rolebinding 8 + roleRef: 9 + apiGroup: rbac.authorization.k8s.io 10 + kind: Role 11 + name: leader-election-role 12 + subjects: 13 + - kind: ServiceAccount 14 + name: controller-manager 15 + namespace: system
+17
config/rbac/metrics_auth_role.yaml
··· 1 + apiVersion: rbac.authorization.k8s.io/v1 2 + kind: ClusterRole 3 + metadata: 4 + name: metrics-auth-role 5 + rules: 6 + - apiGroups: 7 + - authentication.k8s.io 8 + resources: 9 + - tokenreviews 10 + verbs: 11 + - create 12 + - apiGroups: 13 + - authorization.k8s.io 14 + resources: 15 + - subjectaccessreviews 16 + verbs: 17 + - create
+12
config/rbac/metrics_auth_role_binding.yaml
··· 1 + apiVersion: rbac.authorization.k8s.io/v1 2 + kind: ClusterRoleBinding 3 + metadata: 4 + name: metrics-auth-rolebinding 5 + roleRef: 6 + apiGroup: rbac.authorization.k8s.io 7 + kind: ClusterRole 8 + name: metrics-auth-role 9 + subjects: 10 + - kind: ServiceAccount 11 + name: controller-manager 12 + namespace: system
+9
config/rbac/metrics_reader_role.yaml
··· 1 + apiVersion: rbac.authorization.k8s.io/v1 2 + kind: ClusterRole 3 + metadata: 4 + name: metrics-reader 5 + rules: 6 + - nonResourceURLs: 7 + - "/metrics" 8 + verbs: 9 + - get
+54
config/rbac/role.yaml
··· 1 + --- 2 + apiVersion: rbac.authorization.k8s.io/v1 3 + kind: ClusterRole 4 + metadata: 5 + name: manager-role 6 + rules: 7 + - apiGroups: 8 + - "" 9 + resources: 10 + - events 11 + verbs: 12 + - create 13 + - patch 14 + - apiGroups: 15 + - "" 16 + resources: 17 + - secrets 18 + verbs: 19 + - create 20 + - delete 21 + - get 22 + - list 23 + - patch 24 + - update 25 + - watch 26 + - apiGroups: 27 + - hsm.j5t.io 28 + resources: 29 + - hsmdevices 30 + - hsmsecrets 31 + verbs: 32 + - create 33 + - delete 34 + - get 35 + - list 36 + - patch 37 + - update 38 + - watch 39 + - apiGroups: 40 + - hsm.j5t.io 41 + resources: 42 + - hsmdevices/finalizers 43 + - hsmsecrets/finalizers 44 + verbs: 45 + - update 46 + - apiGroups: 47 + - hsm.j5t.io 48 + resources: 49 + - hsmdevices/status 50 + - hsmsecrets/status 51 + verbs: 52 + - get 53 + - patch 54 + - update
+15
config/rbac/role_binding.yaml
··· 1 + apiVersion: rbac.authorization.k8s.io/v1 2 + kind: ClusterRoleBinding 3 + metadata: 4 + labels: 5 + app.kubernetes.io/name: hsm-secrets-operator 6 + app.kubernetes.io/managed-by: kustomize 7 + name: manager-rolebinding 8 + roleRef: 9 + apiGroup: rbac.authorization.k8s.io 10 + kind: ClusterRole 11 + name: manager-role 12 + subjects: 13 + - kind: ServiceAccount 14 + name: controller-manager 15 + namespace: system
+8
config/rbac/service_account.yaml
··· 1 + apiVersion: v1 2 + kind: ServiceAccount 3 + metadata: 4 + labels: 5 + app.kubernetes.io/name: hsm-secrets-operator 6 + app.kubernetes.io/managed-by: kustomize 7 + name: controller-manager 8 + namespace: system
+138
config/samples/daemonset.yaml
··· 1 + apiVersion: apps/v1 2 + kind: DaemonSet 3 + metadata: 4 + name: hsm-device-discovery 5 + namespace: hsm-secrets-operator-system 6 + labels: 7 + app: hsm-device-discovery 8 + component: device-discovery 9 + spec: 10 + selector: 11 + matchLabels: 12 + app: hsm-device-discovery 13 + template: 14 + metadata: 15 + labels: 16 + app: hsm-device-discovery 17 + spec: 18 + serviceAccountName: hsm-secrets-operator-controller-manager 19 + hostNetwork: true 20 + priorityClassName: system-node-critical 21 + tolerations: 22 + - key: node-role.kubernetes.io/control-plane 23 + operator: Exists 24 + effect: NoSchedule 25 + - key: node-role.kubernetes.io/master 26 + operator: Exists 27 + effect: NoSchedule 28 + - key: CriticalAddonsOnly 29 + operator: Exists 30 + - operator: Exists 31 + effect: NoExecute 32 + - operator: Exists 33 + effect: NoSchedule 34 + containers: 35 + - name: device-discovery 36 + image: hsm-secrets-operator:latest 37 + imagePullPolicy: IfNotPresent 38 + command: 39 + - /manager 40 + args: 41 + - --config=controller_manager_config.yaml 42 + - --enable-device-discovery=true 43 + env: 44 + - name: NODE_NAME 45 + valueFrom: 46 + fieldRef: 47 + fieldPath: spec.nodeName 48 + - name: RUNTIME_NAMESPACE 49 + valueFrom: 50 + fieldRef: 51 + fieldPath: metadata.namespace 52 + - name: DEVICE_DISCOVERY_MODE 53 + value: "daemonset" 54 + securityContext: 55 + privileged: true 56 + runAsUser: 0 57 + runAsGroup: 0 58 + resources: 59 + requests: 60 + cpu: 50m 61 + memory: 64Mi 62 + limits: 63 + cpu: 200m 64 + memory: 128Mi 65 + volumeMounts: 66 + - name: device-plugin-path 67 + mountPath: /var/lib/kubelet/device-plugins 68 + - name: dev-path 69 + mountPath: /dev 70 + readOnly: true 71 + - name: sys-path 72 + mountPath: /sys 73 + readOnly: true 74 + - name: usb-devices 75 + mountPath: /proc/bus/usb 76 + readOnly: true 77 + - name: manager-config 78 + mountPath: /controller_manager_config.yaml 79 + subPath: controller_manager_config.yaml 80 + readOnly: true 81 + livenessProbe: 82 + httpGet: 83 + path: /healthz 84 + port: 8081 85 + initialDelaySeconds: 15 86 + periodSeconds: 20 87 + readinessProbe: 88 + httpGet: 89 + path: /readyz 90 + port: 8081 91 + initialDelaySeconds: 5 92 + periodSeconds: 10 93 + volumes: 94 + - name: device-plugin-path 95 + hostPath: 96 + path: /var/lib/kubelet/device-plugins 97 + type: DirectoryOrCreate 98 + - name: dev-path 99 + hostPath: 100 + path: /dev 101 + type: Directory 102 + - name: sys-path 103 + hostPath: 104 + path: /sys 105 + type: Directory 106 + - name: usb-devices 107 + hostPath: 108 + path: /proc/bus/usb 109 + type: DirectoryOrCreate 110 + - name: manager-config 111 + configMap: 112 + name: hsm-secrets-operator-manager-config 113 + terminationGracePeriodSeconds: 10 114 + --- 115 + apiVersion: v1 116 + kind: ConfigMap 117 + metadata: 118 + name: hsm-secrets-operator-manager-config 119 + namespace: hsm-secrets-operator-system 120 + data: 121 + controller_manager_config.yaml: | 122 + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 123 + kind: ControllerManagerConfig 124 + health: 125 + healthProbeBindAddress: :8081 126 + metrics: 127 + bindAddress: 127.0.0.1:8080 128 + webhook: 129 + port: 9443 130 + leaderElection: 131 + leaderElect: true 132 + resourceName: 64b68d60.j5t.io 133 + # HSM-specific configuration 134 + hsm: 135 + deviceDiscoveryInterval: 30s 136 + maxDevicesPerNode: 10 137 + enableUSBDiscovery: true 138 + enablePathDiscovery: true
+67
config/samples/hsm_v1alpha1_hsmdevice.yaml
··· 1 + apiVersion: hsm.j5t.io/v1alpha1 2 + kind: HSMDevice 3 + metadata: 4 + labels: 5 + app.kubernetes.io/name: hsm-secrets-operator 6 + app.kubernetes.io/managed-by: kustomize 7 + name: pico-hsm-discovery 8 + namespace: default 9 + spec: 10 + # Device type - automatically uses well-known USB specs 11 + deviceType: PicoHSM 12 + 13 + # Auto-discovery will use built-in USB specifications 14 + # For Pico HSM: vendorId: "20a0", productId: "4230" 15 + 16 + # Optional: Override USB specification 17 + # usb: 18 + # vendorId: "20a0" 19 + # productId: "4230" 20 + # serialNumber: "PICO123456" # Optional: target specific device 21 + 22 + # Optional: Path-based discovery instead of USB 23 + # devicePath: 24 + # path: "/dev/ttyUSB*" 25 + # permissions: "rw" 26 + 27 + # Optional: Node selector to limit discovery to specific nodes 28 + # nodeSelector: 29 + # kubernetes.io/hostname: "worker-node-1" 30 + 31 + # Optional: PKCS#11 library path 32 + pkcs11LibraryPath: "/usr/local/lib/libsc-hsm-pkcs11.so" 33 + 34 + # Maximum number of devices to discover (default: 10) 35 + maxDevices: 5 36 + --- 37 + apiVersion: hsm.j5t.io/v1alpha1 38 + kind: HSMDevice 39 + metadata: 40 + name: smartcard-hsm-discovery 41 + namespace: default 42 + spec: 43 + deviceType: SmartCardHSM 44 + 45 + # Use custom USB specification 46 + usb: 47 + vendorId: "04e6" 48 + productId: "5816" 49 + 50 + pkcs11LibraryPath: "/usr/lib/opensc-pkcs11.so" 51 + maxDevices: 3 52 + --- 53 + apiVersion: hsm.j5t.io/v1alpha1 54 + kind: HSMDevice 55 + metadata: 56 + name: generic-hsm-path-discovery 57 + namespace: default 58 + spec: 59 + deviceType: Generic 60 + 61 + # Use path-based discovery 62 + devicePath: 63 + path: "/dev/sc-hsm*" 64 + permissions: "rw" 65 + 66 + pkcs11LibraryPath: "/usr/lib/generic-pkcs11.so" 67 + maxDevices: 10
+23
config/samples/hsm_v1alpha1_hsmsecret.yaml
··· 1 + apiVersion: hsm.j5t.io/v1alpha1 2 + kind: HSMSecret 3 + metadata: 4 + labels: 5 + app.kubernetes.io/name: hsm-secrets-operator 6 + app.kubernetes.io/managed-by: kustomize 7 + name: hsmsecret-sample 8 + namespace: default 9 + spec: 10 + # HSM path where the secret is stored 11 + hsmPath: "secrets/default/test-secret" 12 + 13 + # Name of the Kubernetes Secret to create (defaults to HSMSecret name if not specified) 14 + secretName: "database-credentials" 15 + 16 + # Enable automatic synchronization between HSM and Kubernetes Secret 17 + autoSync: true 18 + 19 + # Synchronization interval in seconds (default: 300) 20 + syncInterval: 60 21 + 22 + # Type of Kubernetes Secret to create (default: Opaque) 23 + secretType: Opaque
+5
config/samples/kustomization.yaml
··· 1 + ## Append samples of your project ## 2 + resources: 3 + - hsm_v1alpha1_hsmsecret.yaml 4 + - hsm_v1alpha1_hsmdevice.yaml 5 + # +kubebuilder:scaffold:manifestskustomizesamples
+7
config/scorecard/bases/config.yaml
··· 1 + apiVersion: scorecard.operatorframework.io/v1alpha3 2 + kind: Configuration 3 + metadata: 4 + name: config 5 + stages: 6 + - parallel: true 7 + tests: []
+18
config/scorecard/kustomization.yaml
··· 1 + resources: 2 + - bases/config.yaml 3 + apiVersion: kustomize.config.k8s.io/v1beta1 4 + kind: Kustomization 5 + patches: 6 + - path: patches/basic.config.yaml 7 + target: 8 + group: scorecard.operatorframework.io 9 + kind: Configuration 10 + name: config 11 + version: v1alpha3 12 + - path: patches/olm.config.yaml 13 + target: 14 + group: scorecard.operatorframework.io 15 + kind: Configuration 16 + name: config 17 + version: v1alpha3 18 + # +kubebuilder:scaffold:patches
+10
config/scorecard/patches/basic.config.yaml
··· 1 + - op: add 2 + path: /stages/0/tests/- 3 + value: 4 + entrypoint: 5 + - scorecard-test 6 + - basic-check-spec 7 + image: quay.io/operator-framework/scorecard-test:v1.41.1 8 + labels: 9 + suite: basic 10 + test: basic-check-spec-test
+50
config/scorecard/patches/olm.config.yaml
··· 1 + - op: add 2 + path: /stages/0/tests/- 3 + value: 4 + entrypoint: 5 + - scorecard-test 6 + - olm-bundle-validation 7 + image: quay.io/operator-framework/scorecard-test:v1.41.1 8 + labels: 9 + suite: olm 10 + test: olm-bundle-validation-test 11 + - op: add 12 + path: /stages/0/tests/- 13 + value: 14 + entrypoint: 15 + - scorecard-test 16 + - olm-crds-have-validation 17 + image: quay.io/operator-framework/scorecard-test:v1.41.1 18 + labels: 19 + suite: olm 20 + test: olm-crds-have-validation-test 21 + - op: add 22 + path: /stages/0/tests/- 23 + value: 24 + entrypoint: 25 + - scorecard-test 26 + - olm-crds-have-resources 27 + image: quay.io/operator-framework/scorecard-test:v1.41.1 28 + labels: 29 + suite: olm 30 + test: olm-crds-have-resources-test 31 + - op: add 32 + path: /stages/0/tests/- 33 + value: 34 + entrypoint: 35 + - scorecard-test 36 + - olm-spec-descriptors 37 + image: quay.io/operator-framework/scorecard-test:v1.41.1 38 + labels: 39 + suite: olm 40 + test: olm-spec-descriptors-test 41 + - op: add 42 + path: /stages/0/tests/- 43 + value: 44 + entrypoint: 45 + - scorecard-test 46 + - olm-status-descriptors 47 + image: quay.io/operator-framework/scorecard-test:v1.41.1 48 + labels: 49 + suite: olm 50 + test: olm-status-descriptors-test
+106
examples/README.md
··· 1 + # HSM Secrets Operator Examples 2 + 3 + This directory contains practical examples demonstrating how to use the HSM Secrets Operator in various scenarios. 4 + 5 + ## Directory Structure 6 + 7 + - **[basic/](basic/)** - Basic usage examples for getting started 8 + - **[advanced/](advanced/)** - Advanced configurations and use cases 9 + - **[api/](api/)** - REST API usage examples 10 + - **[high-availability/](high-availability/)** - High availability and mirroring setups 11 + 12 + ## Quick Start 13 + 14 + 1. **Install the Operator** 15 + ```bash 16 + # Install CRDs 17 + kubectl apply -f config/crd/bases/ 18 + 19 + # Deploy the operator 20 + kubectl apply -f config/default/ 21 + ``` 22 + 23 + 2. **Create your first HSM Device** 24 + ```bash 25 + kubectl apply -f examples/basic/pico-hsm-device.yaml 26 + ``` 27 + 28 + 3. **Create an HSM Secret** 29 + ```bash 30 + kubectl apply -f examples/basic/database-secret.yaml 31 + ``` 32 + 33 + 4. **Use the REST API** 34 + ```bash 35 + # Check health 36 + curl http://localhost:8090/api/v1/health 37 + 38 + # Create a secret via API 39 + curl -X POST http://localhost:8090/api/v1/hsm/secrets \ 40 + -H "Content-Type: application/json" \ 41 + -d @examples/api/create-secret.json 42 + ``` 43 + 44 + ## Prerequisites 45 + 46 + - Kubernetes cluster (v1.20+) 47 + - Pico HSM or compatible PKCS#11 device 48 + - OpenSC libraries installed on nodes with HSM devices 49 + 50 + ## Common Use Cases 51 + 52 + ### 1. Database Credentials 53 + Store and rotate database credentials securely using HSM hardware protection. 54 + → See [basic/database-secret.yaml](basic/database-secret.yaml) 55 + 56 + ### 2. TLS Certificates 57 + Manage TLS certificates with automatic sync to Kubernetes Secrets. 58 + → See [basic/tls-certificate.yaml](basic/tls-certificate.yaml) 59 + 60 + ### 3. API Keys 61 + Store third-party API keys with hardware-based security. 62 + → See [basic/api-keys.yaml](basic/api-keys.yaml) 63 + 64 + ### 4. High Availability Setup 65 + Configure cross-node mirroring for fault tolerance. 66 + → See [high-availability/](high-availability/) 67 + 68 + ### 5. Import Existing Secrets 69 + Migrate existing Kubernetes Secrets to HSM storage. 70 + → See [api/import-from-k8s.sh](api/import-from-k8s.sh) 71 + 72 + ## Security Considerations 73 + 74 + - HSM devices should be properly authenticated and configured 75 + - Use RBAC to control access to HSMSecret resources 76 + - Enable audit logging for secret operations 77 + - Regular backup of HSM configurations (not the secrets themselves) 78 + 79 + ## Troubleshooting 80 + 81 + Common issues and solutions: 82 + 83 + 1. **HSM Device Not Found** 84 + - Check USB connection and permissions 85 + - Verify OpenSC installation 86 + - Review HSMDevice status: `kubectl describe hsmdevice` 87 + 88 + 2. **Sync Failures** 89 + - Check HSM connectivity 90 + - Verify PKCS#11 library path 91 + - Review controller logs: `kubectl logs -n hsm-secrets-operator-system` 92 + 93 + 3. **API Server Issues** 94 + - Confirm API is enabled: `--enable-api=true` 95 + - Check port availability: `--api-port=8090` 96 + - Review API server logs 97 + 98 + ## Contributing 99 + 100 + Found an issue or have a suggestion? Please open an issue or submit a pull request. 101 + 102 + ## Additional Resources 103 + 104 + - [Operator Documentation](../README.md) 105 + - [API Reference](../internal/api/types.go) 106 + - [PKCS#11 Guide](https://www.opendnssec.org/softhsm/)
+118
examples/advanced/README.md
··· 1 + # Advanced Examples 2 + 3 + This directory contains advanced configuration examples for complex use cases. 4 + 5 + ## Examples Overview 6 + 7 + 1. **[custom-discovery.yaml](custom-discovery.yaml)** - Custom USB device discovery 8 + 2. **[multi-environment.yaml](multi-environment.yaml)** - Multi-environment secret management 9 + 3. **[secret-rotation.yaml](secret-rotation.yaml)** - Automated secret rotation 10 + 4. **[monitoring.yaml](monitoring.yaml)** - Prometheus monitoring setup 11 + 12 + ## Advanced Use Cases 13 + 14 + ### Custom Device Discovery 15 + 16 + Configure HSM device discovery for non-standard devices or custom paths: 17 + 18 + ```yaml 19 + # Custom USB device 20 + usb: 21 + vendorId: "1234" 22 + productId: "5678" 23 + serialNumber: "CUSTOM-HSM-001" 24 + 25 + # Custom device path 26 + devicePath: 27 + path: "/dev/custom-hsm*" 28 + permissions: "0600" 29 + ``` 30 + 31 + ### Multi-Environment Management 32 + 33 + Organize secrets across different environments with proper isolation: 34 + 35 + - Development secrets in `dev` namespace 36 + - Staging secrets in `staging` namespace 37 + - Production secrets in `production` namespace 38 + - Shared secrets with proper RBAC controls 39 + 40 + ### Secret Rotation 41 + 42 + Implement automated secret rotation workflows: 43 + 44 + - Database password rotation with zero downtime 45 + - API key rotation with gradual rollout 46 + - Certificate renewal with automatic deployment 47 + 48 + ### Monitoring and Alerting 49 + 50 + Set up comprehensive monitoring: 51 + 52 + - HSM device health monitoring 53 + - Secret sync status tracking 54 + - Performance metrics collection 55 + - Alert rules for failures 56 + 57 + ## Advanced Configuration 58 + 59 + ### Node Affinity 60 + 61 + Deploy HSM devices only on specific nodes: 62 + 63 + ```yaml 64 + nodeSelector: 65 + hsm.j5t.io/hardware: "nitrokey" 66 + kubernetes.io/arch: "amd64" 67 + node-role.kubernetes.io/worker: "" 68 + ``` 69 + 70 + ### Resource Limits 71 + 72 + Configure resource limits for the operator: 73 + 74 + ```yaml 75 + resources: 76 + requests: 77 + cpu: 100m 78 + memory: 128Mi 79 + limits: 80 + cpu: 500m 81 + memory: 256Mi 82 + ``` 83 + 84 + ### Security Contexts 85 + 86 + Run with minimal privileges: 87 + 88 + ```yaml 89 + securityContext: 90 + runAsNonRoot: true 91 + runAsUser: 1000 92 + fsGroup: 2000 93 + capabilities: 94 + drop: 95 + - ALL 96 + ``` 97 + 98 + ## Best Practices 99 + 100 + ### 1. Namespace Isolation 101 + - Use separate namespaces for different environments 102 + - Apply NetworkPolicies to restrict access 103 + - Use RBAC to control HSMSecret access 104 + 105 + ### 2. Secret Lifecycle 106 + - Plan for secret rotation and expiration 107 + - Monitor secret age and usage 108 + - Implement backup and recovery procedures 109 + 110 + ### 3. Monitoring 111 + - Track HSM device health 112 + - Monitor sync failures and delays 113 + - Set up alerting for critical issues 114 + 115 + ### 4. Security 116 + - Use least privilege access principles 117 + - Regular security audits of HSM configurations 118 + - Proper key management procedures
+360
examples/advanced/custom-library-guide.md
··· 1 + # Custom PKCS#11 Library Integration Guide 2 + 3 + This guide shows different approaches to integrate custom PKCS#11 libraries with the HSM Secrets Operator. 4 + 5 + ## Overview 6 + 7 + The operator needs access to PKCS#11 libraries to communicate with HSM devices. While common libraries like OpenSC are included by default, custom or vendor-specific libraries require additional setup. 8 + 9 + ## Available Approaches 10 + 11 + ### Method 1: Simple Configuration (Easiest) 12 + **Best for**: Standard libraries already available on nodes 13 + 14 + Simply specify the library path in your HSMDevice: 15 + 16 + ```yaml 17 + apiVersion: hsm.j5t.io/v1alpha1 18 + kind: HSMDevice 19 + metadata: 20 + name: my-hsm 21 + spec: 22 + deviceType: Generic 23 + pkcs11LibraryPath: "/usr/local/lib/libmyhsm.so" 24 + ``` 25 + 26 + **Requirements**: 27 + - Library must be pre-installed on all nodes 28 + - Same path on all nodes 29 + - Proper permissions (readable by operator) 30 + 31 + --- 32 + 33 + ### Method 2: Init Container (Recommended) 34 + **Best for**: Libraries that can be downloaded/installed at runtime 35 + 36 + ```yaml 37 + initContainers: 38 + - name: install-pkcs11 39 + image: alpine:latest 40 + command: ["/install-library.sh"] 41 + volumeMounts: 42 + - name: shared-libs 43 + mountPath: /shared 44 + ``` 45 + 46 + **Advantages**: 47 + - Self-contained deployment 48 + - Version controlled libraries 49 + - Works with custom images 50 + 51 + **Use cases**: 52 + - Vendor libraries available via download 53 + - Custom compiled libraries 54 + - Version-specific library requirements 55 + 56 + --- 57 + 58 + ### Method 3: Sidecar Container (Advanced) 59 + **Best for**: Complex library management or multiple libraries 60 + 61 + ```yaml 62 + containers: 63 + - name: pkcs11-provider 64 + image: custom-pkcs11-provider:latest 65 + volumeMounts: 66 + - name: pkcs11-libs 67 + mountPath: /shared-libs 68 + ``` 69 + 70 + **Advantages**: 71 + - Dedicated library management 72 + - Hot-swappable libraries 73 + - Isolation from main operator 74 + 75 + **Use cases**: 76 + - Multiple vendor libraries 77 + - Libraries requiring specific runtime environments 78 + - Complex licensing scenarios 79 + 80 + --- 81 + 82 + ### Method 4: DaemonSet Installation (Node-level) 83 + **Best for**: System-level library installation 84 + 85 + ```yaml 86 + apiVersion: apps/v1 87 + kind: DaemonSet 88 + metadata: 89 + name: pkcs11-installer 90 + spec: 91 + template: 92 + spec: 93 + hostNetwork: true 94 + containers: 95 + - name: installer 96 + securityContext: 97 + privileged: true 98 + ``` 99 + 100 + **Advantages**: 101 + - Libraries available to all pods on node 102 + - Persistent across pod restarts 103 + - System-level integration 104 + 105 + **Use cases**: 106 + - System drivers required 107 + - Shared across multiple applications 108 + - Hardware-specific installations 109 + 110 + ## Implementation Examples 111 + 112 + ### Example 1: YubiKey Integration 113 + 114 + ```yaml 115 + apiVersion: hsm.j5t.io/v1alpha1 116 + kind: HSMDevice 117 + metadata: 118 + name: yubikey-hsm 119 + spec: 120 + deviceType: Generic 121 + usb: 122 + vendorId: "1050" # Yubico 123 + productId: "0407" # YubiKey 4/5 Series 124 + pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/libykcs11.so.1" 125 + nodeSelector: 126 + yubikey.enabled: "true" 127 + ``` 128 + 129 + ### Example 2: SoftHSM (Software HSM) 130 + 131 + ```yaml 132 + apiVersion: hsm.j5t.io/v1alpha1 133 + kind: HSMDevice 134 + metadata: 135 + name: softhsm 136 + spec: 137 + deviceType: Generic 138 + devicePath: 139 + path: "/var/lib/softhsm/tokens/*" 140 + permissions: "0644" 141 + pkcs11LibraryPath: "/usr/lib/softhsm/libsofthsm2.so" 142 + ``` 143 + 144 + ### Example 3: Custom Vendor HSM 145 + 146 + ```yaml 147 + # ConfigMap with vendor-specific configuration 148 + apiVersion: v1 149 + kind: ConfigMap 150 + metadata: 151 + name: vendor-hsm-config 152 + data: 153 + library-path: "/opt/vendor-hsm/lib/libvendor-pkcs11.so" 154 + slot-config: | 155 + slot_0 = /dev/vendor-hsm0 156 + slot_1 = /dev/vendor-hsm1 157 + 158 + --- 159 + apiVersion: hsm.j5t.io/v1alpha1 160 + kind: HSMDevice 161 + metadata: 162 + name: vendor-hsm 163 + spec: 164 + deviceType: Generic 165 + usb: 166 + vendorId: "ABCD" 167 + productId: "1234" 168 + pkcs11LibraryPath: "/opt/vendor-hsm/lib/libvendor-pkcs11.so" 169 + nodeSelector: 170 + vendor-hsm.installed: "true" 171 + ``` 172 + 173 + ## Custom Container Images 174 + 175 + ### Building a Custom Operator Image 176 + 177 + ```dockerfile 178 + # Dockerfile for custom operator with additional libraries 179 + FROM hsm-secrets-operator:latest 180 + 181 + # Install additional PKCS#11 libraries 182 + RUN apt-get update && apt-get install -y \ 183 + libykcs11-1 \ # YubiKey library 184 + softhsm2 \ # SoftHSM 185 + opensc-pkcs11 \ # OpenSC (if not included) 186 + && apt-get clean 187 + 188 + # Copy custom vendor libraries 189 + COPY vendor-libs/* /usr/local/lib/ 190 + RUN ldconfig 191 + 192 + # Copy custom configurations 193 + COPY pkcs11-configs/* /etc/pkcs11/ 194 + 195 + USER 65532:65532 196 + ENTRYPOINT ["/manager"] 197 + ``` 198 + 199 + ### Building Library Provider Sidecar 200 + 201 + ```dockerfile 202 + # Dockerfile for PKCS#11 library provider sidecar 203 + FROM alpine:latest 204 + 205 + # Install vendor-specific libraries 206 + RUN apk add --no-cache wget unzip 207 + 208 + # Download vendor library 209 + RUN wget -O /tmp/vendor-lib.zip https://vendor.com/pkcs11-lib.zip && \ 210 + unzip /tmp/vendor-lib.zip -d /vendor-libs && \ 211 + chmod 755 /vendor-libs/*.so 212 + 213 + # Copy script to provide libraries 214 + COPY provide-libs.sh /usr/local/bin/ 215 + RUN chmod +x /usr/local/bin/provide-libs.sh 216 + 217 + ENTRYPOINT ["/usr/local/bin/provide-libs.sh"] 218 + ``` 219 + 220 + ## Configuration Patterns 221 + 222 + ### Environment-Specific Libraries 223 + 224 + ```yaml 225 + # Development environment 226 + apiVersion: hsm.j5t.io/v1alpha1 227 + kind: HSMDevice 228 + metadata: 229 + name: dev-hsm 230 + namespace: development 231 + spec: 232 + deviceType: Generic 233 + pkcs11LibraryPath: "/usr/lib/softhsm/libsofthsm2.so" # Software HSM for dev 234 + 235 + --- 236 + # Production environment 237 + apiVersion: hsm.j5t.io/v1alpha1 238 + kind: HSMDevice 239 + metadata: 240 + name: prod-hsm 241 + namespace: production 242 + spec: 243 + deviceType: PicoHSM 244 + pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so" # Hardware HSM 245 + ``` 246 + 247 + ### Multi-Vendor Support 248 + 249 + ```yaml 250 + # HSM Device for Vendor A 251 + apiVersion: hsm.j5t.io/v1alpha1 252 + kind: HSMDevice 253 + metadata: 254 + name: vendor-a-hsm 255 + spec: 256 + deviceType: Generic 257 + usb: 258 + vendorId: "1111" 259 + productId: "AAAA" 260 + pkcs11LibraryPath: "/opt/vendor-a/lib/libvendor-a-pkcs11.so" 261 + nodeSelector: 262 + hsm.vendor: "vendor-a" 263 + 264 + --- 265 + # HSM Device for Vendor B 266 + apiVersion: hsm.j5t.io/v1alpha1 267 + kind: HSMDevice 268 + metadata: 269 + name: vendor-b-hsm 270 + spec: 271 + deviceType: Generic 272 + usb: 273 + vendorId: "2222" 274 + productId: "BBBB" 275 + pkcs11LibraryPath: "/opt/vendor-b/lib/libvendor-b-pkcs11.so" 276 + nodeSelector: 277 + hsm.vendor: "vendor-b" 278 + ``` 279 + 280 + ## Troubleshooting 281 + 282 + ### Common Issues 283 + 284 + 1. **Library Not Found** 285 + ```bash 286 + # Check if library exists 287 + ls -la /path/to/library.so 288 + 289 + # Check library dependencies 290 + ldd /path/to/library.so 291 + 292 + # Test library loading 293 + pkcs11-tool --module /path/to/library.so --list-slots 294 + ``` 295 + 296 + 2. **Permission Issues** 297 + ```bash 298 + # Check file permissions 299 + ls -la /path/to/library.so 300 + 301 + # Fix permissions if needed 302 + chmod 755 /path/to/library.so 303 + 304 + # Check SELinux context (if applicable) 305 + ls -Z /path/to/library.so 306 + ``` 307 + 308 + 3. **Library Compatibility** 309 + ```bash 310 + # Check architecture 311 + file /path/to/library.so 312 + 313 + # Check for missing symbols 314 + nm -D /path/to/library.so | grep C_GetFunctionList 315 + 316 + # Test basic functionality 317 + pkcs11-tool --module /path/to/library.so --list-mechanisms 318 + ``` 319 + 320 + ### Debug Commands 321 + 322 + ```bash 323 + # Check HSMDevice status 324 + kubectl describe hsmdevice my-hsm 325 + 326 + # Check operator logs 327 + kubectl logs -n hsm-secrets-operator-system deployment/hsm-secrets-operator-controller-manager 328 + 329 + # Test PKCS#11 library directly 330 + kubectl exec -it hsm-operator-pod -- pkcs11-tool --module /path/to/library.so --list-slots 331 + 332 + # Check library loading in container 333 + kubectl exec -it hsm-operator-pod -- ldd /path/to/library.so 334 + ``` 335 + 336 + ## Best Practices 337 + 338 + ### Security 339 + - Use minimal container images with only required libraries 340 + - Verify library checksums/signatures 341 + - Use read-only mounts where possible 342 + - Apply least privilege principles 343 + 344 + ### Performance 345 + - Pre-install libraries in base images when possible 346 + - Use shared volumes for multiple containers 347 + - Cache downloaded libraries 348 + - Minimize init container overhead 349 + 350 + ### Maintenance 351 + - Version pin all library dependencies 352 + - Test library compatibility before deployment 353 + - Monitor library load times and errors 354 + - Keep backup copies of working libraries 355 + 356 + ### Testing 357 + - Test library loading in isolation 358 + - Validate all PKCS#11 operations 359 + - Test failover scenarios 360 + - Verify performance under load
+379
examples/advanced/custom-pkcs11-library.yaml
··· 1 + # Custom PKCS#11 Library Configuration Examples 2 + 3 + --- 4 + # Method 1: ConfigMap for library path configuration 5 + apiVersion: v1 6 + kind: ConfigMap 7 + metadata: 8 + name: hsm-library-config 9 + namespace: hsm-secrets-operator-system 10 + data: 11 + # Different library paths for different vendors 12 + opensc-library.conf: | 13 + # OpenSC library (default) 14 + /usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so 15 + 16 + softhsm-library.conf: | 17 + # SoftHSM library 18 + /usr/lib/softhsm/libsofthsm2.so 19 + 20 + yubico-library.conf: | 21 + # YubiKey PKCS#11 library 22 + /usr/lib/x86_64-linux-gnu/libykcs11.so.1 23 + 24 + nitrokey-library.conf: | 25 + # Nitrokey PKCS#11 library 26 + /usr/lib/nitrokey/libnitrokey-pkcs11.so 27 + 28 + custom-vendor.conf: | 29 + # Custom vendor library 30 + /opt/custom-hsm/lib/libcustomhsm-pkcs11.so 31 + 32 + --- 33 + # HSMDevice using different library paths 34 + apiVersion: hsm.j5t.io/v1alpha1 35 + kind: HSMDevice 36 + metadata: 37 + name: custom-hsm-device 38 + namespace: default 39 + labels: 40 + vendor: custom-vendor 41 + spec: 42 + deviceType: Generic # Use Generic for custom devices 43 + 44 + # Custom USB device 45 + usb: 46 + vendorId: "1234" # Your vendor ID 47 + productId: "5678" # Your product ID 48 + # serialNumber: "CUSTOM-001" # Optional specific device 49 + 50 + # Custom PKCS#11 library path 51 + pkcs11LibraryPath: "/opt/custom-hsm/lib/libcustomhsm-pkcs11.so" 52 + 53 + nodeSelector: 54 + hsm.vendor: "custom-vendor" # Only deploy on nodes with this vendor 55 + 56 + --- 57 + # Alternative: Path-based discovery for custom device paths 58 + apiVersion: hsm.j5t.io/v1alpha1 59 + kind: HSMDevice 60 + metadata: 61 + name: path-based-hsm 62 + namespace: default 63 + spec: 64 + deviceType: Generic 65 + 66 + # Use device path instead of USB discovery 67 + devicePath: 68 + path: "/dev/custom-hsm*" 69 + permissions: "0666" 70 + 71 + # Custom library path 72 + pkcs11LibraryPath: "/usr/local/lib/libcustomhsm.so" 73 + 74 + nodeSelector: 75 + custom-hsm.enabled: "true" 76 + 77 + --- 78 + # Method 2: Init container to install custom libraries 79 + apiVersion: apps/v1 80 + kind: Deployment 81 + metadata: 82 + name: hsm-operator-with-custom-lib 83 + namespace: hsm-secrets-operator-system 84 + spec: 85 + replicas: 1 86 + selector: 87 + matchLabels: 88 + app: hsm-operator-custom 89 + template: 90 + metadata: 91 + labels: 92 + app: hsm-operator-custom 93 + spec: 94 + # Init container to install custom PKCS#11 library 95 + initContainers: 96 + - name: install-custom-pkcs11 97 + image: alpine:latest 98 + command: 99 + - sh 100 + - -c 101 + - | 102 + # Download and install custom PKCS#11 library 103 + echo "Installing custom PKCS#11 library..." 104 + 105 + # Example: Download from vendor 106 + # wget -O /shared/libcustomhsm.so https://vendor.com/lib/libcustomhsm.so 107 + 108 + # Or copy from mounted volume 109 + if [ -f /vendor-libs/libcustomhsm.so ]; then 110 + cp /vendor-libs/libcustomhsm.so /shared/ 111 + chmod 755 /shared/libcustomhsm.so 112 + echo "Custom library installed at /shared/libcustomhsm.so" 113 + fi 114 + 115 + # Set permissions 116 + ls -la /shared/ 117 + 118 + volumeMounts: 119 + - name: shared-libs 120 + mountPath: /shared 121 + - name: vendor-libs 122 + mountPath: /vendor-libs 123 + readOnly: true 124 + 125 + securityContext: 126 + runAsUser: 0 # Need root to install libraries 127 + 128 + containers: 129 + - name: manager 130 + image: controller:latest 131 + command: 132 + - /manager 133 + args: 134 + - --leader-elect 135 + - --metrics-bind-address=127.0.0.1:8080 136 + 137 + # Environment variable for custom library path 138 + env: 139 + - name: CUSTOM_PKCS11_PATH 140 + value: "/shared/libcustomhsm.so" 141 + 142 + volumeMounts: 143 + - name: shared-libs 144 + mountPath: /shared 145 + readOnly: true 146 + 147 + securityContext: 148 + allowPrivilegeEscalation: false 149 + capabilities: 150 + drop: 151 + - ALL 152 + 153 + volumes: 154 + - name: shared-libs 155 + emptyDir: {} 156 + - name: vendor-libs 157 + configMap: 158 + name: vendor-library-files 159 + defaultMode: 0755 160 + 161 + --- 162 + # Method 3: Sidecar container approach 163 + apiVersion: apps/v1 164 + kind: Deployment 165 + metadata: 166 + name: hsm-operator-with-sidecar 167 + namespace: hsm-secrets-operator-system 168 + spec: 169 + replicas: 1 170 + selector: 171 + matchLabels: 172 + app: hsm-operator-sidecar 173 + template: 174 + metadata: 175 + labels: 176 + app: hsm-operator-sidecar 177 + spec: 178 + containers: 179 + # Main operator container 180 + - name: manager 181 + image: controller:latest 182 + command: 183 + - /manager 184 + args: 185 + - --leader-elect 186 + 187 + volumeMounts: 188 + - name: pkcs11-libs 189 + mountPath: /usr/local/lib/pkcs11 190 + readOnly: true 191 + 192 + env: 193 + - name: LD_LIBRARY_PATH 194 + value: "/usr/local/lib/pkcs11:$LD_LIBRARY_PATH" 195 + 196 + # Sidecar container providing PKCS#11 libraries 197 + - name: pkcs11-provider 198 + image: custom-pkcs11-provider:latest 199 + command: 200 + - sh 201 + - -c 202 + - | 203 + # Copy libraries to shared volume and keep container running 204 + cp -r /vendor-libs/* /shared-libs/ 205 + echo "PKCS#11 libraries provided" 206 + # Keep container running 207 + tail -f /dev/null 208 + 209 + volumeMounts: 210 + - name: pkcs11-libs 211 + mountPath: /shared-libs 212 + 213 + # Resource limits for sidecar 214 + resources: 215 + requests: 216 + cpu: 10m 217 + memory: 16Mi 218 + limits: 219 + cpu: 50m 220 + memory: 32Mi 221 + 222 + volumes: 223 + - name: pkcs11-libs 224 + emptyDir: {} 225 + 226 + --- 227 + # Method 4: DaemonSet to install libraries on nodes 228 + apiVersion: apps/v1 229 + kind: DaemonSet 230 + metadata: 231 + name: custom-pkcs11-installer 232 + namespace: hsm-secrets-operator-system 233 + labels: 234 + app: pkcs11-installer 235 + spec: 236 + selector: 237 + matchLabels: 238 + app: pkcs11-installer 239 + template: 240 + metadata: 241 + labels: 242 + app: pkcs11-installer 243 + spec: 244 + # Run on nodes with custom HSM hardware 245 + nodeSelector: 246 + hsm.vendor: "custom-vendor" 247 + 248 + hostNetwork: true 249 + hostPID: true 250 + 251 + containers: 252 + - name: installer 253 + image: alpine:latest 254 + command: 255 + - sh 256 + - -c 257 + - | 258 + echo "Installing custom PKCS#11 library on node..." 259 + 260 + # Install to host filesystem (requires privileged access) 261 + if [ ! -f /host/usr/local/lib/libcustomhsm.so ]; then 262 + # Copy from container to host 263 + cp /vendor-libs/libcustomhsm.so /host/usr/local/lib/ 264 + chmod 755 /host/usr/local/lib/libcustomhsm.so 265 + 266 + # Update ldconfig on host 267 + chroot /host ldconfig 268 + 269 + echo "Custom PKCS#11 library installed" 270 + else 271 + echo "Library already installed" 272 + fi 273 + 274 + # Keep container running to maintain installation 275 + sleep infinity 276 + 277 + volumeMounts: 278 + - name: host-root 279 + mountPath: /host 280 + - name: vendor-libs 281 + mountPath: /vendor-libs 282 + readOnly: true 283 + 284 + securityContext: 285 + privileged: true # Required to modify host filesystem 286 + 287 + resources: 288 + requests: 289 + cpu: 10m 290 + memory: 32Mi 291 + limits: 292 + cpu: 100m 293 + memory: 64Mi 294 + 295 + volumes: 296 + - name: host-root 297 + hostPath: 298 + path: / 299 + - name: vendor-libs 300 + configMap: 301 + name: custom-pkcs11-libraries 302 + defaultMode: 0755 303 + 304 + --- 305 + # ConfigMap containing the actual library files (base64 encoded) 306 + apiVersion: v1 307 + kind: ConfigMap 308 + metadata: 309 + name: custom-pkcs11-libraries 310 + namespace: hsm-secrets-operator-system 311 + binaryData: 312 + # Base64 encoded library file - replace with actual library 313 + libcustomhsm.so: <base64-encoded-library-file> 314 + 315 + --- 316 + # Node labeling job to identify HSM-capable nodes 317 + apiVersion: batch/v1 318 + kind: Job 319 + metadata: 320 + name: hsm-node-labeler 321 + namespace: hsm-secrets-operator-system 322 + spec: 323 + template: 324 + spec: 325 + serviceAccountName: hsm-node-labeler 326 + containers: 327 + - name: labeler 328 + image: bitnami/kubectl:latest 329 + command: 330 + - sh 331 + - -c 332 + - | 333 + # Detect nodes with custom HSM hardware 334 + for node in $(kubectl get nodes -o name); do 335 + node_name=$(echo $node | cut -d/ -f2) 336 + echo "Checking node: $node_name" 337 + 338 + # Check if node has custom HSM (this is vendor-specific) 339 + # Example: check for specific USB devices 340 + if kubectl get node $node_name -o jsonpath='{.status.allocatable}' | grep -q "custom-hsm"; then 341 + echo "Labeling $node_name with custom HSM" 342 + kubectl label node $node_name hsm.vendor=custom-vendor --overwrite 343 + kubectl label node $node_name custom-hsm.enabled=true --overwrite 344 + fi 345 + done 346 + 347 + restartPolicy: OnFailure 348 + 349 + --- 350 + # RBAC for node labeling job 351 + apiVersion: v1 352 + kind: ServiceAccount 353 + metadata: 354 + name: hsm-node-labeler 355 + namespace: hsm-secrets-operator-system 356 + 357 + --- 358 + apiVersion: rbac.authorization.k8s.io/v1 359 + kind: ClusterRole 360 + metadata: 361 + name: hsm-node-labeler 362 + rules: 363 + - apiGroups: [""] 364 + resources: ["nodes"] 365 + verbs: ["get", "list", "patch", "update"] 366 + 367 + --- 368 + apiVersion: rbac.authorization.k8s.io/v1 369 + kind: ClusterRoleBinding 370 + metadata: 371 + name: hsm-node-labeler 372 + roleRef: 373 + apiGroup: rbac.authorization.k8s.io 374 + kind: ClusterRole 375 + name: hsm-node-labeler 376 + subjects: 377 + - kind: ServiceAccount 378 + name: hsm-node-labeler 379 + namespace: hsm-secrets-operator-system
+230
examples/advanced/multi-environment.yaml
··· 1 + # Multi-Environment Secret Management 2 + # This example shows how to organize secrets across different environments 3 + 4 + --- 5 + # Development Environment 6 + apiVersion: v1 7 + kind: Namespace 8 + metadata: 9 + name: development 10 + labels: 11 + environment: development 12 + hsm.j5t.io/enabled: "true" 13 + 14 + --- 15 + apiVersion: hsm.j5t.io/v1alpha1 16 + kind: HSMSecret 17 + metadata: 18 + name: database-credentials 19 + namespace: development 20 + labels: 21 + app: myapp 22 + environment: development 23 + spec: 24 + hsmPath: "secrets/development/database-credentials" 25 + secretName: "database-credentials" 26 + autoSync: true 27 + syncInterval: 300 # 5 minutes - frequent sync for dev 28 + 29 + --- 30 + # Staging Environment 31 + apiVersion: v1 32 + kind: Namespace 33 + metadata: 34 + name: staging 35 + labels: 36 + environment: staging 37 + hsm.j5t.io/enabled: "true" 38 + 39 + --- 40 + apiVersion: hsm.j5t.io/v1alpha1 41 + kind: HSMSecret 42 + metadata: 43 + name: database-credentials 44 + namespace: staging 45 + labels: 46 + app: myapp 47 + environment: staging 48 + spec: 49 + hsmPath: "secrets/staging/database-credentials" 50 + secretName: "database-credentials" 51 + autoSync: true 52 + syncInterval: 600 # 10 minutes 53 + 54 + --- 55 + # Production Environment 56 + apiVersion: v1 57 + kind: Namespace 58 + metadata: 59 + name: production 60 + labels: 61 + environment: production 62 + hsm.j5t.io/enabled: "true" 63 + security.level: "high" 64 + 65 + --- 66 + apiVersion: hsm.j5t.io/v1alpha1 67 + kind: HSMSecret 68 + metadata: 69 + name: database-credentials 70 + namespace: production 71 + labels: 72 + app: myapp 73 + environment: production 74 + security.level: "high" 75 + annotations: 76 + hsm.j5t.io/description: "Production database credentials - HIGH SECURITY" 77 + spec: 78 + hsmPath: "secrets/production/database-credentials" 79 + secretName: "database-credentials" 80 + autoSync: true 81 + syncInterval: 1800 # 30 minutes - less frequent for stability 82 + 83 + --- 84 + # Shared Secrets Namespace 85 + apiVersion: v1 86 + kind: Namespace 87 + metadata: 88 + name: shared-secrets 89 + labels: 90 + purpose: shared 91 + hsm.j5t.io/enabled: "true" 92 + 93 + --- 94 + # Shared TLS Certificate (used across environments) 95 + apiVersion: hsm.j5t.io/v1alpha1 96 + kind: HSMSecret 97 + metadata: 98 + name: wildcard-tls-cert 99 + namespace: shared-secrets 100 + labels: 101 + type: tls 102 + scope: global 103 + spec: 104 + hsmPath: "secrets/shared/wildcard-example-com-tls" 105 + secretName: "wildcard-tls" 106 + autoSync: true 107 + syncInterval: 86400 # Daily sync for certificates 108 + secretType: kubernetes.io/tls 109 + 110 + --- 111 + # RBAC Configuration for Environment Isolation 112 + 113 + # Development team access 114 + apiVersion: rbac.authorization.k8s.io/v1 115 + kind: Role 116 + metadata: 117 + namespace: development 118 + name: dev-hsm-secrets-access 119 + rules: 120 + - apiGroups: ["hsm.j5t.io"] 121 + resources: ["hsmsecrets"] 122 + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 123 + - apiGroups: [""] 124 + resources: ["secrets"] 125 + verbs: ["get", "list", "watch"] 126 + 127 + --- 128 + apiVersion: rbac.authorization.k8s.io/v1 129 + kind: RoleBinding 130 + metadata: 131 + name: dev-team-hsm-access 132 + namespace: development 133 + subjects: 134 + - kind: Group 135 + name: development-team 136 + apiGroup: rbac.authorization.k8s.io 137 + roleRef: 138 + kind: Role 139 + name: dev-hsm-secrets-access 140 + apiGroup: rbac.authorization.k8s.io 141 + 142 + --- 143 + # Production team access (more restricted) 144 + apiVersion: rbac.authorization.k8s.io/v1 145 + kind: Role 146 + metadata: 147 + namespace: production 148 + name: prod-hsm-secrets-readonly 149 + rules: 150 + - apiGroups: ["hsm.j5t.io"] 151 + resources: ["hsmsecrets"] 152 + verbs: ["get", "list", "watch"] 153 + - apiGroups: [""] 154 + resources: ["secrets"] 155 + verbs: ["get", "list", "watch"] 156 + 157 + --- 158 + apiVersion: rbac.authorization.k8s.io/v1 159 + kind: RoleBinding 160 + metadata: 161 + name: prod-team-hsm-readonly 162 + namespace: production 163 + subjects: 164 + - kind: Group 165 + name: production-team 166 + apiGroup: rbac.authorization.k8s.io 167 + roleRef: 168 + kind: Role 169 + name: prod-hsm-secrets-readonly 170 + apiGroup: rbac.authorization.k8s.io 171 + 172 + --- 173 + # Admin access to shared secrets 174 + apiVersion: rbac.authorization.k8s.io/v1 175 + kind: Role 176 + metadata: 177 + namespace: shared-secrets 178 + name: shared-secrets-admin 179 + rules: 180 + - apiGroups: ["hsm.j5t.io"] 181 + resources: ["hsmsecrets"] 182 + verbs: ["*"] 183 + - apiGroups: [""] 184 + resources: ["secrets"] 185 + verbs: ["*"] 186 + 187 + --- 188 + apiVersion: rbac.authorization.k8s.io/v1 189 + kind: RoleBinding 190 + metadata: 191 + name: platform-team-shared-access 192 + namespace: shared-secrets 193 + subjects: 194 + - kind: Group 195 + name: platform-team 196 + apiGroup: rbac.authorization.k8s.io 197 + roleRef: 198 + kind: Role 199 + name: shared-secrets-admin 200 + apiGroup: rbac.authorization.k8s.io 201 + 202 + --- 203 + # Network Policy for Production Environment 204 + apiVersion: networking.k8s.io/v1 205 + kind: NetworkPolicy 206 + metadata: 207 + name: production-secrets-isolation 208 + namespace: production 209 + spec: 210 + podSelector: 211 + matchLabels: 212 + security.level: "high" 213 + policyTypes: 214 + - Ingress 215 + - Egress 216 + ingress: 217 + - from: 218 + - namespaceSelector: 219 + matchLabels: 220 + environment: production 221 + - podSelector: {} 222 + egress: 223 + - to: 224 + - namespaceSelector: 225 + matchLabels: 226 + environment: production 227 + - to: [] # Allow DNS 228 + ports: 229 + - protocol: UDP 230 + port: 53
+377
examples/advanced/talos-build-guide.md
··· 1 + # HSM Secrets Operator for Talos Linux 2 + 3 + This guide shows how to deploy the HSM Secrets Operator on Talos Linux, which requires special handling for PKCS#11 libraries due to its immutable filesystem. 4 + 5 + ## Talos Linux Challenges 6 + 7 + Talos Linux presents unique challenges for HSM integration: 8 + 9 + 1. **Immutable Root Filesystem**: Can't install libraries at runtime 10 + 2. **Minimal Base**: No package managers or build tools 11 + 3. **Container-Only**: All software must run in containers 12 + 4. **Security Focus**: Restricted permissions and capabilities 13 + 14 + ## Solutions for Talos 15 + 16 + ### Option 1: Custom Operator Image (Recommended) 17 + 18 + Build the operator with PKCS#11 libraries included: 19 + 20 + ```bash 21 + # Build custom operator image with libraries 22 + docker build -f Dockerfile.talos -t hsm-secrets-operator:talos . 23 + 24 + # Push to your registry 25 + docker tag hsm-secrets-operator:talos your-registry.com/hsm-secrets-operator:talos 26 + docker push your-registry.com/hsm-secrets-operator:talos 27 + ``` 28 + 29 + **Advantages**: 30 + - Single image deployment 31 + - Fast startup (no init containers) 32 + - Libraries tested and verified 33 + - Immutable and reproducible 34 + 35 + **Use when**: 36 + - You control the container registry 37 + - You know which HSM vendors you'll use 38 + - You want the simplest deployment 39 + 40 + ### Option 2: Init Container Pattern 41 + 42 + Use init containers to provide libraries at runtime: 43 + 44 + ```bash 45 + # Build library provider image 46 + docker build -f Dockerfile.pkcs11-init -t pkcs11-libraries:latest . 47 + docker push your-registry.com/pkcs11-libraries:latest 48 + ``` 49 + 50 + **Advantages**: 51 + - Flexible library management 52 + - Can update libraries without rebuilding operator 53 + - Supports multiple vendor libraries 54 + - Good for testing different libraries 55 + 56 + **Use when**: 57 + - You need flexibility for different HSM vendors 58 + - You're still evaluating which libraries to use 59 + - You want to update libraries independently 60 + 61 + ## Building for Talos 62 + 63 + ### Custom Operator Image Build 64 + 65 + ```bash 66 + #!/bin/bash 67 + # build-talos.sh - Build script for Talos deployment 68 + 69 + set -e 70 + 71 + REGISTRY=${REGISTRY:-"your-registry.com"} 72 + TAG=${TAG:-"talos-$(date +%Y%m%d)"} 73 + IMAGE_NAME="hsm-secrets-operator" 74 + 75 + echo "Building HSM Secrets Operator for Talos Linux..." 76 + echo "Registry: $REGISTRY" 77 + echo "Tag: $TAG" 78 + 79 + # Build the custom image with PKCS#11 libraries 80 + docker build \ 81 + -f Dockerfile.talos \ 82 + -t $REGISTRY/$IMAGE_NAME:$TAG \ 83 + -t $REGISTRY/$IMAGE_NAME:talos-latest \ 84 + . 85 + 86 + echo "Build completed successfully!" 87 + echo "Images tagged:" 88 + echo " $REGISTRY/$IMAGE_NAME:$TAG" 89 + echo " $REGISTRY/$IMAGE_NAME:talos-latest" 90 + 91 + # Push to registry 92 + read -p "Push to registry? (y/N): " -n 1 -r 93 + echo 94 + if [[ $REPLY =~ ^[Yy]$ ]]; then 95 + docker push $REGISTRY/$IMAGE_NAME:$TAG 96 + docker push $REGISTRY/$IMAGE_NAME:talos-latest 97 + echo "Images pushed to registry" 98 + else 99 + echo "Skipping registry push" 100 + fi 101 + ``` 102 + 103 + ### Library Testing Script 104 + 105 + ```bash 106 + #!/bin/bash 107 + # test-libraries.sh - Test PKCS#11 libraries in container 108 + 109 + CONTAINER_NAME="test-pkcs11-libs" 110 + IMAGE_NAME="hsm-secrets-operator:talos" 111 + 112 + echo "Testing PKCS#11 libraries in container..." 113 + 114 + # Run container with library testing 115 + docker run --rm --name $CONTAINER_NAME $IMAGE_NAME /bin/sh -c ' 116 + echo "=== Testing PKCS#11 Libraries ===" 117 + echo "Library path: $LD_LIBRARY_PATH" 118 + echo "PKCS#11 module path: $PKCS11_MODULE_PATH" 119 + echo "" 120 + 121 + echo "=== Available Libraries ===" 122 + ls -la /usr/local/lib/pkcs11/ 123 + echo "" 124 + 125 + echo "=== Library Dependencies ===" 126 + for lib in /usr/local/lib/pkcs11/*.so; do 127 + if [ -f "$lib" ]; then 128 + echo "Testing: $lib" 129 + ldd "$lib" 2>/dev/null || echo " Static library or dependencies not found" 130 + fi 131 + done 132 + echo "" 133 + 134 + echo "=== PKCS#11 Function Check ===" 135 + for lib in /usr/local/lib/pkcs11/*.so; do 136 + if [ -f "$lib" ]; then 137 + echo "Checking PKCS#11 functions in: $lib" 138 + objdump -T "$lib" | grep -E "(C_GetFunctionList|C_Initialize)" | head -2 139 + fi 140 + done 141 + ' 142 + 143 + echo "Library testing completed" 144 + ``` 145 + 146 + ## Deployment on Talos 147 + 148 + ### 1. Node Preparation 149 + 150 + Label your Talos nodes that have HSM devices: 151 + 152 + ```bash 153 + # Label nodes with HSM hardware 154 + kubectl label node talos-worker-1 hsm.j5t.io/enabled=true 155 + kubectl label node talos-worker-2 hsm.j5t.io/enabled=true 156 + 157 + # Verify node labels 158 + kubectl get nodes --show-labels | grep hsm 159 + ``` 160 + 161 + ### 2. Deploy Operator 162 + 163 + ```bash 164 + # Apply CRDs first 165 + kubectl apply -f config/crd/bases/ 166 + 167 + # Deploy with Talos-specific configuration 168 + kubectl apply -f examples/advanced/talos-deployment.yaml 169 + 170 + # Wait for deployment 171 + kubectl wait --for=condition=available deployment/hsm-secrets-operator-controller-manager -n hsm-secrets-operator-system --timeout=300s 172 + ``` 173 + 174 + ### 3. Verify Deployment 175 + 176 + ```bash 177 + # Check operator status 178 + kubectl get pods -n hsm-secrets-operator-system 179 + 180 + # Check init container logs (if using init container pattern) 181 + kubectl logs -n hsm-secrets-operator-system deployment/hsm-secrets-operator-controller-manager -c pkcs11-installer 182 + 183 + # Check operator logs 184 + kubectl logs -n hsm-secrets-operator-system deployment/hsm-secrets-operator-controller-manager -c manager 185 + 186 + # Test HSM device discovery 187 + kubectl get hsmdevice 188 + kubectl describe hsmdevice talos-pico-hsm 189 + ``` 190 + 191 + ### 4. Create Test Secret 192 + 193 + ```bash 194 + # Create test HSMSecret 195 + cat <<EOF | kubectl apply -f - 196 + apiVersion: hsm.j5t.io/v1alpha1 197 + kind: HSMSecret 198 + metadata: 199 + name: talos-test-secret 200 + namespace: default 201 + spec: 202 + hsmPath: "secrets/talos/test-secret" 203 + secretName: "talos-test-secret" 204 + autoSync: true 205 + syncInterval: 300 206 + EOF 207 + 208 + # Check secret status 209 + kubectl get hsmsecret talos-test-secret 210 + kubectl get secret talos-test-secret 211 + ``` 212 + 213 + ## Talos-Specific Configuration 214 + 215 + ### Machine Configuration 216 + 217 + Add USB device access to your Talos machine configuration: 218 + 219 + ```yaml 220 + # talos-machine-config.yaml 221 + machine: 222 + kernel: 223 + modules: 224 + - name: usbcore 225 + - name: usb_common 226 + - name: usbhid 227 + 228 + # Allow USB device access 229 + sysctls: 230 + kernel.yama.ptrace_scope: 0 231 + 232 + # Device tree for USB HSM devices 233 + deviceTree: 234 + devices: 235 + - /dev/bus/usb 236 + 237 + cluster: 238 + # Enable device plugins 239 + extraManifests: 240 + - https://raw.githubusercontent.com/kubernetes-sigs/node-feature-discovery/master/deployment/overlays/default/kustomization.yaml 241 + ``` 242 + 243 + ### Talos Extensions (if needed) 244 + 245 + For hardware-specific drivers, you might need custom Talos extensions: 246 + 247 + ```yaml 248 + # talos-extensions.yaml 249 + machine: 250 + install: 251 + extensions: 252 + - image: ghcr.io/siderolabs/intel-ucode:20230613 253 + - image: your-registry.com/hsm-driver-extension:v1.0.0 254 + ``` 255 + 256 + ## Security Considerations for Talos 257 + 258 + ### Pod Security Standards 259 + 260 + ```yaml 261 + apiVersion: v1 262 + kind: Namespace 263 + metadata: 264 + name: hsm-secrets-operator-system 265 + labels: 266 + pod-security.kubernetes.io/enforce: restricted 267 + pod-security.kubernetes.io/audit: restricted 268 + pod-security.kubernetes.io/warn: restricted 269 + ``` 270 + 271 + ### Network Policies 272 + 273 + ```yaml 274 + apiVersion: networking.k8s.io/v1 275 + kind: NetworkPolicy 276 + metadata: 277 + name: hsm-operator-talos-netpol 278 + namespace: hsm-secrets-operator-system 279 + spec: 280 + podSelector: 281 + matchLabels: 282 + control-plane: controller-manager 283 + policyTypes: 284 + - Ingress 285 + - Egress 286 + ingress: 287 + - from: 288 + - namespaceSelector: {} 289 + egress: 290 + - to: [] # Allow all egress for K8s API and HSM communication 291 + ``` 292 + 293 + ## Troubleshooting Talos Deployments 294 + 295 + ### Common Issues 296 + 297 + 1. **USB Device Access** 298 + ```bash 299 + # Check USB devices on Talos node 300 + talosctl -n NODE_IP dmesg | grep -i usb 301 + 302 + # List USB devices 303 + talosctl -n NODE_IP exec -- lsusb 304 + ``` 305 + 306 + 2. **Library Loading Issues** 307 + ```bash 308 + # Check library in container 309 + kubectl exec -it deployment/hsm-secrets-operator-controller-manager -n hsm-secrets-operator-system -- ls -la /usr/local/lib/pkcs11/ 310 + 311 + # Test library loading 312 + kubectl exec -it deployment/hsm-secrets-operator-controller-manager -n hsm-secrets-operator-system -- ldd /usr/local/lib/pkcs11/opensc-pkcs11.so 313 + ``` 314 + 315 + 3. **Permission Issues** 316 + ```bash 317 + # Check container security context 318 + kubectl get pod -n hsm-secrets-operator-system -o yaml | grep -A 10 securityContext 319 + 320 + # Check device permissions 321 + kubectl exec -it deployment/hsm-secrets-operator-controller-manager -n hsm-secrets-operator-system -- ls -la /dev/bus/usb/ 322 + ``` 323 + 324 + ### Debug Commands 325 + 326 + ```bash 327 + # Talos system information 328 + talosctl -n NODE_IP version 329 + talosctl -n NODE_IP get members 330 + 331 + # Container runtime information 332 + talosctl -n NODE_IP containers 333 + 334 + # Kubernetes node information 335 + kubectl describe node TALOS_NODE 336 + 337 + # HSM operator debugging 338 + kubectl logs -f -n hsm-secrets-operator-system deployment/hsm-secrets-operator-controller-manager 339 + 340 + # HSM device status 341 + kubectl get hsmdevice -o yaml 342 + ``` 343 + 344 + ## Performance Optimization for Talos 345 + 346 + ### Resource Management 347 + 348 + ```yaml 349 + resources: 350 + requests: 351 + cpu: 100m 352 + memory: 128Mi 353 + # Request specific resources for HSM devices 354 + vendor.com/hsm: 1 355 + limits: 356 + cpu: 1000m 357 + memory: 512Mi 358 + vendor.com/hsm: 1 359 + ``` 360 + 361 + ### Node Affinity 362 + 363 + ```yaml 364 + affinity: 365 + nodeAffinity: 366 + requiredDuringSchedulingIgnoredDuringExecution: 367 + nodeSelectorTerms: 368 + - matchExpressions: 369 + - key: hsm.j5t.io/enabled 370 + operator: In 371 + values: ["true"] 372 + - key: kubernetes.io/arch 373 + operator: In 374 + values: ["amd64"] 375 + ``` 376 + 377 + This comprehensive guide provides everything needed to successfully deploy the HSM Secrets Operator on Talos Linux with proper PKCS#11 library support!
+386
examples/advanced/talos-deployment.yaml
··· 1 + # HSM Secrets Operator deployment for Talos Linux 2 + # Talos has immutable rootfs, so libraries must be provided via containers 3 + 4 + --- 5 + # ConfigMap with custom PKCS#11 library configuration 6 + apiVersion: v1 7 + kind: ConfigMap 8 + metadata: 9 + name: talos-hsm-config 10 + namespace: hsm-secrets-operator-system 11 + data: 12 + # Library installation script 13 + install-libs.sh: | 14 + #!/bin/sh 15 + set -e 16 + 17 + echo "Installing PKCS#11 libraries for Talos Linux..." 18 + 19 + # Create directories 20 + mkdir -p /shared/lib/pkcs11 21 + mkdir -p /shared/etc/pkcs11 22 + 23 + # Copy libraries from init container 24 + if [ -d "/vendor-libs" ]; then 25 + cp -v /vendor-libs/*.so /shared/lib/pkcs11/ 26 + chmod 755 /shared/lib/pkcs11/*.so 27 + fi 28 + 29 + # Create library configuration 30 + cat > /shared/etc/pkcs11/pkcs11.conf << EOF 31 + # PKCS#11 configuration for HSM devices 32 + module: /shared/lib/pkcs11/opensc-pkcs11.so 33 + slot-description: OpenSC 34 + EOF 35 + 36 + # List installed libraries 37 + echo "Installed libraries:" 38 + ls -la /shared/lib/pkcs11/ 39 + 40 + echo "Library installation completed" 41 + 42 + --- 43 + # Init container image with PKCS#11 libraries 44 + # This would be built separately and pushed to your registry 45 + apiVersion: v1 46 + kind: ConfigMap 47 + metadata: 48 + name: pkcs11-init-dockerfile 49 + namespace: hsm-secrets-operator-system 50 + data: 51 + Dockerfile: | 52 + FROM alpine:3.18 53 + 54 + # Install build dependencies 55 + RUN apk add --no-cache \ 56 + wget \ 57 + unzip \ 58 + build-base \ 59 + autoconf \ 60 + automake \ 61 + libtool \ 62 + pkgconfig \ 63 + openssl-dev \ 64 + libusb-dev \ 65 + pcsc-lite-dev 66 + 67 + # Install OpenSC (most common) 68 + RUN wget https://github.com/OpenSC/OpenSC/releases/download/0.24.0/opensc-0.24.0.tar.gz && \ 69 + tar -xzf opensc-0.24.0.tar.gz && \ 70 + cd opensc-0.24.0 && \ 71 + ./configure --prefix=/usr/local --enable-pcsc --enable-openssl && \ 72 + make && make install 73 + 74 + # Install YubiKey library (optional) 75 + RUN wget https://developers.yubico.com/yubico-piv-tool/Releases/yubico-piv-tool-2.4.0.tar.gz && \ 76 + tar -xzf yubico-piv-tool-2.4.0.tar.gz && \ 77 + cd yubico-piv-tool-2.4.0 && \ 78 + ./configure --prefix=/usr/local && \ 79 + make && make install 80 + 81 + # Copy libraries to vendor-libs directory 82 + RUN mkdir -p /vendor-libs && \ 83 + cp /usr/local/lib/pkcs11/*.so /vendor-libs/ && \ 84 + cp /usr/local/lib/libykcs11*.so /vendor-libs/ 2>/dev/null || true 85 + 86 + # Copy installation script 87 + COPY install-libs.sh /usr/local/bin/ 88 + RUN chmod +x /usr/local/bin/install-libs.sh 89 + 90 + ENTRYPOINT ["/usr/local/bin/install-libs.sh"] 91 + 92 + --- 93 + # Updated HSM Secrets Operator Deployment for Talos 94 + apiVersion: apps/v1 95 + kind: Deployment 96 + metadata: 97 + name: hsm-secrets-operator-controller-manager 98 + namespace: hsm-secrets-operator-system 99 + labels: 100 + app.kubernetes.io/component: manager 101 + app.kubernetes.io/created-by: hsm-secrets-operator 102 + app.kubernetes.io/instance: controller-manager 103 + app.kubernetes.io/managed-by: kustomize 104 + app.kubernetes.io/name: deployment 105 + app.kubernetes.io/part-of: hsm-secrets-operator 106 + control-plane: controller-manager 107 + spec: 108 + replicas: 1 109 + selector: 110 + matchLabels: 111 + control-plane: controller-manager 112 + template: 113 + metadata: 114 + annotations: 115 + kubectl.kubernetes.io/default-container: manager 116 + labels: 117 + control-plane: controller-manager 118 + spec: 119 + # Security context for Talos 120 + securityContext: 121 + runAsNonRoot: true 122 + seccompProfile: 123 + type: RuntimeDefault 124 + 125 + # Init container to provide PKCS#11 libraries 126 + initContainers: 127 + - name: pkcs11-installer 128 + # This image contains the PKCS#11 libraries 129 + image: your-registry.com/pkcs11-libraries:latest 130 + imagePullPolicy: IfNotPresent 131 + 132 + command: 133 + - /bin/sh 134 + - -c 135 + - | 136 + echo "Setting up PKCS#11 libraries for Talos..." 137 + 138 + # Create directory structure 139 + mkdir -p /shared/lib/pkcs11 140 + mkdir -p /shared/etc/pkcs11 141 + 142 + # Copy pre-built libraries 143 + cp -v /usr/local/lib/pkcs11/* /shared/lib/pkcs11/ 2>/dev/null || true 144 + cp -v /usr/local/lib/libykcs11* /shared/lib/pkcs11/ 2>/dev/null || true 145 + 146 + # Set permissions 147 + chmod 755 /shared/lib/pkcs11/*.so 148 + 149 + # Create ldconfig cache equivalent 150 + echo "/shared/lib/pkcs11" > /shared/etc/ld.so.conf 151 + 152 + # Verify libraries 153 + echo "Available PKCS#11 libraries:" 154 + ls -la /shared/lib/pkcs11/ 155 + 156 + # Test library loading (basic check) 157 + for lib in /shared/lib/pkcs11/*.so; do 158 + if [ -f "$lib" ]; then 159 + echo "Testing library: $lib" 160 + # Basic symbol check 161 + if command -v objdump >/dev/null; then 162 + objdump -T "$lib" | grep C_GetFunctionList || echo " Warning: C_GetFunctionList not found" 163 + fi 164 + fi 165 + done 166 + 167 + echo "PKCS#11 library setup completed" 168 + 169 + volumeMounts: 170 + - name: pkcs11-libs 171 + mountPath: /shared 172 + 173 + resources: 174 + requests: 175 + cpu: 100m 176 + memory: 128Mi 177 + limits: 178 + cpu: 500m 179 + memory: 256Mi 180 + 181 + securityContext: 182 + allowPrivilegeEscalation: false 183 + capabilities: 184 + drop: 185 + - ALL 186 + readOnlyRootFilesystem: true 187 + runAsNonRoot: true 188 + runAsUser: 65534 189 + 190 + containers: 191 + - name: manager 192 + args: 193 + - --leader-elect 194 + - --health-probe-bind-address=:8081 195 + - --metrics-bind-address=127.0.0.1:8080 196 + - --enable-api=true 197 + - --api-port=8090 198 + command: 199 + - /manager 200 + image: controller:latest 201 + 202 + # Environment variables for library paths 203 + env: 204 + - name: LD_LIBRARY_PATH 205 + value: "/shared/lib/pkcs11:/usr/local/lib" 206 + - name: PKCS11_MODULE_PATH 207 + value: "/shared/lib/pkcs11" 208 + - name: OPENSC_CONF 209 + value: "/shared/etc/pkcs11/opensc.conf" 210 + 211 + livenessProbe: 212 + httpGet: 213 + path: /healthz 214 + port: 8081 215 + initialDelaySeconds: 15 216 + periodSeconds: 20 217 + 218 + readinessProbe: 219 + httpGet: 220 + path: /readyz 221 + port: 8081 222 + initialDelaySeconds: 5 223 + periodSeconds: 10 224 + 225 + resources: 226 + limits: 227 + cpu: 500m 228 + memory: 128Mi 229 + requests: 230 + cpu: 10m 231 + memory: 64Mi 232 + 233 + securityContext: 234 + allowPrivilegeEscalation: false 235 + capabilities: 236 + drop: 237 + - ALL 238 + readOnlyRootFilesystem: true 239 + runAsNonRoot: true 240 + runAsUser: 65532 241 + 242 + # Mount the shared libraries 243 + volumeMounts: 244 + - name: pkcs11-libs 245 + mountPath: /shared 246 + readOnly: true 247 + - name: tmp 248 + mountPath: /tmp 249 + 250 + ports: 251 + - containerPort: 8090 252 + name: api 253 + protocol: TCP 254 + - containerPort: 8080 255 + name: metrics 256 + protocol: TCP 257 + - containerPort: 8081 258 + name: health 259 + protocol: TCP 260 + 261 + volumes: 262 + - name: pkcs11-libs 263 + emptyDir: {} 264 + - name: tmp 265 + emptyDir: {} 266 + 267 + serviceAccountName: hsm-secrets-operator-controller-manager 268 + terminationGracePeriodSeconds: 10 269 + 270 + # Node selection for Talos with HSM devices 271 + nodeSelector: 272 + node.kubernetes.io/instance-type: worker 273 + 274 + # Tolerations for Talos nodes 275 + tolerations: 276 + - effect: NoSchedule 277 + key: node-role.kubernetes.io/control-plane 278 + operator: Equal 279 + - effect: NoSchedule 280 + key: node-role.kubernetes.io/master 281 + operator: Equal 282 + 283 + --- 284 + # HSM Device configuration for Talos 285 + apiVersion: hsm.j5t.io/v1alpha1 286 + kind: HSMDevice 287 + metadata: 288 + name: talos-pico-hsm 289 + namespace: default 290 + labels: 291 + os: talos 292 + device-type: pico-hsm 293 + spec: 294 + deviceType: PicoHSM 295 + 296 + # USB device discovery 297 + usb: 298 + vendorId: "20a0" 299 + productId: "4230" 300 + 301 + # Use the library from shared volume 302 + pkcs11LibraryPath: "/shared/lib/pkcs11/opensc-pkcs11.so" 303 + 304 + # Select Talos worker nodes 305 + nodeSelector: 306 + node.kubernetes.io/instance-type: worker 307 + 308 + maxDevices: 2 309 + 310 + # Enable mirroring for HA on Talos 311 + mirroring: 312 + policy: "ReadOnly" 313 + syncInterval: 300 314 + autoFailover: true 315 + 316 + --- 317 + # Service for API access 318 + apiVersion: v1 319 + kind: Service 320 + metadata: 321 + name: hsm-secrets-operator-api 322 + namespace: hsm-secrets-operator-system 323 + labels: 324 + app.kubernetes.io/component: api 325 + control-plane: controller-manager 326 + spec: 327 + ports: 328 + - name: api 329 + port: 8090 330 + protocol: TCP 331 + targetPort: 8090 332 + selector: 333 + control-plane: controller-manager 334 + type: ClusterIP 335 + 336 + --- 337 + # NetworkPolicy for Talos security 338 + apiVersion: networking.k8s.io/v1 339 + kind: NetworkPolicy 340 + metadata: 341 + name: hsm-operator-talos-policy 342 + namespace: hsm-secrets-operator-system 343 + spec: 344 + podSelector: 345 + matchLabels: 346 + control-plane: controller-manager 347 + policyTypes: 348 + - Ingress 349 + - Egress 350 + ingress: 351 + - from: 352 + - namespaceSelector: {} 353 + ports: 354 + - protocol: TCP 355 + port: 8090 # API 356 + - protocol: TCP 357 + port: 8080 # Metrics 358 + - protocol: TCP 359 + port: 8081 # Health 360 + egress: 361 + - {} # Allow all egress for HSM communication and K8s API 362 + 363 + --- 364 + # Pod Security Policy for Talos (if PSP is enabled) 365 + apiVersion: policy/v1beta1 366 + kind: PodSecurityPolicy 367 + metadata: 368 + name: hsm-operator-psp 369 + spec: 370 + privileged: false 371 + allowPrivilegeEscalation: false 372 + requiredDropCapabilities: 373 + - ALL 374 + volumes: 375 + - 'configMap' 376 + - 'emptyDir' 377 + - 'projected' 378 + - 'secret' 379 + - 'downwardAPI' 380 + - 'persistentVolumeClaim' 381 + runAsUser: 382 + rule: 'MustRunAsNonRoot' 383 + seLinux: 384 + rule: 'RunAsAny' 385 + fsGroup: 386 + rule: 'RunAsAny'
+143
examples/api/README.md
··· 1 + # REST API Examples 2 + 3 + This directory contains examples for using the HSM Secrets Operator REST API. 4 + 5 + ## API Overview 6 + 7 + The operator provides a REST API server that runs on port 8090 (configurable). The API allows you to: 8 + 9 + - Create, read, update, and delete secrets stored on HSM devices 10 + - Import secrets from external sources (Kubernetes, Vault, etc.) 11 + - Monitor HSM device health and status 12 + - List and manage all HSM-backed secrets 13 + 14 + ## Base URL 15 + 16 + When running locally: `http://localhost:8090` 17 + When deployed in cluster: `http://hsm-secrets-operator-api:8090` 18 + 19 + ## Authentication 20 + 21 + The API currently supports: 22 + - No authentication (development/testing) 23 + - Kubernetes ServiceAccount tokens (when deployed in cluster) 24 + - Future: OAuth2, API keys, mTLS 25 + 26 + ## Examples 27 + 28 + 1. **[health-check.sh](health-check.sh)** - Check API and HSM health 29 + 2. **[create-secret.json](create-secret.json)** - Create a new secret via API 30 + 3. **[create-secret.sh](create-secret.sh)** - Script to create secrets 31 + 4. **[import-from-k8s.sh](import-from-k8s.sh)** - Import existing Kubernetes secrets 32 + 5. **[list-secrets.sh](list-secrets.sh)** - List all HSM secrets 33 + 6. **[update-secret.sh](update-secret.sh)** - Update existing secrets 34 + 7. **[bulk-operations.sh](bulk-operations.sh)** - Bulk secret operations 35 + 36 + ## Quick Start 37 + 38 + 1. **Start the API server** (if running locally): 39 + ```bash 40 + ./bin/manager --enable-api=true --api-port=8090 41 + ``` 42 + 43 + 2. **Check health**: 44 + ```bash 45 + curl http://localhost:8090/api/v1/health 46 + ``` 47 + 48 + 3. **Create a secret**: 49 + ```bash 50 + curl -X POST http://localhost:8090/api/v1/hsm/secrets \ 51 + -H "Content-Type: application/json" \ 52 + -d @create-secret.json 53 + ``` 54 + 55 + 4. **List secrets**: 56 + ```bash 57 + curl http://localhost:8090/api/v1/hsm/secrets 58 + ``` 59 + 60 + ## Response Format 61 + 62 + All API responses follow this format: 63 + 64 + ```json 65 + { 66 + "success": true, 67 + "message": "Operation completed successfully", 68 + "data": { 69 + // Response data here 70 + }, 71 + "error": null 72 + } 73 + ``` 74 + 75 + Error responses: 76 + ```json 77 + { 78 + "success": false, 79 + "message": "", 80 + "data": null, 81 + "error": { 82 + "code": "validation_failed", 83 + "message": "Request validation failed", 84 + "details": { 85 + "field": "validation error details" 86 + } 87 + } 88 + } 89 + ``` 90 + 91 + ## Common Use Cases 92 + 93 + ### 1. Development Workflow 94 + - Create secrets during development 95 + - Import from existing sources 96 + - Test secret rotation 97 + 98 + ### 2. CI/CD Integration 99 + - Automated secret provisioning 100 + - Environment-specific deployments 101 + - Secret validation and testing 102 + 103 + ### 3. Secret Migration 104 + - Import from Kubernetes Secrets 105 + - Migrate from other secret stores 106 + - Bulk operations for large environments 107 + 108 + ### 4. Monitoring and Operations 109 + - Health monitoring 110 + - Secret inventory management 111 + - Troubleshooting sync issues 112 + 113 + ## Error Handling 114 + 115 + Common error codes and solutions: 116 + 117 + - `hsm_unavailable`: HSM device not connected 118 + - `validation_failed`: Invalid request data 119 + - `secret_not_found`: Secret doesn't exist 120 + - `hsm_read_error`: HSM communication failure 121 + - `hsm_write_error`: HSM storage failure 122 + 123 + ## Security Best Practices 124 + 125 + 1. **Network Security** 126 + - Use TLS in production 127 + - Restrict API access with NetworkPolicies 128 + - Use VPN or private networks 129 + 130 + 2. **Authentication** 131 + - Enable authentication in production 132 + - Use strong API keys or tokens 133 + - Implement proper RBAC 134 + 135 + 3. **Data Validation** 136 + - Validate all input data 137 + - Sanitize sensitive information in logs 138 + - Use structured logging 139 + 140 + 4. **Monitoring** 141 + - Monitor API access and usage 142 + - Track secret operations 143 + - Set up alerts for failures
+336
examples/api/advanced-bulk-import.sh
··· 1 + #!/bin/bash 2 + 3 + # Advanced bulk import script with validation and rollback 4 + # Usage: ./advanced-bulk-import.sh [config-file] [options] 5 + 6 + set -e 7 + 8 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 + CONFIG_FILE=${1:-"production-import.json"} 10 + DRY_RUN=${DRY_RUN:-false} 11 + ROLLBACK_ON_FAILURE=${ROLLBACK_ON_FAILURE:-true} 12 + MAX_PARALLEL=${MAX_PARALLEL:-5} 13 + 14 + # Colors for output 15 + RED='\033[0;31m' 16 + GREEN='\033[0;32m' 17 + YELLOW='\033[1;33m' 18 + BLUE='\033[0;34m' 19 + NC='\033[0m' # No Color 20 + 21 + log() { 22 + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" 23 + } 24 + 25 + success() { 26 + echo -e "${GREEN}✅${NC} $1" 27 + } 28 + 29 + error() { 30 + echo -e "${RED}❌${NC} $1" 31 + } 32 + 33 + warning() { 34 + echo -e "${YELLOW}⚠️${NC} $1" 35 + } 36 + 37 + # Validate prerequisites 38 + validate_prerequisites() { 39 + log "Validating prerequisites..." 40 + 41 + if ! command -v jq &> /dev/null; then 42 + error "jq is required but not installed" 43 + exit 1 44 + fi 45 + 46 + if ! command -v curl &> /dev/null; then 47 + error "curl is required but not installed" 48 + exit 1 49 + fi 50 + 51 + if [ ! -f "$CONFIG_FILE" ]; then 52 + error "Config file not found: $CONFIG_FILE" 53 + exit 1 54 + fi 55 + 56 + if ! jq empty "$CONFIG_FILE" 2>/dev/null; then 57 + error "Invalid JSON in config file: $CONFIG_FILE" 58 + exit 1 59 + fi 60 + 61 + # Test API connectivity 62 + if ! curl -s --connect-timeout 5 "$API_BASE_URL/api/v1/health" > /dev/null; then 63 + error "Cannot connect to API at: $API_BASE_URL" 64 + exit 1 65 + fi 66 + 67 + success "Prerequisites validated" 68 + } 69 + 70 + # Pre-import validation 71 + validate_config() { 72 + log "Validating configuration..." 73 + 74 + local issues=0 75 + 76 + # Check for duplicate labels 77 + duplicate_labels=$(jq -r '.secrets[].label' "$CONFIG_FILE" | sort | uniq -d) 78 + if [ -n "$duplicate_labels" ]; then 79 + error "Duplicate labels found:" 80 + echo "$duplicate_labels" | while IFS= read -r label; do 81 + echo " - $label" 82 + done 83 + ((issues++)) 84 + fi 85 + 86 + # Check for duplicate IDs 87 + duplicate_ids=$(jq -r '.secrets[].id' "$CONFIG_FILE" | sort | uniq -d) 88 + if [ -n "$duplicate_ids" ]; then 89 + error "Duplicate IDs found:" 90 + echo "$duplicate_ids" | while IFS= read -r id; do 91 + echo " - $id" 92 + done 93 + ((issues++)) 94 + fi 95 + 96 + # Validate required fields 97 + jq -c '.secrets[]' "$CONFIG_FILE" | while IFS= read -r secret; do 98 + label=$(echo "$secret" | jq -r '.label') 99 + id=$(echo "$secret" | jq -r '.id') 100 + 101 + if [ "$label" = "null" ] || [ -z "$label" ]; then 102 + error "Secret missing label" 103 + ((issues++)) 104 + fi 105 + 106 + if [ "$id" = "null" ] || [ -z "$id" ]; then 107 + error "Secret '$label' missing ID" 108 + ((issues++)) 109 + fi 110 + 111 + if ! echo "$secret" | jq -e '.data' > /dev/null; then 112 + error "Secret '$label' missing data" 113 + ((issues++)) 114 + fi 115 + done 116 + 117 + if [ $issues -gt 0 ]; then 118 + error "Configuration validation failed with $issues issues" 119 + exit 1 120 + fi 121 + 122 + success "Configuration validated" 123 + } 124 + 125 + # Check for existing secrets 126 + check_existing_secrets() { 127 + log "Checking for existing secrets..." 128 + 129 + local conflicts=() 130 + 131 + while IFS= read -r label; do 132 + response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label") 133 + success=$(echo "$response" | jq -r '.success') 134 + 135 + if [ "$success" = "true" ]; then 136 + conflicts+=("$label") 137 + fi 138 + done <<< "$(jq -r '.secrets[].label' "$CONFIG_FILE")" 139 + 140 + if [ ${#conflicts[@]} -gt 0 ]; then 141 + warning "Found ${#conflicts[@]} existing secrets that will be overwritten:" 142 + for conflict in "${conflicts[@]}"; do 143 + echo " - $conflict" 144 + done 145 + 146 + if [ "$DRY_RUN" = "false" ]; then 147 + read -p "Continue? (y/N): " -n 1 -r 148 + echo 149 + if [[ ! $REPLY =~ ^[Yy]$ ]]; then 150 + log "Import cancelled by user" 151 + exit 0 152 + fi 153 + fi 154 + else 155 + success "No conflicts found" 156 + fi 157 + } 158 + 159 + # Import a single secret 160 + import_secret() { 161 + local secret_data="$1" 162 + local label=$(echo "$secret_data" | jq -r '.label') 163 + 164 + if [ "$DRY_RUN" = "true" ]; then 165 + echo "[DRY RUN] Would import: $label" 166 + return 0 167 + fi 168 + 169 + log "Importing: $label" 170 + 171 + response=$(curl -s -X POST \ 172 + -H "Content-Type: application/json" \ 173 + -d "$secret_data" \ 174 + "$API_BASE_URL/api/v1/hsm/secrets" 2>/dev/null) 175 + 176 + if [ $? -ne 0 ]; then 177 + error "Failed to connect to API for $label" 178 + return 1 179 + fi 180 + 181 + success_status=$(echo "$response" | jq -r '.success') 182 + if [ "$success_status" = "true" ]; then 183 + success "Imported: $label" 184 + return 0 185 + else 186 + error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"') 187 + error "Failed to import $label: $error_message" 188 + return 1 189 + fi 190 + } 191 + 192 + # Rollback imported secrets 193 + rollback_secrets() { 194 + local imported_secrets=("$@") 195 + 196 + if [ ${#imported_secrets[@]} -eq 0 ]; then 197 + return 0 198 + fi 199 + 200 + warning "Rolling back ${#imported_secrets[@]} imported secrets..." 201 + 202 + for label in "${imported_secrets[@]}"; do 203 + log "Rolling back: $label" 204 + curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null 205 + done 206 + 207 + warning "Rollback completed" 208 + } 209 + 210 + # Main import process 211 + perform_import() { 212 + log "Starting bulk import..." 213 + 214 + local total_secrets=$(jq '.secrets | length' "$CONFIG_FILE") 215 + local imported_secrets=() 216 + local failed_secrets=() 217 + local success_count=0 218 + local failure_count=0 219 + 220 + log "Importing $total_secrets secrets..." 221 + 222 + # Process secrets sequentially for better error handling 223 + while IFS= read -r secret_json; do 224 + label=$(echo "$secret_json" | jq -r '.label') 225 + 226 + if import_secret "$secret_json"; then 227 + imported_secrets+=("$label") 228 + ((success_count++)) 229 + else 230 + failed_secrets+=("$label") 231 + ((failure_count++)) 232 + 233 + # Rollback on first failure if enabled 234 + if [ "$ROLLBACK_ON_FAILURE" = "true" ] && [ $failure_count -eq 1 ]; then 235 + error "First failure detected, initiating rollback..." 236 + rollback_secrets "${imported_secrets[@]}" 237 + exit 1 238 + fi 239 + fi 240 + 241 + # Progress indicator 242 + local current=$((success_count + failure_count)) 243 + log "Progress: $current/$total_secrets" 244 + 245 + done < <(jq -c '.secrets[]' "$CONFIG_FILE") 246 + 247 + # Summary 248 + echo "" 249 + log "Import Summary:" 250 + success "Successfully imported: $success_count secrets" 251 + if [ $failure_count -gt 0 ]; then 252 + error "Failed to import: $failure_count secrets" 253 + if [ ${#failed_secrets[@]} -gt 0 ]; then 254 + echo "Failed secrets:" 255 + for failed in "${failed_secrets[@]}"; do 256 + echo " - $failed" 257 + done 258 + fi 259 + fi 260 + 261 + # Generate import report 262 + local report_file="import-report-$(date +%Y%m%d-%H%M%S).json" 263 + cat > "$report_file" <<EOF 264 + { 265 + "timestamp": "$(date -Iseconds)", 266 + "config_file": "$CONFIG_FILE", 267 + "api_url": "$API_BASE_URL", 268 + "total_secrets": $total_secrets, 269 + "successful_imports": $success_count, 270 + "failed_imports": $failure_count, 271 + "imported_secrets": $(printf '%s\n' "${imported_secrets[@]}" | jq -R . | jq -s .), 272 + "failed_secrets": $(printf '%s\n' "${failed_secrets[@]}" | jq -R . | jq -s .) 273 + } 274 + EOF 275 + 276 + log "Import report saved to: $report_file" 277 + 278 + if [ $failure_count -eq 0 ]; then 279 + success "All secrets imported successfully!" 280 + exit 0 281 + else 282 + error "Some secrets failed to import" 283 + exit 1 284 + fi 285 + } 286 + 287 + # Parse command line options 288 + while [[ $# -gt 0 ]]; do 289 + case $1 in 290 + --dry-run) 291 + DRY_RUN=true 292 + shift 293 + ;; 294 + --no-rollback) 295 + ROLLBACK_ON_FAILURE=false 296 + shift 297 + ;; 298 + --api-url) 299 + API_BASE_URL="$2" 300 + shift 2 301 + ;; 302 + --help) 303 + echo "Usage: $0 [config-file] [options]" 304 + echo "" 305 + echo "Options:" 306 + echo " --dry-run Show what would be imported without making changes" 307 + echo " --no-rollback Don't rollback on failure" 308 + echo " --api-url URL Override API base URL" 309 + echo " --help Show this help message" 310 + echo "" 311 + echo "Environment variables:" 312 + echo " API_BASE_URL API endpoint (default: http://localhost:8090)" 313 + echo " DRY_RUN Enable dry run mode (default: false)" 314 + echo " ROLLBACK_ON_FAILURE Enable rollback on failure (default: true)" 315 + exit 0 316 + ;; 317 + *) 318 + CONFIG_FILE="$1" 319 + shift 320 + ;; 321 + esac 322 + done 323 + 324 + # Main execution 325 + echo "🔐 Advanced HSM Secrets Bulk Import" 326 + echo "=====================================" 327 + echo "Config file: $CONFIG_FILE" 328 + echo "API URL: $API_BASE_URL" 329 + echo "Dry run: $DRY_RUN" 330 + echo "Rollback on failure: $ROLLBACK_ON_FAILURE" 331 + echo "" 332 + 333 + validate_prerequisites 334 + validate_config 335 + check_existing_secrets 336 + perform_import
+258
examples/api/bulk-operations.sh
··· 1 + #!/bin/bash 2 + 3 + # Bulk operations for HSM secrets via REST API 4 + # Usage: ./bulk-operations.sh [operation] [config-file] 5 + 6 + set -e 7 + 8 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 + OPERATION=${1:-"create"} 10 + CONFIG_FILE=${2:-"bulk-secrets.json"} 11 + 12 + # Function to create bulk secrets config if it doesn't exist 13 + create_sample_config() { 14 + cat > "$CONFIG_FILE" <<EOF 15 + { 16 + "secrets": [ 17 + { 18 + "label": "app1-database", 19 + "id": 1001, 20 + "format": "json", 21 + "description": "Database credentials for app1", 22 + "tags": { 23 + "app": "app1", 24 + "environment": "production", 25 + "type": "database" 26 + }, 27 + "data": { 28 + "database_url": "postgresql://app1:password123@db.example.com:5432/app1", 29 + "username": "app1", 30 + "password": "password123" 31 + } 32 + }, 33 + { 34 + "label": "app2-api-keys", 35 + "id": 1002, 36 + "format": "json", 37 + "description": "External API keys for app2", 38 + "tags": { 39 + "app": "app2", 40 + "environment": "production", 41 + "type": "api-keys" 42 + }, 43 + "data": { 44 + "stripe_key": "sk_live_example123", 45 + "sendgrid_key": "SG.example456", 46 + "aws_access_key": "AKIA1234567890" 47 + } 48 + }, 49 + { 50 + "label": "shared-tls-cert", 51 + "id": 1003, 52 + "format": "text", 53 + "description": "Shared TLS certificate", 54 + "tags": { 55 + "type": "tls", 56 + "scope": "shared", 57 + "environment": "production" 58 + }, 59 + "data": { 60 + "tls.crt": "-----BEGIN CERTIFICATE-----\\nMIIC...\\n-----END CERTIFICATE-----", 61 + "tls.key": "-----BEGIN PRIVATE KEY-----\\nMIIE...\\n-----END PRIVATE KEY-----" 62 + } 63 + } 64 + ] 65 + } 66 + EOF 67 + echo "📝 Created sample config file: $CONFIG_FILE" 68 + } 69 + 70 + # Function to create a single secret 71 + create_secret() { 72 + local secret_data="$1" 73 + local label=$(echo "$secret_data" | jq -r '.label') 74 + 75 + echo " Creating secret: $label" 76 + 77 + response=$(curl -s -X POST \ 78 + -H "Content-Type: application/json" \ 79 + -d "$secret_data" \ 80 + "$API_BASE_URL/api/v1/hsm/secrets") 81 + 82 + success=$(echo "$response" | jq -r '.success') 83 + if [ "$success" = "true" ]; then 84 + echo " ✅ Created successfully" 85 + return 0 86 + else 87 + error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"') 88 + echo " ❌ Failed: $error_message" 89 + return 1 90 + fi 91 + } 92 + 93 + # Function to delete a secret 94 + delete_secret() { 95 + local label="$1" 96 + 97 + echo " Deleting secret: $label" 98 + 99 + response=$(curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label") 100 + 101 + success=$(echo "$response" | jq -r '.success') 102 + if [ "$success" = "true" ]; then 103 + echo " ✅ Deleted successfully" 104 + return 0 105 + else 106 + error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"') 107 + echo " ❌ Failed: $error_message" 108 + return 1 109 + fi 110 + } 111 + 112 + # Function to get secret info 113 + get_secret() { 114 + local label="$1" 115 + 116 + response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label") 117 + success=$(echo "$response" | jq -r '.success') 118 + 119 + if [ "$success" = "true" ]; then 120 + checksum=$(echo "$response" | jq -r '.data.metadata.checksum[0:8]') 121 + replicated=$(echo "$response" | jq -r '.data.metadata.is_replicated') 122 + echo " ✅ $label (checksum: $checksum, replicated: $replicated)" 123 + return 0 124 + else 125 + echo " ❌ $label (not found or error)" 126 + return 1 127 + fi 128 + } 129 + 130 + echo "🔄 HSM Secrets Bulk Operations" 131 + echo "Operation: $OPERATION" 132 + echo "Config File: $CONFIG_FILE" 133 + echo "API Base URL: $API_BASE_URL" 134 + echo "" 135 + 136 + # Check if config file exists 137 + if [ ! -f "$CONFIG_FILE" ]; then 138 + echo "⚠️ Config file not found. Creating sample..." 139 + create_sample_config 140 + echo "" 141 + fi 142 + 143 + # Validate config file 144 + if ! jq empty "$CONFIG_FILE" 2>/dev/null; then 145 + echo "❌ Invalid JSON in config file: $CONFIG_FILE" 146 + exit 1 147 + fi 148 + 149 + # Extract secret labels for operations 150 + secret_labels=$(jq -r '.secrets[].label' "$CONFIG_FILE") 151 + secret_count=$(echo "$secret_labels" | wc -l) 152 + 153 + echo "📊 Found $secret_count secrets in config file" 154 + echo "" 155 + 156 + case "$OPERATION" in 157 + "create") 158 + echo "🔐 Creating secrets..." 159 + success_count=0 160 + failure_count=0 161 + 162 + # Process each secret 163 + while IFS= read -r secret_json; do 164 + if create_secret "$secret_json"; then 165 + ((success_count++)) 166 + else 167 + ((failure_count++)) 168 + fi 169 + done < <(jq -c '.secrets[]' "$CONFIG_FILE") 170 + 171 + echo "" 172 + echo "📈 Results:" 173 + echo " ✅ Successful: $success_count" 174 + echo " ❌ Failed: $failure_count" 175 + ;; 176 + 177 + "delete") 178 + echo "🗑️ Deleting secrets..." 179 + success_count=0 180 + failure_count=0 181 + 182 + # Delete each secret 183 + while IFS= read -r label; do 184 + if delete_secret "$label"; then 185 + ((success_count++)) 186 + else 187 + ((failure_count++)) 188 + fi 189 + done <<< "$secret_labels" 190 + 191 + echo "" 192 + echo "📈 Results:" 193 + echo " ✅ Deleted: $success_count" 194 + echo " ❌ Failed: $failure_count" 195 + ;; 196 + 197 + "status") 198 + echo "📊 Checking secret status..." 199 + available_count=0 200 + missing_count=0 201 + 202 + # Check each secret 203 + while IFS= read -r label; do 204 + if get_secret "$label"; then 205 + ((available_count++)) 206 + else 207 + ((missing_count++)) 208 + fi 209 + done <<< "$secret_labels" 210 + 211 + echo "" 212 + echo "📈 Results:" 213 + echo " ✅ Available: $available_count" 214 + echo " ❌ Missing: $missing_count" 215 + ;; 216 + 217 + "backup") 218 + echo "💾 Backing up secrets..." 219 + backup_file="secrets-backup-$(date +%Y%m%d-%H%M%S).json" 220 + 221 + echo '{"secrets": [' > "$backup_file" 222 + first=true 223 + 224 + while IFS= read -r label; do 225 + response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label") 226 + success=$(echo "$response" | jq -r '.success') 227 + 228 + if [ "$success" = "true" ]; then 229 + if [ "$first" = true ]; then 230 + first=false 231 + else 232 + echo "," >> "$backup_file" 233 + fi 234 + echo "$response" | jq '.data' >> "$backup_file" 235 + echo " ✅ Backed up: $label" 236 + else 237 + echo " ❌ Failed to backup: $label" 238 + fi 239 + done <<< "$secret_labels" 240 + 241 + echo ']}' >> "$backup_file" 242 + echo "" 243 + echo "📁 Backup saved to: $backup_file" 244 + ;; 245 + 246 + *) 247 + echo "❌ Unknown operation: $OPERATION" 248 + echo "" 249 + echo "Available operations:" 250 + echo " create - Create all secrets from config file" 251 + echo " delete - Delete all secrets from config file" 252 + echo " status - Check status of all secrets" 253 + echo " backup - Backup all secrets to file" 254 + echo "" 255 + echo "Usage: $0 [operation] [config-file]" 256 + exit 1 257 + ;; 258 + esac
+23
examples/api/create-secret.json
··· 1 + { 2 + "label": "myapp-database", 3 + "id": 100, 4 + "format": "json", 5 + "description": "Database credentials for myapp service", 6 + "tags": { 7 + "environment": "production", 8 + "app": "myapp", 9 + "type": "database", 10 + "owner": "platform-team" 11 + }, 12 + "data": { 13 + "database_url": "postgresql://myuser:mypass@db.example.com:5432/myapp", 14 + "username": "myuser", 15 + "password": "mypass", 16 + "host": "db.example.com", 17 + "port": "5432", 18 + "database_name": "myapp", 19 + "ssl_mode": "require", 20 + "connection_pool_size": "10", 21 + "timeout": "30s" 22 + } 23 + }
+87
examples/api/create-secret.sh
··· 1 + #!/bin/bash 2 + 3 + # Create HSM Secret via REST API 4 + # Usage: ./create-secret.sh [secret-name] [secret-id] 5 + 6 + set -e 7 + 8 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 + SECRET_NAME=${1:-"example-secret"} 10 + SECRET_ID=${2:-"$(date +%s)"} # Use timestamp as default ID 11 + 12 + echo "🔐 Creating HSM Secret via API..." 13 + echo "Secret Name: $SECRET_NAME" 14 + echo "Secret ID: $SECRET_ID" 15 + echo "API Base URL: $API_BASE_URL" 16 + echo "" 17 + 18 + # Create the JSON payload 19 + payload=$(cat <<EOF 20 + { 21 + "label": "$SECRET_NAME", 22 + "id": $SECRET_ID, 23 + "format": "json", 24 + "description": "Secret created via API on $(date)", 25 + "tags": { 26 + "created_by": "api-script", 27 + "environment": "development", 28 + "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 29 + }, 30 + "data": { 31 + "api_key": "sk_test_$(openssl rand -hex 16)", 32 + "webhook_secret": "whsec_$(openssl rand -hex 20)", 33 + "database_url": "postgresql://user:$(openssl rand -hex 12)@localhost:5432/testdb", 34 + "redis_url": "redis://localhost:6379/0", 35 + "created_timestamp": "$(date +%s)" 36 + } 37 + } 38 + EOF 39 + ) 40 + 41 + echo "📝 Request Payload:" 42 + echo "$payload" | jq '.' 43 + echo "" 44 + 45 + # Make the API call 46 + echo "📤 Sending create request..." 47 + response=$(curl -s -X POST \ 48 + -H "Content-Type: application/json" \ 49 + -d "$payload" \ 50 + "$API_BASE_URL/api/v1/hsm/secrets") 51 + 52 + echo "📥 Response:" 53 + echo "$response" | jq '.' 54 + 55 + # Check if the request was successful 56 + success=$(echo "$response" | jq -r '.success') 57 + if [ "$success" = "true" ]; then 58 + echo "" 59 + echo "✅ Secret created successfully!" 60 + 61 + # Extract created secret info 62 + label=$(echo "$response" | jq -r '.data.label') 63 + id=$(echo "$response" | jq -r '.data.id') 64 + path=$(echo "$response" | jq -r '.data.path') 65 + 66 + echo " Label: $label" 67 + echo " ID: $id" 68 + echo " HSM Path: $path" 69 + 70 + echo "" 71 + echo "🔍 To retrieve this secret:" 72 + echo " curl $API_BASE_URL/api/v1/hsm/secrets/$label" 73 + 74 + echo "" 75 + echo "📋 To check Kubernetes Secret:" 76 + echo " kubectl get secret $label" 77 + echo " kubectl describe secret $label" 78 + 79 + else 80 + echo "" 81 + echo "❌ Failed to create secret!" 82 + error_code=$(echo "$response" | jq -r '.error.code // "unknown"') 83 + error_message=$(echo "$response" | jq -r '.error.message // "No error message"') 84 + echo " Error Code: $error_code" 85 + echo " Error Message: $error_message" 86 + exit 1 87 + fi
+241
examples/api/direct-import-examples.sh
··· 1 + #!/bin/bash 2 + 3 + # Direct API import examples for HSM secrets 4 + # Demonstrates various import patterns 5 + 6 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 7 + 8 + echo "🚀 Direct API Import Examples" 9 + echo "==============================" 10 + 11 + # Example 1: Import from environment variables 12 + echo "" 13 + echo "📋 Example 1: Import from Environment Variables" 14 + curl -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 15 + -H "Content-Type: application/json" \ 16 + -d '{ 17 + "label": "env-config", 18 + "id": 3001, 19 + "format": "json", 20 + "description": "Application configuration from environment", 21 + "tags": { 22 + "source": "environment", 23 + "type": "config" 24 + }, 25 + "data": { 26 + "NODE_ENV": "'${NODE_ENV:-development}'", 27 + "LOG_LEVEL": "'${LOG_LEVEL:-info}'", 28 + "PORT": "'${PORT:-3000}'" 29 + } 30 + }' 31 + 32 + echo "" 33 + echo "" 34 + 35 + # Example 2: Import TLS certificates 36 + echo "📋 Example 2: Import TLS Certificate Bundle" 37 + curl -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 38 + -H "Content-Type: application/json" \ 39 + -d '{ 40 + "label": "app-tls-bundle", 41 + "id": 3002, 42 + "format": "text", 43 + "description": "Application TLS certificate bundle", 44 + "tags": { 45 + "type": "tls", 46 + "app": "web-server" 47 + }, 48 + "data": { 49 + "server.crt": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAK...\n-----END CERTIFICATE-----", 50 + "server.key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG...\n-----END PRIVATE KEY-----", 51 + "ca-bundle.crt": "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQ...\n-----END CERTIFICATE-----" 52 + } 53 + }' 54 + 55 + echo "" 56 + echo "" 57 + 58 + # Example 3: Import database connection strings 59 + echo "📋 Example 3: Import Database Connections" 60 + for db in primary replica analytics; do 61 + echo "Importing $db database connection..." 62 + curl -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 63 + -H "Content-Type: application/json" \ 64 + -d '{ 65 + "label": "db-'$db'", 66 + "id": '$((3003 + $(echo $db | wc -c)))', 67 + "format": "json", 68 + "description": "'$db' database connection details", 69 + "tags": { 70 + "type": "database", 71 + "role": "'$db'" 72 + }, 73 + "data": { 74 + "host": "'$db'.db.internal", 75 + "port": "5432", 76 + "database": "app_'$db'", 77 + "username": "app_user", 78 + "password": "secure_'$db'_password", 79 + "connection_string": "postgresql://app_user:secure_'$db'_password@'$db'.db.internal:5432/app_'$db'" 80 + } 81 + }' 82 + echo "" 83 + done 84 + 85 + echo "" 86 + 87 + # Example 4: Import API keys from CSV-like data 88 + echo "📋 Example 4: Import Multiple API Keys" 89 + api_services=("stripe" "sendgrid" "aws" "github") 90 + api_keys=("sk_live_example123" "SG.example456" "AKIA1234567890" "ghp_example789") 91 + 92 + for i in "${!api_services[@]}"; do 93 + service="${api_services[$i]}" 94 + key="${api_keys[$i]}" 95 + 96 + echo "Importing $service API key..." 97 + curl -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 98 + -H "Content-Type: application/json" \ 99 + -d '{ 100 + "label": "'$service'-api-key", 101 + "id": '$((3100 + $i))', 102 + "format": "text", 103 + "description": "'$service' API authentication key", 104 + "tags": { 105 + "type": "api-key", 106 + "service": "'$service'" 107 + }, 108 + "data": { 109 + "api_key": "'$key'" 110 + } 111 + }' 112 + echo "" 113 + done 114 + 115 + echo "" 116 + 117 + # Example 5: Import from file content 118 + echo "📋 Example 5: Import from File (if exists)" 119 + if [ -f "/tmp/secret-file.txt" ]; then 120 + file_content=$(cat /tmp/secret-file.txt | base64 -w 0) 121 + 122 + curl -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 123 + -H "Content-Type: application/json" \ 124 + -d '{ 125 + "label": "file-based-secret", 126 + "id": 3200, 127 + "format": "binary", 128 + "description": "Secret imported from file", 129 + "tags": { 130 + "source": "file", 131 + "encoding": "base64" 132 + }, 133 + "data": { 134 + "content": "'$file_content'" 135 + } 136 + }' 137 + else 138 + echo "⚠️ /tmp/secret-file.txt not found, skipping file import example" 139 + fi 140 + 141 + echo "" 142 + echo "" 143 + 144 + # Example 6: Batch import with error handling 145 + echo "📋 Example 6: Batch Import with Error Handling" 146 + secrets_to_import='[ 147 + { 148 + "label": "batch-secret-1", 149 + "id": 3301, 150 + "format": "json", 151 + "data": {"key": "value1"} 152 + }, 153 + { 154 + "label": "batch-secret-2", 155 + "id": 3302, 156 + "format": "json", 157 + "data": {"key": "value2"} 158 + }, 159 + { 160 + "label": "batch-secret-3", 161 + "id": 3303, 162 + "format": "json", 163 + "data": {"key": "value3"} 164 + } 165 + ]' 166 + 167 + echo "$secrets_to_import" | jq -c '.[]' | while IFS= read -r secret; do 168 + label=$(echo "$secret" | jq -r '.label') 169 + echo "Importing: $label" 170 + 171 + response=$(curl -s -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 172 + -H "Content-Type: application/json" \ 173 + -d "$secret") 174 + 175 + success=$(echo "$response" | jq -r '.success') 176 + if [ "$success" = "true" ]; then 177 + echo " ✅ Success" 178 + else 179 + error_msg=$(echo "$response" | jq -r '.error.message // "Unknown error"') 180 + echo " ❌ Failed: $error_msg" 181 + fi 182 + done 183 + 184 + echo "" 185 + echo "" 186 + 187 + # Example 7: Import with validation 188 + echo "📋 Example 7: Import with Pre-validation" 189 + validate_and_import() { 190 + local label="$1" 191 + local secret_data="$2" 192 + 193 + # Check if secret already exists 194 + existing=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label") 195 + exists=$(echo "$existing" | jq -r '.success') 196 + 197 + if [ "$exists" = "true" ]; then 198 + echo "⚠️ Secret '$label' already exists, skipping..." 199 + return 1 200 + fi 201 + 202 + # Validate JSON structure 203 + if ! echo "$secret_data" | jq empty 2>/dev/null; then 204 + echo "❌ Invalid JSON for secret '$label'" 205 + return 1 206 + fi 207 + 208 + # Import the secret 209 + echo "Creating new secret: $label" 210 + response=$(curl -s -X POST "$API_BASE_URL/api/v1/hsm/secrets" \ 211 + -H "Content-Type: application/json" \ 212 + -d "$secret_data") 213 + 214 + success=$(echo "$response" | jq -r '.success') 215 + if [ "$success" = "true" ]; then 216 + echo " ✅ Imported successfully" 217 + return 0 218 + else 219 + error_msg=$(echo "$response" | jq -r '.error.message // "Unknown error"') 220 + echo " ❌ Import failed: $error_msg" 221 + return 1 222 + fi 223 + } 224 + 225 + # Test the validation function 226 + validate_and_import "validated-secret" '{ 227 + "label": "validated-secret", 228 + "id": 3400, 229 + "format": "json", 230 + "description": "A secret imported with validation", 231 + "data": { 232 + "validated": true, 233 + "timestamp": "'$(date -Iseconds)'" 234 + } 235 + }' 236 + 237 + echo "" 238 + echo "🎉 All import examples completed!" 239 + echo "" 240 + echo "To verify imports:" 241 + echo " curl $API_BASE_URL/api/v1/hsm/secrets"
+96
examples/api/health-check.sh
··· 1 + #!/bin/bash 2 + 3 + # HSM Secrets Operator API Health Check 4 + # This script checks the health of the API server and HSM devices 5 + 6 + set -e 7 + 8 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 + 10 + echo "🔍 Checking HSM Secrets Operator API Health..." 11 + echo "API Base URL: $API_BASE_URL" 12 + echo "" 13 + 14 + # Function to make API calls with error handling 15 + api_call() { 16 + local method="$1" 17 + local endpoint="$2" 18 + local data="$3" 19 + 20 + if [ -n "$data" ]; then 21 + curl -s -X "$method" \ 22 + -H "Content-Type: application/json" \ 23 + -d "$data" \ 24 + "$API_BASE_URL$endpoint" 25 + else 26 + curl -s -X "$method" "$API_BASE_URL$endpoint" 27 + fi 28 + } 29 + 30 + # Check API health endpoint 31 + echo "📊 API Health Status:" 32 + health_response=$(api_call GET "/api/v1/health") 33 + echo "$health_response" | jq '.' 34 + 35 + # Extract health information 36 + status=$(echo "$health_response" | jq -r '.data.status') 37 + hsm_connected=$(echo "$health_response" | jq -r '.data.hsm_connected') 38 + replication_enabled=$(echo "$health_response" | jq -r '.data.replication_enabled') 39 + active_nodes=$(echo "$health_response" | jq -r '.data.active_nodes') 40 + 41 + echo "" 42 + echo "🏥 Health Summary:" 43 + echo " Overall Status: $status" 44 + echo " HSM Connected: $hsm_connected" 45 + echo " Replication Enabled: $replication_enabled" 46 + echo " Active Nodes: $active_nodes" 47 + 48 + # Check if API is healthy 49 + if [ "$status" = "healthy" ]; then 50 + echo " ✅ API is healthy" 51 + exit_code=0 52 + else 53 + echo " ❌ API is not healthy" 54 + exit_code=1 55 + fi 56 + 57 + # Check HSM connectivity 58 + if [ "$hsm_connected" = "true" ]; then 59 + echo " ✅ HSM is connected" 60 + else 61 + echo " ❌ HSM is not connected" 62 + exit_code=1 63 + fi 64 + 65 + echo "" 66 + echo "📋 Additional Checks:" 67 + 68 + # Test basic API functionality 69 + echo " Testing secret listing endpoint..." 70 + secrets_response=$(api_call GET "/api/v1/hsm/secrets" 2>/dev/null) 71 + if [ $? -eq 0 ]; then 72 + secret_count=$(echo "$secrets_response" | jq -r '.data.total // 0') 73 + echo " ✅ Secrets endpoint working (found $secret_count secrets)" 74 + else 75 + echo " ❌ Secrets endpoint failed" 76 + exit_code=1 77 + fi 78 + 79 + # Test API response format 80 + echo " Validating API response format..." 81 + success=$(echo "$health_response" | jq -r '.success') 82 + if [ "$success" = "true" ]; then 83 + echo " ✅ API response format is valid" 84 + else 85 + echo " ❌ API response format is invalid" 86 + exit_code=1 87 + fi 88 + 89 + echo "" 90 + if [ $exit_code -eq 0 ]; then 91 + echo "🎉 All health checks passed!" 92 + else 93 + echo "❌ Some health checks failed!" 94 + fi 95 + 96 + exit $exit_code
+123
examples/api/import-from-k8s.sh
··· 1 + #!/bin/bash 2 + 3 + # Import existing Kubernetes Secret to HSM via REST API 4 + # Usage: ./import-from-k8s.sh [secret-name] [namespace] [target-label] [target-id] 5 + 6 + set -e 7 + 8 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 + SOURCE_SECRET=${1:-""} 10 + SOURCE_NAMESPACE=${2:-"default"} 11 + TARGET_LABEL=${3:-""} 12 + TARGET_ID=${4:-"$(date +%s)"} 13 + 14 + if [ -z "$SOURCE_SECRET" ]; then 15 + echo "Usage: $0 <source-secret-name> [namespace] [target-label] [target-id]" 16 + echo "" 17 + echo "Available secrets in namespace '$SOURCE_NAMESPACE':" 18 + kubectl get secrets -n "$SOURCE_NAMESPACE" --field-selector type=Opaque -o name | sed 's/secret\///' 19 + exit 1 20 + fi 21 + 22 + if [ -z "$TARGET_LABEL" ]; then 23 + TARGET_LABEL="$SOURCE_SECRET-hsm" 24 + fi 25 + 26 + echo "📦 Importing Kubernetes Secret to HSM..." 27 + echo "Source Secret: $SOURCE_SECRET" 28 + echo "Source Namespace: $SOURCE_NAMESPACE" 29 + echo "Target Label: $TARGET_LABEL" 30 + echo "Target ID: $TARGET_ID" 31 + echo "API Base URL: $API_BASE_URL" 32 + echo "" 33 + 34 + # Check if source secret exists 35 + echo "🔍 Checking source secret..." 36 + if ! kubectl get secret "$SOURCE_SECRET" -n "$SOURCE_NAMESPACE" >/dev/null 2>&1; then 37 + echo "❌ Source secret '$SOURCE_SECRET' not found in namespace '$SOURCE_NAMESPACE'" 38 + exit 1 39 + fi 40 + 41 + # Show source secret info 42 + echo "📋 Source secret info:" 43 + kubectl describe secret "$SOURCE_SECRET" -n "$SOURCE_NAMESPACE" 44 + echo "" 45 + 46 + # Create the import request payload 47 + payload=$(cat <<EOF 48 + { 49 + "source": "kubernetes", 50 + "secret_name": "$SOURCE_SECRET", 51 + "secret_namespace": "$SOURCE_NAMESPACE", 52 + "target_label": "$TARGET_LABEL", 53 + "target_id": $TARGET_ID, 54 + "format": "json", 55 + "key_mapping": { 56 + "username": "db_username", 57 + "password": "db_password" 58 + } 59 + } 60 + EOF 61 + ) 62 + 63 + echo "📝 Import Request:" 64 + echo "$payload" | jq '.' 65 + echo "" 66 + 67 + # Make the import API call 68 + echo "📤 Sending import request..." 69 + response=$(curl -s -X POST \ 70 + -H "Content-Type: application/json" \ 71 + -d "$payload" \ 72 + "$API_BASE_URL/api/v1/hsm/secrets/import") 73 + 74 + echo "📥 Response:" 75 + echo "$response" | jq '.' 76 + 77 + # Check if the import was successful 78 + success=$(echo "$response" | jq -r '.success') 79 + if [ "$success" = "true" ]; then 80 + echo "" 81 + echo "✅ Secret imported successfully!" 82 + 83 + # Extract imported secret info 84 + label=$(echo "$response" | jq -r '.data.label // "N/A"') 85 + id=$(echo "$response" | jq -r '.data.id // "N/A"') 86 + path=$(echo "$response" | jq -r '.data.path // "N/A"') 87 + 88 + echo " Target Label: $label" 89 + echo " Target ID: $id" 90 + echo " HSM Path: $path" 91 + 92 + echo "" 93 + echo "🔍 Verification commands:" 94 + echo " # Check HSM secret via API:" 95 + echo " curl $API_BASE_URL/api/v1/hsm/secrets/$label" 96 + echo "" 97 + echo " # Check HSMSecret resource:" 98 + echo " kubectl get hsmsecret $label" 99 + echo "" 100 + echo " # Check created Kubernetes Secret:" 101 + echo " kubectl get secret $label" 102 + 103 + echo "" 104 + echo "📊 Comparison:" 105 + echo " Original Secret: kubectl get secret $SOURCE_SECRET -n $SOURCE_NAMESPACE -o yaml" 106 + echo " HSM Secret: kubectl get secret $label -o yaml" 107 + 108 + else 109 + echo "" 110 + echo "❌ Failed to import secret!" 111 + error_code=$(echo "$response" | jq -r '.error.code // "unknown"') 112 + error_message=$(echo "$response" | jq -r '.error.message // "No error message"') 113 + echo " Error Code: $error_code" 114 + echo " Error Message: $error_message" 115 + 116 + # Show detailed error if available 117 + if echo "$response" | jq -e '.error.details' >/dev/null; then 118 + echo " Error Details:" 119 + echo "$response" | jq '.error.details' 120 + fi 121 + 122 + exit 1 123 + fi
+59
examples/api/list-secrets.sh
··· 1 + #!/bin/bash 2 + 3 + # List all HSM secrets via REST API 4 + # Usage: ./list-secrets.sh [page] [page_size] 5 + 6 + set -e 7 + 8 + API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 + PAGE=${1:-1} 10 + PAGE_SIZE=${2:-10} 11 + 12 + echo "📋 Listing HSM Secrets via API..." 13 + echo "API Base URL: $API_BASE_URL" 14 + echo "Page: $PAGE, Page Size: $PAGE_SIZE" 15 + echo "" 16 + 17 + # Make the API call 18 + response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets?page=$PAGE&page_size=$PAGE_SIZE") 19 + 20 + # Check if the request was successful 21 + success=$(echo "$response" | jq -r '.success') 22 + if [ "$success" = "true" ]; then 23 + # Extract pagination info 24 + total=$(echo "$response" | jq -r '.data.total') 25 + current_page=$(echo "$response" | jq -r '.data.page') 26 + page_size=$(echo "$response" | jq -r '.data.page_size') 27 + 28 + echo "📊 Summary:" 29 + echo " Total Secrets: $total" 30 + echo " Current Page: $current_page" 31 + echo " Page Size: $page_size" 32 + echo "" 33 + 34 + # List secrets 35 + echo "🔐 Secrets:" 36 + echo "$response" | jq -r '.data.secrets[] | " • \(.label) (ID: \(.id)) - Updated: \(.updated_at // "N/A")"' 37 + 38 + # Show detailed table if there are secrets 39 + if [ "$total" -gt 0 ]; then 40 + echo "" 41 + echo "📋 Detailed List:" 42 + echo "$response" | jq -r ' 43 + .data.secrets | 44 + ["Label", "ID", "Checksum", "Replicated", "Updated"] as $headers | 45 + $headers, 46 + (["-----", "---", "--------", "---------", "-------"]) as $separators | 47 + $separators, 48 + (.[] | [.label, (.id // "N/A"), (.checksum[0:8] // "N/A"), .is_replicated, (.updated_at[0:10] // "N/A")]) | 49 + @tsv 50 + ' | column -t 51 + fi 52 + else 53 + echo "❌ Failed to list secrets!" 54 + error_code=$(echo "$response" | jq -r '.error.code // "unknown"') 55 + error_message=$(echo "$response" | jq -r '.error.message // "No error message"') 56 + echo " Error Code: $error_code" 57 + echo " Error Message: $error_message" 58 + exit 1 59 + fi
+67
examples/api/production-import.json
··· 1 + { 2 + "secrets": [ 3 + { 4 + "label": "postgres-credentials", 5 + "id": 2001, 6 + "format": "json", 7 + "description": "Production PostgreSQL credentials", 8 + "tags": { 9 + "app": "backend", 10 + "environment": "production", 11 + "type": "database" 12 + }, 13 + "data": { 14 + "POSTGRES_USER": "app_user", 15 + "POSTGRES_PASSWORD": "secure_password_123", 16 + "POSTGRES_DB": "production_db", 17 + "DATABASE_URL": "postgresql://app_user:secure_password_123@postgres.internal:5432/production_db" 18 + } 19 + }, 20 + { 21 + "label": "redis-auth", 22 + "id": 2002, 23 + "format": "text", 24 + "description": "Redis authentication token", 25 + "tags": { 26 + "app": "cache", 27 + "environment": "production", 28 + "type": "auth" 29 + }, 30 + "data": { 31 + "AUTH_TOKEN": "redis_secure_token_456" 32 + } 33 + }, 34 + { 35 + "label": "jwt-signing-keys", 36 + "id": 2003, 37 + "format": "json", 38 + "description": "JWT signing keys for authentication", 39 + "tags": { 40 + "app": "auth-service", 41 + "environment": "production", 42 + "type": "keys" 43 + }, 44 + "data": { 45 + "JWT_PRIVATE_KEY": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAK...example...\n-----END RSA PRIVATE KEY-----", 46 + "JWT_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgk...example...\n-----END PUBLIC KEY-----", 47 + "JWT_ALGORITHM": "RS256" 48 + } 49 + }, 50 + { 51 + "label": "api-gateway-certs", 52 + "id": 2004, 53 + "format": "text", 54 + "description": "TLS certificates for API gateway", 55 + "tags": { 56 + "app": "gateway", 57 + "environment": "production", 58 + "type": "tls" 59 + }, 60 + "data": { 61 + "tls.crt": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAK...example...\n-----END CERTIFICATE-----", 62 + "tls.key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0B...example...\n-----END PRIVATE KEY-----", 63 + "ca.crt": "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK...example...\n-----END CERTIFICATE-----" 64 + } 65 + } 66 + ] 67 + }
+130
examples/basic/README.md
··· 1 + # Basic Examples 2 + 3 + This directory contains basic usage examples to get you started with the HSM Secrets Operator. 4 + 5 + ## Examples Overview 6 + 7 + 1. **[pico-hsm-device.yaml](pico-hsm-device.yaml)** - HSM device discovery configuration 8 + 2. **[database-secret.yaml](database-secret.yaml)** - Database credentials management 9 + 3. **[tls-certificate.yaml](tls-certificate.yaml)** - TLS certificate storage 10 + 4. **[api-keys.yaml](api-keys.yaml)** - Third-party API key management 11 + 12 + ## Getting Started 13 + 14 + ### Step 1: Configure HSM Device 15 + 16 + First, create an HSMDevice resource to discover and configure your Pico HSM: 17 + 18 + ```bash 19 + kubectl apply -f pico-hsm-device.yaml 20 + ``` 21 + 22 + Check the device status: 23 + ```bash 24 + kubectl get hsmdevice pico-hsm -o yaml 25 + kubectl describe hsmdevice pico-hsm 26 + ``` 27 + 28 + ### Step 2: Create Your First Secret 29 + 30 + Create a database secret stored on the HSM: 31 + 32 + ```bash 33 + kubectl apply -f database-secret.yaml 34 + ``` 35 + 36 + Verify the secret was created: 37 + ```bash 38 + kubectl get hsmsecret database-credentials 39 + kubectl get secret database-credentials 40 + ``` 41 + 42 + ### Step 3: Use the Secret in Your Application 43 + 44 + The operator automatically creates a Kubernetes Secret that your applications can use: 45 + 46 + ```yaml 47 + apiVersion: apps/v1 48 + kind: Deployment 49 + metadata: 50 + name: myapp 51 + spec: 52 + template: 53 + spec: 54 + containers: 55 + - name: app 56 + image: myapp:latest 57 + env: 58 + - name: DATABASE_URL 59 + valueFrom: 60 + secretKeyRef: 61 + name: database-credentials 62 + key: database_url 63 + - name: DB_USERNAME 64 + valueFrom: 65 + secretKeyRef: 66 + name: database-credentials 67 + key: username 68 + ``` 69 + 70 + ## Key Concepts 71 + 72 + ### HSMDevice 73 + Represents a physical HSM device and handles: 74 + - USB device discovery 75 + - PKCS#11 library configuration 76 + - Device health monitoring 77 + 78 + ### HSMSecret 79 + Represents a secret stored on the HSM and manages: 80 + - Bidirectional sync with Kubernetes Secrets 81 + - Data integrity with checksums 82 + - Automatic updates when HSM data changes 83 + 84 + ### Sync Process 85 + 1. HSMSecret reads data from HSM using PKCS#11 86 + 2. Creates/updates corresponding Kubernetes Secret 87 + 3. Monitors for changes and keeps both in sync 88 + 4. Provides status and health information 89 + 90 + ## Common Patterns 91 + 92 + ### Environment-Specific Secrets 93 + Use namespaces to separate secrets by environment: 94 + 95 + ```bash 96 + # Production 97 + kubectl apply -f database-secret.yaml -n production 98 + 99 + # Staging 100 + kubectl apply -f database-secret.yaml -n staging 101 + ``` 102 + 103 + ### Secret Rotation 104 + Update secrets directly on the HSM, and they'll automatically sync: 105 + 106 + ```bash 107 + # The operator detects HSM changes and updates Kubernetes Secrets 108 + # No manual intervention required 109 + ``` 110 + 111 + ### Multiple Applications 112 + Share the same HSM secret across multiple applications: 113 + 114 + ```yaml 115 + # App 1 116 + apiVersion: v1 117 + kind: Secret 118 + metadata: 119 + name: app1-db-secret 120 + data: 121 + url: <from-hsm-secret> 122 + 123 + # App 2 124 + apiVersion: v1 125 + kind: Secret 126 + metadata: 127 + name: app2-db-secret 128 + data: 129 + url: <from-hsm-secret> 130 + ```
+128
examples/basic/api-keys.yaml
··· 1 + apiVersion: hsm.j5t.io/v1alpha1 2 + kind: HSMSecret 3 + metadata: 4 + name: external-api-keys 5 + namespace: default 6 + labels: 7 + app: myapp 8 + type: api-keys 9 + annotations: 10 + hsm.j5t.io/description: "API keys for external services (Stripe, AWS, etc.)" 11 + spec: 12 + # Path on the HSM where API keys are stored 13 + hsmPath: "secrets/api-keys/external-services" 14 + 15 + # Name of the Secret containing API keys 16 + secretName: "external-api-keys" 17 + 18 + # Enable automatic synchronization 19 + autoSync: true 20 + 21 + # Sync every 10 minutes (API keys might rotate frequently) 22 + syncInterval: 600 23 + 24 + # Standard opaque secret type 25 + secretType: Opaque 26 + 27 + --- 28 + # Example application using the API keys 29 + apiVersion: apps/v1 30 + kind: Deployment 31 + metadata: 32 + name: payment-service 33 + namespace: default 34 + spec: 35 + replicas: 3 36 + selector: 37 + matchLabels: 38 + app: payment-service 39 + template: 40 + metadata: 41 + labels: 42 + app: payment-service 43 + spec: 44 + containers: 45 + - name: payment-service 46 + image: mycompany/payment-service:v1.2.3 47 + env: 48 + # Stripe API key from HSM 49 + - name: STRIPE_API_KEY 50 + valueFrom: 51 + secretKeyRef: 52 + name: external-api-keys 53 + key: stripe_api_key 54 + - name: STRIPE_WEBHOOK_SECRET 55 + valueFrom: 56 + secretKeyRef: 57 + name: external-api-keys 58 + key: stripe_webhook_secret 59 + 60 + # AWS credentials from HSM 61 + - name: AWS_ACCESS_KEY_ID 62 + valueFrom: 63 + secretKeyRef: 64 + name: external-api-keys 65 + key: aws_access_key_id 66 + - name: AWS_SECRET_ACCESS_KEY 67 + valueFrom: 68 + secretKeyRef: 69 + name: external-api-keys 70 + key: aws_secret_access_key 71 + 72 + # Other third-party API keys 73 + - name: SENDGRID_API_KEY 74 + valueFrom: 75 + secretKeyRef: 76 + name: external-api-keys 77 + key: sendgrid_api_key 78 + - name: DATADOG_API_KEY 79 + valueFrom: 80 + secretKeyRef: 81 + name: external-api-keys 82 + key: datadog_api_key 83 + 84 + ports: 85 + - containerPort: 8080 86 + name: http 87 + 88 + # Health checks 89 + livenessProbe: 90 + httpGet: 91 + path: /health 92 + port: 8080 93 + initialDelaySeconds: 30 94 + periodSeconds: 10 95 + 96 + readinessProbe: 97 + httpGet: 98 + path: /ready 99 + port: 8080 100 + initialDelaySeconds: 5 101 + periodSeconds: 5 102 + 103 + # Resource limits 104 + resources: 105 + requests: 106 + cpu: 100m 107 + memory: 128Mi 108 + limits: 109 + cpu: 500m 110 + memory: 512Mi 111 + 112 + --- 113 + # Service for the payment service 114 + apiVersion: v1 115 + kind: Service 116 + metadata: 117 + name: payment-service 118 + namespace: default 119 + labels: 120 + app: payment-service 121 + spec: 122 + selector: 123 + app: payment-service 124 + ports: 125 + - port: 80 126 + targetPort: 8080 127 + name: http 128 + type: ClusterIP
+72
examples/basic/database-secret.yaml
··· 1 + apiVersion: hsm.j5t.io/v1alpha1 2 + kind: HSMSecret 3 + metadata: 4 + name: database-credentials 5 + namespace: default 6 + labels: 7 + app: myapp 8 + type: database 9 + environment: production 10 + annotations: 11 + hsm.j5t.io/description: "PostgreSQL database credentials for production" 12 + spec: 13 + # Path on the HSM where the secret is stored 14 + hsmPath: "secrets/production/database-credentials" 15 + 16 + # Name of the Kubernetes Secret to create/maintain 17 + secretName: "database-credentials" 18 + 19 + # Enable bidirectional synchronization 20 + autoSync: true 21 + 22 + # Check for changes every 5 minutes (300 seconds) 23 + syncInterval: 300 24 + 25 + # Type of Kubernetes Secret to create 26 + secretType: Opaque 27 + 28 + --- 29 + # Example of how to use the secret in a deployment 30 + apiVersion: apps/v1 31 + kind: Deployment 32 + metadata: 33 + name: myapp-database 34 + namespace: default 35 + spec: 36 + replicas: 2 37 + selector: 38 + matchLabels: 39 + app: myapp-database 40 + template: 41 + metadata: 42 + labels: 43 + app: myapp-database 44 + spec: 45 + containers: 46 + - name: app 47 + image: postgres:13 48 + env: 49 + # Use the HSM-backed secret 50 + - name: POSTGRES_DB 51 + valueFrom: 52 + secretKeyRef: 53 + name: database-credentials 54 + key: database_name 55 + - name: POSTGRES_USER 56 + valueFrom: 57 + secretKeyRef: 58 + name: database-credentials 59 + key: username 60 + - name: POSTGRES_PASSWORD 61 + valueFrom: 62 + secretKeyRef: 63 + name: database-credentials 64 + key: password 65 + - name: DATABASE_URL 66 + valueFrom: 67 + secretKeyRef: 68 + name: database-credentials 69 + key: database_url 70 + ports: 71 + - containerPort: 5432 72 + name: postgres
+38
examples/basic/pico-hsm-device.yaml
··· 1 + apiVersion: hsm.j5t.io/v1alpha1 2 + kind: HSMDevice 3 + metadata: 4 + name: pico-hsm 5 + namespace: default 6 + labels: 7 + device-type: pico-hsm 8 + environment: production 9 + spec: 10 + # Device type for auto-discovery 11 + deviceType: PicoHSM 12 + 13 + # USB device specifications for Pico HSM 14 + usb: 15 + vendorId: "20a0" 16 + productId: "4230" 17 + # serialNumber: "12345" # Optional: specific device serial 18 + 19 + # Alternative: Manual path specification 20 + # devicePath: 21 + # path: "/dev/sc-hsm*" 22 + # permissions: "0666" 23 + 24 + # Node selection (optional - runs on all nodes if not specified) 25 + nodeSelector: 26 + # kubernetes.io/hostname: "worker-node-1" 27 + hsm.j5t.io/enabled: "true" 28 + 29 + # PKCS#11 library path (auto-detected for known devices) 30 + pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so" 31 + 32 + # Maximum number of devices to discover 33 + maxDevices: 2 34 + 35 + --- 36 + # Optional: Node label for HSM-enabled nodes 37 + # Run this on nodes with HSM devices: 38 + # kubectl label node worker-node-1 hsm.j5t.io/enabled=true
+69
examples/basic/tls-certificate.yaml
··· 1 + apiVersion: hsm.j5t.io/v1alpha1 2 + kind: HSMSecret 3 + metadata: 4 + name: webapp-tls-cert 5 + namespace: default 6 + labels: 7 + app: webapp 8 + type: tls-certificate 9 + annotations: 10 + hsm.j5t.io/description: "TLS certificate and key for webapp.example.com" 11 + spec: 12 + # Path on the HSM where the TLS cert/key is stored 13 + hsmPath: "secrets/tls/webapp-example-com" 14 + 15 + # Name of the TLS Secret to create 16 + secretName: "webapp-tls" 17 + 18 + # Enable automatic sync 19 + autoSync: true 20 + 21 + # Check for certificate updates every hour 22 + syncInterval: 3600 23 + 24 + # Create a TLS secret type 25 + secretType: kubernetes.io/tls 26 + 27 + --- 28 + # Example Ingress using the TLS secret 29 + apiVersion: networking.k8s.io/v1 30 + kind: Ingress 31 + metadata: 32 + name: webapp-ingress 33 + namespace: default 34 + annotations: 35 + cert-manager.io/cluster-issuer: "letsencrypt-prod" 36 + nginx.ingress.kubernetes.io/ssl-redirect: "true" 37 + spec: 38 + tls: 39 + - hosts: 40 + - webapp.example.com 41 + # Use the HSM-backed TLS secret 42 + secretName: webapp-tls 43 + rules: 44 + - host: webapp.example.com 45 + http: 46 + paths: 47 + - path: / 48 + pathType: Prefix 49 + backend: 50 + service: 51 + name: webapp-service 52 + port: 53 + number: 80 54 + 55 + --- 56 + # Example service for the webapp 57 + apiVersion: v1 58 + kind: Service 59 + metadata: 60 + name: webapp-service 61 + namespace: default 62 + spec: 63 + selector: 64 + app: webapp 65 + ports: 66 + - port: 80 67 + targetPort: 8080 68 + name: http 69 + type: ClusterIP
+224
examples/deployment/README.md
··· 1 + # Deployment Examples 2 + 3 + This directory contains complete deployment examples for the HSM Secrets Operator in production environments. 4 + 5 + ## Files 6 + 7 + - **[complete-setup.yaml](complete-setup.yaml)** - Full production deployment with all components 8 + - **[operator-deployment.yaml](operator-deployment.yaml)** - Just the operator deployment 9 + - **[monitoring-setup.yaml](monitoring-setup.yaml)** - Prometheus monitoring configuration 10 + 11 + ## Complete Setup 12 + 13 + The `complete-setup.yaml` file demonstrates a full production deployment including: 14 + 15 + ### Core Components 16 + - HSM Secrets Operator deployment 17 + - HSMDevice configuration with mirroring 18 + - Production HSMSecret resources 19 + - RBAC configuration 20 + 21 + ### Sample Application 22 + - Web application deployment using HSM secrets 23 + - Database credentials from HSM 24 + - TLS certificates from HSM 25 + - High availability configuration 26 + 27 + ### Production Features 28 + - Pod anti-affinity for distribution across nodes 29 + - Horizontal Pod Autoscaler for scaling 30 + - Pod Disruption Budget for availability 31 + - Network policies for security 32 + - Resource limits and health checks 33 + 34 + ### Monitoring 35 + - ServiceMonitor for Prometheus integration 36 + - Metrics collection from operator 37 + - HSM device health monitoring 38 + 39 + ## Deployment Steps 40 + 41 + ### 1. Prerequisites 42 + 43 + Ensure you have: 44 + - Kubernetes cluster (v1.20+) 45 + - HSM devices connected to nodes 46 + - OpenSC libraries installed 47 + - Prometheus Operator (for monitoring) 48 + 49 + ### 2. Label HSM Nodes 50 + 51 + Label nodes that have HSM devices: 52 + ```bash 53 + kubectl label node <node-name> hsm.j5t.io/enabled=true 54 + ``` 55 + 56 + ### 3. Deploy the Operator 57 + 58 + ```bash 59 + # Deploy CRDs first 60 + kubectl apply -f config/crd/bases/ 61 + 62 + # Deploy the operator 63 + kubectl apply -f config/default/ 64 + 65 + # Or use the complete setup 66 + kubectl apply -f examples/deployment/complete-setup.yaml 67 + ``` 68 + 69 + ### 4. Verify Deployment 70 + 71 + ```bash 72 + # Check operator pods 73 + kubectl get pods -n hsm-secrets-operator-system 74 + 75 + # Check HSM devices 76 + kubectl get hsmdevice 77 + 78 + # Check secrets 79 + kubectl get hsmsecret 80 + kubectl get secret 81 + ``` 82 + 83 + ### 5. Test the API 84 + 85 + If API is enabled: 86 + ```bash 87 + # Port forward to access API locally 88 + kubectl port-forward -n hsm-secrets-operator-system service/hsm-secrets-operator-api 8090:8090 89 + 90 + # Test health endpoint 91 + curl http://localhost:8090/api/v1/health 92 + ``` 93 + 94 + ## Configuration Options 95 + 96 + ### HSM Device Configuration 97 + 98 + ```yaml 99 + spec: 100 + deviceType: PicoHSM # or SmartCardHSM, Generic 101 + usb: 102 + vendorId: "20a0" 103 + productId: "4230" 104 + nodeSelector: 105 + hsm.j5t.io/enabled: "true" 106 + mirroring: 107 + policy: "ReadOnly" 108 + syncInterval: 300 109 + autoFailover: true 110 + ``` 111 + 112 + ### Secret Configuration 113 + 114 + ```yaml 115 + spec: 116 + hsmPath: "secrets/production/my-secret" 117 + secretName: "my-secret" 118 + autoSync: true 119 + syncInterval: 600 120 + secretType: Opaque # or kubernetes.io/tls, etc. 121 + ``` 122 + 123 + ## Security Considerations 124 + 125 + ### 1. RBAC 126 + - Limit access to HSMSecret resources 127 + - Use service accounts with minimal permissions 128 + - Separate dev/staging/prod access 129 + 130 + ### 2. Network Security 131 + - Use NetworkPolicies to restrict traffic 132 + - Enable TLS for API communication 133 + - Use private networks where possible 134 + 135 + ### 3. HSM Security 136 + - Properly configure HSM authentication 137 + - Regular security updates for OpenSC libraries 138 + - Monitor HSM access and operations 139 + 140 + ### 4. Secret Management 141 + - Use strong passwords and keys 142 + - Implement secret rotation policies 143 + - Monitor secret access and changes 144 + 145 + ## Monitoring and Alerting 146 + 147 + ### Key Metrics to Monitor 148 + - HSM device connectivity 149 + - Secret sync status and lag 150 + - API response times and errors 151 + - Operator pod health 152 + 153 + ### Sample Alerts 154 + ```yaml 155 + # HSM Device Down 156 + - alert: HSMDeviceDown 157 + expr: hsm_device_connected == 0 158 + for: 5m 159 + labels: 160 + severity: critical 161 + annotations: 162 + summary: "HSM device is disconnected" 163 + 164 + # Secret Sync Failed 165 + - alert: SecretSyncFailed 166 + expr: hsm_secret_sync_failed > 0 167 + for: 2m 168 + labels: 169 + severity: warning 170 + annotations: 171 + summary: "Secret synchronization failed" 172 + ``` 173 + 174 + ## Troubleshooting 175 + 176 + ### Common Issues 177 + 178 + 1. **HSM Device Not Found** 179 + ```bash 180 + # Check USB devices 181 + lsusb 182 + 183 + # Check OpenSC 184 + pkcs11-tool --list-slots 185 + 186 + # Check node labels 187 + kubectl describe node <node-name> 188 + ``` 189 + 190 + 2. **Secrets Not Syncing** 191 + ```bash 192 + # Check HSMSecret status 193 + kubectl describe hsmsecret <secret-name> 194 + 195 + # Check operator logs 196 + kubectl logs -n hsm-secrets-operator-system deployment/hsm-secrets-operator-controller-manager 197 + ``` 198 + 199 + 3. **API Not Responding** 200 + ```bash 201 + # Check API service 202 + kubectl get service -n hsm-secrets-operator-system 203 + 204 + # Check API logs 205 + kubectl logs -n hsm-secrets-operator-system -l control-plane=controller-manager 206 + ``` 207 + 208 + ## Backup and Recovery 209 + 210 + ### Backup HSM Configuration 211 + ```bash 212 + # Export HSMDevice configurations 213 + kubectl get hsmdevice -o yaml > hsm-devices-backup.yaml 214 + 215 + # Export HSMSecret configurations 216 + kubectl get hsmsecret --all-namespaces -o yaml > hsm-secrets-backup.yaml 217 + ``` 218 + 219 + ### Recovery Process 220 + 1. Restore HSM devices and configure authentication 221 + 2. Deploy operator and CRDs 222 + 3. Apply HSMDevice configurations 223 + 4. Apply HSMSecret configurations 224 + 5. Verify secret synchronization
+368
examples/deployment/complete-setup.yaml
··· 1 + # Complete HSM Secrets Operator Setup Example 2 + # This file demonstrates a full deployment with all components 3 + 4 + --- 5 + # Namespace for the operator 6 + apiVersion: v1 7 + kind: Namespace 8 + metadata: 9 + name: hsm-secrets-operator-system 10 + labels: 11 + control-plane: controller-manager 12 + app.kubernetes.io/name: hsm-secrets-operator 13 + app.kubernetes.io/version: v1.0.0 14 + 15 + --- 16 + # HSM Device Configuration 17 + apiVersion: hsm.j5t.io/v1alpha1 18 + kind: HSMDevice 19 + metadata: 20 + name: production-hsm 21 + namespace: hsm-secrets-operator-system 22 + labels: 23 + environment: production 24 + device-type: pico-hsm 25 + spec: 26 + deviceType: PicoHSM 27 + usb: 28 + vendorId: "20a0" 29 + productId: "4230" 30 + nodeSelector: 31 + hsm.j5t.io/enabled: "true" 32 + pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so" 33 + maxDevices: 2 34 + mirroring: 35 + policy: "ReadOnly" 36 + syncInterval: 300 37 + autoFailover: true 38 + 39 + --- 40 + # Production Database Secret 41 + apiVersion: hsm.j5t.io/v1alpha1 42 + kind: HSMSecret 43 + metadata: 44 + name: production-database 45 + namespace: production 46 + labels: 47 + app: webapp 48 + type: database 49 + criticality: high 50 + spec: 51 + hsmPath: "secrets/production/webapp-database" 52 + secretName: "webapp-database-credentials" 53 + autoSync: true 54 + syncInterval: 600 # 10 minutes 55 + secretType: Opaque 56 + 57 + --- 58 + # TLS Certificate Secret 59 + apiVersion: hsm.j5t.io/v1alpha1 60 + kind: HSMSecret 61 + metadata: 62 + name: webapp-tls 63 + namespace: production 64 + labels: 65 + app: webapp 66 + type: tls 67 + spec: 68 + hsmPath: "secrets/tls/webapp-example-com" 69 + secretName: "webapp-tls-cert" 70 + autoSync: true 71 + syncInterval: 3600 # 1 hour 72 + secretType: kubernetes.io/tls 73 + 74 + --- 75 + # Production Namespace 76 + apiVersion: v1 77 + kind: Namespace 78 + metadata: 79 + name: production 80 + labels: 81 + environment: production 82 + hsm.j5t.io/enabled: "true" 83 + 84 + --- 85 + # Web Application Deployment 86 + apiVersion: apps/v1 87 + kind: Deployment 88 + metadata: 89 + name: webapp 90 + namespace: production 91 + labels: 92 + app: webapp 93 + version: v1.0.0 94 + spec: 95 + replicas: 3 96 + strategy: 97 + type: RollingUpdate 98 + rollingUpdate: 99 + maxUnavailable: 1 100 + maxSurge: 1 101 + selector: 102 + matchLabels: 103 + app: webapp 104 + template: 105 + metadata: 106 + labels: 107 + app: webapp 108 + version: v1.0.0 109 + spec: 110 + # Pod anti-affinity for high availability 111 + affinity: 112 + podAntiAffinity: 113 + preferredDuringSchedulingIgnoredDuringExecution: 114 + - weight: 100 115 + podAffinityTerm: 116 + labelSelector: 117 + matchExpressions: 118 + - key: app 119 + operator: In 120 + values: 121 + - webapp 122 + topologyKey: kubernetes.io/hostname 123 + 124 + containers: 125 + - name: webapp 126 + image: nginx:1.21-alpine 127 + ports: 128 + - containerPort: 8080 129 + name: http 130 + 131 + # Use HSM-backed secrets 132 + env: 133 + - name: DATABASE_URL 134 + valueFrom: 135 + secretKeyRef: 136 + name: webapp-database-credentials 137 + key: database_url 138 + - name: DB_USERNAME 139 + valueFrom: 140 + secretKeyRef: 141 + name: webapp-database-credentials 142 + key: username 143 + - name: DB_PASSWORD 144 + valueFrom: 145 + secretKeyRef: 146 + name: webapp-database-credentials 147 + key: password 148 + 149 + # Mount TLS certificate 150 + volumeMounts: 151 + - name: tls-certs 152 + mountPath: /etc/ssl/certs/webapp 153 + readOnly: true 154 + 155 + # Health checks 156 + livenessProbe: 157 + httpGet: 158 + path: /health 159 + port: 8080 160 + scheme: HTTP 161 + initialDelaySeconds: 30 162 + periodSeconds: 10 163 + timeoutSeconds: 5 164 + 165 + readinessProbe: 166 + httpGet: 167 + path: /ready 168 + port: 8080 169 + scheme: HTTP 170 + initialDelaySeconds: 5 171 + periodSeconds: 5 172 + timeoutSeconds: 3 173 + 174 + resources: 175 + requests: 176 + cpu: 100m 177 + memory: 128Mi 178 + limits: 179 + cpu: 500m 180 + memory: 512Mi 181 + 182 + securityContext: 183 + runAsNonRoot: true 184 + runAsUser: 1000 185 + allowPrivilegeEscalation: false 186 + capabilities: 187 + drop: 188 + - ALL 189 + 190 + volumes: 191 + - name: tls-certs 192 + secret: 193 + secretName: webapp-tls-cert 194 + 195 + securityContext: 196 + fsGroup: 2000 197 + 198 + --- 199 + # Service for the web application 200 + apiVersion: v1 201 + kind: Service 202 + metadata: 203 + name: webapp-service 204 + namespace: production 205 + labels: 206 + app: webapp 207 + spec: 208 + selector: 209 + app: webapp 210 + ports: 211 + - port: 80 212 + targetPort: 8080 213 + name: http 214 + type: ClusterIP 215 + 216 + --- 217 + # Ingress with TLS 218 + apiVersion: networking.k8s.io/v1 219 + kind: Ingress 220 + metadata: 221 + name: webapp-ingress 222 + namespace: production 223 + labels: 224 + app: webapp 225 + annotations: 226 + nginx.ingress.kubernetes.io/ssl-redirect: "true" 227 + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 228 + spec: 229 + tls: 230 + - hosts: 231 + - webapp.example.com 232 + secretName: webapp-tls-cert # HSM-backed TLS certificate 233 + rules: 234 + - host: webapp.example.com 235 + http: 236 + paths: 237 + - path: / 238 + pathType: Prefix 239 + backend: 240 + service: 241 + name: webapp-service 242 + port: 243 + number: 80 244 + 245 + --- 246 + # Horizontal Pod Autoscaler 247 + apiVersion: autoscaling/v2 248 + kind: HorizontalPodAutoscaler 249 + metadata: 250 + name: webapp-hpa 251 + namespace: production 252 + spec: 253 + scaleTargetRef: 254 + apiVersion: apps/v1 255 + kind: Deployment 256 + name: webapp 257 + minReplicas: 3 258 + maxReplicas: 10 259 + metrics: 260 + - type: Resource 261 + resource: 262 + name: cpu 263 + target: 264 + type: Utilization 265 + averageUtilization: 70 266 + - type: Resource 267 + resource: 268 + name: memory 269 + target: 270 + type: Utilization 271 + averageUtilization: 80 272 + 273 + --- 274 + # Pod Disruption Budget 275 + apiVersion: policy/v1 276 + kind: PodDisruptionBudget 277 + metadata: 278 + name: webapp-pdb 279 + namespace: production 280 + spec: 281 + minAvailable: 2 282 + selector: 283 + matchLabels: 284 + app: webapp 285 + 286 + --- 287 + # Network Policy for production environment 288 + apiVersion: networking.k8s.io/v1 289 + kind: NetworkPolicy 290 + metadata: 291 + name: webapp-network-policy 292 + namespace: production 293 + spec: 294 + podSelector: 295 + matchLabels: 296 + app: webapp 297 + policyTypes: 298 + - Ingress 299 + - Egress 300 + ingress: 301 + - from: 302 + - namespaceSelector: 303 + matchLabels: 304 + name: ingress-nginx 305 + ports: 306 + - protocol: TCP 307 + port: 8080 308 + egress: 309 + - to: 310 + - namespaceSelector: {} 311 + ports: 312 + - protocol: TCP 313 + port: 5432 # Database 314 + - protocol: TCP 315 + port: 443 # HTTPS 316 + - protocol: UDP 317 + port: 53 # DNS 318 + 319 + --- 320 + # RBAC for production applications 321 + apiVersion: rbac.authorization.k8s.io/v1 322 + kind: Role 323 + metadata: 324 + namespace: production 325 + name: webapp-secrets-reader 326 + rules: 327 + - apiGroups: [""] 328 + resources: ["secrets"] 329 + verbs: ["get", "list"] 330 + - apiGroups: ["hsm.j5t.io"] 331 + resources: ["hsmsecrets"] 332 + verbs: ["get", "list", "watch"] 333 + 334 + --- 335 + apiVersion: rbac.authorization.k8s.io/v1 336 + kind: RoleBinding 337 + metadata: 338 + name: webapp-secrets-access 339 + namespace: production 340 + subjects: 341 + - kind: ServiceAccount 342 + name: default 343 + namespace: production 344 + roleRef: 345 + kind: Role 346 + name: webapp-secrets-reader 347 + apiGroup: rbac.authorization.k8s.io 348 + 349 + --- 350 + # Monitoring: ServiceMonitor for Prometheus 351 + apiVersion: monitoring.coreos.com/v1 352 + kind: ServiceMonitor 353 + metadata: 354 + name: hsm-operator-metrics 355 + namespace: hsm-secrets-operator-system 356 + labels: 357 + app: hsm-secrets-operator 358 + spec: 359 + selector: 360 + matchLabels: 361 + control-plane: controller-manager 362 + endpoints: 363 + - port: https 364 + path: /metrics 365 + scheme: https 366 + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 367 + tlsConfig: 368 + insecureSkipVerify: true
+160
examples/high-availability/README.md
··· 1 + # High Availability Examples 2 + 3 + This directory contains examples for setting up high availability configurations with HSM device mirroring. 4 + 5 + ## Overview 6 + 7 + The HSM Secrets Operator supports high availability through cross-node device mirroring. When primary HSM devices become unavailable, the system automatically provides readonly access from mirror nodes. 8 + 9 + ## Examples 10 + 11 + 1. **[mirrored-hsm-device.yaml](mirrored-hsm-device.yaml)** - HSM device with mirroring enabled 12 + 2. **[ha-deployment.yaml](ha-deployment.yaml)** - Complete HA deployment example 13 + 3. **[multi-region.yaml](multi-region.yaml)** - Multi-region deployment with mirroring 14 + 4. **[failover-testing.yaml](failover-testing.yaml)** - Failover testing scenarios 15 + 16 + ## Architecture 17 + 18 + ``` 19 + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 20 + │ Node 1 │ │ Node 2 │ │ Node 3 │ 21 + │ (Primary) │ │ (Mirror) │ │ (Mirror) │ 22 + ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ 23 + │ HSM Device │ │ HSM Device │ │ HSM Device │ 24 + │ - Read/Write │────│ - Read Only │────│ - Read Only │ 25 + │ - Sync Source │ │ - Sync Target │ │ - Sync Target │ 26 + └─────────────────┘ └─────────────────┘ └─────────────────┘ 27 + │ │ │ 28 + └────────────────────────┼────────────────────────┘ 29 + 30 + ┌─────────▼─────────┐ 31 + │ Mirroring Manager │ 32 + │ - Sync Control │ 33 + │ - Failover Logic │ 34 + │ - Health Monitor │ 35 + └───────────────────┘ 36 + ``` 37 + 38 + ## Key Features 39 + 40 + ### Automatic Mirroring 41 + - Secrets are automatically synchronized from primary to mirror nodes 42 + - Configurable sync intervals based on requirements 43 + - Checksum validation ensures data integrity 44 + 45 + ### Readonly Fallback 46 + - When primary HSM is unavailable, mirrors provide readonly access 47 + - Applications continue to function with existing secrets 48 + - No write operations allowed on mirror nodes 49 + 50 + ### Automatic Failover 51 + - Failed primary nodes are automatically detected 52 + - Healthy mirror nodes can be promoted to primary 53 + - Configurable failover policies and thresholds 54 + 55 + ## Configuration Options 56 + 57 + ### Mirroring Policies 58 + 59 + ```yaml 60 + mirroring: 61 + policy: "ReadOnly" # None, ReadOnly, Active 62 + syncInterval: 300 # Sync every 5 minutes 63 + autoFailover: true # Enable automatic failover 64 + primaryNode: "worker-1" # Preferred primary node 65 + targetNodes: # Specific mirror nodes 66 + - "worker-2" 67 + - "worker-3" 68 + ``` 69 + 70 + ### Device Roles 71 + 72 + - **Primary**: Read/write access, source of truth 73 + - **ReadOnly**: Synchronized copy, read-only access 74 + - **Standby**: Available for failover promotion 75 + 76 + ### Health Monitoring 77 + 78 + - Continuous health checks of HSM devices 79 + - Network connectivity monitoring 80 + - Sync status and lag detection 81 + - Automatic alerts on failures 82 + 83 + ## Deployment Scenarios 84 + 85 + ### 1. Basic HA (2 Nodes) 86 + - One primary, one mirror node 87 + - Simple failover configuration 88 + - Suitable for development/staging 89 + 90 + ### 2. Multi-Node HA (3+ Nodes) 91 + - One primary, multiple mirrors 92 + - Enhanced redundancy 93 + - Load distribution for read operations 94 + 95 + ### 3. Multi-Region HA 96 + - Primary and mirrors across regions 97 + - Network partition tolerance 98 + - Disaster recovery capabilities 99 + 100 + ### 4. Active-Passive Clusters 101 + - Dedicated HSM clusters 102 + - Automated failover between clusters 103 + - Geographic distribution 104 + 105 + ## Best Practices 106 + 107 + ### 1. Node Distribution 108 + - Place HSM devices on different physical nodes 109 + - Use node affinity to ensure proper distribution 110 + - Consider network latency for sync operations 111 + 112 + ### 2. Sync Configuration 113 + - Set appropriate sync intervals based on change frequency 114 + - More frequent syncs for critical secrets 115 + - Less frequent syncs for stable configurations 116 + 117 + ### 3. Monitoring 118 + - Monitor sync lag and failures 119 + - Set up alerts for health issues 120 + - Track failover events and performance 121 + 122 + ### 4. Testing 123 + - Regular failover testing 124 + - Validate readonly access during failures 125 + - Test recovery procedures 126 + 127 + ## Troubleshooting 128 + 129 + ### Common Issues 130 + 131 + 1. **Sync Failures** 132 + - Check network connectivity between nodes 133 + - Verify HSM device health on all nodes 134 + - Review PKCS#11 library configuration 135 + 136 + 2. **Failover Not Working** 137 + - Confirm autoFailover is enabled 138 + - Check node health detection thresholds 139 + - Verify mirror nodes have healthy devices 140 + 141 + 3. **Read Performance Issues** 142 + - Review sync intervals and adjust as needed 143 + - Check network latency between nodes 144 + - Consider local caching strategies 145 + 146 + ### Monitoring Queries 147 + 148 + ```bash 149 + # Check HSM device status 150 + kubectl get hsmdevice -o wide 151 + 152 + # Review mirroring status 153 + kubectl describe hsmdevice hsm-primary 154 + 155 + # Check secret sync status 156 + kubectl get hsmsecret -o custom-columns=NAME:.metadata.name,STATUS:.status.syncStatus,LAST-SYNC:.status.lastSyncTime 157 + 158 + # Monitor failover events 159 + kubectl get events --field-selector reason=HSMFailover 160 + ```
+214
examples/high-availability/mirrored-hsm-device.yaml
··· 1 + --- 2 + # Primary HSM Device with Mirroring Configuration 3 + apiVersion: hsm.j5t.io/v1alpha1 4 + kind: HSMDevice 5 + metadata: 6 + name: hsm-primary 7 + namespace: default 8 + labels: 9 + role: primary 10 + environment: production 11 + ha.enabled: "true" 12 + spec: 13 + deviceType: PicoHSM 14 + 15 + # USB discovery for Pico HSM 16 + usb: 17 + vendorId: "20a0" 18 + productId: "4230" 19 + 20 + # Deploy on nodes with HSM hardware 21 + nodeSelector: 22 + hsm.j5t.io/hardware: "available" 23 + kubernetes.io/arch: "amd64" 24 + 25 + # PKCS#11 library configuration 26 + pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so" 27 + 28 + # Maximum devices to discover 29 + maxDevices: 3 30 + 31 + # High Availability Mirroring Configuration 32 + mirroring: 33 + # Enable readonly mirroring 34 + policy: "ReadOnly" 35 + 36 + # Sync every 5 minutes 37 + syncInterval: 300 38 + 39 + # Enable automatic failover 40 + autoFailover: true 41 + 42 + # Preferred primary node (optional) 43 + primaryNode: "worker-1" 44 + 45 + # Target nodes for mirroring (empty = all available nodes) 46 + targetNodes: 47 + - "worker-2" 48 + - "worker-3" 49 + 50 + --- 51 + # Service Monitor for Prometheus (if using) 52 + apiVersion: monitoring.coreos.com/v1 53 + kind: ServiceMonitor 54 + metadata: 55 + name: hsm-device-metrics 56 + namespace: default 57 + labels: 58 + app: hsm-secrets-operator 59 + spec: 60 + selector: 61 + matchLabels: 62 + app: hsm-secrets-operator 63 + endpoints: 64 + - port: metrics 65 + path: /metrics 66 + interval: 30s 67 + scrapeTimeout: 10s 68 + 69 + --- 70 + # Pod Disruption Budget for HSM operator 71 + apiVersion: policy/v1 72 + kind: PodDisruptionBudget 73 + metadata: 74 + name: hsm-operator-pdb 75 + namespace: hsm-secrets-operator-system 76 + spec: 77 + minAvailable: 1 78 + selector: 79 + matchLabels: 80 + control-plane: controller-manager 81 + 82 + --- 83 + # Network Policy for HSM Communication 84 + apiVersion: networking.k8s.io/v1 85 + kind: NetworkPolicy 86 + metadata: 87 + name: hsm-operator-network-policy 88 + namespace: default 89 + spec: 90 + podSelector: 91 + matchLabels: 92 + app: hsm-secrets-operator 93 + policyTypes: 94 + - Ingress 95 + - Egress 96 + ingress: 97 + - from: 98 + - podSelector: 99 + matchLabels: 100 + app: hsm-client 101 + ports: 102 + - protocol: TCP 103 + port: 8090 # API port 104 + egress: 105 + - to: [] # Allow all egress for HSM communication 106 + ports: 107 + - protocol: TCP 108 + - protocol: UDP 109 + 110 + --- 111 + # Example HSM Secret with HA consideration 112 + apiVersion: hsm.j5t.io/v1alpha1 113 + kind: HSMSecret 114 + metadata: 115 + name: ha-database-credentials 116 + namespace: default 117 + labels: 118 + app: myapp 119 + criticality: high 120 + ha.enabled: "true" 121 + annotations: 122 + hsm.j5t.io/description: "HA database credentials with mirroring support" 123 + spec: 124 + hsmPath: "secrets/ha/database-credentials" 125 + secretName: "ha-database-credentials" 126 + 127 + # Enable auto-sync for HA 128 + autoSync: true 129 + 130 + # More frequent sync for critical secrets 131 + syncInterval: 180 # 3 minutes 132 + 133 + secretType: Opaque 134 + 135 + --- 136 + # Deployment using HA secrets 137 + apiVersion: apps/v1 138 + kind: Deployment 139 + metadata: 140 + name: ha-database-app 141 + namespace: default 142 + spec: 143 + replicas: 3 # Multiple replicas for HA 144 + selector: 145 + matchLabels: 146 + app: ha-database-app 147 + template: 148 + metadata: 149 + labels: 150 + app: ha-database-app 151 + spec: 152 + # Spread across nodes for HA 153 + affinity: 154 + podAntiAffinity: 155 + preferredDuringSchedulingIgnoredDuringExecution: 156 + - weight: 100 157 + podAffinityTerm: 158 + labelSelector: 159 + matchExpressions: 160 + - key: app 161 + operator: In 162 + values: 163 + - ha-database-app 164 + topologyKey: kubernetes.io/hostname 165 + 166 + containers: 167 + - name: app 168 + image: postgres:13-alpine 169 + env: 170 + # Use HA-backed secrets 171 + - name: POSTGRES_DB 172 + valueFrom: 173 + secretKeyRef: 174 + name: ha-database-credentials 175 + key: database_name 176 + - name: POSTGRES_USER 177 + valueFrom: 178 + secretKeyRef: 179 + name: ha-database-credentials 180 + key: username 181 + - name: POSTGRES_PASSWORD 182 + valueFrom: 183 + secretKeyRef: 184 + name: ha-database-credentials 185 + key: password 186 + 187 + # Health checks for HA 188 + livenessProbe: 189 + exec: 190 + command: 191 + - pg_isready 192 + - -U 193 + - $(POSTGRES_USER) 194 + initialDelaySeconds: 30 195 + periodSeconds: 10 196 + timeoutSeconds: 5 197 + 198 + readinessProbe: 199 + exec: 200 + command: 201 + - pg_isready 202 + - -U 203 + - $(POSTGRES_USER) 204 + initialDelaySeconds: 5 205 + periodSeconds: 5 206 + timeoutSeconds: 3 207 + 208 + resources: 209 + requests: 210 + cpu: 100m 211 + memory: 128Mi 212 + limits: 213 + cpu: 500m 214 + memory: 512Mi
+116
go.mod
··· 1 + module github.com/evanjarrett/hsm-secrets-operator 2 + 3 + go 1.24.0 4 + 5 + require ( 6 + github.com/gin-gonic/gin v1.10.1 7 + github.com/go-logr/logr v1.4.2 8 + github.com/go-playground/validator/v10 v10.27.0 9 + github.com/onsi/ginkgo/v2 v2.22.0 10 + github.com/onsi/gomega v1.36.1 11 + k8s.io/api v0.33.0 12 + k8s.io/apimachinery v0.33.0 13 + k8s.io/client-go v0.33.0 14 + sigs.k8s.io/controller-runtime v0.21.0 15 + ) 16 + 17 + require ( 18 + cel.dev/expr v0.19.1 // indirect 19 + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 20 + github.com/beorn7/perks v1.0.1 // indirect 21 + github.com/blang/semver/v4 v4.0.0 // indirect 22 + github.com/bytedance/sonic v1.11.6 // indirect 23 + github.com/bytedance/sonic/loader v0.1.1 // indirect 24 + github.com/cenkalti/backoff/v4 v4.3.0 // indirect 25 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 + github.com/cloudwego/base64x v0.1.4 // indirect 27 + github.com/cloudwego/iasm v0.2.0 // indirect 28 + github.com/davecgh/go-spew v1.1.1 // indirect 29 + github.com/emicklei/go-restful/v3 v3.11.0 // indirect 30 + github.com/evanphx/json-patch/v5 v5.9.11 // indirect 31 + github.com/felixge/httpsnoop v1.0.4 // indirect 32 + github.com/fsnotify/fsnotify v1.7.0 // indirect 33 + github.com/fxamacker/cbor/v2 v2.7.0 // indirect 34 + github.com/gabriel-vasile/mimetype v1.4.8 // indirect 35 + github.com/gin-contrib/sse v0.1.0 // indirect 36 + github.com/go-logr/stdr v1.2.2 // indirect 37 + github.com/go-logr/zapr v1.3.0 // indirect 38 + github.com/go-openapi/jsonpointer v0.21.0 // indirect 39 + github.com/go-openapi/jsonreference v0.20.2 // indirect 40 + github.com/go-openapi/swag v0.23.0 // indirect 41 + github.com/go-playground/locales v0.14.1 // indirect 42 + github.com/go-playground/universal-translator v0.18.1 // indirect 43 + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 44 + github.com/goccy/go-json v0.10.2 // indirect 45 + github.com/gogo/protobuf v1.3.2 // indirect 46 + github.com/google/btree v1.1.3 // indirect 47 + github.com/google/cel-go v0.23.2 // indirect 48 + github.com/google/gnostic-models v0.6.9 // indirect 49 + github.com/google/go-cmp v0.7.0 // indirect 50 + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 51 + github.com/google/uuid v1.6.0 // indirect 52 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect 53 + github.com/inconshreveable/mousetrap v1.1.0 // indirect 54 + github.com/josharian/intern v1.0.0 // indirect 55 + github.com/json-iterator/go v1.1.12 // indirect 56 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 57 + github.com/leodido/go-urn v1.4.0 // indirect 58 + github.com/mailru/easyjson v0.7.7 // indirect 59 + github.com/mattn/go-isatty v0.0.20 // indirect 60 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 61 + github.com/modern-go/reflect2 v1.0.2 // indirect 62 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 63 + github.com/pelletier/go-toml/v2 v2.2.2 // indirect 64 + github.com/pkg/errors v0.9.1 // indirect 65 + github.com/prometheus/client_golang v1.22.0 // indirect 66 + github.com/prometheus/client_model v0.6.1 // indirect 67 + github.com/prometheus/common v0.62.0 // indirect 68 + github.com/prometheus/procfs v0.15.1 // indirect 69 + github.com/spf13/cobra v1.8.1 // indirect 70 + github.com/spf13/pflag v1.0.5 // indirect 71 + github.com/stoewer/go-strcase v1.3.0 // indirect 72 + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 73 + github.com/ugorji/go/codec v1.2.12 // indirect 74 + github.com/x448/float16 v0.8.4 // indirect 75 + go.opentelemetry.io/auto/sdk v1.1.0 // indirect 76 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 77 + go.opentelemetry.io/otel v1.33.0 // indirect 78 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect 79 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect 80 + go.opentelemetry.io/otel/metric v1.33.0 // indirect 81 + go.opentelemetry.io/otel/sdk v1.33.0 // indirect 82 + go.opentelemetry.io/otel/trace v1.33.0 // indirect 83 + go.opentelemetry.io/proto/otlp v1.4.0 // indirect 84 + go.uber.org/multierr v1.11.0 // indirect 85 + go.uber.org/zap v1.27.0 // indirect 86 + golang.org/x/arch v0.8.0 // indirect 87 + golang.org/x/crypto v0.36.0 // indirect 88 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 89 + golang.org/x/net v0.38.0 // indirect 90 + golang.org/x/oauth2 v0.27.0 // indirect 91 + golang.org/x/sync v0.12.0 // indirect 92 + golang.org/x/sys v0.31.0 // indirect 93 + golang.org/x/term v0.30.0 // indirect 94 + golang.org/x/text v0.23.0 // indirect 95 + golang.org/x/time v0.9.0 // indirect 96 + golang.org/x/tools v0.26.0 // indirect 97 + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 98 + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 99 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 100 + google.golang.org/grpc v1.68.1 // indirect 101 + google.golang.org/protobuf v1.36.5 // indirect 102 + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 103 + gopkg.in/inf.v0 v0.9.1 // indirect 104 + gopkg.in/yaml.v3 v3.0.1 // indirect 105 + k8s.io/apiextensions-apiserver v0.33.0 // indirect 106 + k8s.io/apiserver v0.33.0 // indirect 107 + k8s.io/component-base v0.33.0 // indirect 108 + k8s.io/klog/v2 v2.130.1 // indirect 109 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 110 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 111 + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect 112 + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 113 + sigs.k8s.io/randfill v1.0.0 // indirect 114 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 115 + sigs.k8s.io/yaml v1.4.0 // indirect 116 + )
+304
go.sum
··· 1 + cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= 2 + cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= 3 + github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 4 + github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 5 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 + github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 8 + github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 9 + github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 10 + github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 11 + github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 12 + github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 13 + github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 14 + github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 16 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 + github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 18 + github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 19 + github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 20 + github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 21 + github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 22 + github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 23 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 + github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 27 + github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 28 + github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= 29 + github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 30 + github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 31 + github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 32 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 33 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 34 + github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 35 + github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 36 + github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 37 + github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 38 + github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 39 + github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 40 + github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 41 + github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 42 + github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 43 + github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 44 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 45 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 46 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 47 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 48 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 49 + github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 50 + github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 51 + github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 52 + github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 53 + github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 54 + github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 55 + github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 56 + github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 57 + github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 58 + github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 59 + github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 60 + github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 61 + github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 62 + github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 63 + github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 64 + github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 65 + github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 66 + github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 67 + github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 68 + github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 69 + github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 70 + github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 71 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 72 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 73 + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 74 + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 75 + github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 76 + github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 77 + github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= 78 + github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= 79 + github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 80 + github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 81 + github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 82 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 83 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 84 + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 85 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 86 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 87 + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 88 + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 89 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 90 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 91 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= 92 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= 93 + github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 94 + github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 95 + github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 96 + github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 97 + github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 98 + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 99 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 100 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 101 + github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 102 + github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 103 + github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 104 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 105 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 106 + github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 107 + github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 108 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 109 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 110 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 111 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 112 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 113 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 114 + github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 115 + github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 116 + github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 117 + github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 118 + github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 119 + github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 120 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 121 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 122 + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 123 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 124 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 125 + github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 126 + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 127 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 128 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 129 + github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= 130 + github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 131 + github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= 132 + github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 133 + github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 134 + github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 135 + github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 136 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 137 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 138 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 139 + github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 140 + github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 141 + github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 142 + github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 143 + github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 144 + github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 145 + github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 146 + github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 147 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 148 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 149 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 150 + github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 151 + github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 152 + github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 153 + github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 154 + github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= 155 + github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 156 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 157 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 158 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 159 + github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 160 + github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 161 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 162 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 163 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 164 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 165 + github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 166 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 167 + github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 168 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 169 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 170 + github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 171 + github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 172 + github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 173 + github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 174 + github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 175 + github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 176 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 177 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 178 + go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 179 + go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 180 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= 181 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= 182 + go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= 183 + go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= 184 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= 185 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= 186 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= 187 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= 188 + go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= 189 + go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= 190 + go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= 191 + go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= 192 + go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= 193 + go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 194 + go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= 195 + go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= 196 + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 197 + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 198 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 199 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 200 + go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 201 + go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 202 + golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 203 + golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 204 + golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 205 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 206 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 207 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 208 + golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 209 + golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 210 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 211 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 212 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 213 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 214 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 215 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 217 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 218 + golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 219 + golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 220 + golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 221 + golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 222 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 + golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 226 + golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 227 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 231 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 232 + golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 233 + golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 234 + golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 235 + golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 236 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 237 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 238 + golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 239 + golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 240 + golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 241 + golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 242 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 243 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 244 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 245 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 246 + golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 247 + golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 248 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 250 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 251 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 252 + gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 253 + gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 254 + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 255 + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 256 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= 257 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= 258 + google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= 259 + google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= 260 + google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 261 + google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 262 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 263 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 264 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 265 + gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 266 + gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 267 + gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 268 + gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 269 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 270 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 271 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 272 + k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= 273 + k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= 274 + k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= 275 + k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= 276 + k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 277 + k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 278 + k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc= 279 + k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8= 280 + k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= 281 + k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= 282 + k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= 283 + k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= 284 + k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 285 + k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 286 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 287 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 288 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 289 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 290 + nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 291 + rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 292 + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= 293 + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= 294 + sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= 295 + sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= 296 + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 297 + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 298 + sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 299 + sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 300 + sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 301 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 302 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 303 + sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 304 + sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+15
hack/boilerplate.go.txt
··· 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 + */
+234
internal/api/helpers.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 + "encoding/json" 22 + "fmt" 23 + 24 + corev1 "k8s.io/api/core/v1" 25 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 + "k8s.io/apimachinery/pkg/types" 27 + 28 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 29 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 30 + ) 31 + 32 + // generateHSMPath creates an HSM path from label and ID 33 + func (s *Server) generateHSMPath(label string, id uint32) string { 34 + return fmt.Sprintf("secrets/api/%s", label) 35 + } 36 + 37 + // convertToHSMData converts API data to HSM format based on the specified format 38 + func (s *Server) convertToHSMData(data map[string]interface{}, format SecretFormat) (hsm.SecretData, error) { 39 + hsmData := make(hsm.SecretData) 40 + 41 + switch format { 42 + case SecretFormatJSON: 43 + // Convert each key-value pair to JSON bytes 44 + for key, value := range data { 45 + jsonBytes, err := json.Marshal(value) 46 + if err != nil { 47 + return nil, fmt.Errorf("failed to marshal value for key %s: %w", key, err) 48 + } 49 + hsmData[key] = jsonBytes 50 + } 51 + case SecretFormatText: 52 + // Convert values to string bytes 53 + for key, value := range data { 54 + str := fmt.Sprintf("%v", value) 55 + hsmData[key] = []byte(str) 56 + } 57 + case SecretFormatBinary: 58 + // Expect values to be base64 encoded strings or byte arrays 59 + for key, value := range data { 60 + switch v := value.(type) { 61 + case string: 62 + hsmData[key] = []byte(v) 63 + case []byte: 64 + hsmData[key] = v 65 + default: 66 + return nil, fmt.Errorf("binary format requires string or byte array values for key %s", key) 67 + } 68 + } 69 + default: 70 + return nil, fmt.Errorf("unsupported format: %s", format) 71 + } 72 + 73 + return hsmData, nil 74 + } 75 + 76 + // convertFromHSMData converts HSM data back to API format 77 + func (s *Server) convertFromHSMData(hsmData hsm.SecretData) (map[string]interface{}, error) { 78 + data := make(map[string]interface{}) 79 + 80 + for key, value := range hsmData { 81 + // Try to unmarshal as JSON first 82 + var jsonValue interface{} 83 + if err := json.Unmarshal(value, &jsonValue); err == nil { 84 + data[key] = jsonValue 85 + } else { 86 + // Fall back to string representation 87 + data[key] = string(value) 88 + } 89 + } 90 + 91 + return data, nil 92 + } 93 + 94 + // createHSMSecretResource creates a corresponding HSMSecret Kubernetes resource 95 + func (s *Server) createHSMSecretResource(ctx context.Context, label, hsmPath, description string, tags map[string]string) error { 96 + hsmSecret := &hsmv1alpha1.HSMSecret{ 97 + ObjectMeta: metav1.ObjectMeta{ 98 + Name: label, 99 + Namespace: "default", // TODO: make configurable 100 + Labels: map[string]string{ 101 + "managed-by": "hsm-api", 102 + "app": "hsm-secrets-operator", 103 + }, 104 + }, 105 + Spec: hsmv1alpha1.HSMSecretSpec{ 106 + HSMPath: hsmPath, 107 + SecretName: label, 108 + AutoSync: true, 109 + SyncInterval: 300, 110 + SecretType: corev1.SecretTypeOpaque, 111 + }, 112 + } 113 + 114 + // Add tags as annotations 115 + if len(tags) > 0 { 116 + if hsmSecret.Annotations == nil { 117 + hsmSecret.Annotations = make(map[string]string) 118 + } 119 + for k, v := range tags { 120 + hsmSecret.Annotations[fmt.Sprintf("hsm.j5t.io/tag-%s", k)] = v 121 + } 122 + } 123 + 124 + // Add description as annotation 125 + if description != "" { 126 + if hsmSecret.Annotations == nil { 127 + hsmSecret.Annotations = make(map[string]string) 128 + } 129 + hsmSecret.Annotations["hsm.j5t.io/description"] = description 130 + } 131 + 132 + return s.client.Create(ctx, hsmSecret) 133 + } 134 + 135 + // findHSMSecretByLabel finds an HSMSecret resource by its label/name 136 + func (s *Server) findHSMSecretByLabel(ctx context.Context, label string) (*hsmv1alpha1.HSMSecret, error) { 137 + // Try default namespace first 138 + hsmSecret := &hsmv1alpha1.HSMSecret{} 139 + err := s.client.Get(ctx, types.NamespacedName{ 140 + Name: label, 141 + Namespace: "default", 142 + }, hsmSecret) 143 + 144 + if err == nil { 145 + return hsmSecret, nil 146 + } 147 + 148 + // If not found in default, search across all namespaces 149 + var hsmSecretList hsmv1alpha1.HSMSecretList 150 + if err := s.client.List(ctx, &hsmSecretList); err != nil { 151 + return nil, fmt.Errorf("failed to list HSMSecret resources: %w", err) 152 + } 153 + 154 + for _, secret := range hsmSecretList.Items { 155 + if secret.Name == label { 156 + return &secret, nil 157 + } 158 + } 159 + 160 + return nil, fmt.Errorf("HSMSecret with label %s not found", label) 161 + } 162 + 163 + // findHSMDevice finds a suitable HSMDevice for readonly operations 164 + func (s *Server) findHSMDevice(ctx context.Context) (*hsmv1alpha1.HSMDevice, error) { 165 + var hsmDeviceList hsmv1alpha1.HSMDeviceList 166 + if err := s.client.List(ctx, &hsmDeviceList); err != nil { 167 + return nil, fmt.Errorf("failed to list HSM devices: %w", err) 168 + } 169 + 170 + // Look for devices that have mirroring enabled and are in a ready state 171 + for _, device := range hsmDeviceList.Items { 172 + if device.Spec.Mirroring != nil && 173 + device.Spec.Mirroring.Policy != hsmv1alpha1.MirroringPolicyNone && 174 + device.Status.Phase == hsmv1alpha1.HSMDevicePhaseReady && 175 + len(device.Status.DiscoveredDevices) > 0 { 176 + return &device, nil 177 + } 178 + } 179 + 180 + return nil, fmt.Errorf("no suitable HSM device found") 181 + } 182 + 183 + // importFromKubernetes imports secret data from a Kubernetes Secret 184 + func (s *Server) importFromKubernetes(ctx context.Context, secretName, namespace string, keyMapping map[string]string) (map[string]interface{}, error) { 185 + if namespace == "" { 186 + namespace = "default" 187 + } 188 + 189 + // Get the Kubernetes Secret 190 + secret := &corev1.Secret{} 191 + err := s.client.Get(ctx, types.NamespacedName{ 192 + Name: secretName, 193 + Namespace: namespace, 194 + }, secret) 195 + if err != nil { 196 + return nil, fmt.Errorf("failed to get Kubernetes secret %s/%s: %w", namespace, secretName, err) 197 + } 198 + 199 + // Convert secret data to API format 200 + data := make(map[string]interface{}) 201 + for key, value := range secret.Data { 202 + targetKey := key 203 + 204 + // Apply key mapping if provided 205 + if keyMapping != nil { 206 + if mappedKey, exists := keyMapping[key]; exists { 207 + targetKey = mappedKey 208 + } 209 + } 210 + 211 + // Try to unmarshal as JSON, otherwise use as string 212 + var jsonValue interface{} 213 + if err := json.Unmarshal(value, &jsonValue); err == nil { 214 + data[targetKey] = jsonValue 215 + } else { 216 + data[targetKey] = string(value) 217 + } 218 + } 219 + 220 + if len(data) == 0 { 221 + return nil, fmt.Errorf("no data found in Kubernetes secret %s/%s", namespace, secretName) 222 + } 223 + 224 + return data, nil 225 + } 226 + 227 + // validateSecretAccess checks if the current user has access to the secret (placeholder for future authorization) 228 + func (s *Server) validateSecretAccess(ctx context.Context, label string, operation string) error { 229 + // TODO: Implement proper authorization logic 230 + // This could integrate with Kubernetes RBAC, external auth systems, etc. 231 + 232 + s.logger.V(1).Info("Access validation", "label", label, "operation", operation) 233 + return nil 234 + }
+480
internal/api/server.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 + "fmt" 21 + "net/http" 22 + "strconv" 23 + "time" 24 + 25 + "github.com/gin-gonic/gin" 26 + "github.com/go-logr/logr" 27 + "github.com/go-playground/validator/v10" 28 + "sigs.k8s.io/controller-runtime/pkg/client" 29 + 30 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 31 + "github.com/evanjarrett/hsm-secrets-operator/internal/discovery" 32 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 33 + ) 34 + 35 + // Server represents the HSM REST API server 36 + type Server struct { 37 + client client.Client 38 + hsmClient hsm.Client 39 + mirroringManager *discovery.MirroringManager 40 + validator *validator.Validate 41 + logger logr.Logger 42 + router *gin.Engine 43 + } 44 + 45 + // NewServer creates a new API server instance 46 + func NewServer(client client.Client, hsmClient hsm.Client, mirroringManager *discovery.MirroringManager, logger logr.Logger) *Server { 47 + s := &Server{ 48 + client: client, 49 + hsmClient: hsmClient, 50 + mirroringManager: mirroringManager, 51 + validator: validator.New(), 52 + logger: logger.WithName("api-server"), 53 + } 54 + 55 + s.setupRouter() 56 + return s 57 + } 58 + 59 + // setupRouter configures the HTTP routes 60 + func (s *Server) setupRouter() { 61 + // Set gin mode to release for production 62 + gin.SetMode(gin.ReleaseMode) 63 + 64 + s.router = gin.New() 65 + 66 + // Add middleware 67 + s.router.Use(gin.Recovery()) 68 + s.router.Use(s.loggingMiddleware()) 69 + s.router.Use(s.corsMiddleware()) 70 + 71 + // API v1 routes 72 + v1 := s.router.Group("/api/v1") 73 + { 74 + // Health check 75 + v1.GET("/health", s.handleHealth) 76 + 77 + // HSM secrets management 78 + hsm := v1.Group("/hsm") 79 + { 80 + secrets := hsm.Group("/secrets") 81 + { 82 + secrets.POST("", s.handleCreateSecret) 83 + secrets.GET("", s.handleListSecrets) 84 + secrets.GET("/:label", s.handleGetSecret) 85 + secrets.PUT("/:label", s.handleUpdateSecret) 86 + secrets.DELETE("/:label", s.handleDeleteSecret) 87 + secrets.POST("/import", s.handleImportSecret) 88 + } 89 + } 90 + } 91 + } 92 + 93 + // Start starts the API server on the specified port 94 + func (s *Server) Start(port int) error { 95 + addr := fmt.Sprintf(":%d", port) 96 + s.logger.Info("Starting API server", "addr", addr) 97 + return s.router.Run(addr) 98 + } 99 + 100 + // handleHealth handles health check requests 101 + func (s *Server) handleHealth(c *gin.Context) { 102 + 103 + hsmConnected := s.hsmClient != nil && s.hsmClient.IsConnected() 104 + replicationEnabled := s.mirroringManager != nil 105 + activeNodes := 0 106 + 107 + if s.mirroringManager != nil { 108 + // Count active nodes (simplified - in real implementation would check actual node health) 109 + activeNodes = 1 // Current node 110 + } 111 + 112 + status := "healthy" 113 + if !hsmConnected { 114 + status = "degraded" 115 + } 116 + 117 + health := HealthStatus{ 118 + Status: status, 119 + HSMConnected: hsmConnected, 120 + ReplicationEnabled: replicationEnabled, 121 + ActiveNodes: activeNodes, 122 + Timestamp: time.Now(), 123 + } 124 + 125 + s.sendResponse(c, http.StatusOK, "Health check completed", health) 126 + } 127 + 128 + // handleCreateSecret handles secret creation requests 129 + func (s *Server) handleCreateSecret(c *gin.Context) { 130 + ctx := c.Request.Context() 131 + 132 + var req CreateSecretRequest 133 + if err := c.ShouldBindJSON(&req); err != nil { 134 + s.sendError(c, http.StatusBadRequest, "invalid_request", "Invalid JSON payload", map[string]interface{}{ 135 + "parse_error": err.Error(), 136 + }) 137 + return 138 + } 139 + 140 + if err := s.validator.Struct(&req); err != nil { 141 + s.sendError(c, http.StatusBadRequest, "validation_failed", "Request validation failed", map[string]interface{}{ 142 + "validation_errors": err.Error(), 143 + }) 144 + return 145 + } 146 + 147 + // Check if HSM client is available 148 + if s.hsmClient == nil || !s.hsmClient.IsConnected() { 149 + s.sendError(c, http.StatusServiceUnavailable, "hsm_unavailable", "HSM client is not available", nil) 150 + return 151 + } 152 + 153 + // Convert request data to HSM format 154 + hsmData, err := s.convertToHSMData(req.Data, req.Format) 155 + if err != nil { 156 + s.sendError(c, http.StatusBadRequest, "data_conversion_error", err.Error(), nil) 157 + return 158 + } 159 + 160 + // Create HSM path from label 161 + hsmPath := s.generateHSMPath(req.Label, req.ID) 162 + 163 + // Store secret in HSM 164 + if err := s.hsmClient.WriteSecret(ctx, hsmPath, hsmData); err != nil { 165 + s.logger.Error(err, "Failed to write secret to HSM", "label", req.Label, "id", req.ID) 166 + s.sendError(c, http.StatusInternalServerError, "hsm_write_error", "Failed to store secret in HSM", map[string]interface{}{ 167 + "hsm_error": err.Error(), 168 + }) 169 + return 170 + } 171 + 172 + // Create corresponding HSMSecret resource in Kubernetes 173 + if err := s.createHSMSecretResource(ctx, req.Label, hsmPath, req.Description, req.Tags); err != nil { 174 + s.logger.Error(err, "Failed to create HSMSecret resource", "label", req.Label) 175 + // Continue - the secret is stored in HSM, just log the error 176 + } 177 + 178 + s.logger.Info("Secret created successfully", "label", req.Label, "id", req.ID) 179 + s.sendResponse(c, http.StatusCreated, "Secret created successfully", map[string]interface{}{ 180 + "label": req.Label, 181 + "id": req.ID, 182 + "path": hsmPath, 183 + }) 184 + } 185 + 186 + // handleGetSecret handles secret retrieval requests 187 + func (s *Server) handleGetSecret(c *gin.Context) { 188 + ctx := c.Request.Context() 189 + label := c.Param("label") 190 + 191 + if label == "" { 192 + s.sendError(c, http.StatusBadRequest, "invalid_label", "Label parameter is required", nil) 193 + return 194 + } 195 + 196 + // Find HSMSecret resource to get the HSM path 197 + hsmSecret, err := s.findHSMSecretByLabel(ctx, label) 198 + if err != nil { 199 + s.logger.Error(err, "Failed to find HSMSecret resource", "label", label) 200 + s.sendError(c, http.StatusNotFound, "secret_not_found", "Secret not found", nil) 201 + return 202 + } 203 + 204 + // Read from HSM with fallback support 205 + var hsmData hsm.SecretData 206 + if s.hsmClient != nil && s.hsmClient.IsConnected() { 207 + hsmData, err = s.hsmClient.ReadSecret(ctx, hsmSecret.Spec.HSMPath) 208 + if err != nil && s.mirroringManager != nil { 209 + // Try readonly fallback 210 + if hsmDevice, devErr := s.findHSMDevice(ctx); devErr == nil && hsmDevice != nil { 211 + hsmData, err = s.mirroringManager.GetReadOnlyAccess(ctx, hsmSecret.Spec.HSMPath, hsmDevice) 212 + } 213 + } 214 + } else if s.mirroringManager != nil { 215 + // Primary HSM unavailable, try readonly access 216 + if hsmDevice, devErr := s.findHSMDevice(ctx); devErr == nil && hsmDevice != nil { 217 + hsmData, err = s.mirroringManager.GetReadOnlyAccess(ctx, hsmSecret.Spec.HSMPath, hsmDevice) 218 + } 219 + } 220 + 221 + if err != nil { 222 + s.logger.Error(err, "Failed to read secret from HSM", "label", label) 223 + s.sendError(c, http.StatusInternalServerError, "hsm_read_error", "Failed to read secret from HSM", nil) 224 + return 225 + } 226 + 227 + // Convert HSM data back to API format 228 + data, err := s.convertFromHSMData(hsmData) 229 + if err != nil { 230 + s.sendError(c, http.StatusInternalServerError, "data_conversion_error", err.Error(), nil) 231 + return 232 + } 233 + 234 + // Create metadata 235 + checksum := hsm.CalculateChecksum(hsmData) 236 + metadata := SecretInfo{ 237 + Label: label, 238 + Checksum: checksum, 239 + UpdatedAt: time.Now(), 240 + IsReplicated: s.mirroringManager != nil, 241 + } 242 + 243 + secretData := SecretData{ 244 + Data: data, 245 + Metadata: metadata, 246 + } 247 + 248 + s.sendResponse(c, http.StatusOK, "Secret retrieved successfully", secretData) 249 + } 250 + 251 + // handleUpdateSecret handles secret update requests 252 + func (s *Server) handleUpdateSecret(c *gin.Context) { 253 + ctx := c.Request.Context() 254 + label := c.Param("label") 255 + 256 + var req UpdateSecretRequest 257 + if err := c.ShouldBindJSON(&req); err != nil { 258 + s.sendError(c, http.StatusBadRequest, "invalid_request", "Invalid JSON payload", nil) 259 + return 260 + } 261 + 262 + if err := s.validator.Struct(&req); err != nil { 263 + s.sendError(c, http.StatusBadRequest, "validation_failed", "Request validation failed", nil) 264 + return 265 + } 266 + 267 + // Find existing HSMSecret resource 268 + hsmSecret, err := s.findHSMSecretByLabel(ctx, label) 269 + if err != nil { 270 + s.sendError(c, http.StatusNotFound, "secret_not_found", "Secret not found", nil) 271 + return 272 + } 273 + 274 + // Check if HSM client is available for write operations 275 + if s.hsmClient == nil || !s.hsmClient.IsConnected() { 276 + s.sendError(c, http.StatusServiceUnavailable, "hsm_unavailable", "HSM client is not available for write operations", nil) 277 + return 278 + } 279 + 280 + // Convert request data to HSM format (assume JSON for updates) 281 + hsmData, err := s.convertToHSMData(req.Data, SecretFormatJSON) 282 + if err != nil { 283 + s.sendError(c, http.StatusBadRequest, "data_conversion_error", err.Error(), nil) 284 + return 285 + } 286 + 287 + // Update secret in HSM 288 + if err := s.hsmClient.WriteSecret(ctx, hsmSecret.Spec.HSMPath, hsmData); err != nil { 289 + s.logger.Error(err, "Failed to update secret in HSM", "label", label) 290 + s.sendError(c, http.StatusInternalServerError, "hsm_write_error", "Failed to update secret in HSM", nil) 291 + return 292 + } 293 + 294 + s.logger.Info("Secret updated successfully", "label", label) 295 + s.sendResponse(c, http.StatusOK, "Secret updated successfully", map[string]interface{}{ 296 + "label": label, 297 + "path": hsmSecret.Spec.HSMPath, 298 + }) 299 + } 300 + 301 + // handleDeleteSecret handles secret deletion requests 302 + func (s *Server) handleDeleteSecret(c *gin.Context) { 303 + ctx := c.Request.Context() 304 + label := c.Param("label") 305 + 306 + // Find existing HSMSecret resource 307 + hsmSecret, err := s.findHSMSecretByLabel(ctx, label) 308 + if err != nil { 309 + s.sendError(c, http.StatusNotFound, "secret_not_found", "Secret not found", nil) 310 + return 311 + } 312 + 313 + // Delete the HSMSecret resource (this will trigger cleanup via finalizers) 314 + if err := s.client.Delete(ctx, hsmSecret); err != nil { 315 + s.logger.Error(err, "Failed to delete HSMSecret resource", "label", label) 316 + s.sendError(c, http.StatusInternalServerError, "delete_error", "Failed to delete secret", nil) 317 + return 318 + } 319 + 320 + s.logger.Info("Secret deleted successfully", "label", label) 321 + s.sendResponse(c, http.StatusOK, "Secret deleted successfully", map[string]interface{}{ 322 + "label": label, 323 + }) 324 + } 325 + 326 + // handleListSecrets handles secret listing requests 327 + func (s *Server) handleListSecrets(c *gin.Context) { 328 + ctx := c.Request.Context() 329 + 330 + // Get pagination parameters 331 + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) 332 + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) 333 + 334 + if page < 1 { 335 + page = 1 336 + } 337 + if pageSize < 1 || pageSize > 100 { 338 + pageSize = 50 339 + } 340 + 341 + // List HSMSecret resources 342 + var hsmSecretList hsmv1alpha1.HSMSecretList 343 + if err := s.client.List(ctx, &hsmSecretList); err != nil { 344 + s.logger.Error(err, "Failed to list HSMSecret resources") 345 + s.sendError(c, http.StatusInternalServerError, "list_error", "Failed to list secrets", nil) 346 + return 347 + } 348 + 349 + // Convert to API format with pagination 350 + secrets := make([]SecretInfo, 0) 351 + start := (page - 1) * pageSize 352 + end := start + pageSize 353 + 354 + for i, hsmSecret := range hsmSecretList.Items { 355 + if i >= start && i < end { 356 + info := SecretInfo{ 357 + Label: hsmSecret.Name, 358 + Checksum: hsmSecret.Status.HSMChecksum, 359 + IsReplicated: s.mirroringManager != nil, 360 + } 361 + 362 + if hsmSecret.Status.LastSyncTime != nil { 363 + info.UpdatedAt = hsmSecret.Status.LastSyncTime.Time 364 + } 365 + 366 + secrets = append(secrets, info) 367 + } 368 + } 369 + 370 + secretList := SecretList{ 371 + Secrets: secrets, 372 + Total: len(hsmSecretList.Items), 373 + Page: page, 374 + PageSize: pageSize, 375 + } 376 + 377 + s.sendResponse(c, http.StatusOK, "Secrets listed successfully", secretList) 378 + } 379 + 380 + // handleImportSecret handles secret import requests 381 + func (s *Server) handleImportSecret(c *gin.Context) { 382 + ctx := c.Request.Context() 383 + 384 + var req ImportSecretRequest 385 + if err := c.ShouldBindJSON(&req); err != nil { 386 + s.sendError(c, http.StatusBadRequest, "invalid_request", "Invalid JSON payload", nil) 387 + return 388 + } 389 + 390 + if err := s.validator.Struct(&req); err != nil { 391 + s.sendError(c, http.StatusBadRequest, "validation_failed", "Request validation failed", nil) 392 + return 393 + } 394 + 395 + // Import logic depends on source 396 + var data map[string]interface{} 397 + var err error 398 + 399 + switch req.Source { 400 + case "kubernetes": 401 + data, err = s.importFromKubernetes(ctx, req.SecretName, req.SecretNamespace, req.KeyMapping) 402 + default: 403 + s.sendError(c, http.StatusBadRequest, "unsupported_source", fmt.Sprintf("Import source '%s' is not supported", req.Source), nil) 404 + return 405 + } 406 + 407 + if err != nil { 408 + s.logger.Error(err, "Failed to import secret", "source", req.Source, "name", req.SecretName) 409 + s.sendError(c, http.StatusInternalServerError, "import_error", err.Error(), nil) 410 + return 411 + } 412 + 413 + // Create the secret using the imported data 414 + createReq := CreateSecretRequest{ 415 + Label: req.TargetLabel, 416 + ID: req.TargetID, 417 + Format: req.Format, 418 + Data: data, 419 + } 420 + 421 + // Use existing creation logic 422 + c.Set("create_request", createReq) 423 + s.handleCreateSecret(c) 424 + } 425 + 426 + // sendResponse sends a successful API response 427 + func (s *Server) sendResponse(c *gin.Context, statusCode int, message string, data interface{}) { 428 + response := APIResponse{ 429 + Success: true, 430 + Message: message, 431 + Data: data, 432 + } 433 + c.JSON(statusCode, response) 434 + } 435 + 436 + // sendError sends an error API response 437 + func (s *Server) sendError(c *gin.Context, statusCode int, code, message string, details map[string]interface{}) { 438 + response := APIResponse{ 439 + Success: false, 440 + Error: &APIError{ 441 + Code: code, 442 + Message: message, 443 + Details: details, 444 + }, 445 + } 446 + c.JSON(statusCode, response) 447 + } 448 + 449 + // loggingMiddleware provides request logging 450 + func (s *Server) loggingMiddleware() gin.HandlerFunc { 451 + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 452 + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", 453 + param.ClientIP, 454 + param.TimeStamp.Format(time.RFC1123), 455 + param.Method, 456 + param.Path, 457 + param.Request.Proto, 458 + param.StatusCode, 459 + param.Latency, 460 + param.Request.UserAgent(), 461 + param.ErrorMessage, 462 + ) 463 + }) 464 + } 465 + 466 + // corsMiddleware provides CORS headers 467 + func (s *Server) corsMiddleware() gin.HandlerFunc { 468 + return func(c *gin.Context) { 469 + c.Header("Access-Control-Allow-Origin", "*") 470 + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 471 + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") 472 + 473 + if c.Request.Method == "OPTIONS" { 474 + c.AbortWithStatus(204) 475 + return 476 + } 477 + 478 + c.Next() 479 + } 480 + }
+192
internal/api/types.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 + "time" 21 + ) 22 + 23 + // SecretFormat defines the format of the secret data 24 + type SecretFormat string 25 + 26 + const ( 27 + // SecretFormatJSON stores data as JSON key-value pairs 28 + SecretFormatJSON SecretFormat = "json" 29 + // SecretFormatBinary stores raw binary data 30 + SecretFormatBinary SecretFormat = "binary" 31 + // SecretFormatText stores plain text data 32 + SecretFormatText SecretFormat = "text" 33 + ) 34 + 35 + // CreateSecretRequest represents a request to create a new secret 36 + type CreateSecretRequest struct { 37 + // Label is the human-readable identifier for the secret 38 + Label string `json:"label" validate:"required,min=1,max=255"` 39 + 40 + // ID is the unique numeric identifier for the secret on the HSM 41 + ID uint32 `json:"id" validate:"required,min=1"` 42 + 43 + // Format specifies how the data should be stored 44 + Format SecretFormat `json:"format" validate:"required,oneof=json binary text"` 45 + 46 + // Data contains the actual secret data 47 + Data map[string]interface{} `json:"data" validate:"required"` 48 + 49 + // Description is an optional description of the secret 50 + Description string `json:"description,omitempty" validate:"max=1000"` 51 + 52 + // Tags are optional metadata tags 53 + Tags map[string]string `json:"tags,omitempty"` 54 + } 55 + 56 + // UpdateSecretRequest represents a request to update an existing secret 57 + type UpdateSecretRequest struct { 58 + // Data contains the updated secret data 59 + Data map[string]interface{} `json:"data" validate:"required"` 60 + 61 + // Description is an optional updated description 62 + Description string `json:"description,omitempty" validate:"max=1000"` 63 + 64 + // Tags are optional updated metadata tags 65 + Tags map[string]string `json:"tags,omitempty"` 66 + } 67 + 68 + // ImportSecretRequest represents a request to import a secret from external source 69 + type ImportSecretRequest struct { 70 + // Source specifies where to import from (kubernetes, vault, etc.) 71 + Source string `json:"source" validate:"required,oneof=kubernetes vault file"` 72 + 73 + // SecretName is the name of the source secret 74 + SecretName string `json:"secret_name" validate:"required"` 75 + 76 + // SecretNamespace is the namespace for Kubernetes secrets 77 + SecretNamespace string `json:"secret_namespace,omitempty"` 78 + 79 + // TargetLabel is the label for the imported secret on HSM 80 + TargetLabel string `json:"target_label" validate:"required,min=1,max=255"` 81 + 82 + // TargetID is the ID for the imported secret on HSM 83 + TargetID uint32 `json:"target_id" validate:"required,min=1"` 84 + 85 + // Format specifies how the imported data should be stored 86 + Format SecretFormat `json:"format" validate:"required,oneof=json binary text"` 87 + 88 + // KeyMapping maps source keys to target keys (optional) 89 + KeyMapping map[string]string `json:"key_mapping,omitempty"` 90 + } 91 + 92 + // SecretInfo represents information about a secret 93 + type SecretInfo struct { 94 + // Label is the human-readable identifier 95 + Label string `json:"label"` 96 + 97 + // ID is the unique numeric identifier on the HSM 98 + ID uint32 `json:"id"` 99 + 100 + // Format specifies the data format 101 + Format SecretFormat `json:"format"` 102 + 103 + // Description is the secret description 104 + Description string `json:"description,omitempty"` 105 + 106 + // Tags are metadata tags 107 + Tags map[string]string `json:"tags,omitempty"` 108 + 109 + // CreatedAt is when the secret was created 110 + CreatedAt time.Time `json:"created_at"` 111 + 112 + // UpdatedAt is when the secret was last updated 113 + UpdatedAt time.Time `json:"updated_at"` 114 + 115 + // Size is the size of the secret data in bytes 116 + Size int64 `json:"size"` 117 + 118 + // Checksum is the SHA256 checksum of the data 119 + Checksum string `json:"checksum"` 120 + 121 + // IsReplicated indicates if the secret is replicated across nodes 122 + IsReplicated bool `json:"is_replicated"` 123 + } 124 + 125 + // SecretData represents the actual secret data 126 + type SecretData struct { 127 + // Data contains the secret key-value pairs 128 + Data map[string]interface{} `json:"data"` 129 + 130 + // Metadata contains additional information about the secret 131 + Metadata SecretInfo `json:"metadata"` 132 + } 133 + 134 + // SecretList represents a list of secrets 135 + type SecretList struct { 136 + // Secrets is the list of secret information 137 + Secrets []SecretInfo `json:"secrets"` 138 + 139 + // Total is the total number of secrets 140 + Total int `json:"total"` 141 + 142 + // Page is the current page number (for pagination) 143 + Page int `json:"page,omitempty"` 144 + 145 + // PageSize is the number of items per page 146 + PageSize int `json:"page_size,omitempty"` 147 + } 148 + 149 + // APIResponse represents a standard API response 150 + type APIResponse struct { 151 + // Success indicates if the operation was successful 152 + Success bool `json:"success"` 153 + 154 + // Message provides additional information about the result 155 + Message string `json:"message,omitempty"` 156 + 157 + // Data contains the response data 158 + Data interface{} `json:"data,omitempty"` 159 + 160 + // Error contains error details if the operation failed 161 + Error *APIError `json:"error,omitempty"` 162 + } 163 + 164 + // APIError represents an API error 165 + type APIError struct { 166 + // Code is the error code 167 + Code string `json:"code"` 168 + 169 + // Message is the human-readable error message 170 + Message string `json:"message"` 171 + 172 + // Details contains additional error details 173 + Details map[string]interface{} `json:"details,omitempty"` 174 + } 175 + 176 + // HealthStatus represents the health status of the API server 177 + type HealthStatus struct { 178 + // Status is the overall health status 179 + Status string `json:"status"` 180 + 181 + // HSMConnected indicates if HSM is connected 182 + HSMConnected bool `json:"hsm_connected"` 183 + 184 + // ReplicationEnabled indicates if replication is enabled 185 + ReplicationEnabled bool `json:"replication_enabled"` 186 + 187 + // ActiveNodes is the number of active HSM nodes 188 + ActiveNodes int `json:"active_nodes"` 189 + 190 + // Timestamp is when the health check was performed 191 + Timestamp time.Time `json:"timestamp"` 192 + }
+354
internal/controller/hsmdevice_controller.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 controller 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "os" 23 + "time" 24 + 25 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 + "k8s.io/apimachinery/pkg/runtime" 27 + ctrl "sigs.k8s.io/controller-runtime" 28 + "sigs.k8s.io/controller-runtime/pkg/client" 29 + "sigs.k8s.io/controller-runtime/pkg/log" 30 + 31 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 32 + "github.com/evanjarrett/hsm-secrets-operator/internal/discovery" 33 + ) 34 + 35 + const ( 36 + // DefaultDiscoveryInterval is the default interval for device discovery 37 + DefaultDiscoveryInterval = 30 * time.Second 38 + ) 39 + 40 + // HSMDeviceReconciler reconciles a HSMDevice object 41 + type HSMDeviceReconciler struct { 42 + client.Client 43 + Scheme *runtime.Scheme 44 + NodeName string 45 + USBDiscoverer *discovery.USBDiscoverer 46 + MirroringManager *discovery.MirroringManager 47 + } 48 + 49 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmdevices,verbs=get;list;watch;create;update;patch;delete 50 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmdevices/status,verbs=get;update;patch 51 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmdevices/finalizers,verbs=update 52 + 53 + // Reconcile handles HSMDevice reconciliation - discovers USB HSM devices on nodes 54 + func (r *HSMDeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 + logger := log.FromContext(ctx) 56 + 57 + // Fetch the HSMDevice instance 58 + var hsmDevice hsmv1alpha1.HSMDevice 59 + if err := r.Get(ctx, req.NamespacedName, &hsmDevice); err != nil { 60 + logger.Error(err, "Unable to fetch HSMDevice") 61 + return ctrl.Result{}, client.IgnoreNotFound(err) 62 + } 63 + 64 + // Check if this device should be discovered on this node 65 + if !r.shouldDiscoverOnNode(&hsmDevice) { 66 + logger.V(1).Info("Device discovery not required on this node") 67 + return ctrl.Result{}, nil 68 + } 69 + 70 + // Set initial phase if not set 71 + if hsmDevice.Status.Phase == "" { 72 + hsmDevice.Status.Phase = hsmv1alpha1.HSMDevicePhasePending 73 + if err := r.Status().Update(ctx, &hsmDevice); err != nil { 74 + return ctrl.Result{}, err 75 + } 76 + } 77 + 78 + // Start device discovery 79 + return r.reconcileDeviceDiscovery(ctx, &hsmDevice) 80 + } 81 + 82 + // reconcileDeviceDiscovery performs the actual device discovery 83 + func (r *HSMDeviceReconciler) reconcileDeviceDiscovery(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) (ctrl.Result, error) { 84 + logger := log.FromContext(ctx).WithValues("deviceType", hsmDevice.Spec.DeviceType) 85 + 86 + // Update phase to discovering 87 + if hsmDevice.Status.Phase != hsmv1alpha1.HSMDevicePhaseDiscovering { 88 + hsmDevice.Status.Phase = hsmv1alpha1.HSMDevicePhaseDiscovering 89 + if err := r.Status().Update(ctx, hsmDevice); err != nil { 90 + return ctrl.Result{}, err 91 + } 92 + } 93 + 94 + var discoveredDevices []hsmv1alpha1.DiscoveredDevice 95 + var err error 96 + 97 + // Perform discovery based on specification 98 + if hsmDevice.Spec.USB != nil { 99 + discoveredDevices, err = r.discoverUSBDevices(ctx, hsmDevice) 100 + } else if hsmDevice.Spec.DevicePath != nil { 101 + discoveredDevices, err = r.discoverPathDevices(ctx, hsmDevice) 102 + } else { 103 + // Auto-discovery based on device type 104 + discoveredDevices, err = r.autoDiscoverDevices(ctx, hsmDevice) 105 + } 106 + 107 + if err != nil { 108 + logger.Error(err, "Device discovery failed") 109 + return r.updateStatus(ctx, hsmDevice, hsmv1alpha1.HSMDevicePhaseError, 110 + discoveredDevices, err.Error()) 111 + } 112 + 113 + logger.Info("Device discovery completed", "foundDevices", len(discoveredDevices)) 114 + 115 + // Update status with discovered devices 116 + phase := hsmv1alpha1.HSMDevicePhaseReady 117 + if len(discoveredDevices) == 0 { 118 + phase = hsmv1alpha1.HSMDevicePhasePending 119 + } 120 + 121 + result, err := r.updateStatus(ctx, hsmDevice, phase, discoveredDevices, "") 122 + if err != nil { 123 + return result, err 124 + } 125 + 126 + // Handle device mirroring if configured 127 + if r.MirroringManager != nil && hsmDevice.Spec.Mirroring != nil && 128 + hsmDevice.Spec.Mirroring.Policy != hsmv1alpha1.MirroringPolicyNone { 129 + 130 + logger.Info("Starting device mirroring", "policy", hsmDevice.Spec.Mirroring.Policy) 131 + 132 + if err := r.MirroringManager.SyncDevices(ctx, hsmDevice); err != nil { 133 + logger.Error(err, "Device mirroring failed") 134 + // Don't fail the reconciliation, just log the error 135 + // The mirroring will be retried on the next reconcile cycle 136 + } 137 + } 138 + 139 + return result, err 140 + } 141 + 142 + // discoverUSBDevices discovers devices using USB specifications 143 + func (r *HSMDeviceReconciler) discoverUSBDevices(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) ([]hsmv1alpha1.DiscoveredDevice, error) { 144 + logger := log.FromContext(ctx) 145 + 146 + if r.USBDiscoverer == nil { 147 + return nil, fmt.Errorf("USB discoverer not available") 148 + } 149 + 150 + usbDevices, err := r.USBDiscoverer.DiscoverDevices(ctx, hsmDevice.Spec.USB) 151 + if err != nil { 152 + return nil, fmt.Errorf("USB discovery failed: %w", err) 153 + } 154 + 155 + var devices []hsmv1alpha1.DiscoveredDevice 156 + for _, usbDev := range usbDevices { 157 + device := hsmv1alpha1.DiscoveredDevice{ 158 + DevicePath: usbDev.DevicePath, 159 + SerialNumber: usbDev.SerialNumber, 160 + NodeName: r.NodeName, 161 + LastSeen: metav1.Now(), 162 + Available: true, 163 + DeviceInfo: map[string]string{ 164 + "vendor-id": usbDev.VendorID, 165 + "product-id": usbDev.ProductID, 166 + "manufacturer": usbDev.Manufacturer, 167 + "product": usbDev.Product, 168 + "discovery-type": "usb", 169 + }, 170 + } 171 + 172 + // Add additional device info 173 + for k, v := range usbDev.DeviceInfo { 174 + device.DeviceInfo[k] = v 175 + } 176 + 177 + devices = append(devices, device) 178 + } 179 + 180 + logger.V(1).Info("USB device discovery completed", "devicesFound", len(devices)) 181 + return devices, nil 182 + } 183 + 184 + // discoverPathDevices discovers devices using path-based specifications 185 + func (r *HSMDeviceReconciler) discoverPathDevices(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) ([]hsmv1alpha1.DiscoveredDevice, error) { 186 + logger := log.FromContext(ctx) 187 + 188 + if r.USBDiscoverer == nil { 189 + return nil, fmt.Errorf("USB discoverer not available") 190 + } 191 + 192 + usbDevices, err := r.USBDiscoverer.DiscoverByPath(ctx, hsmDevice.Spec.DevicePath) 193 + if err != nil { 194 + return nil, fmt.Errorf("path discovery failed: %w", err) 195 + } 196 + 197 + var devices []hsmv1alpha1.DiscoveredDevice 198 + for _, usbDev := range usbDevices { 199 + device := hsmv1alpha1.DiscoveredDevice{ 200 + DevicePath: usbDev.DevicePath, 201 + SerialNumber: usbDev.SerialNumber, 202 + NodeName: r.NodeName, 203 + LastSeen: metav1.Now(), 204 + Available: true, 205 + DeviceInfo: map[string]string{ 206 + "discovery-type": "path", 207 + "path-pattern": hsmDevice.Spec.DevicePath.Path, 208 + }, 209 + } 210 + 211 + // Add additional device info 212 + for k, v := range usbDev.DeviceInfo { 213 + device.DeviceInfo[k] = v 214 + } 215 + 216 + devices = append(devices, device) 217 + } 218 + 219 + logger.V(1).Info("Path device discovery completed", "devicesFound", len(devices)) 220 + return devices, nil 221 + } 222 + 223 + // autoDiscoverDevices performs auto-discovery based on device type 224 + func (r *HSMDeviceReconciler) autoDiscoverDevices(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) ([]hsmv1alpha1.DiscoveredDevice, error) { 225 + logger := log.FromContext(ctx) 226 + 227 + // Get well-known USB specs for device type 228 + wellKnownSpecs := discovery.GetWellKnownHSMSpecs() 229 + spec, exists := wellKnownSpecs[hsmDevice.Spec.DeviceType] 230 + 231 + if !exists { 232 + return nil, fmt.Errorf("no well-known specification for device type %s", hsmDevice.Spec.DeviceType) 233 + } 234 + 235 + logger.V(1).Info("Using well-known USB specification", 236 + "deviceType", hsmDevice.Spec.DeviceType, 237 + "vendorId", spec.VendorID, 238 + "productId", spec.ProductID) 239 + 240 + // Use the well-known spec for discovery 241 + tempDevice := *hsmDevice 242 + tempDevice.Spec.USB = spec 243 + 244 + return r.discoverUSBDevices(ctx, &tempDevice) 245 + } 246 + 247 + // shouldDiscoverOnNode determines if device discovery should run on this node 248 + func (r *HSMDeviceReconciler) shouldDiscoverOnNode(hsmDevice *hsmv1alpha1.HSMDevice) bool { 249 + // If no node selector is specified, discover on all nodes 250 + if len(hsmDevice.Spec.NodeSelector) == 0 { 251 + return true 252 + } 253 + 254 + // Check if this node matches the node selector 255 + // This is a simplified check - in production, you'd want to fetch 256 + // the actual node labels and compare them 257 + nodeName := r.getNodeName() 258 + for key, value := range hsmDevice.Spec.NodeSelector { 259 + if key == "kubernetes.io/hostname" && value == nodeName { 260 + return true 261 + } 262 + } 263 + 264 + return false 265 + } 266 + 267 + // getNodeName returns the current node name 268 + func (r *HSMDeviceReconciler) getNodeName() string { 269 + if r.NodeName != "" { 270 + return r.NodeName 271 + } 272 + 273 + // Try to get from environment 274 + if nodeName := os.Getenv("NODE_NAME"); nodeName != "" { 275 + r.NodeName = nodeName 276 + return nodeName 277 + } 278 + 279 + // Fallback to hostname 280 + if hostname, err := os.Hostname(); err == nil { 281 + r.NodeName = hostname 282 + return hostname 283 + } 284 + 285 + return "unknown" 286 + } 287 + 288 + // updateStatus updates the HSMDevice status 289 + func (r *HSMDeviceReconciler) updateStatus(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, phase hsmv1alpha1.HSMDevicePhase, devices []hsmv1alpha1.DiscoveredDevice, errorMsg string) (ctrl.Result, error) { 290 + now := metav1.Now() 291 + 292 + // Update basic status fields 293 + hsmDevice.Status.Phase = phase 294 + hsmDevice.Status.LastDiscoveryTime = &now 295 + hsmDevice.Status.DiscoveredDevices = devices 296 + hsmDevice.Status.TotalDevices = int32(len(devices)) 297 + 298 + // Count available devices 299 + availableCount := int32(0) 300 + for _, device := range devices { 301 + if device.Available { 302 + availableCount++ 303 + } 304 + } 305 + hsmDevice.Status.AvailableDevices = availableCount 306 + 307 + // Update conditions 308 + conditionType := "DeviceDiscovery" 309 + conditionStatus := metav1.ConditionTrue 310 + reason := string(phase) 311 + message := fmt.Sprintf("Discovered %d devices", len(devices)) 312 + 313 + if phase == hsmv1alpha1.HSMDevicePhaseError { 314 + conditionStatus = metav1.ConditionFalse 315 + message = errorMsg 316 + } 317 + 318 + condition := metav1.Condition{ 319 + Type: conditionType, 320 + Status: conditionStatus, 321 + LastTransitionTime: now, 322 + Reason: reason, 323 + Message: message, 324 + } 325 + 326 + // Update or add condition 327 + found := false 328 + for i, cond := range hsmDevice.Status.Conditions { 329 + if cond.Type == conditionType { 330 + hsmDevice.Status.Conditions[i] = condition 331 + found = true 332 + break 333 + } 334 + } 335 + if !found { 336 + hsmDevice.Status.Conditions = append(hsmDevice.Status.Conditions, condition) 337 + } 338 + 339 + // Update status 340 + if err := r.Status().Update(ctx, hsmDevice); err != nil { 341 + return ctrl.Result{}, err 342 + } 343 + 344 + // Requeue for periodic discovery 345 + return ctrl.Result{RequeueAfter: DefaultDiscoveryInterval}, nil 346 + } 347 + 348 + // SetupWithManager sets up the controller with the Manager. 349 + func (r *HSMDeviceReconciler) SetupWithManager(mgr ctrl.Manager) error { 350 + return ctrl.NewControllerManagedBy(mgr). 351 + For(&hsmv1alpha1.HSMDevice{}). 352 + Named("hsmdevice"). 353 + Complete(r) 354 + }
+84
internal/controller/hsmdevice_controller_test.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 controller 18 + 19 + import ( 20 + "context" 21 + 22 + . "github.com/onsi/ginkgo/v2" 23 + . "github.com/onsi/gomega" 24 + "k8s.io/apimachinery/pkg/api/errors" 25 + "k8s.io/apimachinery/pkg/types" 26 + "sigs.k8s.io/controller-runtime/pkg/reconcile" 27 + 28 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 + 30 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 31 + ) 32 + 33 + var _ = Describe("HSMDevice Controller", func() { 34 + Context("When reconciling a resource", func() { 35 + const resourceName = "test-resource" 36 + 37 + ctx := context.Background() 38 + 39 + typeNamespacedName := types.NamespacedName{ 40 + Name: resourceName, 41 + Namespace: "default", // TODO(user):Modify as needed 42 + } 43 + hsmdevice := &hsmv1alpha1.HSMDevice{} 44 + 45 + BeforeEach(func() { 46 + By("creating the custom resource for the Kind HSMDevice") 47 + err := k8sClient.Get(ctx, typeNamespacedName, hsmdevice) 48 + if err != nil && errors.IsNotFound(err) { 49 + resource := &hsmv1alpha1.HSMDevice{ 50 + ObjectMeta: metav1.ObjectMeta{ 51 + Name: resourceName, 52 + Namespace: "default", 53 + }, 54 + // TODO(user): Specify other spec details if needed. 55 + } 56 + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) 57 + } 58 + }) 59 + 60 + AfterEach(func() { 61 + // TODO(user): Cleanup logic after each test, like removing the resource instance. 62 + resource := &hsmv1alpha1.HSMDevice{} 63 + err := k8sClient.Get(ctx, typeNamespacedName, resource) 64 + Expect(err).NotTo(HaveOccurred()) 65 + 66 + By("Cleanup the specific resource instance HSMDevice") 67 + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) 68 + }) 69 + It("should successfully reconcile the resource", func() { 70 + By("Reconciling the created resource") 71 + controllerReconciler := &HSMDeviceReconciler{ 72 + Client: k8sClient, 73 + Scheme: k8sClient.Scheme(), 74 + } 75 + 76 + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ 77 + NamespacedName: typeNamespacedName, 78 + }) 79 + Expect(err).NotTo(HaveOccurred()) 80 + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. 81 + // Example: If you expect a certain status condition after reconciliation, verify it here. 82 + }) 83 + }) 84 + })
+379
internal/controller/hsmsecret_controller.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 controller 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "time" 23 + 24 + corev1 "k8s.io/api/core/v1" 25 + "k8s.io/apimachinery/pkg/api/errors" 26 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 + "k8s.io/apimachinery/pkg/runtime" 28 + "k8s.io/apimachinery/pkg/types" 29 + ctrl "sigs.k8s.io/controller-runtime" 30 + "sigs.k8s.io/controller-runtime/pkg/client" 31 + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 32 + "sigs.k8s.io/controller-runtime/pkg/log" 33 + 34 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 35 + "github.com/evanjarrett/hsm-secrets-operator/internal/discovery" 36 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 37 + ) 38 + 39 + const ( 40 + // HSMSecretFinalizer is the finalizer used by the HSMSecret controller 41 + HSMSecretFinalizer = "hsmsecret.hsm.j5t.io/finalizer" 42 + 43 + // DefaultSyncInterval is the default sync interval in seconds 44 + DefaultSyncInterval = 300 45 + ) 46 + 47 + // HSMSecretReconciler reconciles a HSMSecret object 48 + type HSMSecretReconciler struct { 49 + client.Client 50 + Scheme *runtime.Scheme 51 + HSMClient hsm.Client 52 + MirroringManager *discovery.MirroringManager 53 + } 54 + 55 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets,verbs=get;list;watch;create;update;patch;delete 56 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets/status,verbs=get;update;patch 57 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmsecrets/finalizers,verbs=update 58 + // +kubebuilder:rbac:groups=hsm.j5t.io,resources=hsmdevices,verbs=get;list;watch 59 + // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 60 + // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch 61 + 62 + // Reconcile handles HSMSecret reconciliation 63 + func (r *HSMSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 64 + logger := log.FromContext(ctx) 65 + 66 + // Fetch the HSMSecret instance 67 + var hsmSecret hsmv1alpha1.HSMSecret 68 + if err := r.Get(ctx, req.NamespacedName, &hsmSecret); err != nil { 69 + if errors.IsNotFound(err) { 70 + logger.Info("HSMSecret resource not found, ignoring since object must be deleted") 71 + return ctrl.Result{}, nil 72 + } 73 + logger.Error(err, "Failed to get HSMSecret") 74 + return ctrl.Result{}, err 75 + } 76 + 77 + // Check if HSM client is available 78 + if r.HSMClient == nil || !r.HSMClient.IsConnected() { 79 + logger.Error(fmt.Errorf("HSM client not available"), "HSM client not connected") 80 + return ctrl.Result{RequeueAfter: time.Minute * 5}, nil 81 + } 82 + 83 + // Handle deletion 84 + if hsmSecret.DeletionTimestamp != nil { 85 + return r.reconcileDelete(ctx, &hsmSecret) 86 + } 87 + 88 + // Add finalizer if not present 89 + if !controllerutil.ContainsFinalizer(&hsmSecret, HSMSecretFinalizer) { 90 + controllerutil.AddFinalizer(&hsmSecret, HSMSecretFinalizer) 91 + if err := r.Update(ctx, &hsmSecret); err != nil { 92 + logger.Error(err, "Failed to add finalizer") 93 + return ctrl.Result{}, err 94 + } 95 + return ctrl.Result{Requeue: true}, nil 96 + } 97 + 98 + // Reconcile the HSMSecret 99 + result, err := r.reconcileNormal(ctx, &hsmSecret) 100 + if err != nil { 101 + logger.Error(err, "Failed to reconcile HSMSecret") 102 + r.updateStatus(ctx, &hsmSecret, hsmv1alpha1.SyncStatusError, err.Error()) 103 + } 104 + 105 + return result, err 106 + } 107 + 108 + // reconcileNormal handles normal reconciliation logic 109 + func (r *HSMSecretReconciler) reconcileNormal(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) (ctrl.Result, error) { 110 + logger := log.FromContext(ctx) 111 + 112 + // Set default values 113 + secretName := hsmSecret.Spec.SecretName 114 + if secretName == "" { 115 + secretName = hsmSecret.Name 116 + } 117 + 118 + syncInterval := hsmSecret.Spec.SyncInterval 119 + if syncInterval == 0 { 120 + syncInterval = DefaultSyncInterval 121 + } 122 + 123 + // Read secret from HSM with readonly fallback support 124 + hsmData, err := r.readSecretWithFallback(ctx, hsmSecret) 125 + if err != nil { 126 + logger.Error(err, "Failed to read secret from HSM and mirrors", "path", hsmSecret.Spec.HSMPath) 127 + return ctrl.Result{RequeueAfter: time.Minute * 2}, err 128 + } 129 + 130 + // Calculate HSM checksum 131 + hsmChecksum := hsm.CalculateChecksum(hsmData) 132 + 133 + // Get or create Kubernetes Secret 134 + var k8sSecret corev1.Secret 135 + secretKey := types.NamespacedName{ 136 + Namespace: hsmSecret.Namespace, 137 + Name: secretName, 138 + } 139 + 140 + err = r.Get(ctx, secretKey, &k8sSecret) 141 + if err != nil { 142 + if errors.IsNotFound(err) { 143 + // Create new secret 144 + k8sSecret = r.buildSecret(hsmSecret, secretName, hsmData) 145 + if err := r.Create(ctx, &k8sSecret); err != nil { 146 + logger.Error(err, "Failed to create Secret") 147 + return ctrl.Result{}, err 148 + } 149 + logger.Info("Created new Secret", "secret", secretKey) 150 + } else { 151 + logger.Error(err, "Failed to get Secret") 152 + return ctrl.Result{}, err 153 + } 154 + } else { 155 + // Update existing secret if needed 156 + k8sSecret.Data = r.convertHSMDataToSecretData(hsmData) 157 + if err := r.Update(ctx, &k8sSecret); err != nil { 158 + logger.Error(err, "Failed to update Secret") 159 + return ctrl.Result{}, err 160 + } 161 + logger.V(1).Info("Updated existing Secret", "secret", secretKey) 162 + } 163 + 164 + // Calculate K8s Secret checksum 165 + secretChecksum := hsm.CalculateChecksum(r.convertSecretDataToHSMData(k8sSecret.Data)) 166 + 167 + // Update status 168 + syncStatus := hsmv1alpha1.SyncStatusInSync 169 + if hsmChecksum != secretChecksum { 170 + syncStatus = hsmv1alpha1.SyncStatusOutOfSync 171 + } 172 + 173 + r.updateStatus(ctx, hsmSecret, syncStatus, "") 174 + hsmSecret.Status.HSMChecksum = hsmChecksum 175 + hsmSecret.Status.SecretChecksum = secretChecksum 176 + hsmSecret.Status.SecretRef = &corev1.ObjectReference{ 177 + APIVersion: "v1", 178 + Kind: "Secret", 179 + Name: k8sSecret.Name, 180 + Namespace: k8sSecret.Namespace, 181 + UID: k8sSecret.UID, 182 + } 183 + 184 + if err := r.Status().Update(ctx, hsmSecret); err != nil { 185 + logger.Error(err, "Failed to update HSMSecret status") 186 + return ctrl.Result{}, err 187 + } 188 + 189 + // Schedule next sync if AutoSync is enabled 190 + if hsmSecret.Spec.AutoSync { 191 + return ctrl.Result{RequeueAfter: time.Second * time.Duration(syncInterval)}, nil 192 + } 193 + 194 + return ctrl.Result{}, nil 195 + } 196 + 197 + // reconcileDelete handles HSMSecret deletion 198 + func (r *HSMSecretReconciler) reconcileDelete(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) (ctrl.Result, error) { 199 + logger := log.FromContext(ctx) 200 + 201 + if controllerutil.ContainsFinalizer(hsmSecret, HSMSecretFinalizer) { 202 + logger.Info("Cleaning up HSMSecret resources") 203 + 204 + // Optionally delete the Kubernetes Secret 205 + secretName := hsmSecret.Spec.SecretName 206 + if secretName == "" { 207 + secretName = hsmSecret.Name 208 + } 209 + 210 + secretKey := types.NamespacedName{ 211 + Namespace: hsmSecret.Namespace, 212 + Name: secretName, 213 + } 214 + 215 + var k8sSecret corev1.Secret 216 + if err := r.Get(ctx, secretKey, &k8sSecret); err == nil { 217 + if err := r.Delete(ctx, &k8sSecret); err != nil { 218 + logger.Error(err, "Failed to delete associated Secret") 219 + return ctrl.Result{}, err 220 + } 221 + logger.Info("Deleted associated Secret", "secret", secretKey) 222 + } 223 + 224 + // Remove finalizer 225 + controllerutil.RemoveFinalizer(hsmSecret, HSMSecretFinalizer) 226 + if err := r.Update(ctx, hsmSecret); err != nil { 227 + logger.Error(err, "Failed to remove finalizer") 228 + return ctrl.Result{}, err 229 + } 230 + } 231 + 232 + return ctrl.Result{}, nil 233 + } 234 + 235 + // buildSecret creates a new Kubernetes Secret from HSM data 236 + func (r *HSMSecretReconciler) buildSecret(hsmSecret *hsmv1alpha1.HSMSecret, secretName string, hsmData hsm.SecretData) corev1.Secret { 237 + secretType := hsmSecret.Spec.SecretType 238 + if secretType == "" { 239 + secretType = corev1.SecretTypeOpaque 240 + } 241 + 242 + secret := corev1.Secret{ 243 + ObjectMeta: metav1.ObjectMeta{ 244 + Name: secretName, 245 + Namespace: hsmSecret.Namespace, 246 + Labels: map[string]string{ 247 + "managed-by": "hsm-secrets-operator", 248 + "hsm-path": hsmSecret.Spec.HSMPath, 249 + }, 250 + }, 251 + Type: secretType, 252 + Data: r.convertHSMDataToSecretData(hsmData), 253 + } 254 + 255 + // Set owner reference 256 + ctrl.SetControllerReference(hsmSecret, &secret, r.Scheme) 257 + 258 + return secret 259 + } 260 + 261 + // convertHSMDataToSecretData converts HSM data format to Kubernetes Secret data format 262 + func (r *HSMSecretReconciler) convertHSMDataToSecretData(hsmData hsm.SecretData) map[string][]byte { 263 + result := make(map[string][]byte) 264 + for k, v := range hsmData { 265 + result[k] = v 266 + } 267 + return result 268 + } 269 + 270 + // convertSecretDataToHSMData converts Kubernetes Secret data format to HSM data format 271 + func (r *HSMSecretReconciler) convertSecretDataToHSMData(secretData map[string][]byte) hsm.SecretData { 272 + result := make(hsm.SecretData) 273 + for k, v := range secretData { 274 + result[k] = v 275 + } 276 + return result 277 + } 278 + 279 + // updateStatus updates the HSMSecret status 280 + func (r *HSMSecretReconciler) updateStatus(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret, status hsmv1alpha1.SyncStatus, errorMsg string) { 281 + now := metav1.Now() 282 + hsmSecret.Status.SyncStatus = status 283 + hsmSecret.Status.LastError = errorMsg 284 + 285 + if status == hsmv1alpha1.SyncStatusInSync { 286 + hsmSecret.Status.LastSyncTime = &now 287 + } 288 + 289 + // Update conditions 290 + condition := metav1.Condition{ 291 + Type: "Ready", 292 + Status: metav1.ConditionTrue, 293 + LastTransitionTime: now, 294 + Reason: string(status), 295 + Message: errorMsg, 296 + } 297 + 298 + if status == hsmv1alpha1.SyncStatusError { 299 + condition.Status = metav1.ConditionFalse 300 + } 301 + 302 + // Update or add condition 303 + found := false 304 + for i, cond := range hsmSecret.Status.Conditions { 305 + if cond.Type == condition.Type { 306 + hsmSecret.Status.Conditions[i] = condition 307 + found = true 308 + break 309 + } 310 + } 311 + if !found { 312 + hsmSecret.Status.Conditions = append(hsmSecret.Status.Conditions, condition) 313 + } 314 + } 315 + 316 + // readSecretWithFallback attempts to read a secret from primary HSM, falling back to mirrors if needed 317 + func (r *HSMSecretReconciler) readSecretWithFallback(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) (hsm.SecretData, error) { 318 + logger := log.FromContext(ctx) 319 + 320 + // Try to read from primary HSM first 321 + if r.HSMClient != nil && r.HSMClient.IsConnected() { 322 + data, err := r.HSMClient.ReadSecret(ctx, hsmSecret.Spec.HSMPath) 323 + if err == nil { 324 + logger.V(1).Info("Successfully read secret from primary HSM", "path", hsmSecret.Spec.HSMPath) 325 + return data, nil 326 + } 327 + logger.V(1).Info("Failed to read from primary HSM, attempting fallback", "error", err) 328 + } 329 + 330 + // If primary failed and we have a mirroring manager, try readonly access from mirrors 331 + if r.MirroringManager != nil { 332 + // Find relevant HSMDevice for this secret path 333 + hsmDevice, err := r.findHSMDeviceForSecret(ctx, hsmSecret) 334 + if err != nil { 335 + logger.Error(err, "Failed to find HSM device for readonly fallback") 336 + } else if hsmDevice != nil { 337 + data, err := r.MirroringManager.GetReadOnlyAccess(ctx, hsmSecret.Spec.HSMPath, hsmDevice) 338 + if err == nil { 339 + logger.Info("Successfully read secret from readonly mirror", "path", hsmSecret.Spec.HSMPath) 340 + return data, nil 341 + } 342 + logger.V(1).Info("Failed to read from mirrors", "error", err) 343 + } 344 + } 345 + 346 + return nil, fmt.Errorf("secret not accessible from primary HSM or mirrors") 347 + } 348 + 349 + // findHSMDeviceForSecret finds the HSMDevice that should contain the secret 350 + func (r *HSMSecretReconciler) findHSMDeviceForSecret(ctx context.Context, hsmSecret *hsmv1alpha1.HSMSecret) (*hsmv1alpha1.HSMDevice, error) { 351 + // List all HSMDevices in the same namespace 352 + var hsmDeviceList hsmv1alpha1.HSMDeviceList 353 + if err := r.List(ctx, &hsmDeviceList, client.InNamespace(hsmSecret.Namespace)); err != nil { 354 + return nil, fmt.Errorf("failed to list HSM devices: %w", err) 355 + } 356 + 357 + // Look for devices that have mirroring enabled and are in a ready state 358 + for _, device := range hsmDeviceList.Items { 359 + if device.Spec.Mirroring != nil && 360 + device.Spec.Mirroring.Policy != hsmv1alpha1.MirroringPolicyNone && 361 + device.Status.Phase == hsmv1alpha1.HSMDevicePhaseReady && 362 + len(device.Status.DiscoveredDevices) > 0 { 363 + 364 + // This is a suitable device for readonly access 365 + return &device, nil 366 + } 367 + } 368 + 369 + return nil, fmt.Errorf("no suitable HSM device found with mirroring enabled") 370 + } 371 + 372 + // SetupWithManager sets up the controller with the Manager. 373 + func (r *HSMSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 374 + return ctrl.NewControllerManagedBy(mgr). 375 + For(&hsmv1alpha1.HSMSecret{}). 376 + Owns(&corev1.Secret{}). 377 + Named("hsmsecret"). 378 + Complete(r) 379 + }
+84
internal/controller/hsmsecret_controller_test.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 controller 18 + 19 + import ( 20 + "context" 21 + 22 + . "github.com/onsi/ginkgo/v2" 23 + . "github.com/onsi/gomega" 24 + "k8s.io/apimachinery/pkg/api/errors" 25 + "k8s.io/apimachinery/pkg/types" 26 + "sigs.k8s.io/controller-runtime/pkg/reconcile" 27 + 28 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 + 30 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 31 + ) 32 + 33 + var _ = Describe("HSMSecret Controller", func() { 34 + Context("When reconciling a resource", func() { 35 + const resourceName = "test-resource" 36 + 37 + ctx := context.Background() 38 + 39 + typeNamespacedName := types.NamespacedName{ 40 + Name: resourceName, 41 + Namespace: "default", // TODO(user):Modify as needed 42 + } 43 + hsmsecret := &hsmv1alpha1.HSMSecret{} 44 + 45 + BeforeEach(func() { 46 + By("creating the custom resource for the Kind HSMSecret") 47 + err := k8sClient.Get(ctx, typeNamespacedName, hsmsecret) 48 + if err != nil && errors.IsNotFound(err) { 49 + resource := &hsmv1alpha1.HSMSecret{ 50 + ObjectMeta: metav1.ObjectMeta{ 51 + Name: resourceName, 52 + Namespace: "default", 53 + }, 54 + // TODO(user): Specify other spec details if needed. 55 + } 56 + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) 57 + } 58 + }) 59 + 60 + AfterEach(func() { 61 + // TODO(user): Cleanup logic after each test, like removing the resource instance. 62 + resource := &hsmv1alpha1.HSMSecret{} 63 + err := k8sClient.Get(ctx, typeNamespacedName, resource) 64 + Expect(err).NotTo(HaveOccurred()) 65 + 66 + By("Cleanup the specific resource instance HSMSecret") 67 + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) 68 + }) 69 + It("should successfully reconcile the resource", func() { 70 + By("Reconciling the created resource") 71 + controllerReconciler := &HSMSecretReconciler{ 72 + Client: k8sClient, 73 + Scheme: k8sClient.Scheme(), 74 + } 75 + 76 + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ 77 + NamespacedName: typeNamespacedName, 78 + }) 79 + Expect(err).NotTo(HaveOccurred()) 80 + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. 81 + // Example: If you expect a certain status condition after reconciliation, verify it here. 82 + }) 83 + }) 84 + })
+116
internal/controller/suite_test.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 controller 18 + 19 + import ( 20 + "context" 21 + "os" 22 + "path/filepath" 23 + "testing" 24 + 25 + . "github.com/onsi/ginkgo/v2" 26 + . "github.com/onsi/gomega" 27 + 28 + "k8s.io/client-go/kubernetes/scheme" 29 + "k8s.io/client-go/rest" 30 + "sigs.k8s.io/controller-runtime/pkg/client" 31 + "sigs.k8s.io/controller-runtime/pkg/envtest" 32 + logf "sigs.k8s.io/controller-runtime/pkg/log" 33 + "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 + 35 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 36 + // +kubebuilder:scaffold:imports 37 + ) 38 + 39 + // These tests use Ginkgo (BDD-style Go testing framework). Refer to 40 + // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 41 + 42 + var ( 43 + ctx context.Context 44 + cancel context.CancelFunc 45 + testEnv *envtest.Environment 46 + cfg *rest.Config 47 + k8sClient client.Client 48 + ) 49 + 50 + func TestControllers(t *testing.T) { 51 + RegisterFailHandler(Fail) 52 + 53 + RunSpecs(t, "Controller Suite") 54 + } 55 + 56 + var _ = BeforeSuite(func() { 57 + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 58 + 59 + ctx, cancel = context.WithCancel(context.TODO()) 60 + 61 + var err error 62 + err = hsmv1alpha1.AddToScheme(scheme.Scheme) 63 + Expect(err).NotTo(HaveOccurred()) 64 + 65 + // +kubebuilder:scaffold:scheme 66 + 67 + By("bootstrapping test environment") 68 + testEnv = &envtest.Environment{ 69 + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 70 + ErrorIfCRDPathMissing: true, 71 + } 72 + 73 + // Retrieve the first found binary directory to allow running tests from IDEs 74 + if getFirstFoundEnvTestBinaryDir() != "" { 75 + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() 76 + } 77 + 78 + // cfg is defined in this file globally. 79 + cfg, err = testEnv.Start() 80 + Expect(err).NotTo(HaveOccurred()) 81 + Expect(cfg).NotTo(BeNil()) 82 + 83 + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 84 + Expect(err).NotTo(HaveOccurred()) 85 + Expect(k8sClient).NotTo(BeNil()) 86 + }) 87 + 88 + var _ = AfterSuite(func() { 89 + By("tearing down the test environment") 90 + cancel() 91 + err := testEnv.Stop() 92 + Expect(err).NotTo(HaveOccurred()) 93 + }) 94 + 95 + // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. 96 + // ENVTEST-based tests depend on specific binaries, usually located in paths set by 97 + // controller-runtime. When running tests directly (e.g., via an IDE) without using 98 + // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. 99 + // 100 + // This function streamlines the process by finding the required binaries, similar to 101 + // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are 102 + // properly set up, run 'make setup-envtest' beforehand. 103 + func getFirstFoundEnvTestBinaryDir() string { 104 + basePath := filepath.Join("..", "..", "bin", "k8s") 105 + entries, err := os.ReadDir(basePath) 106 + if err != nil { 107 + logf.Log.Error(err, "Failed to read directory", "path", basePath) 108 + return "" 109 + } 110 + for _, entry := range entries { 111 + if entry.IsDir() { 112 + return filepath.Join(basePath, entry.Name()) 113 + } 114 + } 115 + return "" 116 + }
+151
internal/discovery/deviceplugin.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 discovery 18 + 19 + import ( 20 + "fmt" 21 + "strings" 22 + "sync" 23 + 24 + "github.com/go-logr/logr" 25 + ctrl "sigs.k8s.io/controller-runtime" 26 + 27 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 28 + ) 29 + 30 + const ( 31 + // ResourceNamePrefix is the prefix for HSM device resources 32 + ResourceNamePrefix = "hsm.j5t.io" 33 + ) 34 + 35 + // Device represents a managed HSM device 36 + type Device struct { 37 + ID string 38 + DevicePath string 39 + SerialNumber string 40 + Available bool 41 + NodeName string 42 + DeviceInfo map[string]string 43 + } 44 + 45 + // HSMDeviceManager manages HSM devices for Kubernetes integration 46 + type HSMDeviceManager struct { 47 + logger logr.Logger 48 + resourceName string 49 + deviceType hsmv1alpha1.HSMDeviceType 50 + devices map[string]*Device 51 + devicesMutex sync.RWMutex 52 + } 53 + 54 + // NewHSMDeviceManager creates a new HSM device manager 55 + func NewHSMDeviceManager(deviceType hsmv1alpha1.HSMDeviceType, resourceName string) *HSMDeviceManager { 56 + return &HSMDeviceManager{ 57 + logger: ctrl.Log.WithName("hsm-device-manager").WithValues("deviceType", deviceType), 58 + resourceName: resourceName, 59 + deviceType: deviceType, 60 + devices: make(map[string]*Device), 61 + } 62 + } 63 + 64 + // UpdateDevices updates the list of managed devices 65 + func (m *HSMDeviceManager) UpdateDevices(discoveredDevices []hsmv1alpha1.DiscoveredDevice) { 66 + m.devicesMutex.Lock() 67 + defer m.devicesMutex.Unlock() 68 + 69 + // Clear existing devices 70 + m.devices = make(map[string]*Device) 71 + 72 + // Add discovered devices 73 + for _, discovered := range discoveredDevices { 74 + deviceID := m.generateDeviceID(discovered) 75 + 76 + m.devices[deviceID] = &Device{ 77 + ID: deviceID, 78 + DevicePath: discovered.DevicePath, 79 + SerialNumber: discovered.SerialNumber, 80 + Available: discovered.Available, 81 + NodeName: discovered.NodeName, 82 + DeviceInfo: discovered.DeviceInfo, 83 + } 84 + 85 + m.logger.V(1).Info("Updated device", 86 + "deviceId", deviceID, 87 + "path", discovered.DevicePath, 88 + "available", discovered.Available) 89 + } 90 + 91 + m.logger.Info("Updated device list", "deviceCount", len(m.devices)) 92 + } 93 + 94 + // GetAvailableDevices returns a list of available devices 95 + func (m *HSMDeviceManager) GetAvailableDevices() []*Device { 96 + m.devicesMutex.RLock() 97 + defer m.devicesMutex.RUnlock() 98 + 99 + var available []*Device 100 + for _, device := range m.devices { 101 + if device.Available { 102 + available = append(available, device) 103 + } 104 + } 105 + 106 + return available 107 + } 108 + 109 + // GetDevice returns a device by ID 110 + func (m *HSMDeviceManager) GetDevice(deviceID string) (*Device, bool) { 111 + m.devicesMutex.RLock() 112 + defer m.devicesMutex.RUnlock() 113 + 114 + device, exists := m.devices[deviceID] 115 + return device, exists 116 + } 117 + 118 + // GetDevicesForNode returns all devices for a specific node 119 + func (m *HSMDeviceManager) GetDevicesForNode(nodeName string) []*Device { 120 + m.devicesMutex.RLock() 121 + defer m.devicesMutex.RUnlock() 122 + 123 + var nodeDevices []*Device 124 + for _, device := range m.devices { 125 + if device.NodeName == nodeName { 126 + nodeDevices = append(nodeDevices, device) 127 + } 128 + } 129 + 130 + return nodeDevices 131 + } 132 + 133 + // GetResourceName returns the Kubernetes resource name for this device type 134 + func (m *HSMDeviceManager) GetResourceName() string { 135 + return fmt.Sprintf("%s/%s", ResourceNamePrefix, strings.ToLower(string(m.deviceType))) 136 + } 137 + 138 + // generateDeviceID generates a unique device ID 139 + func (m *HSMDeviceManager) generateDeviceID(device hsmv1alpha1.DiscoveredDevice) string { 140 + // Create a unique ID based on node name, device path, and serial 141 + parts := []string{ 142 + device.NodeName, 143 + strings.ReplaceAll(device.DevicePath, "/", "_"), 144 + } 145 + 146 + if device.SerialNumber != "" { 147 + parts = append(parts, device.SerialNumber) 148 + } 149 + 150 + return strings.Join(parts, "-") 151 + }
+426
internal/discovery/mirroring.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 discovery 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "sort" 23 + "sync" 24 + "time" 25 + 26 + "github.com/go-logr/logr" 27 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 + ctrl "sigs.k8s.io/controller-runtime" 29 + "sigs.k8s.io/controller-runtime/pkg/client" 30 + 31 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 32 + "github.com/evanjarrett/hsm-secrets-operator/internal/hsm" 33 + ) 34 + 35 + // MirroredSecretData represents secret data with metadata for mirroring 36 + type MirroredSecretData struct { 37 + Path string `json:"path"` 38 + Data hsm.SecretData `json:"data"` 39 + Checksum string `json:"checksum"` 40 + LastModified time.Time `json:"lastModified"` 41 + SourceNode string `json:"sourceNode"` 42 + Metadata map[string]string `json:"metadata"` 43 + } 44 + 45 + // MirroringManager handles HSM device mirroring and cross-node synchronization 46 + type MirroringManager struct { 47 + client client.Client 48 + logger logr.Logger 49 + mutex sync.RWMutex 50 + hsmClients map[string]hsm.Client 51 + mirrorCache map[string]*MirroredSecretData 52 + nodeHealth map[string]time.Time 53 + currentNode string 54 + } 55 + 56 + // NewMirroringManager creates a new mirroring manager 57 + func NewMirroringManager(client client.Client, nodeName string) *MirroringManager { 58 + return &MirroringManager{ 59 + client: client, 60 + logger: ctrl.Log.WithName("hsm-mirroring-manager"), 61 + hsmClients: make(map[string]hsm.Client), 62 + mirrorCache: make(map[string]*MirroredSecretData), 63 + nodeHealth: make(map[string]time.Time), 64 + currentNode: nodeName, 65 + } 66 + } 67 + 68 + // RegisterHSMClient registers an HSM client for a specific node 69 + func (m *MirroringManager) RegisterHSMClient(nodeName string, client hsm.Client) { 70 + m.mutex.Lock() 71 + defer m.mutex.Unlock() 72 + 73 + m.hsmClients[nodeName] = client 74 + m.nodeHealth[nodeName] = time.Now() 75 + m.logger.Info("Registered HSM client for node", "node", nodeName) 76 + } 77 + 78 + // SyncDevices synchronizes secrets across mirrored HSM devices 79 + func (m *MirroringManager) SyncDevices(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) error { 80 + if hsmDevice.Spec.Mirroring == nil || hsmDevice.Spec.Mirroring.Policy == hsmv1alpha1.MirroringPolicyNone { 81 + return nil 82 + } 83 + 84 + m.logger.Info("Starting device synchronization", 85 + "device", hsmDevice.Name, 86 + "policy", hsmDevice.Spec.Mirroring.Policy) 87 + 88 + // Determine primary and mirror nodes 89 + primaryNode, mirrorNodes, err := m.determineMirrorTopology(ctx, hsmDevice) 90 + if err != nil { 91 + return fmt.Errorf("failed to determine mirror topology: %w", err) 92 + } 93 + 94 + // Sync secrets from primary to mirrors 95 + if err := m.syncFromPrimary(ctx, hsmDevice, primaryNode, mirrorNodes); err != nil { 96 + return fmt.Errorf("failed to sync from primary: %w", err) 97 + } 98 + 99 + // Update mirroring status 100 + if err := m.updateMirroringStatus(ctx, hsmDevice, primaryNode, mirrorNodes); err != nil { 101 + return fmt.Errorf("failed to update mirroring status: %w", err) 102 + } 103 + 104 + return nil 105 + } 106 + 107 + // determineMirrorTopology determines which nodes should be primary vs mirrors 108 + func (m *MirroringManager) determineMirrorTopology(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) (string, []string, error) { 109 + availableNodes := make([]string, 0) 110 + 111 + // Collect nodes with available devices 112 + for _, device := range hsmDevice.Status.DiscoveredDevices { 113 + if device.Available && device.Health != "Unhealthy" { 114 + availableNodes = append(availableNodes, device.NodeName) 115 + } 116 + } 117 + 118 + if len(availableNodes) == 0 { 119 + return "", nil, fmt.Errorf("no healthy devices available for mirroring") 120 + } 121 + 122 + // Determine primary node 123 + primaryNode := "" 124 + if hsmDevice.Spec.Mirroring.PrimaryNode != "" { 125 + // Use specified primary if available and healthy 126 + for _, node := range availableNodes { 127 + if node == hsmDevice.Spec.Mirroring.PrimaryNode { 128 + primaryNode = node 129 + break 130 + } 131 + } 132 + } 133 + 134 + if primaryNode == "" { 135 + // Choose primary based on health and availability 136 + sort.Strings(availableNodes) // Deterministic selection 137 + primaryNode = availableNodes[0] 138 + } 139 + 140 + // Determine mirror nodes 141 + mirrorNodes := make([]string, 0) 142 + for _, node := range availableNodes { 143 + if node != primaryNode { 144 + // Check if node should be a mirror target 145 + if len(hsmDevice.Spec.Mirroring.TargetNodes) == 0 { 146 + // Mirror to all available nodes if no targets specified 147 + mirrorNodes = append(mirrorNodes, node) 148 + } else { 149 + // Only mirror to specified target nodes 150 + for _, target := range hsmDevice.Spec.Mirroring.TargetNodes { 151 + if node == target { 152 + mirrorNodes = append(mirrorNodes, node) 153 + break 154 + } 155 + } 156 + } 157 + } 158 + } 159 + 160 + m.logger.V(1).Info("Determined mirror topology", 161 + "primary", primaryNode, 162 + "mirrors", mirrorNodes) 163 + 164 + return primaryNode, mirrorNodes, nil 165 + } 166 + 167 + // syncFromPrimary synchronizes secrets from the primary node to mirror nodes 168 + func (m *MirroringManager) syncFromPrimary(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, primaryNode string, mirrorNodes []string) error { 169 + m.mutex.RLock() 170 + primaryClient, exists := m.hsmClients[primaryNode] 171 + m.mutex.RUnlock() 172 + 173 + if !exists || !primaryClient.IsConnected() { 174 + return fmt.Errorf("primary HSM client not available on node %s", primaryNode) 175 + } 176 + 177 + // List all secrets on the primary device 178 + secrets, err := m.listSecretsFromHSM(ctx, primaryClient, hsmDevice) 179 + if err != nil { 180 + return fmt.Errorf("failed to list secrets from primary: %w", err) 181 + } 182 + 183 + m.logger.Info("Found secrets on primary", "count", len(secrets), "primary", primaryNode) 184 + 185 + // Sync each secret to mirror nodes 186 + for _, secretPath := range secrets { 187 + if err := m.syncSecretToMirrors(ctx, secretPath, primaryClient, primaryNode, mirrorNodes); err != nil { 188 + m.logger.Error(err, "Failed to sync secret to mirrors", 189 + "secret", secretPath, "primary", primaryNode) 190 + continue 191 + } 192 + } 193 + 194 + return nil 195 + } 196 + 197 + // syncSecretToMirrors syncs a single secret to all mirror nodes 198 + func (m *MirroringManager) syncSecretToMirrors(ctx context.Context, secretPath string, primaryClient hsm.Client, primaryNode string, mirrorNodes []string) error { 199 + // Read secret from primary 200 + secretData, err := primaryClient.ReadSecret(ctx, secretPath) 201 + if err != nil { 202 + return fmt.Errorf("failed to read secret from primary: %w", err) 203 + } 204 + 205 + // Calculate checksum 206 + checksum := hsm.CalculateChecksum(secretData) 207 + 208 + // Create mirrored data entry 209 + mirroredData := &MirroredSecretData{ 210 + Path: secretPath, 211 + Data: secretData, 212 + Checksum: checksum, 213 + LastModified: time.Now(), 214 + SourceNode: primaryNode, 215 + Metadata: map[string]string{ 216 + "source-node": primaryNode, 217 + "sync-time": time.Now().Format(time.RFC3339), 218 + }, 219 + } 220 + 221 + // Update cache 222 + m.mutex.Lock() 223 + m.mirrorCache[secretPath] = mirroredData 224 + m.mutex.Unlock() 225 + 226 + // Sync to mirror nodes (readonly) 227 + for _, mirrorNode := range mirrorNodes { 228 + if err := m.syncToMirrorNode(ctx, mirroredData, mirrorNode); err != nil { 229 + m.logger.Error(err, "Failed to sync to mirror node", 230 + "secret", secretPath, "mirror", mirrorNode) 231 + } 232 + } 233 + 234 + return nil 235 + } 236 + 237 + // syncToMirrorNode syncs secret data to a specific mirror node 238 + func (m *MirroringManager) syncToMirrorNode(ctx context.Context, data *MirroredSecretData, mirrorNode string) error { 239 + m.mutex.RLock() 240 + mirrorClient, exists := m.hsmClients[mirrorNode] 241 + m.mutex.RUnlock() 242 + 243 + if !exists || !mirrorClient.IsConnected() { 244 + return fmt.Errorf("mirror HSM client not available on node %s", mirrorNode) 245 + } 246 + 247 + // For readonly mirrors, we store the secret data in a readonly format 248 + // In a real implementation, this might involve writing to a readonly partition 249 + // or using HSM-specific mirroring capabilities 250 + 251 + // Check if secret already exists and is up to date 252 + existingChecksum, err := mirrorClient.GetChecksum(ctx, data.Path) 253 + if err == nil && existingChecksum == data.Checksum { 254 + // Secret is already up to date 255 + m.logger.V(2).Info("Secret already up to date on mirror", 256 + "secret", data.Path, "mirror", mirrorNode) 257 + return nil 258 + } 259 + 260 + // Write secret to mirror (readonly) 261 + if err := mirrorClient.WriteSecret(ctx, data.Path, data.Data); err != nil { 262 + return fmt.Errorf("failed to write secret to mirror: %w", err) 263 + } 264 + 265 + m.logger.V(1).Info("Successfully synced secret to mirror", 266 + "secret", data.Path, "mirror", mirrorNode) 267 + 268 + return nil 269 + } 270 + 271 + // listSecretsFromHSM lists all secrets from an HSM client 272 + func (m *MirroringManager) listSecretsFromHSM(ctx context.Context, client hsm.Client, hsmDevice *hsmv1alpha1.HSMDevice) ([]string, error) { 273 + // This is a simplified implementation 274 + // In a real implementation, you would use HSM-specific APIs to list secrets 275 + 276 + var secrets []string 277 + 278 + // For demo purposes, we'll simulate some secret paths 279 + // In reality, this would query the HSM for all available secret paths 280 + basePaths := []string{ 281 + "secrets/default/database-credentials", 282 + "secrets/production/api-keys", 283 + "secrets/staging/certificates", 284 + } 285 + 286 + for _, path := range basePaths { 287 + // Check if secret exists 288 + if _, err := client.ReadSecret(ctx, path); err == nil { 289 + secrets = append(secrets, path) 290 + } 291 + } 292 + 293 + return secrets, nil 294 + } 295 + 296 + // GetReadOnlyAccess provides readonly access to secrets from mirrors when primary is down 297 + func (m *MirroringManager) GetReadOnlyAccess(ctx context.Context, secretPath string, hsmDevice *hsmv1alpha1.HSMDevice) (hsm.SecretData, error) { 298 + m.logger.Info("Attempting readonly access", "secret", secretPath) 299 + 300 + // Check cache first 301 + m.mutex.RLock() 302 + cachedData, exists := m.mirrorCache[secretPath] 303 + m.mutex.RUnlock() 304 + 305 + if exists && time.Since(cachedData.LastModified) < time.Hour { 306 + m.logger.V(1).Info("Using cached mirror data", "secret", secretPath) 307 + return cachedData.Data, nil 308 + } 309 + 310 + // Try to read from available mirror nodes 311 + for _, device := range hsmDevice.Status.DiscoveredDevices { 312 + if device.Role == hsmv1alpha1.DeviceRoleReadOnly && device.Available { 313 + m.mutex.RLock() 314 + mirrorClient, exists := m.hsmClients[device.NodeName] 315 + m.mutex.RUnlock() 316 + 317 + if exists && mirrorClient.IsConnected() { 318 + if data, err := mirrorClient.ReadSecret(ctx, secretPath); err == nil { 319 + m.logger.Info("Successfully read from mirror", 320 + "secret", secretPath, "mirror", device.NodeName) 321 + return data, nil 322 + } 323 + } 324 + } 325 + } 326 + 327 + return nil, fmt.Errorf("no readable mirrors available for secret %s", secretPath) 328 + } 329 + 330 + // HandleFailover handles failover from a failed primary to a healthy mirror 331 + func (m *MirroringManager) HandleFailover(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice) error { 332 + if hsmDevice.Spec.Mirroring == nil || !hsmDevice.Spec.Mirroring.AutoFailover { 333 + return nil 334 + } 335 + 336 + m.logger.Info("Handling device failover", "device", hsmDevice.Name) 337 + 338 + // Find a healthy mirror to promote to primary 339 + var newPrimary string 340 + for _, device := range hsmDevice.Status.DiscoveredDevices { 341 + if device.Role == hsmv1alpha1.DeviceRoleReadOnly && device.Available { 342 + newPrimary = device.NodeName 343 + break 344 + } 345 + } 346 + 347 + if newPrimary == "" { 348 + return fmt.Errorf("no healthy mirrors available for failover") 349 + } 350 + 351 + // Update device roles 352 + for i, device := range hsmDevice.Status.DiscoveredDevices { 353 + if device.NodeName == newPrimary { 354 + hsmDevice.Status.DiscoveredDevices[i].Role = hsmv1alpha1.DeviceRolePrimary 355 + } 356 + } 357 + 358 + // Update mirroring status 359 + if hsmDevice.Status.Mirroring != nil { 360 + hsmDevice.Status.Mirroring.PrimaryNode = newPrimary 361 + hsmDevice.Status.Mirroring.FailoverCount++ 362 + } 363 + 364 + // Update the HSMDevice status 365 + if err := m.client.Status().Update(ctx, hsmDevice); err != nil { 366 + return fmt.Errorf("failed to update device status after failover: %w", err) 367 + } 368 + 369 + m.logger.Info("Successfully failed over to new primary", 370 + "device", hsmDevice.Name, "newPrimary", newPrimary) 371 + 372 + return nil 373 + } 374 + 375 + // updateMirroringStatus updates the mirroring status in the HSMDevice 376 + func (m *MirroringManager) updateMirroringStatus(ctx context.Context, hsmDevice *hsmv1alpha1.HSMDevice, primaryNode string, mirrorNodes []string) error { 377 + now := metav1.Now() 378 + 379 + if hsmDevice.Status.Mirroring == nil { 380 + hsmDevice.Status.Mirroring = &hsmv1alpha1.MirroringStatus{} 381 + } 382 + 383 + hsmDevice.Status.Mirroring.Enabled = true 384 + hsmDevice.Status.Mirroring.PrimaryNode = primaryNode 385 + hsmDevice.Status.Mirroring.MirroredNodes = mirrorNodes 386 + hsmDevice.Status.Mirroring.LastSyncTime = &now 387 + 388 + // Update device roles 389 + for i, device := range hsmDevice.Status.DiscoveredDevices { 390 + if device.NodeName == primaryNode { 391 + hsmDevice.Status.DiscoveredDevices[i].Role = hsmv1alpha1.DeviceRolePrimary 392 + } else { 393 + for _, mirrorNode := range mirrorNodes { 394 + if device.NodeName == mirrorNode { 395 + hsmDevice.Status.DiscoveredDevices[i].Role = hsmv1alpha1.DeviceRoleReadOnly 396 + hsmDevice.Status.DiscoveredDevices[i].MirroredFrom = primaryNode 397 + hsmDevice.Status.DiscoveredDevices[i].LastSyncTime = &now 398 + break 399 + } 400 + } 401 + } 402 + } 403 + 404 + return nil 405 + } 406 + 407 + // IsNodeHealthy checks if a node is healthy based on last seen time 408 + func (m *MirroringManager) IsNodeHealthy(nodeName string, maxAge time.Duration) bool { 409 + m.mutex.RLock() 410 + defer m.mutex.RUnlock() 411 + 412 + lastSeen, exists := m.nodeHealth[nodeName] 413 + if !exists { 414 + return false 415 + } 416 + 417 + return time.Since(lastSeen) <= maxAge 418 + } 419 + 420 + // UpdateNodeHealth updates the health status of a node 421 + func (m *MirroringManager) UpdateNodeHealth(nodeName string) { 422 + m.mutex.Lock() 423 + defer m.mutex.Unlock() 424 + 425 + m.nodeHealth[nodeName] = time.Now() 426 + }
+336
internal/discovery/usb.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 discovery 18 + 19 + import ( 20 + "bufio" 21 + "context" 22 + "fmt" 23 + "io/fs" 24 + "os" 25 + "path/filepath" 26 + "regexp" 27 + "strings" 28 + "sync" 29 + 30 + "github.com/go-logr/logr" 31 + ctrl "sigs.k8s.io/controller-runtime" 32 + 33 + hsmv1alpha1 "github.com/evanjarrett/hsm-secrets-operator/api/v1alpha1" 34 + ) 35 + 36 + // USBDevice represents a discovered USB device 37 + type USBDevice struct { 38 + VendorID string 39 + ProductID string 40 + SerialNumber string 41 + DevicePath string 42 + Manufacturer string 43 + Product string 44 + DeviceInfo map[string]string 45 + } 46 + 47 + // USBDiscoverer handles USB device discovery 48 + type USBDiscoverer struct { 49 + logger logr.Logger 50 + mutex sync.RWMutex 51 + 52 + // Known USB paths to scan 53 + usbSysPaths []string 54 + devicePaths []string 55 + } 56 + 57 + // NewUSBDiscoverer creates a new USB device discoverer 58 + func NewUSBDiscoverer() *USBDiscoverer { 59 + return &USBDiscoverer{ 60 + logger: ctrl.Log.WithName("usb-discoverer"), 61 + usbSysPaths: []string{ 62 + "/sys/bus/usb/devices", 63 + "/sys/class/usbmisc", 64 + }, 65 + devicePaths: []string{ 66 + "/dev", 67 + }, 68 + } 69 + } 70 + 71 + // DiscoverDevices finds USB devices matching the given specification 72 + func (u *USBDiscoverer) DiscoverDevices(ctx context.Context, spec *hsmv1alpha1.USBDeviceSpec) ([]USBDevice, error) { 73 + u.logger.V(1).Info("Starting USB device discovery", 74 + "vendorId", spec.VendorID, 75 + "productId", spec.ProductID) 76 + 77 + var devices []USBDevice 78 + 79 + // Scan USB devices in sysfs 80 + usbDevices, err := u.scanUSBDevices(ctx) 81 + if err != nil { 82 + return nil, fmt.Errorf("failed to scan USB devices: %w", err) 83 + } 84 + 85 + // Filter devices based on spec 86 + for _, device := range usbDevices { 87 + if u.matchesSpec(device, spec) { 88 + u.logger.V(1).Info("Found matching USB device", 89 + "vendorId", device.VendorID, 90 + "productId", device.ProductID, 91 + "serial", device.SerialNumber, 92 + "path", device.DevicePath) 93 + devices = append(devices, device) 94 + } 95 + } 96 + 97 + u.logger.Info("USB device discovery completed", 98 + "matchedDevices", len(devices)) 99 + 100 + return devices, nil 101 + } 102 + 103 + // DiscoverByPath finds devices using path-based discovery 104 + func (u *USBDiscoverer) DiscoverByPath(ctx context.Context, pathSpec *hsmv1alpha1.DevicePathSpec) ([]USBDevice, error) { 105 + u.logger.V(1).Info("Starting path-based device discovery", "path", pathSpec.Path) 106 + 107 + var devices []USBDevice 108 + 109 + // Handle glob patterns 110 + matches, err := filepath.Glob(pathSpec.Path) 111 + if err != nil { 112 + return nil, fmt.Errorf("failed to glob path %s: %w", pathSpec.Path, err) 113 + } 114 + 115 + for _, match := range matches { 116 + // Check if the path exists and is accessible 117 + if _, err := os.Stat(match); err != nil { 118 + u.logger.V(2).Info("Skipping inaccessible device path", "path", match, "error", err) 119 + continue 120 + } 121 + 122 + // Create USB device entry for path-based discovery 123 + device := USBDevice{ 124 + DevicePath: match, 125 + DeviceInfo: map[string]string{ 126 + "discovery-method": "path", 127 + "permissions": pathSpec.Permissions, 128 + }, 129 + } 130 + 131 + // Try to get additional device info if possible 132 + if info := u.getDeviceInfoFromPath(match); info != nil { 133 + device.VendorID = info["vendor_id"] 134 + device.ProductID = info["product_id"] 135 + device.SerialNumber = info["serial"] 136 + device.Manufacturer = info["manufacturer"] 137 + device.Product = info["product"] 138 + for k, v := range info { 139 + device.DeviceInfo[k] = v 140 + } 141 + } 142 + 143 + devices = append(devices, device) 144 + } 145 + 146 + u.logger.Info("Path-based device discovery completed", 147 + "matchedDevices", len(devices)) 148 + 149 + return devices, nil 150 + } 151 + 152 + // scanUSBDevices scans the USB subsystem for devices 153 + func (u *USBDiscoverer) scanUSBDevices(ctx context.Context) ([]USBDevice, error) { 154 + var devices []USBDevice 155 + 156 + usbSysPath := "/sys/bus/usb/devices" 157 + if _, err := os.Stat(usbSysPath); err != nil { 158 + u.logger.V(1).Info("USB sysfs path not available", "path", usbSysPath) 159 + return devices, nil 160 + } 161 + 162 + err := filepath.WalkDir(usbSysPath, func(path string, d fs.DirEntry, err error) error { 163 + if err != nil { 164 + return err 165 + } 166 + 167 + // Skip if not a directory or if it doesn't look like a USB device 168 + if !d.IsDir() { 169 + return nil 170 + } 171 + 172 + // Check if this is a USB device directory (e.g., 1-1.2) 173 + name := d.Name() 174 + if !regexp.MustCompile(`^\d+-[\d.]+$`).MatchString(name) { 175 + return nil 176 + } 177 + 178 + device, err := u.parseUSBDevice(path) 179 + if err != nil { 180 + u.logger.V(2).Info("Failed to parse USB device", "path", path, "error", err) 181 + return nil 182 + } 183 + 184 + if device != nil { 185 + devices = append(devices, *device) 186 + } 187 + 188 + return nil 189 + }) 190 + 191 + if err != nil { 192 + return nil, fmt.Errorf("failed to walk USB devices: %w", err) 193 + } 194 + 195 + return devices, nil 196 + } 197 + 198 + // parseUSBDevice parses USB device information from sysfs 199 + func (u *USBDiscoverer) parseUSBDevice(devicePath string) (*USBDevice, error) { 200 + device := &USBDevice{ 201 + DeviceInfo: make(map[string]string), 202 + } 203 + 204 + // Read vendor ID 205 + if vendorID, err := u.readSysfsFile(filepath.Join(devicePath, "idVendor")); err == nil { 206 + device.VendorID = strings.TrimSpace(vendorID) 207 + } 208 + 209 + // Read product ID 210 + if productID, err := u.readSysfsFile(filepath.Join(devicePath, "idProduct")); err == nil { 211 + device.ProductID = strings.TrimSpace(productID) 212 + } 213 + 214 + // Read serial number 215 + if serial, err := u.readSysfsFile(filepath.Join(devicePath, "serial")); err == nil { 216 + device.SerialNumber = strings.TrimSpace(serial) 217 + } 218 + 219 + // Read manufacturer 220 + if manufacturer, err := u.readSysfsFile(filepath.Join(devicePath, "manufacturer")); err == nil { 221 + device.Manufacturer = strings.TrimSpace(manufacturer) 222 + } 223 + 224 + // Read product name 225 + if product, err := u.readSysfsFile(filepath.Join(devicePath, "product")); err == nil { 226 + device.Product = strings.TrimSpace(product) 227 + } 228 + 229 + // Skip devices without vendor/product IDs 230 + if device.VendorID == "" || device.ProductID == "" { 231 + return nil, nil 232 + } 233 + 234 + // Try to find associated device paths 235 + device.DevicePath = u.findDevicePaths(device.VendorID, device.ProductID, device.SerialNumber) 236 + 237 + // Add additional device info 238 + device.DeviceInfo["sysfs-path"] = devicePath 239 + device.DeviceInfo["discovery-method"] = "usb" 240 + 241 + return device, nil 242 + } 243 + 244 + // findDevicePaths attempts to find device paths for a USB device 245 + func (u *USBDiscoverer) findDevicePaths(vendorID, productID, serial string) string { 246 + // This is a simplified implementation 247 + // In a real implementation, you'd want to scan /dev and match devices 248 + // For now, we'll look for common HSM device paths 249 + 250 + commonPaths := []string{ 251 + "/dev/ttyUSB0", 252 + "/dev/ttyUSB1", 253 + "/dev/ttyACM0", 254 + "/dev/ttyACM1", 255 + "/dev/sc-hsm", 256 + "/dev/pkcs11", 257 + } 258 + 259 + for _, path := range commonPaths { 260 + if _, err := os.Stat(path); err == nil { 261 + // In a real implementation, you'd verify this device matches 262 + // the USB device we found 263 + return path 264 + } 265 + } 266 + 267 + return "" 268 + } 269 + 270 + // readSysfsFile reads a single-line file from sysfs 271 + func (u *USBDiscoverer) readSysfsFile(path string) (string, error) { 272 + file, err := os.Open(path) 273 + if err != nil { 274 + return "", err 275 + } 276 + defer file.Close() 277 + 278 + scanner := bufio.NewScanner(file) 279 + if scanner.Scan() { 280 + return scanner.Text(), nil 281 + } 282 + 283 + return "", fmt.Errorf("empty file or read error") 284 + } 285 + 286 + // getDeviceInfoFromPath attempts to get device info from a device path 287 + func (u *USBDiscoverer) getDeviceInfoFromPath(devicePath string) map[string]string { 288 + // This is a placeholder implementation 289 + // In a real implementation, you'd use udev or similar to get device info 290 + info := make(map[string]string) 291 + 292 + // Try to determine device type from path 293 + if strings.Contains(devicePath, "ttyUSB") || strings.Contains(devicePath, "ttyACM") { 294 + info["device_type"] = "serial" 295 + } else if strings.Contains(devicePath, "sc-hsm") { 296 + info["device_type"] = "hsm" 297 + info["vendor_id"] = "20a0" // Example: Pico HSM vendor ID 298 + info["product_id"] = "4230" // Example: Pico HSM product ID 299 + } 300 + 301 + return info 302 + } 303 + 304 + // matchesSpec checks if a USB device matches the given specification 305 + func (u *USBDiscoverer) matchesSpec(device USBDevice, spec *hsmv1alpha1.USBDeviceSpec) bool { 306 + // Check vendor ID 307 + if spec.VendorID != "" && !strings.EqualFold(device.VendorID, spec.VendorID) { 308 + return false 309 + } 310 + 311 + // Check product ID 312 + if spec.ProductID != "" && !strings.EqualFold(device.ProductID, spec.ProductID) { 313 + return false 314 + } 315 + 316 + // Check serial number if specified 317 + if spec.SerialNumber != "" && device.SerialNumber != spec.SerialNumber { 318 + return false 319 + } 320 + 321 + return true 322 + } 323 + 324 + // GetWellKnownHSMSpecs returns USB specifications for well-known HSM devices 325 + func GetWellKnownHSMSpecs() map[hsmv1alpha1.HSMDeviceType]*hsmv1alpha1.USBDeviceSpec { 326 + return map[hsmv1alpha1.HSMDeviceType]*hsmv1alpha1.USBDeviceSpec{ 327 + hsmv1alpha1.HSMDeviceTypePicoHSM: { 328 + VendorID: "20a0", // Pico HSM vendor ID 329 + ProductID: "4230", // Pico HSM product ID 330 + }, 331 + hsmv1alpha1.HSMDeviceTypeSmartCardHSM: { 332 + VendorID: "04e6", // Example SmartCard-HSM vendor ID 333 + ProductID: "5816", // Example SmartCard-HSM product ID 334 + }, 335 + } 336 + }
+128
internal/hsm/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 hsm 18 + 19 + import ( 20 + "context" 21 + "crypto/sha256" 22 + "fmt" 23 + "time" 24 + ) 25 + 26 + // SecretData represents secret key-value pairs 27 + type SecretData map[string][]byte 28 + 29 + // HSMInfo contains information about the HSM device 30 + type HSMInfo struct { 31 + Label string 32 + Manufacturer string 33 + Model string 34 + SerialNumber string 35 + FirmwareVersion string 36 + } 37 + 38 + // Client defines the interface for HSM operations 39 + type Client interface { 40 + // Initialize establishes connection to the HSM 41 + Initialize(ctx context.Context, config Config) error 42 + 43 + // Close terminates the HSM connection 44 + Close() error 45 + 46 + // GetInfo returns information about the HSM device 47 + GetInfo(ctx context.Context) (*HSMInfo, error) 48 + 49 + // ReadSecret reads secret data from the specified HSM path 50 + ReadSecret(ctx context.Context, path string) (SecretData, error) 51 + 52 + // WriteSecret writes secret data to the specified HSM path 53 + WriteSecret(ctx context.Context, path string, data SecretData) error 54 + 55 + // DeleteSecret removes secret data from the specified HSM path 56 + DeleteSecret(ctx context.Context, path string) error 57 + 58 + // ListSecrets returns a list of secret paths 59 + ListSecrets(ctx context.Context, prefix string) ([]string, error) 60 + 61 + // GetChecksum returns the SHA256 checksum of the secret data at the given path 62 + GetChecksum(ctx context.Context, path string) (string, error) 63 + 64 + // IsConnected returns true if the HSM is connected and responsive 65 + IsConnected() bool 66 + } 67 + 68 + // Config holds HSM client configuration 69 + type Config struct { 70 + // PKCS11LibraryPath is the path to the PKCS#11 library 71 + PKCS11LibraryPath string 72 + 73 + // SlotID is the HSM slot identifier 74 + SlotID uint 75 + 76 + // PIN is the user PIN for authentication 77 + PIN string 78 + 79 + // TokenLabel is the token label to use 80 + TokenLabel string 81 + 82 + // ConnectionTimeout for HSM operations 83 + ConnectionTimeout time.Duration 84 + 85 + // RetryAttempts for failed operations 86 + RetryAttempts int 87 + 88 + // RetryDelay between retry attempts 89 + RetryDelay time.Duration 90 + } 91 + 92 + // DefaultConfig returns a default HSM configuration 93 + func DefaultConfig() Config { 94 + return Config{ 95 + PKCS11LibraryPath: "/usr/lib/opensc-pkcs11.so", 96 + SlotID: 0, 97 + ConnectionTimeout: 30 * time.Second, 98 + RetryAttempts: 3, 99 + RetryDelay: 2 * time.Second, 100 + } 101 + } 102 + 103 + // CalculateChecksum calculates SHA256 checksum of secret data 104 + func CalculateChecksum(data SecretData) string { 105 + h := sha256.New() 106 + 107 + // Sort keys for consistent checksum 108 + keys := make([]string, 0, len(data)) 109 + for k := range data { 110 + keys = append(keys, k) 111 + } 112 + 113 + // Simple sort (for production, use sort.Strings) 114 + for i := 0; i < len(keys)-1; i++ { 115 + for j := i + 1; j < len(keys); j++ { 116 + if keys[i] > keys[j] { 117 + keys[i], keys[j] = keys[j], keys[i] 118 + } 119 + } 120 + } 121 + 122 + for _, key := range keys { 123 + h.Write([]byte(key)) 124 + h.Write(data[key]) 125 + } 126 + 127 + return fmt.Sprintf("sha256:%x", h.Sum(nil)) 128 + }
+234
internal/hsm/mock_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 hsm 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "strings" 23 + "sync" 24 + 25 + "github.com/go-logr/logr" 26 + ctrl "sigs.k8s.io/controller-runtime" 27 + ) 28 + 29 + // MockClient implements the Client interface for testing 30 + type MockClient struct { 31 + logger logr.Logger 32 + mutex sync.RWMutex 33 + connected bool 34 + secrets map[string]SecretData 35 + config Config 36 + } 37 + 38 + // NewMockClient creates a new mock HSM client for testing 39 + func NewMockClient() *MockClient { 40 + return &MockClient{ 41 + logger: ctrl.Log.WithName("hsm-mock-client"), 42 + secrets: make(map[string]SecretData), 43 + } 44 + } 45 + 46 + // Initialize simulates HSM connection 47 + func (m *MockClient) Initialize(ctx context.Context, config Config) error { 48 + m.mutex.Lock() 49 + defer m.mutex.Unlock() 50 + 51 + m.config = config 52 + m.connected = true 53 + 54 + // Pre-populate with some test data 55 + m.secrets["secrets/default/test-secret"] = SecretData{ 56 + "username": []byte("testuser"), 57 + "password": []byte("testpass123"), 58 + } 59 + 60 + m.secrets["secrets/production/database-credentials"] = SecretData{ 61 + "host": []byte("db.example.com"), 62 + "username": []byte("produser"), 63 + "password": []byte("prod-secret-password"), 64 + "database": []byte("application"), 65 + } 66 + 67 + m.logger.Info("Mock HSM client initialized", "secretCount", len(m.secrets)) 68 + return nil 69 + } 70 + 71 + // Close simulates HSM disconnection 72 + func (m *MockClient) Close() error { 73 + m.mutex.Lock() 74 + defer m.mutex.Unlock() 75 + 76 + m.connected = false 77 + m.logger.Info("Mock HSM client closed") 78 + return nil 79 + } 80 + 81 + // GetInfo returns mock HSM device information 82 + func (m *MockClient) GetInfo(ctx context.Context) (*HSMInfo, error) { 83 + m.mutex.RLock() 84 + defer m.mutex.RUnlock() 85 + 86 + if !m.connected { 87 + return nil, fmt.Errorf("HSM not connected") 88 + } 89 + 90 + return &HSMInfo{ 91 + Label: "Mock HSM Token", 92 + Manufacturer: "Test Manufacturer", 93 + Model: "Mock HSM v1.0", 94 + SerialNumber: "MOCK123456", 95 + FirmwareVersion: "1.0.0", 96 + }, nil 97 + } 98 + 99 + // ReadSecret reads secret data from mock storage 100 + func (m *MockClient) ReadSecret(ctx context.Context, path string) (SecretData, error) { 101 + m.mutex.RLock() 102 + defer m.mutex.RUnlock() 103 + 104 + if !m.connected { 105 + return nil, fmt.Errorf("HSM not connected") 106 + } 107 + 108 + data, exists := m.secrets[path] 109 + if !exists { 110 + return nil, fmt.Errorf("secret not found at path: %s", path) 111 + } 112 + 113 + // Return a copy to avoid data races 114 + result := make(SecretData) 115 + for k, v := range data { 116 + result[k] = append([]byte(nil), v...) 117 + } 118 + 119 + m.logger.V(1).Info("Read secret from mock HSM", 120 + "path", path, "keys", len(result)) 121 + return result, nil 122 + } 123 + 124 + // WriteSecret writes secret data to mock storage 125 + func (m *MockClient) WriteSecret(ctx context.Context, path string, data SecretData) error { 126 + m.mutex.Lock() 127 + defer m.mutex.Unlock() 128 + 129 + if !m.connected { 130 + return fmt.Errorf("HSM not connected") 131 + } 132 + 133 + // Store a copy to avoid data races 134 + stored := make(SecretData) 135 + for k, v := range data { 136 + stored[k] = append([]byte(nil), v...) 137 + } 138 + 139 + m.secrets[path] = stored 140 + m.logger.Info("Wrote secret to mock HSM", 141 + "path", path, "keys", len(data)) 142 + return nil 143 + } 144 + 145 + // DeleteSecret removes secret data from mock storage 146 + func (m *MockClient) DeleteSecret(ctx context.Context, path string) error { 147 + m.mutex.Lock() 148 + defer m.mutex.Unlock() 149 + 150 + if !m.connected { 151 + return fmt.Errorf("HSM not connected") 152 + } 153 + 154 + if _, exists := m.secrets[path]; !exists { 155 + return fmt.Errorf("secret not found at path: %s", path) 156 + } 157 + 158 + delete(m.secrets, path) 159 + m.logger.Info("Deleted secret from mock HSM", "path", path) 160 + return nil 161 + } 162 + 163 + // ListSecrets returns a list of secret paths with the given prefix 164 + func (m *MockClient) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 165 + m.mutex.RLock() 166 + defer m.mutex.RUnlock() 167 + 168 + if !m.connected { 169 + return nil, fmt.Errorf("HSM not connected") 170 + } 171 + 172 + var paths []string 173 + for path := range m.secrets { 174 + if strings.HasPrefix(path, prefix) { 175 + paths = append(paths, path) 176 + } 177 + } 178 + 179 + m.logger.V(1).Info("Listed secrets from mock HSM", 180 + "prefix", prefix, "count", len(paths)) 181 + return paths, nil 182 + } 183 + 184 + // GetChecksum returns the SHA256 checksum of the secret data at the given path 185 + func (m *MockClient) GetChecksum(ctx context.Context, path string) (string, error) { 186 + data, err := m.ReadSecret(ctx, path) 187 + if err != nil { 188 + return "", fmt.Errorf("failed to read secret for checksum: %w", err) 189 + } 190 + 191 + checksum := CalculateChecksum(data) 192 + m.logger.V(2).Info("Calculated checksum for mock secret", 193 + "path", path, "checksum", checksum) 194 + 195 + return checksum, nil 196 + } 197 + 198 + // IsConnected returns the mock connection status 199 + func (m *MockClient) IsConnected() bool { 200 + m.mutex.RLock() 201 + defer m.mutex.RUnlock() 202 + 203 + return m.connected 204 + } 205 + 206 + // AddSecret adds a secret to the mock storage for testing 207 + func (m *MockClient) AddSecret(path string, data SecretData) { 208 + m.mutex.Lock() 209 + defer m.mutex.Unlock() 210 + 211 + stored := make(SecretData) 212 + for k, v := range data { 213 + stored[k] = append([]byte(nil), v...) 214 + } 215 + 216 + m.secrets[path] = stored 217 + } 218 + 219 + // GetAllSecrets returns all secrets in mock storage for testing 220 + func (m *MockClient) GetAllSecrets() map[string]SecretData { 221 + m.mutex.RLock() 222 + defer m.mutex.RUnlock() 223 + 224 + result := make(map[string]SecretData) 225 + for path, data := range m.secrets { 226 + copied := make(SecretData) 227 + for k, v := range data { 228 + copied[k] = append([]byte(nil), v...) 229 + } 230 + result[path] = copied 231 + } 232 + 233 + return result 234 + }
+267
internal/hsm/pkcs11_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 hsm 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "strings" 23 + "sync" 24 + "time" 25 + 26 + "github.com/go-logr/logr" 27 + ctrl "sigs.k8s.io/controller-runtime" 28 + ) 29 + 30 + // PKCS11Client implements the Client interface using PKCS#11 31 + type PKCS11Client struct { 32 + config Config 33 + logger logr.Logger 34 + mutex sync.RWMutex 35 + 36 + // These would be actual PKCS#11 objects in a real implementation 37 + session interface{} 38 + connected bool 39 + } 40 + 41 + // NewPKCS11Client creates a new PKCS#11 HSM client 42 + func NewPKCS11Client() *PKCS11Client { 43 + return &PKCS11Client{ 44 + logger: ctrl.Log.WithName("hsm-pkcs11-client"), 45 + } 46 + } 47 + 48 + // Initialize establishes connection to the HSM via PKCS#11 49 + func (c *PKCS11Client) Initialize(ctx context.Context, config Config) error { 50 + c.mutex.Lock() 51 + defer c.mutex.Unlock() 52 + 53 + c.config = config 54 + c.logger.Info("Initializing HSM connection", 55 + "library", config.PKCS11LibraryPath, 56 + "slot", config.SlotID, 57 + "tokenLabel", config.TokenLabel) 58 + 59 + // TODO: Implement actual PKCS#11 initialization 60 + // For now, we'll simulate the connection 61 + 62 + // Validate configuration 63 + if config.PKCS11LibraryPath == "" { 64 + return fmt.Errorf("PKCS11LibraryPath is required") 65 + } 66 + 67 + if config.PIN == "" { 68 + return fmt.Errorf("PIN is required for HSM authentication") 69 + } 70 + 71 + // Simulate connection establishment 72 + select { 73 + case <-ctx.Done(): 74 + return ctx.Err() 75 + case <-time.After(100 * time.Millisecond): // Simulate connection time 76 + c.connected = true 77 + c.logger.Info("HSM connection established successfully") 78 + return nil 79 + } 80 + } 81 + 82 + // Close terminates the HSM connection 83 + func (c *PKCS11Client) Close() error { 84 + c.mutex.Lock() 85 + defer c.mutex.Unlock() 86 + 87 + if !c.connected { 88 + return nil 89 + } 90 + 91 + c.logger.Info("Closing HSM connection") 92 + 93 + // TODO: Implement actual PKCS#11 cleanup 94 + c.connected = false 95 + c.session = nil 96 + 97 + return nil 98 + } 99 + 100 + // GetInfo returns information about the HSM device 101 + func (c *PKCS11Client) GetInfo(ctx context.Context) (*HSMInfo, error) { 102 + c.mutex.RLock() 103 + defer c.mutex.RUnlock() 104 + 105 + if !c.connected { 106 + return nil, fmt.Errorf("HSM not connected") 107 + } 108 + 109 + // TODO: Implement actual device info retrieval via PKCS#11 110 + info := &HSMInfo{ 111 + Label: c.config.TokenLabel, 112 + Manufacturer: "SmartCard-HSM", 113 + Model: "Pico HSM", 114 + SerialNumber: "000000000000", 115 + FirmwareVersion: "3.5", 116 + } 117 + 118 + return info, nil 119 + } 120 + 121 + // ReadSecret reads secret data from the specified HSM path 122 + func (c *PKCS11Client) ReadSecret(ctx context.Context, path string) (SecretData, error) { 123 + c.mutex.RLock() 124 + defer c.mutex.RUnlock() 125 + 126 + if !c.connected { 127 + return nil, fmt.Errorf("HSM not connected") 128 + } 129 + 130 + c.logger.V(1).Info("Reading secret from HSM", "path", path) 131 + 132 + // TODO: Implement actual PKCS#11 secret reading 133 + // For now, return simulated data based on path 134 + data := make(SecretData) 135 + 136 + // Simulate different secret types based on path 137 + if strings.Contains(path, "database") { 138 + data["username"] = []byte("dbuser") 139 + data["password"] = []byte("dbpass123") 140 + } else if strings.Contains(path, "api") { 141 + data["api-key"] = []byte("sk-1234567890abcdef") 142 + data["api-secret"] = []byte("secret-abcdef1234567890") 143 + } else { 144 + // Default secret structure 145 + data["data"] = []byte("secret-value") 146 + } 147 + 148 + c.logger.V(1).Info("Successfully read secret from HSM", 149 + "path", path, "keys", len(data)) 150 + 151 + return data, nil 152 + } 153 + 154 + // WriteSecret writes secret data to the specified HSM path 155 + func (c *PKCS11Client) WriteSecret(ctx context.Context, path string, data SecretData) error { 156 + c.mutex.Lock() 157 + defer c.mutex.Unlock() 158 + 159 + if !c.connected { 160 + return fmt.Errorf("HSM not connected") 161 + } 162 + 163 + c.logger.V(1).Info("Writing secret to HSM", 164 + "path", path, "keys", len(data)) 165 + 166 + // TODO: Implement actual PKCS#11 secret writing 167 + // For now, just log the operation 168 + for key := range data { 169 + c.logger.V(2).Info("Writing secret key", "path", path, "key", key) 170 + } 171 + 172 + c.logger.Info("Successfully wrote secret to HSM", "path", path) 173 + return nil 174 + } 175 + 176 + // DeleteSecret removes secret data from the specified HSM path 177 + func (c *PKCS11Client) DeleteSecret(ctx context.Context, path string) error { 178 + c.mutex.Lock() 179 + defer c.mutex.Unlock() 180 + 181 + if !c.connected { 182 + return fmt.Errorf("HSM not connected") 183 + } 184 + 185 + c.logger.Info("Deleting secret from HSM", "path", path) 186 + 187 + // TODO: Implement actual PKCS#11 secret deletion 188 + c.logger.Info("Successfully deleted secret from HSM", "path", path) 189 + return nil 190 + } 191 + 192 + // ListSecrets returns a list of secret paths with the given prefix 193 + func (c *PKCS11Client) ListSecrets(ctx context.Context, prefix string) ([]string, error) { 194 + c.mutex.RLock() 195 + defer c.mutex.RUnlock() 196 + 197 + if !c.connected { 198 + return nil, fmt.Errorf("HSM not connected") 199 + } 200 + 201 + c.logger.V(1).Info("Listing secrets from HSM", "prefix", prefix) 202 + 203 + // TODO: Implement actual PKCS#11 secret listing 204 + // For now, return some simulated paths 205 + paths := []string{ 206 + prefix + "/database-credentials", 207 + prefix + "/api-keys", 208 + prefix + "/certificates", 209 + } 210 + 211 + c.logger.V(1).Info("Successfully listed secrets from HSM", 212 + "prefix", prefix, "count", len(paths)) 213 + 214 + return paths, nil 215 + } 216 + 217 + // GetChecksum returns the SHA256 checksum of the secret data at the given path 218 + func (c *PKCS11Client) GetChecksum(ctx context.Context, path string) (string, error) { 219 + data, err := c.ReadSecret(ctx, path) 220 + if err != nil { 221 + return "", fmt.Errorf("failed to read secret for checksum: %w", err) 222 + } 223 + 224 + checksum := CalculateChecksum(data) 225 + c.logger.V(2).Info("Calculated checksum for secret", 226 + "path", path, "checksum", checksum) 227 + 228 + return checksum, nil 229 + } 230 + 231 + // IsConnected returns true if the HSM is connected and responsive 232 + func (c *PKCS11Client) IsConnected() bool { 233 + c.mutex.RLock() 234 + defer c.mutex.RUnlock() 235 + 236 + return c.connected 237 + } 238 + 239 + // WithRetry wraps an operation with retry logic 240 + func (c *PKCS11Client) WithRetry(ctx context.Context, operation func() error) error { 241 + var lastErr error 242 + 243 + for attempt := 0; attempt <= c.config.RetryAttempts; attempt++ { 244 + if attempt > 0 { 245 + c.logger.V(1).Info("Retrying HSM operation", 246 + "attempt", attempt, "maxAttempts", c.config.RetryAttempts) 247 + 248 + select { 249 + case <-ctx.Done(): 250 + return ctx.Err() 251 + case <-time.After(c.config.RetryDelay): 252 + } 253 + } 254 + 255 + if err := operation(); err != nil { 256 + lastErr = err 257 + c.logger.V(1).Info("HSM operation failed", 258 + "attempt", attempt, "error", err) 259 + continue 260 + } 261 + 262 + return nil 263 + } 264 + 265 + return fmt.Errorf("operation failed after %d attempts: %w", 266 + c.config.RetryAttempts, lastErr) 267 + }
+273
scripts/build-talos.sh
··· 1 + #!/bin/bash 2 + 3 + # Build script for HSM Secrets Operator on Talos Linux 4 + # This script builds a custom operator image with PKCS#11 libraries included 5 + 6 + set -e 7 + 8 + # Configuration 9 + REGISTRY=${REGISTRY:-"localhost:5000"} 10 + IMAGE_NAME=${IMAGE_NAME:-"hsm-secrets-operator"} 11 + TAG=${TAG:-"talos-$(date +%Y%m%d-%H%M)"} 12 + DOCKERFILE=${DOCKERFILE:-"Dockerfile.talos"} 13 + 14 + # Colors for output 15 + RED='\033[0;31m' 16 + GREEN='\033[0;32m' 17 + YELLOW='\033[1;33m' 18 + BLUE='\033[0;34m' 19 + NC='\033[0m' # No Color 20 + 21 + print_status() { 22 + echo -e "${BLUE}[INFO]${NC} $1" 23 + } 24 + 25 + print_success() { 26 + echo -e "${GREEN}[SUCCESS]${NC} $1" 27 + } 28 + 29 + print_warning() { 30 + echo -e "${YELLOW}[WARNING]${NC} $1" 31 + } 32 + 33 + print_error() { 34 + echo -e "${RED}[ERROR]${NC} $1" 35 + } 36 + 37 + # Check prerequisites 38 + check_prerequisites() { 39 + print_status "Checking prerequisites..." 40 + 41 + if ! command -v docker &> /dev/null; then 42 + print_error "Docker is required but not installed" 43 + exit 1 44 + fi 45 + 46 + if ! command -v kubectl &> /dev/null; then 47 + print_warning "kubectl not found - you won't be able to deploy directly" 48 + fi 49 + 50 + if [ ! -f "$DOCKERFILE" ]; then 51 + print_error "Dockerfile not found: $DOCKERFILE" 52 + exit 1 53 + fi 54 + 55 + print_success "Prerequisites check completed" 56 + } 57 + 58 + # Build the custom operator image 59 + build_image() { 60 + print_status "Building HSM Secrets Operator for Talos Linux..." 61 + print_status "Registry: $REGISTRY" 62 + print_status "Image: $IMAGE_NAME" 63 + print_status "Tag: $TAG" 64 + print_status "Dockerfile: $DOCKERFILE" 65 + 66 + # Build multi-arch image if buildx is available 67 + if docker buildx version &> /dev/null; then 68 + print_status "Building multi-architecture image with buildx..." 69 + docker buildx build \ 70 + --platform linux/amd64,linux/arm64 \ 71 + -f "$DOCKERFILE" \ 72 + -t "$REGISTRY/$IMAGE_NAME:$TAG" \ 73 + -t "$REGISTRY/$IMAGE_NAME:talos-latest" \ 74 + --load \ 75 + . 76 + else 77 + print_status "Building single-architecture image..." 78 + docker build \ 79 + -f "$DOCKERFILE" \ 80 + -t "$REGISTRY/$IMAGE_NAME:$TAG" \ 81 + -t "$REGISTRY/$IMAGE_NAME:talos-latest" \ 82 + . 83 + fi 84 + 85 + print_success "Image build completed successfully!" 86 + } 87 + 88 + # Test the built image 89 + test_image() { 90 + print_status "Testing the built image..." 91 + 92 + # Test library availability 93 + print_status "Checking PKCS#11 libraries in image..." 94 + docker run --rm "$REGISTRY/$IMAGE_NAME:$TAG" /bin/sh -c ' 95 + echo "=== Library Path ===" 96 + echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" 97 + echo "PKCS11_MODULE_PATH: $PKCS11_MODULE_PATH" 98 + echo "" 99 + 100 + echo "=== Available Libraries ===" 101 + if [ -d "/usr/local/lib/pkcs11" ]; then 102 + ls -la /usr/local/lib/pkcs11/ 103 + else 104 + echo "PKCS#11 directory not found" 105 + fi 106 + echo "" 107 + 108 + echo "=== Library Dependencies ===" 109 + for lib in /usr/local/lib/pkcs11/*.so 2>/dev/null; do 110 + if [ -f "$lib" ]; then 111 + echo "Testing: $lib" 112 + if command -v ldd >/dev/null; then 113 + ldd "$lib" 2>/dev/null || echo " Dependencies check not available" 114 + else 115 + echo " ldd not available in distroless image" 116 + fi 117 + fi 118 + done 119 + ' || print_warning "Library test had issues (this may be expected with distroless images)" 120 + 121 + # Test basic operator startup (dry run) 122 + print_status "Testing operator startup..." 123 + timeout 10 docker run --rm "$REGISTRY/$IMAGE_NAME:$TAG" --help || true 124 + 125 + print_success "Image testing completed" 126 + } 127 + 128 + # Push image to registry 129 + push_image() { 130 + if [ "$1" = "--push" ] || [ "$1" = "-p" ]; then 131 + print_status "Pushing images to registry..." 132 + docker push "$REGISTRY/$IMAGE_NAME:$TAG" 133 + docker push "$REGISTRY/$IMAGE_NAME:talos-latest" 134 + print_success "Images pushed successfully!" 135 + else 136 + print_status "Skipping push (use --push flag to push to registry)" 137 + print_status "To push manually:" 138 + print_status " docker push $REGISTRY/$IMAGE_NAME:$TAG" 139 + print_status " docker push $REGISTRY/$IMAGE_NAME:talos-latest" 140 + fi 141 + } 142 + 143 + # Generate deployment manifests 144 + generate_manifests() { 145 + print_status "Generating Talos deployment manifests..." 146 + 147 + MANIFEST_DIR="deploy/talos" 148 + mkdir -p "$MANIFEST_DIR" 149 + 150 + # Update deployment manifest with new image 151 + cat > "$MANIFEST_DIR/kustomization.yaml" << EOF 152 + apiVersion: kustomize.config.k8s.io/v1beta1 153 + kind: Kustomization 154 + 155 + resources: 156 + - ../default 157 + 158 + images: 159 + - name: controller 160 + newName: $REGISTRY/$IMAGE_NAME 161 + newTag: $TAG 162 + 163 + patches: 164 + - patch: |- 165 + - op: add 166 + path: /spec/template/spec/nodeSelector 167 + value: 168 + kubernetes.io/arch: amd64 169 + hsm.j5t.io/enabled: "true" 170 + - op: add 171 + path: /spec/template/metadata/labels/talos 172 + value: "enabled" 173 + target: 174 + kind: Deployment 175 + name: hsm-secrets-operator-controller-manager 176 + EOF 177 + 178 + print_success "Deployment manifests generated in $MANIFEST_DIR/" 179 + } 180 + 181 + # Display usage information 182 + show_usage() { 183 + echo "Usage: $0 [OPTIONS]" 184 + echo "" 185 + echo "Build HSM Secrets Operator image optimized for Talos Linux" 186 + echo "" 187 + echo "Options:" 188 + echo " --push, -p Push images to registry after building" 189 + echo " --test, -t Run additional image tests" 190 + echo " --manifests, -m Generate Talos deployment manifests" 191 + echo " --help, -h Show this help message" 192 + echo "" 193 + echo "Environment variables:" 194 + echo " REGISTRY Container registry (default: localhost:5000)" 195 + echo " IMAGE_NAME Image name (default: hsm-secrets-operator)" 196 + echo " TAG Image tag (default: talos-YYYYMMDD-HHMM)" 197 + echo " DOCKERFILE Dockerfile to use (default: Dockerfile.talos)" 198 + echo "" 199 + echo "Examples:" 200 + echo " $0 # Build image locally" 201 + echo " $0 --push # Build and push to registry" 202 + echo " $0 --test --manifests # Build, test, and generate manifests" 203 + echo " REGISTRY=myregistry.com $0 --push # Use custom registry" 204 + } 205 + 206 + # Main execution 207 + main() { 208 + print_status "HSM Secrets Operator Talos Build Script" 209 + print_status "========================================" 210 + 211 + # Parse command line arguments 212 + PUSH=false 213 + TEST=false 214 + MANIFESTS=false 215 + 216 + while [[ $# -gt 0 ]]; do 217 + case $1 in 218 + --push|-p) 219 + PUSH=true 220 + shift 221 + ;; 222 + --test|-t) 223 + TEST=true 224 + shift 225 + ;; 226 + --manifests|-m) 227 + MANIFESTS=true 228 + shift 229 + ;; 230 + --help|-h) 231 + show_usage 232 + exit 0 233 + ;; 234 + *) 235 + print_error "Unknown option: $1" 236 + show_usage 237 + exit 1 238 + ;; 239 + esac 240 + done 241 + 242 + # Execute build steps 243 + check_prerequisites 244 + build_image 245 + 246 + if [ "$TEST" = true ]; then 247 + test_image 248 + fi 249 + 250 + if [ "$PUSH" = true ]; then 251 + push_image --push 252 + else 253 + push_image 254 + fi 255 + 256 + if [ "$MANIFESTS" = true ]; then 257 + generate_manifests 258 + fi 259 + 260 + print_success "Build process completed!" 261 + print_status "Image: $REGISTRY/$IMAGE_NAME:$TAG" 262 + 263 + if command -v kubectl &> /dev/null && [ "$MANIFESTS" = true ]; then 264 + print_status "To deploy on Talos:" 265 + print_status " kubectl apply -k deploy/talos/" 266 + elif command -v kubectl &> /dev/null; then 267 + print_status "To deploy on Talos:" 268 + print_status " kubectl apply -f examples/advanced/talos-deployment.yaml" 269 + fi 270 + } 271 + 272 + # Run main function with all arguments 273 + main "$@"
+89
test/e2e/e2e_suite_test.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 e2e 18 + 19 + import ( 20 + "fmt" 21 + "os" 22 + "os/exec" 23 + "testing" 24 + 25 + . "github.com/onsi/ginkgo/v2" 26 + . "github.com/onsi/gomega" 27 + 28 + "github.com/evanjarrett/hsm-secrets-operator/test/utils" 29 + ) 30 + 31 + var ( 32 + // Optional Environment Variables: 33 + // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. 34 + // These variables are useful if CertManager is already installed, avoiding 35 + // re-installation and conflicts. 36 + skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" 37 + // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster 38 + isCertManagerAlreadyInstalled = false 39 + 40 + // projectImage is the name of the image which will be build and loaded 41 + // with the code source changes to be tested. 42 + projectImage = "example.com/hsm-secrets-operator:v0.0.1" 43 + ) 44 + 45 + // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, 46 + // temporary environment to validate project changes with the purposed to be used in CI jobs. 47 + // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs 48 + // CertManager. 49 + func TestE2E(t *testing.T) { 50 + RegisterFailHandler(Fail) 51 + _, _ = fmt.Fprintf(GinkgoWriter, "Starting hsm-secrets-operator integration test suite\n") 52 + RunSpecs(t, "e2e suite") 53 + } 54 + 55 + var _ = BeforeSuite(func() { 56 + By("building the manager(Operator) image") 57 + cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage)) 58 + _, err := utils.Run(cmd) 59 + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image") 60 + 61 + // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is 62 + // built and available before running the tests. Also, remove the following block. 63 + By("loading the manager(Operator) image on Kind") 64 + err = utils.LoadImageToKindClusterWithName(projectImage) 65 + ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") 66 + 67 + // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. 68 + // To prevent errors when tests run in environments with CertManager already installed, 69 + // we check for its presence before execution. 70 + // Setup CertManager before the suite if not skipped and if not already installed 71 + if !skipCertManagerInstall { 72 + By("checking if cert manager is installed already") 73 + isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() 74 + if !isCertManagerAlreadyInstalled { 75 + _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") 76 + Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") 77 + } else { 78 + _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") 79 + } 80 + } 81 + }) 82 + 83 + var _ = AfterSuite(func() { 84 + // Teardown CertManager after the suite if not skipped and if it was not already installed 85 + if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { 86 + _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") 87 + utils.UninstallCertManager() 88 + } 89 + })
+329
test/e2e/e2e_test.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 e2e 18 + 19 + import ( 20 + "encoding/json" 21 + "fmt" 22 + "os" 23 + "os/exec" 24 + "path/filepath" 25 + "time" 26 + 27 + . "github.com/onsi/ginkgo/v2" 28 + . "github.com/onsi/gomega" 29 + 30 + "github.com/evanjarrett/hsm-secrets-operator/test/utils" 31 + ) 32 + 33 + // namespace where the project is deployed in 34 + const namespace = "hsm-secrets-operator-system" 35 + 36 + // serviceAccountName created for the project 37 + const serviceAccountName = "hsm-secrets-operator-controller-manager" 38 + 39 + // metricsServiceName is the name of the metrics service of the project 40 + const metricsServiceName = "hsm-secrets-operator-controller-manager-metrics-service" 41 + 42 + // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data 43 + const metricsRoleBindingName = "hsm-secrets-operator-metrics-binding" 44 + 45 + var _ = Describe("Manager", Ordered, func() { 46 + var controllerPodName string 47 + 48 + // Before running the tests, set up the environment by creating the namespace, 49 + // enforce the restricted security policy to the namespace, installing CRDs, 50 + // and deploying the controller. 51 + BeforeAll(func() { 52 + By("creating manager namespace") 53 + cmd := exec.Command("kubectl", "create", "ns", namespace) 54 + _, err := utils.Run(cmd) 55 + Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") 56 + 57 + By("labeling the namespace to enforce the restricted security policy") 58 + cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, 59 + "pod-security.kubernetes.io/enforce=restricted") 60 + _, err = utils.Run(cmd) 61 + Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") 62 + 63 + By("installing CRDs") 64 + cmd = exec.Command("make", "install") 65 + _, err = utils.Run(cmd) 66 + Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") 67 + 68 + By("deploying the controller-manager") 69 + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) 70 + _, err = utils.Run(cmd) 71 + Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") 72 + }) 73 + 74 + // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, 75 + // and deleting the namespace. 76 + AfterAll(func() { 77 + By("cleaning up the curl pod for metrics") 78 + cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) 79 + _, _ = utils.Run(cmd) 80 + 81 + By("undeploying the controller-manager") 82 + cmd = exec.Command("make", "undeploy") 83 + _, _ = utils.Run(cmd) 84 + 85 + By("uninstalling CRDs") 86 + cmd = exec.Command("make", "uninstall") 87 + _, _ = utils.Run(cmd) 88 + 89 + By("removing manager namespace") 90 + cmd = exec.Command("kubectl", "delete", "ns", namespace) 91 + _, _ = utils.Run(cmd) 92 + }) 93 + 94 + // After each test, check for failures and collect logs, events, 95 + // and pod descriptions for debugging. 96 + AfterEach(func() { 97 + specReport := CurrentSpecReport() 98 + if specReport.Failed() { 99 + By("Fetching controller manager pod logs") 100 + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) 101 + controllerLogs, err := utils.Run(cmd) 102 + if err == nil { 103 + _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) 104 + } else { 105 + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) 106 + } 107 + 108 + By("Fetching Kubernetes events") 109 + cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") 110 + eventsOutput, err := utils.Run(cmd) 111 + if err == nil { 112 + _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) 113 + } else { 114 + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) 115 + } 116 + 117 + By("Fetching curl-metrics logs") 118 + cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) 119 + metricsOutput, err := utils.Run(cmd) 120 + if err == nil { 121 + _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) 122 + } else { 123 + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) 124 + } 125 + 126 + By("Fetching controller manager pod description") 127 + cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) 128 + podDescription, err := utils.Run(cmd) 129 + if err == nil { 130 + fmt.Println("Pod description:\n", podDescription) 131 + } else { 132 + fmt.Println("Failed to describe controller pod") 133 + } 134 + } 135 + }) 136 + 137 + SetDefaultEventuallyTimeout(2 * time.Minute) 138 + SetDefaultEventuallyPollingInterval(time.Second) 139 + 140 + Context("Manager", func() { 141 + It("should run successfully", func() { 142 + By("validating that the controller-manager pod is running as expected") 143 + verifyControllerUp := func(g Gomega) { 144 + // Get the name of the controller-manager pod 145 + cmd := exec.Command("kubectl", "get", 146 + "pods", "-l", "control-plane=controller-manager", 147 + "-o", "go-template={{ range .items }}"+ 148 + "{{ if not .metadata.deletionTimestamp }}"+ 149 + "{{ .metadata.name }}"+ 150 + "{{ \"\\n\" }}{{ end }}{{ end }}", 151 + "-n", namespace, 152 + ) 153 + 154 + podOutput, err := utils.Run(cmd) 155 + g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") 156 + podNames := utils.GetNonEmptyLines(podOutput) 157 + g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") 158 + controllerPodName = podNames[0] 159 + g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) 160 + 161 + // Validate the pod's status 162 + cmd = exec.Command("kubectl", "get", 163 + "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 164 + "-n", namespace, 165 + ) 166 + output, err := utils.Run(cmd) 167 + g.Expect(err).NotTo(HaveOccurred()) 168 + g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") 169 + } 170 + Eventually(verifyControllerUp).Should(Succeed()) 171 + }) 172 + 173 + It("should ensure the metrics endpoint is serving metrics", func() { 174 + By("creating a ClusterRoleBinding for the service account to allow access to metrics") 175 + cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, 176 + "--clusterrole=hsm-secrets-operator-metrics-reader", 177 + fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), 178 + ) 179 + _, err := utils.Run(cmd) 180 + Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") 181 + 182 + By("validating that the metrics service is available") 183 + cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) 184 + _, err = utils.Run(cmd) 185 + Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") 186 + 187 + By("getting the service account token") 188 + token, err := serviceAccountToken() 189 + Expect(err).NotTo(HaveOccurred()) 190 + Expect(token).NotTo(BeEmpty()) 191 + 192 + By("waiting for the metrics endpoint to be ready") 193 + verifyMetricsEndpointReady := func(g Gomega) { 194 + cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) 195 + output, err := utils.Run(cmd) 196 + g.Expect(err).NotTo(HaveOccurred()) 197 + g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") 198 + } 199 + Eventually(verifyMetricsEndpointReady).Should(Succeed()) 200 + 201 + By("verifying that the controller manager is serving the metrics server") 202 + verifyMetricsServerStarted := func(g Gomega) { 203 + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) 204 + output, err := utils.Run(cmd) 205 + g.Expect(err).NotTo(HaveOccurred()) 206 + g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), 207 + "Metrics server not yet started") 208 + } 209 + Eventually(verifyMetricsServerStarted).Should(Succeed()) 210 + 211 + By("creating the curl-metrics pod to access the metrics endpoint") 212 + cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", 213 + "--namespace", namespace, 214 + "--image=curlimages/curl:latest", 215 + "--overrides", 216 + fmt.Sprintf(`{ 217 + "spec": { 218 + "containers": [{ 219 + "name": "curl", 220 + "image": "curlimages/curl:latest", 221 + "command": ["/bin/sh", "-c"], 222 + "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], 223 + "securityContext": { 224 + "allowPrivilegeEscalation": false, 225 + "capabilities": { 226 + "drop": ["ALL"] 227 + }, 228 + "runAsNonRoot": true, 229 + "runAsUser": 1000, 230 + "seccompProfile": { 231 + "type": "RuntimeDefault" 232 + } 233 + } 234 + }], 235 + "serviceAccount": "%s" 236 + } 237 + }`, token, metricsServiceName, namespace, serviceAccountName)) 238 + _, err = utils.Run(cmd) 239 + Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") 240 + 241 + By("waiting for the curl-metrics pod to complete.") 242 + verifyCurlUp := func(g Gomega) { 243 + cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", 244 + "-o", "jsonpath={.status.phase}", 245 + "-n", namespace) 246 + output, err := utils.Run(cmd) 247 + g.Expect(err).NotTo(HaveOccurred()) 248 + g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") 249 + } 250 + Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) 251 + 252 + By("getting the metrics by checking curl-metrics logs") 253 + metricsOutput := getMetricsOutput() 254 + Expect(metricsOutput).To(ContainSubstring( 255 + "controller_runtime_reconcile_total", 256 + )) 257 + }) 258 + 259 + // +kubebuilder:scaffold:e2e-webhooks-checks 260 + 261 + // TODO: Customize the e2e test suite with scenarios specific to your project. 262 + // Consider applying sample/CR(s) and check their status and/or verifying 263 + // the reconciliation by using the metrics, i.e.: 264 + // metricsOutput := getMetricsOutput() 265 + // Expect(metricsOutput).To(ContainSubstring( 266 + // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, 267 + // strings.ToLower(<Kind>), 268 + // )) 269 + }) 270 + }) 271 + 272 + // serviceAccountToken returns a token for the specified service account in the given namespace. 273 + // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request 274 + // and parsing the resulting token from the API response. 275 + func serviceAccountToken() (string, error) { 276 + const tokenRequestRawString = `{ 277 + "apiVersion": "authentication.k8s.io/v1", 278 + "kind": "TokenRequest" 279 + }` 280 + 281 + // Temporary file to store the token request 282 + secretName := fmt.Sprintf("%s-token-request", serviceAccountName) 283 + tokenRequestFile := filepath.Join("/tmp", secretName) 284 + err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) 285 + if err != nil { 286 + return "", err 287 + } 288 + 289 + var out string 290 + verifyTokenCreation := func(g Gomega) { 291 + // Execute kubectl command to create the token 292 + cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( 293 + "/api/v1/namespaces/%s/serviceaccounts/%s/token", 294 + namespace, 295 + serviceAccountName, 296 + ), "-f", tokenRequestFile) 297 + 298 + output, err := cmd.CombinedOutput() 299 + g.Expect(err).NotTo(HaveOccurred()) 300 + 301 + // Parse the JSON output to extract the token 302 + var token tokenRequest 303 + err = json.Unmarshal(output, &token) 304 + g.Expect(err).NotTo(HaveOccurred()) 305 + 306 + out = token.Status.Token 307 + } 308 + Eventually(verifyTokenCreation).Should(Succeed()) 309 + 310 + return out, err 311 + } 312 + 313 + // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. 314 + func getMetricsOutput() string { 315 + By("getting the curl-metrics logs") 316 + cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) 317 + metricsOutput, err := utils.Run(cmd) 318 + Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") 319 + Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) 320 + return metricsOutput 321 + } 322 + 323 + // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, 324 + // containing only the token field that we need to extract. 325 + type tokenRequest struct { 326 + Status struct { 327 + Token string `json:"token"` 328 + } `json:"status"` 329 + }
+254
test/utils/utils.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 utils 18 + 19 + import ( 20 + "bufio" 21 + "bytes" 22 + "fmt" 23 + "os" 24 + "os/exec" 25 + "strings" 26 + 27 + . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck 28 + ) 29 + 30 + const ( 31 + prometheusOperatorVersion = "v0.77.1" 32 + prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + 33 + "releases/download/%s/bundle.yaml" 34 + 35 + certmanagerVersion = "v1.16.3" 36 + certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" 37 + ) 38 + 39 + func warnError(err error) { 40 + _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) 41 + } 42 + 43 + // Run executes the provided command within this context 44 + func Run(cmd *exec.Cmd) (string, error) { 45 + dir, _ := GetProjectDir() 46 + cmd.Dir = dir 47 + 48 + if err := os.Chdir(cmd.Dir); err != nil { 49 + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) 50 + } 51 + 52 + cmd.Env = append(os.Environ(), "GO111MODULE=on") 53 + command := strings.Join(cmd.Args, " ") 54 + _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) 55 + output, err := cmd.CombinedOutput() 56 + if err != nil { 57 + return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) 58 + } 59 + 60 + return string(output), nil 61 + } 62 + 63 + // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. 64 + func InstallPrometheusOperator() error { 65 + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 66 + cmd := exec.Command("kubectl", "create", "-f", url) 67 + _, err := Run(cmd) 68 + return err 69 + } 70 + 71 + // UninstallPrometheusOperator uninstalls the prometheus 72 + func UninstallPrometheusOperator() { 73 + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 74 + cmd := exec.Command("kubectl", "delete", "-f", url) 75 + if _, err := Run(cmd); err != nil { 76 + warnError(err) 77 + } 78 + } 79 + 80 + // IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed 81 + // by verifying the existence of key CRDs related to Prometheus. 82 + func IsPrometheusCRDsInstalled() bool { 83 + // List of common Prometheus CRDs 84 + prometheusCRDs := []string{ 85 + "prometheuses.monitoring.coreos.com", 86 + "prometheusrules.monitoring.coreos.com", 87 + "prometheusagents.monitoring.coreos.com", 88 + } 89 + 90 + cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") 91 + output, err := Run(cmd) 92 + if err != nil { 93 + return false 94 + } 95 + crdList := GetNonEmptyLines(output) 96 + for _, crd := range prometheusCRDs { 97 + for _, line := range crdList { 98 + if strings.Contains(line, crd) { 99 + return true 100 + } 101 + } 102 + } 103 + 104 + return false 105 + } 106 + 107 + // UninstallCertManager uninstalls the cert manager 108 + func UninstallCertManager() { 109 + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 110 + cmd := exec.Command("kubectl", "delete", "-f", url) 111 + if _, err := Run(cmd); err != nil { 112 + warnError(err) 113 + } 114 + } 115 + 116 + // InstallCertManager installs the cert manager bundle. 117 + func InstallCertManager() error { 118 + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 119 + cmd := exec.Command("kubectl", "apply", "-f", url) 120 + if _, err := Run(cmd); err != nil { 121 + return err 122 + } 123 + // Wait for cert-manager-webhook to be ready, which can take time if cert-manager 124 + // was re-installed after uninstalling on a cluster. 125 + cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", 126 + "--for", "condition=Available", 127 + "--namespace", "cert-manager", 128 + "--timeout", "5m", 129 + ) 130 + 131 + _, err := Run(cmd) 132 + return err 133 + } 134 + 135 + // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed 136 + // by verifying the existence of key CRDs related to Cert Manager. 137 + func IsCertManagerCRDsInstalled() bool { 138 + // List of common Cert Manager CRDs 139 + certManagerCRDs := []string{ 140 + "certificates.cert-manager.io", 141 + "issuers.cert-manager.io", 142 + "clusterissuers.cert-manager.io", 143 + "certificaterequests.cert-manager.io", 144 + "orders.acme.cert-manager.io", 145 + "challenges.acme.cert-manager.io", 146 + } 147 + 148 + // Execute the kubectl command to get all CRDs 149 + cmd := exec.Command("kubectl", "get", "crds") 150 + output, err := Run(cmd) 151 + if err != nil { 152 + return false 153 + } 154 + 155 + // Check if any of the Cert Manager CRDs are present 156 + crdList := GetNonEmptyLines(output) 157 + for _, crd := range certManagerCRDs { 158 + for _, line := range crdList { 159 + if strings.Contains(line, crd) { 160 + return true 161 + } 162 + } 163 + } 164 + 165 + return false 166 + } 167 + 168 + // LoadImageToKindClusterWithName loads a local docker image to the kind cluster 169 + func LoadImageToKindClusterWithName(name string) error { 170 + cluster := "kind" 171 + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { 172 + cluster = v 173 + } 174 + kindOptions := []string{"load", "docker-image", name, "--name", cluster} 175 + cmd := exec.Command("kind", kindOptions...) 176 + _, err := Run(cmd) 177 + return err 178 + } 179 + 180 + // GetNonEmptyLines converts given command output string into individual objects 181 + // according to line breakers, and ignores the empty elements in it. 182 + func GetNonEmptyLines(output string) []string { 183 + var res []string 184 + elements := strings.Split(output, "\n") 185 + for _, element := range elements { 186 + if element != "" { 187 + res = append(res, element) 188 + } 189 + } 190 + 191 + return res 192 + } 193 + 194 + // GetProjectDir will return the directory where the project is 195 + func GetProjectDir() (string, error) { 196 + wd, err := os.Getwd() 197 + if err != nil { 198 + return wd, fmt.Errorf("failed to get current working directory: %w", err) 199 + } 200 + wd = strings.ReplaceAll(wd, "/test/e2e", "") 201 + return wd, nil 202 + } 203 + 204 + // UncommentCode searches for target in the file and remove the comment prefix 205 + // of the target content. The target content may span multiple lines. 206 + func UncommentCode(filename, target, prefix string) error { 207 + // false positive 208 + // nolint:gosec 209 + content, err := os.ReadFile(filename) 210 + if err != nil { 211 + return fmt.Errorf("failed to read file %q: %w", filename, err) 212 + } 213 + strContent := string(content) 214 + 215 + idx := strings.Index(strContent, target) 216 + if idx < 0 { 217 + return fmt.Errorf("unable to find the code %q to be uncomment", target) 218 + } 219 + 220 + out := new(bytes.Buffer) 221 + _, err = out.Write(content[:idx]) 222 + if err != nil { 223 + return fmt.Errorf("failed to write to output: %w", err) 224 + } 225 + 226 + scanner := bufio.NewScanner(bytes.NewBufferString(target)) 227 + if !scanner.Scan() { 228 + return nil 229 + } 230 + for { 231 + if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { 232 + return fmt.Errorf("failed to write to output: %w", err) 233 + } 234 + // Avoid writing a newline in case the previous line was the last in target. 235 + if !scanner.Scan() { 236 + break 237 + } 238 + if _, err = out.WriteString("\n"); err != nil { 239 + return fmt.Errorf("failed to write to output: %w", err) 240 + } 241 + } 242 + 243 + if _, err = out.Write(content[idx+len(target):]); err != nil { 244 + return fmt.Errorf("failed to write to output: %w", err) 245 + } 246 + 247 + // false positive 248 + // nolint:gosec 249 + if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil { 250 + return fmt.Errorf("failed to write file %q: %w", filename, err) 251 + } 252 + 253 + return nil 254 + }