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.

add in new hsm plugin, clean up examples dir

+2762 -748
+44 -16
examples/README.md
··· 4 4 5 5 ## Directory Structure 6 6 7 - - **[basic/](basic/)** - Basic usage examples for getting started 7 + - **[basic/](basic/)** - Basic CRD resource examples for getting started 8 8 - **[advanced/](advanced/)** - Advanced configurations and use cases 9 - - **[api/](api/)** - REST API usage examples 9 + - **[api/](api/)** - REST API usage examples and bulk operation scripts 10 + - **[deployment/](deployment/)** - Complete deployment configurations 10 11 - **[high-availability/](high-availability/)** - High availability and mirroring setups 11 12 12 13 ## Quick Start 13 14 15 + ### Method 1: kubectl-hsm Plugin (Recommended) 16 + 14 17 1. **Install the Operator** 15 18 ```bash 16 - # Install CRDs 17 - kubectl apply -f config/crd/bases/ 18 - 19 - # Deploy the operator 19 + # Install CRDs and deploy the operator 20 + kubectl apply -f config/default/ 21 + ``` 22 + 23 + 2. **Install kubectl-hsm plugin** 24 + ```bash 25 + cd kubectl-hsm && make install 26 + ``` 27 + 28 + 3. **Create your first secret** 29 + ```bash 30 + kubectl hsm create my-secret --from-literal=password=secret123 31 + ``` 32 + 33 + 4. **List and get secrets** 34 + ```bash 35 + kubectl hsm list 36 + kubectl hsm get my-secret 37 + ``` 38 + 39 + ### Method 2: CRD Resources 40 + 41 + 1. **Install the Operator** 42 + ```bash 20 43 kubectl apply -f config/default/ 21 44 ``` 22 45 ··· 30 53 kubectl apply -f examples/basic/database-secret.yaml 31 54 ``` 32 55 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 - ``` 56 + ### Method 3: REST API (Advanced) 57 + 58 + For automation and bulk operations: 59 + ```bash 60 + # Port forward to API 61 + kubectl port-forward -n hsm-secrets-operator-system svc/hsm-secrets-operator-api 8090:8090 62 + 63 + # Check health 64 + curl http://localhost:8090/api/v1/health 65 + 66 + # Create a secret via API 67 + curl -X POST http://localhost:8090/api/v1/hsm/secrets \ 68 + -H "Content-Type: application/json" \ 69 + -d @examples/api/create-secret.json 70 + ``` 43 71 44 72 ## Prerequisites 45 73
+7 -4
examples/advanced/README.md
··· 2 2 3 3 This directory contains advanced configuration examples for complex use cases. 4 4 5 + > **Note:** For interactive secret management during development and testing, consider using the `kubectl hsm` plugin alongside these CRD configurations. See [kubectl-hsm documentation](../../kubectl-hsm/README.md) for details. 6 + 5 7 ## Examples Overview 6 8 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 9 + 1. **[custom-pkcs11-library.yaml](custom-pkcs11-library.yaml)** - Custom PKCS#11 library configuration 10 + 2. **[multi-environment.yaml](multi-environment.yaml)** - Multi-environment secret management 11 + 3. **[custom-library-guide.md](custom-library-guide.md)** - Guide for using custom PKCS#11 libraries 12 + 13 + > **Additional Examples:** See the [deployment](../deployment/) directory for monitoring configurations and [high-availability](../high-availability/) directory for advanced failover setups. 11 14 12 15 ## Advanced Use Cases 13 16
+29 -13
examples/advanced/custom-pkcs11-library.yaml
··· 41 41 spec: 42 42 deviceType: Generic # Use Generic for custom devices 43 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 44 + # Discovery configuration 45 + discovery: 46 + # Custom USB device 47 + usb: 48 + vendorId: "1234" # Your vendor ID 49 + productId: "5678" # Your product ID 50 + # serialNumber: "CUSTOM-001" # Optional specific device 49 51 50 - # Custom PKCS#11 library path 51 - pkcs11LibraryPath: "/opt/custom-hsm/lib/libcustomhsm-pkcs11.so" 52 + # PKCS#11 configuration 53 + pkcs11: 54 + libraryPath: "/opt/custom-hsm/lib/libcustomhsm-pkcs11.so" 55 + slotId: 0 56 + pinSecret: 57 + name: "custom-hsm-pin" 58 + key: "pin" 59 + tokenLabel: "CustomHSM" 52 60 53 61 nodeSelector: 54 62 hsm.vendor: "custom-vendor" # Only deploy on nodes with this vendor ··· 63 71 spec: 64 72 deviceType: Generic 65 73 66 - # Use device path instead of USB discovery 67 - devicePath: 68 - path: "/dev/custom-hsm*" 69 - permissions: "0666" 74 + # Discovery configuration 75 + discovery: 76 + # Use device path instead of USB discovery 77 + devicePath: 78 + path: "/dev/custom-hsm*" 79 + permissions: "0666" 70 80 71 - # Custom library path 72 - pkcs11LibraryPath: "/usr/local/lib/libcustomhsm.so" 81 + # PKCS#11 configuration 82 + pkcs11: 83 + libraryPath: "/usr/local/lib/libcustomhsm.so" 84 + slotId: 0 85 + pinSecret: 86 + name: "custom-hsm-pin" 87 + key: "pin" 88 + tokenLabel: "CustomHSM" 73 89 74 90 nodeSelector: 75 91 custom-hsm.enabled: "true"
+16 -3
examples/advanced/multi-environment.yaml
··· 21 21 app: myapp 22 22 environment: development 23 23 spec: 24 - secretName: "database-credentials" 24 + # HSM path is automatically set to the metadata.name (database-credentials) 25 + parentRef: 26 + name: controller-manager 27 + namespace: hsm-secrets-operator-system 25 28 autoSync: true 26 29 syncInterval: 300 # 5 minutes - frequent sync for dev 27 30 ··· 45 48 app: myapp 46 49 environment: staging 47 50 spec: 48 - secretName: "database-credentials" 51 + # HSM path is automatically set to the metadata.name (database-credentials) 52 + parentRef: 53 + name: controller-manager 54 + namespace: hsm-secrets-operator-system 49 55 autoSync: true 50 56 syncInterval: 600 # 10 minutes 51 57 ··· 73 79 annotations: 74 80 hsm.j5t.io/description: "Production database credentials - HIGH SECURITY" 75 81 spec: 76 - secretName: "database-credentials" 82 + # HSM path is automatically set to the metadata.name (database-credentials) 83 + parentRef: 84 + name: controller-manager 85 + namespace: hsm-secrets-operator-system 77 86 autoSync: true 78 87 syncInterval: 1800 # 30 minutes - less frequent for stability 79 88 ··· 98 107 type: tls 99 108 scope: global 100 109 spec: 110 + # HSM path is automatically set to the metadata.name (wildcard-tls-cert) 111 + parentRef: 112 + name: controller-manager 113 + namespace: hsm-secrets-operator-system 101 114 secretName: "wildcard-tls" 102 115 autoSync: true 103 116 syncInterval: 86400 # Daily sync for certificates
-82
examples/agent-deployment/README.md
··· 1 - # HSM Agent Pod Architecture 2 - 3 - This directory contains examples of how the HSM agent pod system works for distributed HSM operations. 4 - 5 - ## Architecture Overview 6 - 7 - The HSM Secrets Operator now uses a **3-binary architecture**: 8 - 9 - 1. **Manager** (`/manager`) - Main operator that orchestrates everything 10 - 2. **Discovery** (`/discovery`) - Lightweight USB device discovery (DaemonSet) 11 - 3. **Agent** (`/agent`) - HSM operation execution pods (deployed on-demand) 12 - 13 - ## How It Works 14 - 15 - ```mermaid 16 - graph TB 17 - subgraph "Manager Pod (Any Node)" 18 - M[Manager Controller] 19 - end 20 - 21 - subgraph "Node with HSM Hardware" 22 - D[Discovery Pod<br/>DaemonSet] 23 - A[Agent Pod<br/>On-demand] 24 - H[HSM Hardware] 25 - end 26 - 27 - subgraph "Other Nodes" 28 - D2[Discovery Pod<br/>DaemonSet] 29 - end 30 - 31 - M -->|1. Find HSMDevice| D 32 - M -->|2. Deploy Agent| A 33 - M -->|3. HTTP API calls| A 34 - A -->|4. PKCS#11| H 35 - D -->|USB discovery| H 36 - ``` 37 - 38 - ## Process Flow 39 - 40 - 1. **HSMSecret Created**: User creates an HSMSecret resource 41 - 2. **Device Discovery**: Manager finds available HSMDevice (discovered by DaemonSet) 42 - 3. **Agent Deployment**: Manager deploys HSM agent pod on node with hardware 43 - 4. **Node Affinity**: Agent pod pinned to specific node via `kubernetes.io/hostname` 44 - 5. **HTTP Communication**: Manager makes HTTP calls to agent for HSM operations 45 - 6. **Hardware Access**: Agent executes PKCS#11 operations locally on HSM 46 - 47 - ## Key Benefits 48 - 49 - ✅ **Remote Execution**: Manager can be anywhere, agents run where hardware exists 50 - ✅ **Resource Efficiency**: Agents only deployed when HSMSecrets exist 51 - ✅ **Auto-cleanup**: Agents removed when no longer needed 52 - ✅ **Node Targeting**: Perfect placement via HSMDevice discovery 53 - ✅ **Clean Architecture**: Each component has single responsibility 54 - 55 - ## Example Deployment 56 - 57 - See `agent-example.yaml` for a complete example of: 58 - - HSMDevice discovery configuration 59 - - HSMSecret that triggers agent deployment 60 - - Secret with PIN configuration 61 - 62 - ## Agent Pod Configuration 63 - 64 - Agent pods are automatically configured with: 65 - - **Environment Variables**: PKCS#11 library path, slot ID, token label 66 - - **Secrets**: PIN from Kubernetes Secret reference 67 - - **Node Affinity**: Pinned to node with actual hardware 68 - - **Security**: Non-privileged execution with proper security contexts 69 - - **Health Checks**: Liveness and readiness probes 70 - - **Resources**: CPU/memory limits and requests 71 - 72 - ## API Endpoints 73 - 74 - Each agent exposes: 75 - - `GET /api/v1/hsm/info` - HSM device information 76 - - `GET /api/v1/hsm/secrets/{path}` - Read secret 77 - - `POST /api/v1/hsm/secrets/{path}` - Write secret 78 - - `DELETE /api/v1/hsm/secrets/{path}` - Delete secret 79 - - `GET /api/v1/hsm/secrets` - List secrets 80 - - `GET /api/v1/hsm/checksum/{path}` - Get checksum 81 - - `GET /healthz` - Health check 82 - - `GET /readyz` - Readiness check
-140
examples/agent-deployment/agent-example.yaml
··· 1 - # Complete example showing HSM agent pod deployment 2 - # This demonstrates the full flow from HSMDevice discovery to agent-based secret operations 3 - 4 - --- 5 - # Step 1: Create PIN secret for HSM authentication 6 - apiVersion: v1 7 - kind: Secret 8 - metadata: 9 - name: pico-hsm-pin 10 - namespace: default 11 - type: Opaque 12 - data: 13 - pin: MTIzNDU2 # base64 encoded "123456" 14 - 15 - --- 16 - # Step 2: Configure HSMDevice for discovery and agent deployment 17 - apiVersion: hsm.j5t.io/v1alpha1 18 - kind: HSMDevice 19 - metadata: 20 - name: pico-hsm-main 21 - namespace: default 22 - labels: 23 - hsm-type: pico 24 - environment: production 25 - spec: 26 - # Device type - uses well-known specifications 27 - deviceType: PicoHSM 28 - 29 - # Discovery configuration 30 - discovery: 31 - usb: 32 - vendorId: "20a0" 33 - productId: "4230" 34 - # Optional: target specific device by serial number 35 - # serialNumber: "PICO123456" 36 - 37 - # PKCS#11 configuration for agent pods 38 - pkcs11: 39 - libraryPath: "/usr/local/lib/libsc-hsm-pkcs11.so" 40 - slotId: 0 41 - pinSecret: 42 - name: "pico-hsm-pin" 43 - key: "pin" 44 - namespace: "default" 45 - tokenLabel: "PicoHSM-Production" 46 - 47 - # Optional: restrict to specific nodes 48 - nodeSelector: 49 - hsm-enabled: "true" 50 - node-type: "worker" 51 - 52 - # Maximum devices to discover (usually 1 for production) 53 - maxDevices: 1 54 - 55 - --- 56 - # Step 3: Create HSMSecret that will trigger agent deployment 57 - apiVersion: hsm.j5t.io/v1alpha1 58 - kind: HSMSecret 59 - metadata: 60 - name: database-credentials 61 - namespace: default 62 - labels: 63 - app: myapp 64 - tier: database 65 - spec: 66 - # Path on HSM where secret is stored 67 - 68 - # Name of Kubernetes Secret to create/sync 69 - secretName: "database-credentials" 70 - 71 - # Enable automatic synchronization 72 - autoSync: true 73 - 74 - # Sync every 5 minutes (300 seconds) 75 - syncInterval: 300 76 - 77 - # Secret type (default: Opaque) 78 - secretType: "Opaque" 79 - 80 - --- 81 - # Step 4: Example application using the synced secret 82 - apiVersion: apps/v1 83 - kind: Deployment 84 - metadata: 85 - name: database-app 86 - namespace: default 87 - spec: 88 - replicas: 1 89 - selector: 90 - matchLabels: 91 - app: database-app 92 - template: 93 - metadata: 94 - labels: 95 - app: database-app 96 - spec: 97 - containers: 98 - - name: app 99 - image: nginx:1.21 100 - env: 101 - - name: DB_USERNAME 102 - valueFrom: 103 - secretKeyRef: 104 - name: database-credentials # Synced from HSM 105 - key: username 106 - - name: DB_PASSWORD 107 - valueFrom: 108 - secretKeyRef: 109 - name: database-credentials # Synced from HSM 110 - key: password 111 - - name: DB_HOST 112 - valueFrom: 113 - secretKeyRef: 114 - name: database-credentials # Synced from HSM 115 - key: host 116 - ports: 117 - - containerPort: 80 118 - resources: 119 - requests: 120 - cpu: 100m 121 - memory: 128Mi 122 - limits: 123 - cpu: 200m 124 - memory: 256Mi 125 - 126 - --- 127 - # Optional: Service to expose the application 128 - apiVersion: v1 129 - kind: Service 130 - metadata: 131 - name: database-app-service 132 - namespace: default 133 - spec: 134 - selector: 135 - app: database-app 136 - ports: 137 - - name: http 138 - port: 80 139 - targetPort: 80 140 - type: ClusterIP
+53 -23
examples/api/README.md
··· 23 23 - Kubernetes ServiceAccount tokens (when deployed in cluster) 24 24 - Future: OAuth2, API keys, mTLS 25 25 26 - ## Examples 26 + ## kubectl-hsm Plugin (Recommended) 27 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 28 + The easiest way to interact with HSM secrets is through the `kubectl-hsm` plugin. This provides a native kubectl experience while automatically handling port forwarding and API communication. 35 29 36 - ## Quick Start 30 + ### Quick Start with kubectl-hsm 37 31 38 - 1. **Start the API server** (if running locally): 32 + 1. **Install the plugin**: 39 33 ```bash 40 - ./bin/manager --enable-api=true --api-port=8090 34 + cd kubectl-hsm && make install 35 + ``` 36 + 37 + 2. **Check health**: 38 + ```bash 39 + kubectl hsm health 40 + ``` 41 + 42 + 3. **Create a secret**: 43 + ```bash 44 + kubectl hsm create my-secret --from-literal=password=secret123 45 + ``` 46 + 47 + 4. **List secrets**: 48 + ```bash 49 + kubectl hsm list 50 + ``` 51 + 52 + 5. **Get a secret**: 53 + ```bash 54 + kubectl hsm get my-secret 55 + ``` 56 + 57 + See the [kubectl-hsm documentation](../../kubectl-hsm/README.md) for full usage. 58 + 59 + ## REST API Examples 60 + 61 + For advanced use cases or automation that requires direct API access, the following scripts demonstrate REST API usage: 62 + 63 + 1. **[import-from-k8s.sh](import-from-k8s.sh)** - Import existing Kubernetes secrets 64 + 2. **[bulk-operations.sh](bulk-operations.sh)** - Bulk secret operations (auto-detects kubectl-hsm) 65 + 3. **[advanced-bulk-import.sh](advanced-bulk-import.sh)** - Advanced import with validation and rollback 66 + 4. **[create-secret.json](create-secret.json)** - Sample secret data structure 67 + 5. **[production-import.json](production-import.json)** - Production-ready import configuration 68 + 69 + ## Quick Start with REST API 70 + 71 + 1. **Port forward to API server**: 72 + ```bash 73 + kubectl port-forward -n hsm-secrets-operator-system svc/hsm-secrets-operator-api 8090:8090 41 74 ``` 42 75 43 76 2. **Check health**: ··· 91 124 ## Common Use Cases 92 125 93 126 ### 1. Development Workflow 94 - - Create secrets during development 95 - - Import from existing sources 96 - - Test secret rotation 127 + - **kubectl-hsm**: Interactive secret management during development 128 + - **REST API**: Automated testing and CI/CD integration 97 129 98 130 ### 2. CI/CD Integration 99 - - Automated secret provisioning 100 - - Environment-specific deployments 101 - - Secret validation and testing 131 + - **kubectl-hsm**: Simple command-line operations in pipelines 132 + - **REST API**: Complex automation and bulk operations 102 133 103 134 ### 3. Secret Migration 104 - - Import from Kubernetes Secrets 105 - - Migrate from other secret stores 106 - - Bulk operations for large environments 135 + - **bulk-operations.sh**: Mass import/export operations 136 + - **advanced-bulk-import.sh**: Production migrations with validation 137 + - **import-from-k8s.sh**: Migrate existing Kubernetes secrets 107 138 108 139 ### 4. Monitoring and Operations 109 - - Health monitoring 110 - - Secret inventory management 111 - - Troubleshooting sync issues 140 + - **kubectl-hsm health**: Quick health checks 141 + - **REST API**: Detailed monitoring and troubleshooting 112 142 113 143 ## Error Handling 114 144
+18 -1
examples/api/advanced-bulk-import.sh
··· 1 1 #!/bin/bash 2 2 3 3 # Advanced bulk import script with validation and rollback 4 + # Automatically uses kubectl-hsm plugin if available, falls back to REST API 4 5 # Usage: ./advanced-bulk-import.sh [config-file] [options] 5 6 6 7 set -e ··· 187 188 local conflicts=() 188 189 189 190 while IFS= read -r label; do 191 + # Try kubectl-hsm first if available 192 + if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then 193 + if kubectl hsm get "$label" >/dev/null 2>&1; then 194 + conflicts+=("$label") 195 + continue 196 + fi 197 + fi 198 + 199 + # Fallback to API 190 200 response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label") 191 201 success=$(echo "$response" | jq -r '.success') 192 202 ··· 259 269 260 270 for label in "${imported_secrets[@]}"; do 261 271 log "Rolling back: $label" 262 - curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null 272 + 273 + # Try kubectl-hsm first if available 274 + if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then 275 + kubectl hsm delete "$label" --force >/dev/null 2>&1 || \ 276 + curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null 277 + else 278 + curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null 279 + fi 263 280 done 264 281 265 282 warning "Rollback completed"
+41 -3
examples/api/bulk-operations.sh
··· 1 1 #!/bin/bash 2 2 3 - # Bulk operations for HSM secrets via REST API 3 + # Bulk operations for HSM secrets 4 + # Automatically uses kubectl-hsm plugin if available, falls back to REST API 4 5 # Usage: ./bulk-operations.sh [operation] [config-file] 5 6 6 7 set -e ··· 74 75 75 76 echo " Creating secret: $label" 76 77 78 + # Try using kubectl-hsm first if available, fallback to API 79 + if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then 80 + # Convert secret_data to create command format 81 + local temp_file=$(mktemp) 82 + echo "$secret_data" | jq '.data' > "$temp_file" 83 + 84 + if kubectl hsm create "$label" --from-file="$temp_file" >/dev/null 2>&1; then 85 + rm "$temp_file" 86 + echo " ✅ Created successfully (kubectl-hsm)" 87 + return 0 88 + else 89 + rm "$temp_file" 90 + echo " ⚠️ kubectl-hsm failed, trying API..." 91 + fi 92 + fi 93 + 94 + # Fallback to API 77 95 response=$(curl -s -X POST \ 78 96 -H "Content-Type: application/json" \ 79 97 -d "$secret_data" \ ··· 81 99 82 100 success=$(echo "$response" | jq -r '.success') 83 101 if [ "$success" = "true" ]; then 84 - echo " ✅ Created successfully" 102 + echo " ✅ Created successfully (API)" 85 103 return 0 86 104 else 87 105 error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"') ··· 96 114 97 115 echo " Deleting secret: $label" 98 116 117 + # Try using kubectl-hsm first if available, fallback to API 118 + if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then 119 + if kubectl hsm delete "$label" --force >/dev/null 2>&1; then 120 + echo " ✅ Deleted successfully (kubectl-hsm)" 121 + return 0 122 + else 123 + echo " ⚠️ kubectl-hsm failed, trying API..." 124 + fi 125 + fi 126 + 127 + # Fallback to API 99 128 response=$(curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label") 100 129 101 130 success=$(echo "$response" | jq -r '.success') 102 131 if [ "$success" = "true" ]; then 103 - echo " ✅ Deleted successfully" 132 + echo " ✅ Deleted successfully (API)" 104 133 return 0 105 134 else 106 135 error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"') ··· 113 142 get_secret() { 114 143 local label="$1" 115 144 145 + # Try using kubectl-hsm first if available, fallback to API 146 + if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then 147 + if kubectl hsm get "$label" >/dev/null 2>&1; then 148 + echo " ✅ $label (available via kubectl-hsm)" 149 + return 0 150 + fi 151 + fi 152 + 153 + # Fallback to API for detailed status 116 154 response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label") 117 155 success=$(echo "$response" | jq -r '.success') 118 156
-90
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 for agent API (path-based) 19 - # The agent expects the path in the URL and data directly in the request body 20 - payload=$(cat <<EOF 21 - { 22 - "data": { 23 - "api_key": "sk_test_$(openssl rand -hex 16)", 24 - "webhook_secret": "whsec_$(openssl rand -hex 20)", 25 - "database_url": "postgresql://user:$(openssl rand -hex 12)@localhost:5432/testdb", 26 - "redis_url": "redis://localhost:6379/0", 27 - "created_timestamp": "$(date +%s)", 28 - "label": "$SECRET_NAME", 29 - "id": "$SECRET_ID", 30 - "description": "Secret created via API on $(date)", 31 - "created_by": "api-script", 32 - "environment": "development", 33 - "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 34 - } 35 - } 36 - EOF 37 - ) 38 - 39 - echo "📝 Request Payload:" 40 - echo "$payload" | jq '.' 41 - echo "" 42 - 43 - # Create HSM path from secret name (just use the secret name as path) 44 - HSM_PATH="$SECRET_NAME" 45 - 46 - # Make the API call - using path-based endpoint 47 - echo "📤 Sending create request to path: $HSM_PATH" 48 - response=$(curl -s -X POST \ 49 - -H "Content-Type: application/json" \ 50 - -d "$payload" \ 51 - "$API_BASE_URL/api/v1/hsm/secrets/$HSM_PATH") 52 - 53 - echo "📥 Response:" 54 - echo "$response" | jq '.' 55 - 56 - # Check if the request was successful 57 - success=$(echo "$response" | jq -r '.success') 58 - if [ "$success" = "true" ]; then 59 - echo "" 60 - echo "✅ Secret created successfully!" 61 - 62 - # Extract created secret info from agent response 63 - checksum=$(echo "$response" | jq -r '.data.checksum // "unknown"') 64 - path=$(echo "$response" | jq -r '.data.path // "unknown"') 65 - 66 - echo " Secret Name: $SECRET_NAME" 67 - echo " HSM Path: $HSM_PATH" 68 - echo " Checksum: ${checksum:0:16}..." 69 - 70 - echo "" 71 - echo "🔍 To retrieve this secret:" 72 - echo " curl $API_BASE_URL/api/v1/hsm/secrets/$HSM_PATH" 73 - 74 - echo "" 75 - echo "📋 To list all secrets:" 76 - echo " curl $API_BASE_URL/api/v1/hsm/secrets" 77 - 78 - echo "" 79 - echo "🗑️ To delete this secret:" 80 - echo " curl -X DELETE $API_BASE_URL/api/v1/hsm/secrets/$HSM_PATH" 81 - 82 - else 83 - echo "" 84 - echo "❌ Failed to create secret!" 85 - error_code=$(echo "$response" | jq -r '.error.code // "unknown"') 86 - error_message=$(echo "$response" | jq -r '.error.message // "No error message"') 87 - echo " Error Code: $error_code" 88 - echo " Error Message: $error_message" 89 - exit 1 90 - fi
-125
examples/api/delete-secret.sh
··· 1 - #!/bin/bash 2 - 3 - # Delete HSM Secret via REST API 4 - # Usage: ./delete-secret.sh [secret-name] [options] 5 - 6 - set -e 7 - 8 - API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"} 9 - SECRET_NAME="$1" 10 - FORCE=${FORCE:-false} 11 - 12 - # Colors for output 13 - RED='\033[0;31m' 14 - GREEN='\033[0;32m' 15 - YELLOW='\033[1;33m' 16 - BLUE='\033[0;34m' 17 - NC='\033[0m' # No Color 18 - 19 - # Check if secret name is provided 20 - if [ -z "$SECRET_NAME" ]; then 21 - echo "Usage: $0 <secret-name> [--force]" 22 - echo "" 23 - echo "Options:" 24 - echo " --force Skip confirmation prompt" 25 - echo "" 26 - echo "Environment variables:" 27 - echo " API_BASE_URL API endpoint (default: http://localhost:8090)" 28 - echo " FORCE Skip confirmation (default: false)" 29 - echo "" 30 - echo "Examples:" 31 - echo " $0 my-secret" 32 - echo " $0 my-secret --force" 33 - echo " FORCE=true $0 my-secret" 34 - exit 1 35 - fi 36 - 37 - # Parse command line options 38 - while [[ $# -gt 1 ]]; do 39 - case $2 in 40 - --force) 41 - FORCE=true 42 - shift 43 - ;; 44 - *) 45 - echo "Unknown option: $2" 46 - exit 1 47 - ;; 48 - esac 49 - done 50 - 51 - echo -e "${RED}🗑️ Deleting HSM Secret via API...${NC}" 52 - echo "Secret Name: $SECRET_NAME" 53 - echo "API Base URL: $API_BASE_URL" 54 - echo "" 55 - 56 - # First, check if the secret exists 57 - echo -e "${BLUE}🔍 Checking if secret exists...${NC}" 58 - check_response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$SECRET_NAME") 59 - check_success=$(echo "$check_response" | jq -r '.success') 60 - 61 - if [ "$check_success" != "true" ]; then 62 - echo -e "${YELLOW}⚠️ Secret '$SECRET_NAME' not found${NC}" 63 - error_message=$(echo "$check_response" | jq -r '.error.message // "No error message"') 64 - echo " Error: $error_message" 65 - exit 1 66 - fi 67 - 68 - echo -e "${GREEN}✅ Secret found${NC}" 69 - 70 - # Show secret details 71 - echo "" 72 - echo -e "${BLUE}📋 Secret Details:${NC}" 73 - echo "$check_response" | jq -r '.data | to_entries[] | " \(.key): \(.value)"' 74 - 75 - # Confirmation prompt (unless --force is used) 76 - if [ "$FORCE" != "true" ]; then 77 - echo "" 78 - echo -e "${YELLOW}⚠️ This action cannot be undone!${NC}" 79 - read -p "Are you sure you want to delete secret '$SECRET_NAME'? (y/N): " -n 1 -r 80 - echo "" 81 - if [[ ! $REPLY =~ ^[Yy]$ ]]; then 82 - echo "❌ Delete cancelled by user" 83 - exit 0 84 - fi 85 - fi 86 - 87 - # Make the delete API call 88 - echo "" 89 - echo -e "${BLUE}📤 Sending delete request...${NC}" 90 - response=$(curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$SECRET_NAME") 91 - 92 - echo -e "${BLUE}📥 Response:${NC}" 93 - echo "$response" | jq '.' 94 - 95 - # Check if the request was successful 96 - success=$(echo "$response" | jq -r '.success') 97 - if [ "$success" = "true" ]; then 98 - echo "" 99 - echo -e "${GREEN}✅ Secret deleted successfully!${NC}" 100 - 101 - # Extract deletion info 102 - path=$(echo "$response" | jq -r '.data.path // "unknown"') 103 - message=$(echo "$response" | jq -r '.message // "Secret deleted"') 104 - 105 - echo " Secret Name: $SECRET_NAME" 106 - echo " HSM Path: $path" 107 - echo " Status: $message" 108 - 109 - echo "" 110 - echo -e "${BLUE}📋 To verify deletion:${NC}" 111 - echo " curl $API_BASE_URL/api/v1/hsm/secrets/$SECRET_NAME" 112 - 113 - echo "" 114 - echo -e "${BLUE}📋 To list remaining secrets:${NC}" 115 - echo " curl $API_BASE_URL/api/v1/hsm/secrets" 116 - 117 - else 118 - echo "" 119 - echo -e "${RED}❌ Failed to delete secret!${NC}" 120 - error_code=$(echo "$response" | jq -r '.error.code // "unknown"') 121 - error_message=$(echo "$response" | jq -r '.error.message // "No error message"') 122 - echo " Error Code: $error_code" 123 - echo " Error Message: $error_message" 124 - exit 1 125 - fi
-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
+3
examples/api/import-from-k8s.sh
··· 91 91 92 92 echo "" 93 93 echo "🔍 Verification commands:" 94 + echo " # Check HSM secret via kubectl plugin:" 95 + echo " kubectl hsm get $label" 96 + echo "" 94 97 echo " # Check HSM secret via API:" 95 98 echo " curl $API_BASE_URL/api/v1/hsm/secrets/$label" 96 99 echo ""
-48
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 secret info 24 - count=$(echo "$response" | jq -r '.data.count') 25 - prefix=$(echo "$response" | jq -r '.data.prefix // ""') 26 - 27 - echo "📊 Summary:" 28 - echo " Total Secrets: $count" 29 - if [ -n "$prefix" ] && [ "$prefix" != "" ]; then 30 - echo " Prefix Filter: $prefix" 31 - fi 32 - echo "" 33 - 34 - # List secrets 35 - echo "🔐 Secrets:" 36 - if [ "$count" -gt 0 ]; then 37 - echo "$response" | jq -r '.data.paths[] | " • \(.)"' 38 - else 39 - echo " No secrets found" 40 - fi 41 - else 42 - echo "❌ Failed to list secrets!" 43 - error_code=$(echo "$response" | jq -r '.error.code // "unknown"') 44 - error_message=$(echo "$response" | jq -r '.error.message // "No error message"') 45 - echo " Error Code: $error_code" 46 - echo " Error Message: $error_message" 47 - exit 1 48 - fi
+20 -3
examples/basic/README.md
··· 27 27 28 28 ### Step 2: Create Your First Secret 29 29 30 - Create a database secret stored on the HSM: 30 + **Option A: Using kubectl-hsm plugin (recommended for interactive use):** 31 + ```bash 32 + kubectl hsm create database-credentials \ 33 + --from-literal=database_url="postgresql://user:pass@db:5432/mydb" \ 34 + --from-literal=username="dbuser" \ 35 + --from-literal=password="secret123" 36 + ``` 31 37 38 + **Option B: Using CRD resources (recommended for GitOps):** 32 39 ```bash 33 40 kubectl apply -f database-secret.yaml 34 41 ``` 35 42 36 43 Verify the secret was created: 37 44 ```bash 45 + # Using kubectl-hsm 46 + kubectl hsm get database-credentials 47 + kubectl hsm list 48 + 49 + # Using standard kubectl 38 50 kubectl get hsmsecret database-credentials 39 51 kubectl get secret database-credentials 40 52 ``` ··· 104 116 Update secrets directly on the HSM, and they'll automatically sync: 105 117 106 118 ```bash 107 - # The operator detects HSM changes and updates Kubernetes Secrets 108 - # No manual intervention required 119 + # Option 1: Update via kubectl-hsm (writes to HSM, then syncs to K8s) 120 + kubectl hsm create database-credentials \ 121 + --from-literal=password="new-secret123" \ 122 + --dry-run=false 123 + 124 + # Option 2: Direct HSM update (via pkcs11-tool or HSM tools) 125 + # The operator detects HSM changes and updates Kubernetes Secrets automatically 109 126 ``` 110 127 111 128 ### Multiple Applications
+5 -6
examples/basic/api-keys.yaml
··· 9 9 annotations: 10 10 hsm.j5t.io/description: "API keys for external services (Stripe, AWS, etc.)" 11 11 spec: 12 - # Path on the HSM where API keys are stored 12 + # HSM path is automatically set to the metadata.name (external-api-keys) 13 13 14 - # Name of the Secret containing API keys 15 - secretName: "external-api-keys" 14 + # ParentRef identifies which operator instance should handle this HSMSecret 15 + parentRef: 16 + name: controller-manager 17 + namespace: hsm-secrets-operator-system 16 18 17 19 # Enable automatic synchronization 18 20 autoSync: true 19 21 20 22 # Sync every 10 minutes (API keys might rotate frequently) 21 23 syncInterval: 600 22 - 23 - # Standard opaque secret type 24 - secretType: Opaque 25 24 26 25 --- 27 26 # Example application using the API keys
+5 -6
examples/basic/database-secret.yaml
··· 10 10 annotations: 11 11 hsm.j5t.io/description: "PostgreSQL database credentials for production" 12 12 spec: 13 - # Path on the HSM where the secret is stored 13 + # HSM path is automatically set to the metadata.name (database-credentials) 14 14 15 - # Name of the Kubernetes Secret to create/maintain 16 - secretName: "database-credentials" 15 + # ParentRef identifies which operator instance should handle this HSMSecret 16 + parentRef: 17 + name: controller-manager 18 + namespace: hsm-secrets-operator-system 17 19 18 20 # Enable automatic sync from HSM to Kubernetes 19 21 autoSync: true 20 22 21 23 # Check for changes every 5 minutes (300 seconds) 22 24 syncInterval: 300 23 - 24 - # Type of Kubernetes Secret to create 25 - secretType: Opaque 26 25 27 26 --- 28 27 # Example of how to use the secret in a deployment
+20 -12
examples/basic/pico-hsm-device.yaml
··· 10 10 # Device type for auto-discovery 11 11 deviceType: PicoHSM 12 12 13 - # USB device specifications for Pico HSM 14 - usb: 15 - vendorId: "20a0" 16 - productId: "4230" 17 - # serialNumber: "12345" # Optional: specific device serial 13 + # Discovery configuration 14 + discovery: 15 + # USB device specifications for Pico HSM 16 + usb: 17 + vendorId: "20a0" 18 + productId: "4230" 19 + # serialNumber: "12345" # Optional: specific device serial 20 + 21 + # Alternative: Manual path specification 22 + # devicePath: 23 + # path: "/dev/sc-hsm*" 24 + # permissions: "0666" 18 25 19 - # Alternative: Manual path specification 20 - # devicePath: 21 - # path: "/dev/sc-hsm*" 22 - # permissions: "0666" 26 + # PKCS#11 configuration 27 + pkcs11: 28 + libraryPath: "/usr/lib/opensc-pkcs11.so" # Use OpenSC for Pico HSM 29 + slotId: 0 30 + pinSecret: 31 + name: "pico-hsm-pin" 32 + key: "pin" 33 + tokenLabel: "PicoHSM" 23 34 24 35 # Node selection (optional - runs on all nodes if not specified) 25 36 nodeSelector: 26 37 # kubernetes.io/hostname: "worker-node-1" 27 38 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 39 32 40 # Maximum number of devices to discover 33 41 maxDevices: 2
+7 -2
examples/basic/tls-certificate.yaml
··· 9 9 annotations: 10 10 hsm.j5t.io/description: "TLS certificate and key for webapp.example.com" 11 11 spec: 12 - # Path on the HSM where the TLS cert/key is stored 12 + # HSM path is automatically set to the metadata.name (webapp-tls-cert) 13 + 14 + # ParentRef identifies which operator instance should handle this HSMSecret 15 + parentRef: 16 + name: controller-manager 17 + namespace: hsm-secrets-operator-system 13 18 14 - # Name of the TLS Secret to create 19 + # Name of the TLS Secret to create (optional, defaults to metadata.name) 15 20 secretName: "webapp-tls" 16 21 17 22 # Enable automatic sync
+23 -7
examples/deployment/README.md
··· 5 5 ## Files 6 6 7 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 8 + 9 + > **Additional Configurations:** See the [config](../../config/) directory for CRDs, RBAC, and operator deployments, or the [helm](../../helm/) directory for Helm chart deployments. 10 10 11 11 ## Complete Setup 12 12 ··· 75 75 # Check HSM devices 76 76 kubectl get hsmdevice 77 77 78 - # Check secrets 79 - kubectl get hsmsecret 80 - kubectl get secret 78 + # Check secrets (multiple ways) 79 + kubectl hsm list # via kubectl-hsm plugin 80 + kubectl get hsmsecret # via CRDs 81 + kubectl get secret # via K8s secrets 81 82 ``` 82 83 83 - ### 5. Test the API 84 + ### 5. Test Secret Operations 84 85 85 - If API is enabled: 86 + **Option A: Using kubectl-hsm plugin (recommended):** 87 + ```bash 88 + # Check health 89 + kubectl hsm health 90 + 91 + # Create a test secret 92 + kubectl hsm create test-secret --from-literal=key=value 93 + 94 + # List secrets 95 + kubectl hsm list 96 + 97 + # Get secret details 98 + kubectl hsm get test-secret 99 + ``` 100 + 101 + **Option B: Using REST API (advanced):** 86 102 ```bash 87 103 # Port forward to access API locally 88 104 kubectl port-forward -n hsm-secrets-operator-system service/hsm-secrets-operator-api 8090:8090
+24 -8
examples/deployment/complete-setup.yaml
··· 24 24 device-type: pico-hsm 25 25 spec: 26 26 deviceType: PicoHSM 27 - usb: 28 - vendorId: "20a0" 29 - productId: "4230" 27 + 28 + # Discovery configuration 29 + discovery: 30 + usb: 31 + vendorId: "20a0" 32 + productId: "4230" 33 + 34 + # PKCS#11 configuration 35 + pkcs11: 36 + libraryPath: "/usr/lib/opensc-pkcs11.so" 37 + slotId: 0 38 + pinSecret: 39 + name: "production-hsm-pin" 40 + key: "pin" 41 + tokenLabel: "PicoHSM" 42 + 30 43 nodeSelector: 31 44 hsm.j5t.io/enabled: "true" 32 - pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so" 33 45 maxDevices: 2 34 - mirroring: 35 - policy: "ReadOnly" 36 - syncInterval: 300 37 - autoFailover: true 38 46 39 47 --- 40 48 # Production Database Secret ··· 48 56 type: database 49 57 criticality: high 50 58 spec: 59 + # HSM path is automatically set to the metadata.name (production-database) 60 + parentRef: 61 + name: controller-manager 62 + namespace: hsm-secrets-operator-system 51 63 secretName: "webapp-database-credentials" 52 64 autoSync: true 53 65 syncInterval: 600 # 10 minutes ··· 64 76 app: webapp 65 77 type: tls 66 78 spec: 79 + # HSM path is automatically set to the metadata.name (webapp-tls) 80 + parentRef: 81 + name: controller-manager 82 + namespace: hsm-secrets-operator-system 67 83 secretName: "webapp-tls-cert" 68 84 autoSync: true 69 85 syncInterval: 3600 # 1 hour
+5 -4
examples/high-availability/README.md
··· 9 9 ## Examples 10 10 11 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 12 + 13 + > **Additional HA Examples:** See the [deployment/complete-setup.yaml](../deployment/complete-setup.yaml) for production HA configurations and the [advanced](../advanced/) directory for multi-environment setups. 15 14 16 15 ## Architecture 17 16 ··· 152 151 # Review mirroring status 153 152 kubectl describe hsmdevice hsm-primary 154 153 155 - # Check secret sync status 154 + # Check secret sync status (multiple options) 155 + kubectl hsm health # via kubectl-hsm plugin 156 + kubectl hsm list # shows all secrets with status 156 157 kubectl get hsmsecret -o custom-columns=NAME:.metadata.name,STATUS:.status.syncStatus,LAST-SYNC:.status.lastSyncTime 157 158 158 159 # Monitor failover events
+25 -34
examples/high-availability/mirrored-hsm-device.yaml
··· 1 1 --- 2 - # Primary HSM Device with Mirroring Configuration 2 + # High Availability HSM Device Configuration 3 + # The operator automatically discovers devices on multiple nodes for HA 3 4 apiVersion: hsm.j5t.io/v1alpha1 4 5 kind: HSMDevice 5 6 metadata: 6 - name: hsm-primary 7 + name: ha-pico-hsm 7 8 namespace: default 8 9 labels: 9 - role: primary 10 + role: production 10 11 environment: production 11 12 ha.enabled: "true" 12 13 spec: 13 14 deviceType: PicoHSM 14 15 15 - # USB discovery for Pico HSM 16 - usb: 17 - vendorId: "20a0" 18 - productId: "4230" 16 + # Discovery configuration 17 + discovery: 18 + # USB discovery for Pico HSM 19 + usb: 20 + vendorId: "20a0" 21 + productId: "4230" 22 + 23 + # PKCS#11 configuration 24 + pkcs11: 25 + libraryPath: "/usr/lib/opensc-pkcs11.so" # Use OpenSC for Pico HSM 26 + slotId: 0 27 + pinSecret: 28 + name: "pico-hsm-pin" 29 + key: "pin" 30 + tokenLabel: "PicoHSM" 19 31 20 32 # Deploy on nodes with HSM hardware 21 33 nodeSelector: 22 34 hsm.j5t.io/hardware: "available" 23 35 kubernetes.io/arch: "amd64" 24 36 25 - # PKCS#11 library configuration 26 - pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so" 27 - 28 - # Maximum devices to discover 37 + # Allow multiple devices for HA 29 38 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 39 50 40 --- 51 41 # Service Monitor for Prometheus (if using) ··· 119 109 criticality: high 120 110 ha.enabled: "true" 121 111 annotations: 122 - hsm.j5t.io/description: "HA database credentials with mirroring support" 112 + hsm.j5t.io/description: "HA database credentials for high availability" 123 113 spec: 124 - secretName: "ha-database-credentials" 114 + # HSM path is automatically set to the metadata.name (ha-database-credentials) 115 + parentRef: 116 + name: controller-manager 117 + namespace: hsm-secrets-operator-system 125 118 126 119 # Enable auto-sync for HA 127 120 autoSync: true 128 121 129 122 # More frequent sync for critical secrets 130 123 syncInterval: 180 # 3 minutes 131 - 132 - secretType: Opaque 133 124 134 125 --- 135 126 # Deployment using HA secrets
-16
examples/new-architecture/test-hsmdevice.yaml
··· 1 - apiVersion: hsm.j5t.io/v1alpha1 2 - kind: HSMDevice 3 - metadata: 4 - name: pico-hsm-test 5 - namespace: secrets 6 - spec: 7 - deviceType: "PicoHSM" 8 - discovery: 9 - autoDiscovery: true 10 - pkcs11: 11 - libraryPath: "/usr/lib/libsc-hsm-pkcs11.so" 12 - slotId: 0 13 - pinSecret: 14 - name: "pico-hsm-pin" 15 - key: "pin" 16 - tokenLabel: "PicoHSM"
+2 -2
go.mod
··· 96 96 golang.org/x/net v0.38.0 // indirect 97 97 golang.org/x/oauth2 v0.27.0 // indirect 98 98 golang.org/x/sync v0.12.0 // indirect 99 - golang.org/x/sys v0.31.0 // indirect 100 - golang.org/x/term v0.30.0 // indirect 99 + golang.org/x/sys v0.35.0 // indirect 100 + golang.org/x/term v0.34.0 // indirect 101 101 golang.org/x/text v0.23.0 // indirect 102 102 golang.org/x/time v0.9.0 // indirect 103 103 golang.org/x/tools v0.26.0 // indirect
+4 -4
go.sum
··· 231 231 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 232 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 233 233 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 234 - golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 235 - golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 236 - golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 237 - golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 234 + golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 235 + golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 236 + golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= 237 + golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 238 238 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 239 239 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 240 240 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+177
kubectl-hsm/Makefile
··· 1 + # kubectl-hsm Plugin Makefile 2 + # Copyright 2025. Licensed under the Apache License, Version 2.0. 3 + 4 + # Build variables 5 + PLUGIN_NAME=kubectl-hsm 6 + VERSION?=dev 7 + BUILD_DIR=bin 8 + DIST_DIR=dist 9 + 10 + # Go build settings 11 + GO_VERSION=1.24 12 + CGO_ENABLED=0 13 + LDFLAGS=-ldflags "-s -w -X main.version=$(VERSION)" 14 + 15 + # Supported platforms for cross-compilation 16 + PLATFORMS = \ 17 + linux/amd64 \ 18 + linux/arm64 \ 19 + darwin/amd64 \ 20 + darwin/arm64 \ 21 + windows/amd64 22 + 23 + # Default target 24 + .PHONY: all 25 + all: build 26 + 27 + # Build for current platform 28 + .PHONY: build 29 + build: 30 + @echo "Building kubectl-hsm for current platform..." 31 + @mkdir -p $(BUILD_DIR) 32 + CGO_ENABLED=$(CGO_ENABLED) go build $(LDFLAGS) -o $(BUILD_DIR)/$(PLUGIN_NAME) ./cmd 33 + 34 + # Build for all supported platforms 35 + .PHONY: build-all 36 + build-all: clean-dist 37 + @echo "Building kubectl-hsm for all platforms..." 38 + @mkdir -p $(DIST_DIR) 39 + @$(foreach platform,$(PLATFORMS), \ 40 + echo "Building for $(platform)..."; \ 41 + GOOS=$(shell echo $(platform) | cut -d'/' -f1) \ 42 + GOARCH=$(shell echo $(platform) | cut -d'/' -f2) \ 43 + CGO_ENABLED=$(CGO_ENABLED) go build $(LDFLAGS) \ 44 + -o $(DIST_DIR)/$(PLUGIN_NAME)-$(shell echo $(platform) | sed 's/\//-/')$(shell [ "$(shell echo $(platform) | cut -d'/' -f1)" = "windows" ] && echo ".exe" || echo "") ./cmd \ 45 + && echo "✅ Built $(DIST_DIR)/$(PLUGIN_NAME)-$(shell echo $(platform) | sed 's/\//-/')$(shell [ "$(shell echo $(platform) | cut -d'/' -f1)" = "windows" ] && echo ".exe" || echo "")";) 46 + 47 + # Install to local bin directory (for testing) 48 + .PHONY: install 49 + install: build 50 + @echo "Installing kubectl-hsm to ~/bin/..." 51 + @mkdir -p ~/bin 52 + @cp $(BUILD_DIR)/$(PLUGIN_NAME) ~/bin/ 53 + @echo "✅ Installed to ~/bin/$(PLUGIN_NAME)" 54 + @echo "" 55 + @echo "To use the plugin:" 56 + @echo " 1. Ensure ~/bin is in your PATH" 57 + @echo " 2. Run: kubectl hsm --help" 58 + 59 + # Install to system-wide location (requires sudo) 60 + .PHONY: install-system 61 + install-system: build 62 + @echo "Installing kubectl-hsm to /usr/local/bin/..." 63 + @sudo cp $(BUILD_DIR)/$(PLUGIN_NAME) /usr/local/bin/ 64 + @echo "✅ Installed to /usr/local/bin/$(PLUGIN_NAME)" 65 + @echo "" 66 + @echo "Plugin is now available system-wide:" 67 + @echo " kubectl hsm --help" 68 + 69 + # Create installation script 70 + .PHONY: install-script 71 + install-script: 72 + @echo "Creating installation script..." 73 + @mkdir -p $(DIST_DIR) 74 + @echo '#!/bin/bash' > $(DIST_DIR)/install.sh 75 + @echo 'set -e' >> $(DIST_DIR)/install.sh 76 + @echo '' >> $(DIST_DIR)/install.sh 77 + @echo '# kubectl-hsm Installation Script' >> $(DIST_DIR)/install.sh 78 + @echo '# This script downloads and installs the kubectl-hsm plugin' >> $(DIST_DIR)/install.sh 79 + @echo '' >> $(DIST_DIR)/install.sh 80 + @echo 'PLUGIN_NAME="kubectl-hsm"' >> $(DIST_DIR)/install.sh 81 + @echo 'VERSION="$${VERSION:-latest}"' >> $(DIST_DIR)/install.sh 82 + @echo 'INSTALL_DIR="$${INSTALL_DIR:-/usr/local/bin}"' >> $(DIST_DIR)/install.sh 83 + @echo '' >> $(DIST_DIR)/install.sh 84 + @echo 'echo "Installing kubectl-hsm..."' >> $(DIST_DIR)/install.sh 85 + @echo 'echo "✅ kubectl-hsm installation script created"' >> $(DIST_DIR)/install.sh 86 + @chmod +x $(DIST_DIR)/install.sh 87 + @echo "✅ Created installation script: $(DIST_DIR)/install.sh" 88 + 89 + # Create archive packages for distribution 90 + .PHONY: package 91 + package: build-all install-script 92 + @echo "Creating distribution packages..." 93 + @$(foreach platform,$(PLATFORMS), \ 94 + os=$$(echo $(platform) | cut -d'/' -f1); \ 95 + arch=$$(echo $(platform) | cut -d'/' -f2); \ 96 + binary="$(PLUGIN_NAME)-$$os-$$arch"; \ 97 + [ "$$os" = "windows" ] && binary="$$binary.exe"; \ 98 + archive_name="$(PLUGIN_NAME)-$(VERSION)-$$os-$$arch"; \ 99 + if [ "$$os" = "windows" ]; then \ 100 + (cd $(DIST_DIR) && zip "$$archive_name.zip" "$$binary" install.sh README.md 2>/dev/null || zip "$$archive_name.zip" "$$binary" install.sh); \ 101 + else \ 102 + (cd $(DIST_DIR) && tar czf "$$archive_name.tar.gz" "$$binary" install.sh README.md 2>/dev/null || tar czf "$$archive_name.tar.gz" "$$binary" install.sh); \ 103 + fi; \ 104 + echo "📦 Created $$archive_name archive";) 105 + 106 + # Generate checksums for release artifacts 107 + .PHONY: checksums 108 + checksums: package 109 + @echo "Generating checksums..." 110 + @cd $(DIST_DIR) && find . -name "*.tar.gz" -o -name "*.zip" | xargs shasum -a 256 > SHA256SUMS 111 + @echo "✅ Generated checksums: $(DIST_DIR)/SHA256SUMS" 112 + 113 + # Test the built binary 114 + .PHONY: test-binary 115 + test-binary: build 116 + @echo "Testing built binary..." 117 + @$(BUILD_DIR)/$(PLUGIN_NAME) version 118 + @echo "✅ Binary test passed" 119 + 120 + # Clean build artifacts 121 + .PHONY: clean 122 + clean: 123 + @echo "Cleaning build directory..." 124 + @rm -rf $(BUILD_DIR) 125 + 126 + .PHONY: clean-dist 127 + clean-dist: 128 + @echo "Cleaning distribution directory..." 129 + @rm -rf $(DIST_DIR) 130 + 131 + .PHONY: clean-all 132 + clean-all: clean clean-dist 133 + 134 + # Development targets 135 + .PHONY: dev 136 + dev: build test-binary 137 + @echo "Development build complete" 138 + 139 + .PHONY: dev-install 140 + dev-install: dev install 141 + @echo "Development installation complete" 142 + 143 + # Help target 144 + .PHONY: help 145 + help: 146 + @echo "kubectl-hsm Plugin Build System" 147 + @echo "" 148 + @echo "Targets:" 149 + @echo " build - Build for current platform" 150 + @echo " build-all - Build for all supported platforms" 151 + @echo " install - Install to ~/bin/" 152 + @echo " install-system - Install to /usr/local/bin/ (requires sudo)" 153 + @echo " install-script - Generate installation script" 154 + @echo " package - Create distribution packages" 155 + @echo " checksums - Generate checksums for packages" 156 + @echo " test-binary - Test the built binary" 157 + @echo " dev - Development build and test" 158 + @echo " dev-install - Development build, test, and install" 159 + @echo " clean - Clean build artifacts" 160 + @echo " clean-all - Clean all artifacts" 161 + @echo " help - Show this help" 162 + @echo "" 163 + @echo "Variables:" 164 + @echo " VERSION - Plugin version (default: dev)" 165 + @echo " INSTALL_DIR - Installation directory (default: /usr/local/bin)" 166 + @echo "" 167 + @echo "Examples:" 168 + @echo " make build" 169 + @echo " make build-all VERSION=v1.0.0" 170 + @echo " make install" 171 + @echo " make package VERSION=v1.0.0" 172 + 173 + # Show supported platforms 174 + .PHONY: platforms 175 + platforms: 176 + @echo "Supported platforms:" 177 + @$(foreach platform,$(PLATFORMS),echo " $(platform)";)
+241
kubectl-hsm/README.md
··· 1 + # kubectl-hsm Plugin 2 + 3 + A kubectl plugin that provides a Kubernetes-native command-line interface for Hardware Security Module (HSM) secret management. 4 + 5 + ## Overview 6 + 7 + The `kubectl-hsm` plugin integrates with the [HSM Secrets Operator](https://github.com/evanjarrett/hsm-secrets-operator) to provide secure secret storage using hardware-based security modules while maintaining seamless integration with Kubernetes workflows. 8 + 9 + ## Features 10 + 11 + - **Kubernetes-native**: Works seamlessly with kubectl and respects namespace context 12 + - **Secure**: All secrets stored in HSM hardware for maximum security 13 + - **Interactive**: Support for secure password input and interactive secret creation 14 + - **Flexible**: Multiple input methods (literals, files, interactive prompts) 15 + - **Cross-platform**: Supports Linux, macOS, and Windows 16 + 17 + ## Installation 18 + 19 + ### Quick Install (Recommended) 20 + 21 + ```bash 22 + curl -fsSL https://github.com/evanjarrett/hsm-secrets-operator/releases/latest/download/install.sh | bash 23 + ``` 24 + 25 + ### Manual Installation 26 + 27 + 1. Download the binary for your platform from the [releases page](https://github.com/evanjarrett/hsm-secrets-operator/releases) 28 + 2. Rename it to `kubectl-hsm` 29 + 3. Make it executable: `chmod +x kubectl-hsm` 30 + 4. Move it to a directory in your PATH (e.g., `/usr/local/bin/`) 31 + 32 + ### Build from Source 33 + 34 + ```bash 35 + git clone https://github.com/evanjarrett/hsm-secrets-operator.git 36 + cd hsm-secrets-operator/cmd/kubectl-hsm 37 + make build 38 + make install 39 + ``` 40 + 41 + ## Prerequisites 42 + 43 + - kubectl installed and configured 44 + - HSM Secrets Operator deployed in your cluster 45 + - Access to the operator's API service 46 + 47 + ## Usage 48 + 49 + ### Basic Commands 50 + 51 + ```bash 52 + # Check plugin version 53 + kubectl hsm version 54 + 55 + # Check operator health 56 + kubectl hsm health 57 + 58 + # List all secrets 59 + kubectl hsm list 60 + 61 + # Create a secret interactively (recommended for sensitive data) 62 + kubectl hsm create database-creds --interactive 63 + 64 + # Create a secret with literal values 65 + kubectl hsm create api-config \ 66 + --from-literal api_key=sk_test_123 \ 67 + --from-literal endpoint=https://api.example.com 68 + 69 + # Load secret values from files 70 + kubectl hsm create tls-cert \ 71 + --from-file cert=server.crt \ 72 + --from-file key=server.key 73 + 74 + # Get a secret (shows metadata, not values) 75 + kubectl hsm get database-creds 76 + 77 + # Get a specific key value 78 + kubectl hsm get database-creds --key password 79 + 80 + # Delete a secret (with confirmation) 81 + kubectl hsm delete old-credentials 82 + 83 + # Force delete without confirmation 84 + kubectl hsm delete old-credentials --force 85 + ``` 86 + 87 + ### Namespace Handling 88 + 89 + The plugin respects your current kubectl namespace context: 90 + 91 + ```bash 92 + # Use current namespace context (set by kubens, kubectl config, etc.) 93 + kubectl hsm list 94 + 95 + # Override namespace for a single command 96 + kubectl hsm list -n hsm-secrets-operator-system 97 + 98 + # Switch namespace context (affects all kubectl commands) 99 + kubens hsm-secrets-operator-system 100 + kubectl hsm list 101 + ``` 102 + 103 + ### Output Formats 104 + 105 + ```bash 106 + # Human-readable output (default) 107 + kubectl hsm get my-secret 108 + 109 + # JSON output 110 + kubectl hsm get my-secret -o json 111 + 112 + # YAML output 113 + kubectl hsm get my-secret -o yaml 114 + ``` 115 + 116 + ### Interactive Mode 117 + 118 + Interactive mode is recommended for creating secrets with sensitive data: 119 + 120 + ```bash 121 + kubectl hsm create database-creds --interactive 122 + ``` 123 + 124 + This will prompt you for each key-value pair. Fields that look like passwords (containing "password", "secret", "token", or "key") will hide input for security. 125 + 126 + ## How It Works 127 + 128 + 1. **Service Discovery**: The plugin automatically discovers the HSM operator API service in your current namespace 129 + 2. **Port Forwarding**: Creates a secure port-forward connection to the operator 130 + 3. **API Proxy**: Routes commands through the operator's REST API 131 + 4. **HSM Integration**: The operator handles the actual HSM hardware communication 132 + 133 + ## Error Handling 134 + 135 + The plugin provides helpful error messages and suggestions: 136 + 137 + ```bash 138 + # If operator not found 139 + ❌ HSM secrets operator service not found in namespace 'default' 140 + 141 + Please check: 142 + - Is the operator installed? Try: kubectl get deploy -n default 143 + - Are you in the correct namespace? Try: kubens <operator-namespace> 144 + 145 + # If connection fails 146 + ❌ Failed to connect to HSM operator: connection refused 147 + 148 + Please check: 149 + - Operator pods are running: kubectl get pods -l app.kubernetes.io/name=hsm-secrets-operator 150 + - Service is available: kubectl get svc hsm-secrets-operator-api 151 + ``` 152 + 153 + ## Security Considerations 154 + 155 + - **No Secret Values in Transit**: The plugin never logs or exposes secret values 156 + - **Secure Communication**: All communication uses port-forwarded connections 157 + - **HSM Storage**: Secrets are stored in HSM hardware, not Kubernetes etcd 158 + - **Interactive Input**: Passwords are hidden during interactive input 159 + - **Confirmation Required**: Destructive operations require explicit confirmation 160 + 161 + ## Configuration 162 + 163 + The plugin uses standard kubectl configuration: 164 + 165 + - **Kubeconfig**: Reads from `~/.kube/config` or `$KUBECONFIG` 166 + - **Current Context**: Respects the current kubectl context 167 + - **Namespace**: Uses current namespace or explicit `-n` flag 168 + 169 + ## Examples 170 + 171 + ### Database Credentials 172 + 173 + ```bash 174 + # Create database credentials interactively 175 + kubectl hsm create database-creds --interactive 176 + # Prompts for: username, password, host, port, database 177 + 178 + # Retrieve for use in deployment 179 + kubectl hsm get database-creds --key username 180 + kubectl hsm get database-creds --key password -o json | jq -r '.password' 181 + ``` 182 + 183 + ### API Keys and Tokens 184 + 185 + ```bash 186 + # Create API configuration 187 + kubectl hsm create stripe-config \ 188 + --from-literal publishable_key=pk_live_123 \ 189 + --from-literal secret_key=sk_live_456 \ 190 + --from-literal webhook_secret=whsec_789 191 + 192 + # View configuration (metadata only) 193 + kubectl hsm get stripe-config 194 + ``` 195 + 196 + ### TLS Certificates 197 + 198 + ```bash 199 + # Load certificate files 200 + kubectl hsm create tls-server \ 201 + --from-file tls.crt=server.crt \ 202 + --from-file tls.key=server.key \ 203 + --from-file ca.crt=ca.crt 204 + 205 + # Check certificate info 206 + kubectl hsm get tls-server 207 + ``` 208 + 209 + ## Troubleshooting 210 + 211 + ### Plugin Not Found 212 + 213 + If kubectl can't find the plugin: 214 + 215 + 1. Ensure the binary is named `kubectl-hsm` 216 + 2. Verify it's in your PATH: `which kubectl-hsm` 217 + 3. Check permissions: `ls -la $(which kubectl-hsm)` 218 + 219 + ### Connection Issues 220 + 221 + If you can't connect to the operator: 222 + 223 + 1. Check if the operator is running: `kubectl get pods -n hsm-secrets-operator-system` 224 + 2. Verify the service exists: `kubectl get svc hsm-secrets-operator-api -n hsm-secrets-operator-system` 225 + 3. Test direct connection: `kubectl port-forward -n hsm-secrets-operator-system svc/hsm-secrets-operator-api 8090:8090` 226 + 227 + ### Permission Errors 228 + 229 + If you get permission errors: 230 + 231 + 1. Verify your kubectl access: `kubectl auth can-i get services` 232 + 2. Check if you're in the right namespace: `kubectl config get-contexts` 233 + 3. Ensure the operator is accessible from your namespace 234 + 235 + ## Contributing 236 + 237 + Contributions are welcome! Please see the main [repository](https://github.com/evanjarrett/hsm-secrets-operator) for contribution guidelines. 238 + 239 + ## License 240 + 241 + This project is licensed under the Apache License, Version 2.0. See the [LICENSE](../../LICENSE) file for details.
kubectl-hsm/bin/kubectl-hsm

This is a binary file and will not be displayed.

+100
kubectl-hsm/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 + "fmt" 21 + "os" 22 + 23 + "github.com/spf13/cobra" 24 + 25 + "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/commands" 26 + ) 27 + 28 + var version = "dev" // Set during build 29 + 30 + func main() { 31 + rootCmd := newRootCmd() 32 + if err := rootCmd.Execute(); err != nil { 33 + fmt.Fprintf(os.Stderr, "Error: %v\n", err) 34 + os.Exit(1) 35 + } 36 + } 37 + 38 + func newRootCmd() *cobra.Command { 39 + cmd := &cobra.Command{ 40 + Use: "kubectl-hsm", 41 + Short: "Kubernetes plugin for HSM secret management", 42 + Long: `kubectl-hsm is a kubectl plugin that provides a Kubernetes-native 43 + command-line interface for Hardware Security Module (HSM) secret management. 44 + 45 + This plugin integrates with the HSM Secrets Operator to provide secure 46 + secret storage using hardware-based security modules while maintaining 47 + seamless integration with Kubernetes workflows. 48 + 49 + Examples: 50 + # Create a new secret interactively (recommended for sensitive data) 51 + kubectl hsm create database-creds --interactive 52 + 53 + # Create a secret with literal values 54 + kubectl hsm create api-config --from-literal api_key=abc123 --from-literal endpoint=https://api.example.com 55 + 56 + # List all secrets 57 + kubectl hsm list 58 + 59 + # Get a specific secret 60 + kubectl hsm get database-creds 61 + 62 + # Delete a secret (with confirmation) 63 + kubectl hsm delete old-credentials 64 + 65 + # Check operator health 66 + kubectl hsm health 67 + 68 + For more information about the HSM Secrets Operator, visit: 69 + https://github.com/evanjarrett/hsm-secrets-operator`, 70 + SilenceUsage: true, 71 + SilenceErrors: true, 72 + } 73 + 74 + // Add version command 75 + cmd.AddCommand(newVersionCmd()) 76 + 77 + // Add core secret management commands 78 + cmd.AddCommand(commands.NewCreateCmd()) 79 + cmd.AddCommand(commands.NewGetCmd()) 80 + cmd.AddCommand(commands.NewListCmd()) 81 + cmd.AddCommand(commands.NewDeleteCmd()) 82 + 83 + // Add operational commands 84 + cmd.AddCommand(commands.NewHealthCmd()) 85 + 86 + return cmd 87 + } 88 + 89 + func newVersionCmd() *cobra.Command { 90 + return &cobra.Command{ 91 + Use: "version", 92 + Short: "Show version information", 93 + Long: "Display the version of kubectl-hsm plugin and related information.", 94 + Run: func(cmd *cobra.Command, args []string) { 95 + fmt.Printf("kubectl-hsm version: %s\n", version) 96 + fmt.Printf("Compatible with: HSM Secrets Operator v1alpha1\n") 97 + fmt.Printf("Plugin type: kubectl plugin\n") 98 + }, 99 + } 100 + }
+54
kubectl-hsm/go.mod
··· 1 + module github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm 2 + 3 + go 1.24.0 4 + 5 + require ( 6 + github.com/spf13/cobra v1.8.1 7 + golang.org/x/term v0.34.0 8 + k8s.io/apimachinery v0.33.4 9 + k8s.io/client-go v0.33.4 10 + sigs.k8s.io/yaml v1.4.0 11 + ) 12 + 13 + require ( 14 + github.com/davecgh/go-spew v1.1.1 // indirect 15 + github.com/emicklei/go-restful/v3 v3.11.0 // indirect 16 + github.com/fxamacker/cbor/v2 v2.7.0 // indirect 17 + github.com/go-logr/logr v1.4.2 // indirect 18 + github.com/go-openapi/jsonpointer v0.21.0 // indirect 19 + github.com/go-openapi/jsonreference v0.20.2 // indirect 20 + github.com/go-openapi/swag v0.23.0 // indirect 21 + github.com/gogo/protobuf v1.3.2 // indirect 22 + github.com/google/gnostic-models v0.6.9 // indirect 23 + github.com/google/go-cmp v0.7.0 // indirect 24 + github.com/google/uuid v1.6.0 // indirect 25 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 26 + github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 + github.com/josharian/intern v1.0.0 // indirect 28 + github.com/json-iterator/go v1.1.12 // indirect 29 + github.com/mailru/easyjson v0.7.7 // indirect 30 + github.com/moby/spdystream v0.5.0 // indirect 31 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 + github.com/modern-go/reflect2 v1.0.2 // indirect 33 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 34 + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 35 + github.com/pkg/errors v0.9.1 // indirect 36 + github.com/spf13/pflag v1.0.5 // indirect 37 + github.com/x448/float16 v0.8.4 // indirect 38 + golang.org/x/net v0.38.0 // indirect 39 + golang.org/x/oauth2 v0.27.0 // indirect 40 + golang.org/x/sys v0.35.0 // indirect 41 + golang.org/x/text v0.23.0 // indirect 42 + golang.org/x/time v0.9.0 // indirect 43 + google.golang.org/protobuf v1.36.5 // indirect 44 + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 45 + gopkg.in/inf.v0 v0.9.1 // indirect 46 + gopkg.in/yaml.v3 v3.0.1 // indirect 47 + k8s.io/api v0.33.4 // indirect 48 + k8s.io/klog/v2 v2.130.1 // indirect 49 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 50 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 51 + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 52 + sigs.k8s.io/randfill v1.0.0 // indirect 53 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 54 + )
+167
kubectl-hsm/go.sum
··· 1 + github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 2 + github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 3 + github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 + github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 + github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 9 + github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 10 + github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 11 + github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 12 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 13 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 + github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 15 + github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 16 + github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 17 + github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 18 + github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 19 + github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 20 + github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 21 + github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 22 + github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 23 + github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 24 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 25 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 26 + github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 27 + github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 28 + github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 30 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 31 + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 33 + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 34 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 37 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 38 + github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 39 + github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 40 + github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 41 + github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 42 + github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 43 + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 44 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 45 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 46 + github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 47 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 48 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 49 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 50 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 52 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 53 + github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 54 + github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 55 + github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= 56 + github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 57 + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 59 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 60 + github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 61 + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 62 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 63 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 64 + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 65 + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 66 + github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 67 + github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 68 + github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 69 + github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 70 + github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 71 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 75 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 76 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 77 + github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 78 + github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 79 + github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 80 + github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 81 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 82 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 83 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 84 + github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 85 + github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 86 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 87 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 88 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 89 + github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 90 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 91 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 92 + github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 93 + github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 94 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 95 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 96 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 97 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 98 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 99 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 100 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 101 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 103 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 105 + golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 106 + golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 107 + golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 108 + golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 109 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 + golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 116 + golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 117 + golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= 118 + golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 119 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 120 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 121 + golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 122 + golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 123 + golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 124 + golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 125 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 127 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 128 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 129 + golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 130 + golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 131 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 + google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 136 + google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 137 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 139 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 140 + gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 141 + gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 142 + gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 143 + gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 144 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 146 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 + k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= 148 + k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= 149 + k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= 150 + k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 151 + k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= 152 + k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= 153 + k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 154 + k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 155 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 156 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 157 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 158 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 159 + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 160 + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 161 + sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 162 + sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 163 + sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 164 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 165 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 166 + sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 167 + sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+166
kubectl-hsm/pkg/client/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 client 18 + 19 + import ( 20 + "bytes" 21 + "context" 22 + "encoding/json" 23 + "fmt" 24 + "io" 25 + "net/http" 26 + "net/url" 27 + "strconv" 28 + "time" 29 + ) 30 + 31 + // Client provides methods for interacting with the HSM operator API 32 + type Client struct { 33 + baseURL string 34 + httpClient *http.Client 35 + } 36 + 37 + // NewClient creates a new HSM API client 38 + func NewClient(baseURL string) *Client { 39 + return &Client{ 40 + baseURL: baseURL, 41 + httpClient: &http.Client{ 42 + Timeout: 30 * time.Second, 43 + }, 44 + } 45 + } 46 + 47 + // CreateSecret creates a new secret in the HSM 48 + func (c *Client) CreateSecret(ctx context.Context, name string, data map[string]any) error { 49 + req := CreateSecretRequest{ 50 + Data: data, 51 + } 52 + 53 + return c.doRequest(ctx, "POST", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), req, nil) 54 + } 55 + 56 + // GetSecret retrieves a secret from the HSM 57 + func (c *Client) GetSecret(ctx context.Context, name string) (*SecretData, error) { 58 + var result SecretData 59 + err := c.doRequest(ctx, "GET", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), nil, &result) 60 + if err != nil { 61 + return nil, err 62 + } 63 + return &result, nil 64 + } 65 + 66 + // ListSecrets lists all secrets in the HSM 67 + func (c *Client) ListSecrets(ctx context.Context, page, pageSize int) (*SecretList, error) { 68 + path := "/api/v1/hsm/secrets" 69 + 70 + // Add pagination parameters if specified 71 + if page > 0 || pageSize > 0 { 72 + params := url.Values{} 73 + if page > 0 { 74 + params.Add("page", strconv.Itoa(page)) 75 + } 76 + if pageSize > 0 { 77 + params.Add("page_size", strconv.Itoa(pageSize)) 78 + } 79 + if len(params) > 0 { 80 + path += "?" + params.Encode() 81 + } 82 + } 83 + 84 + var result SecretList 85 + err := c.doRequest(ctx, "GET", path, nil, &result) 86 + if err != nil { 87 + return nil, err 88 + } 89 + return &result, nil 90 + } 91 + 92 + // DeleteSecret deletes a secret from the HSM 93 + func (c *Client) DeleteSecret(ctx context.Context, name string) error { 94 + return c.doRequest(ctx, "DELETE", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), nil, nil) 95 + } 96 + 97 + // GetHealth checks the health status of the HSM operator 98 + func (c *Client) GetHealth(ctx context.Context) (*HealthStatus, error) { 99 + var result HealthStatus 100 + err := c.doRequest(ctx, "GET", "/api/v1/health", nil, &result) 101 + if err != nil { 102 + return nil, err 103 + } 104 + return &result, nil 105 + } 106 + 107 + // doRequest performs an HTTP request and handles the standard API response format 108 + func (c *Client) doRequest(ctx context.Context, method, path string, requestBody any, responseData any) error { 109 + url := c.baseURL + path 110 + 111 + var body io.Reader 112 + if requestBody != nil { 113 + jsonData, err := json.Marshal(requestBody) 114 + if err != nil { 115 + return fmt.Errorf("failed to marshal request body: %w", err) 116 + } 117 + body = bytes.NewBuffer(jsonData) 118 + } 119 + 120 + req, err := http.NewRequestWithContext(ctx, method, url, body) 121 + if err != nil { 122 + return fmt.Errorf("failed to create request: %w", err) 123 + } 124 + 125 + if requestBody != nil { 126 + req.Header.Set("Content-Type", "application/json") 127 + } 128 + 129 + resp, err := c.httpClient.Do(req) 130 + if err != nil { 131 + return fmt.Errorf("request failed: %w", err) 132 + } 133 + defer resp.Body.Close() 134 + 135 + respBody, err := io.ReadAll(resp.Body) 136 + if err != nil { 137 + return fmt.Errorf("failed to read response body: %w", err) 138 + } 139 + 140 + var apiResp APIResponse 141 + if err := json.Unmarshal(respBody, &apiResp); err != nil { 142 + return fmt.Errorf("failed to parse API response: %w", err) 143 + } 144 + 145 + // Check if the API reported an error 146 + if !apiResp.Success { 147 + if apiResp.Error != nil { 148 + return fmt.Errorf("API error (%s): %s", apiResp.Error.Code, apiResp.Error.Message) 149 + } 150 + return fmt.Errorf("API request failed: %s", apiResp.Message) 151 + } 152 + 153 + // If we need to extract specific data from the response 154 + if responseData != nil && apiResp.Data != nil { 155 + dataBytes, err := json.Marshal(apiResp.Data) 156 + if err != nil { 157 + return fmt.Errorf("failed to marshal response data: %w", err) 158 + } 159 + 160 + if err := json.Unmarshal(dataBytes, responseData); err != nil { 161 + return fmt.Errorf("failed to unmarshal response data: %w", err) 162 + } 163 + } 164 + 165 + return nil 166 + }
+83
kubectl-hsm/pkg/client/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 client 18 + 19 + import ( 20 + "time" 21 + ) 22 + 23 + // APIResponse represents a standard API response from HSM operator 24 + type APIResponse struct { 25 + Success bool `json:"success"` 26 + Message string `json:"message,omitempty"` 27 + Data any `json:"data,omitempty"` 28 + Error *APIError `json:"error,omitempty"` 29 + } 30 + 31 + // APIError represents an API error response 32 + type APIError struct { 33 + Code string `json:"code"` 34 + Message string `json:"message"` 35 + Details map[string]any `json:"details,omitempty"` 36 + } 37 + 38 + // SecretData represents the actual secret data 39 + type SecretData struct { 40 + Data map[string]any `json:"data"` 41 + Metadata *SecretInfo `json:"metadata,omitempty"` 42 + } 43 + 44 + // SecretInfo represents information about a secret 45 + type SecretInfo struct { 46 + Label string `json:"label"` 47 + ID uint32 `json:"id"` 48 + Format string `json:"format"` 49 + Description string `json:"description,omitempty"` 50 + Tags map[string]string `json:"tags,omitempty"` 51 + CreatedAt time.Time `json:"created_at"` 52 + UpdatedAt time.Time `json:"updated_at"` 53 + Size int64 `json:"size"` 54 + Checksum string `json:"checksum"` 55 + IsReplicated bool `json:"is_replicated"` 56 + } 57 + 58 + // SecretList represents a list of secrets 59 + type SecretList struct { 60 + Secrets []string `json:"secrets,omitempty"` 61 + Paths []string `json:"paths,omitempty"` 62 + Count int `json:"count"` 63 + Total int `json:"total"` 64 + Page int `json:"page,omitempty"` 65 + PageSize int `json:"page_size,omitempty"` 66 + Prefix string `json:"prefix,omitempty"` 67 + } 68 + 69 + // CreateSecretRequest represents a request to create a secret 70 + type CreateSecretRequest struct { 71 + Data map[string]any `json:"data"` 72 + Description string `json:"description,omitempty"` 73 + Tags map[string]string `json:"tags,omitempty"` 74 + } 75 + 76 + // HealthStatus represents the health status of the HSM operator 77 + type HealthStatus struct { 78 + Status string `json:"status"` 79 + HSMConnected bool `json:"hsm_connected"` 80 + ReplicationEnabled bool `json:"replication_enabled"` 81 + ActiveNodes int `json:"active_nodes"` 82 + Timestamp time.Time `json:"timestamp"` 83 + }
+206
kubectl-hsm/pkg/commands/common.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 commands 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "os" 23 + "path/filepath" 24 + "strings" 25 + "syscall" 26 + "time" 27 + 28 + "golang.org/x/term" 29 + 30 + "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client" 31 + "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/util" 32 + ) 33 + 34 + // CommonOptions holds common options for all commands 35 + type CommonOptions struct { 36 + Namespace string 37 + Output string 38 + Verbose bool 39 + } 40 + 41 + // ClientManager manages the connection to the HSM operator API 42 + type ClientManager struct { 43 + kubectl *util.KubectlUtil 44 + portForward *util.PortForward 45 + hsmClient *client.Client 46 + verbose bool 47 + } 48 + 49 + // NewClientManager creates a new client manager 50 + func NewClientManager(namespace string, verbose bool) (*ClientManager, error) { 51 + kubectl, err := util.NewKubectlUtil(namespace) 52 + if err != nil { 53 + return nil, fmt.Errorf("failed to initialize kubectl utilities: %w", err) 54 + } 55 + 56 + return &ClientManager{ 57 + kubectl: kubectl, 58 + verbose: verbose, 59 + }, nil 60 + } 61 + 62 + // GetClient returns an HSM API client, setting up port forwarding if necessary 63 + func (cm *ClientManager) GetClient(ctx context.Context) (*client.Client, error) { 64 + if cm.hsmClient != nil { 65 + return cm.hsmClient, nil 66 + } 67 + 68 + // Set up port forwarding to the operator 69 + localPort := 8090 // Default port, could be made configurable 70 + pf, err := cm.kubectl.CreatePortForward(ctx, localPort, cm.verbose) 71 + if err != nil { 72 + return nil, err 73 + } 74 + 75 + cm.portForward = pf 76 + 77 + // Create HSM client pointing to the forwarded port 78 + baseURL := fmt.Sprintf("http://localhost:%d", pf.GetLocalPort()) 79 + cm.hsmClient = client.NewClient(baseURL) 80 + 81 + return cm.hsmClient, nil 82 + } 83 + 84 + // Close cleans up the client manager resources 85 + func (cm *ClientManager) Close() { 86 + if cm.portForward != nil { 87 + cm.portForward.Stop() 88 + } 89 + } 90 + 91 + // GetCurrentNamespace returns the current namespace 92 + func (cm *ClientManager) GetCurrentNamespace() string { 93 + return cm.kubectl.GetCurrentNamespace() 94 + } 95 + 96 + // readSecretValue reads a secret value, optionally hiding input for passwords 97 + func readSecretValue(prompt string, hidden bool) (string, error) { 98 + fmt.Print(prompt) 99 + 100 + if hidden { 101 + // Read password without echoing 102 + byteValue, err := term.ReadPassword(int(syscall.Stdin)) 103 + fmt.Println() // Add newline after hidden input 104 + if err != nil { 105 + return "", fmt.Errorf("failed to read hidden input: %w", err) 106 + } 107 + return string(byteValue), nil 108 + } 109 + 110 + // Read normal input 111 + var value string 112 + if _, err := fmt.Scanln(&value); err != nil { 113 + return "", fmt.Errorf("failed to read input: %w", err) 114 + } 115 + return value, nil 116 + } 117 + 118 + // parseFromLiteral parses key=value pairs from --from-literal flags 119 + func parseFromLiteral(literals []string) (map[string]any, error) { 120 + data := make(map[string]any) 121 + 122 + for _, literal := range literals { 123 + parts := strings.SplitN(literal, "=", 2) 124 + if len(parts) != 2 { 125 + return nil, fmt.Errorf("invalid --from-literal format: %s (expected key=value)", literal) 126 + } 127 + data[parts[0]] = parts[1] 128 + } 129 + 130 + return data, nil 131 + } 132 + 133 + // readFromFile reads content from a file for --from-file flags 134 + func readFromFile(key, filename string) (map[string]any, error) { 135 + // Handle both "key=file" and "file" formats 136 + if filename == "" { 137 + filename = key 138 + key = filepath.Base(filename) 139 + // Remove file extension for the key 140 + if ext := filepath.Ext(key); ext != "" { 141 + key = strings.TrimSuffix(key, ext) 142 + } 143 + } 144 + 145 + content, err := os.ReadFile(filename) 146 + if err != nil { 147 + return nil, fmt.Errorf("failed to read file %s: %w", filename, err) 148 + } 149 + 150 + data := map[string]any{ 151 + key: string(content), 152 + } 153 + 154 + return data, nil 155 + } 156 + 157 + // promptForInteractiveInput prompts the user for secret values interactively 158 + func promptForInteractiveInput() (map[string]any, error) { 159 + data := make(map[string]any) 160 + 161 + fmt.Println("Enter secret data (press Enter with empty key to finish):") 162 + 163 + for { 164 + key, err := readSecretValue("Key: ", false) 165 + if err != nil { 166 + return nil, err 167 + } 168 + 169 + if key == "" { 170 + break 171 + } 172 + 173 + // Determine if this looks like a password field 174 + isPassword := strings.Contains(strings.ToLower(key), "password") || 175 + strings.Contains(strings.ToLower(key), "secret") || 176 + strings.Contains(strings.ToLower(key), "token") || 177 + strings.Contains(strings.ToLower(key), "key") 178 + 179 + value, err := readSecretValue(fmt.Sprintf("Value for '%s': ", key), isPassword) 180 + if err != nil { 181 + return nil, err 182 + } 183 + 184 + data[key] = value 185 + } 186 + 187 + if len(data) == 0 { 188 + return nil, fmt.Errorf("no secret data provided") 189 + } 190 + 191 + return data, nil 192 + } 193 + 194 + // formatDuration formats a time duration in a human-readable way 195 + func formatDuration(d time.Duration) string { 196 + if d < time.Minute { 197 + return fmt.Sprintf("%ds ago", int(d.Seconds())) 198 + } 199 + if d < time.Hour { 200 + return fmt.Sprintf("%dm ago", int(d.Minutes())) 201 + } 202 + if d < 24*time.Hour { 203 + return fmt.Sprintf("%dh ago", int(d.Hours())) 204 + } 205 + return fmt.Sprintf("%dd ago", int(d.Hours()/24)) 206 + }
+167
kubectl-hsm/pkg/commands/create.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 commands 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "strings" 23 + 24 + "github.com/spf13/cobra" 25 + ) 26 + 27 + // CreateOptions holds options for the create command 28 + type CreateOptions struct { 29 + CommonOptions 30 + FromLiteral []string 31 + FromFile []string 32 + Interactive bool 33 + } 34 + 35 + // NewCreateCmd creates the create command 36 + func NewCreateCmd() *cobra.Command { 37 + opts := &CreateOptions{} 38 + 39 + cmd := &cobra.Command{ 40 + Use: "create SECRET_NAME [flags]", 41 + Short: "Create a new HSM secret", 42 + Long: `Create a new secret in the HSM. 43 + 44 + The secret data can be provided in several ways: 45 + - --from-literal key=value: Specify key-value pairs directly 46 + - --from-file key=path: Load values from files 47 + - --interactive: Prompt for values interactively (recommended for passwords) 48 + 49 + Examples: 50 + # Create secret with literal values 51 + kubectl hsm create database-creds --from-literal username=admin --from-literal password=secret123 52 + 53 + # Load values from files 54 + kubectl hsm create tls-cert --from-file cert=server.crt --from-file key=server.key 55 + 56 + # Interactive creation (prompts for input, hides passwords) 57 + kubectl hsm create api-keys --interactive`, 58 + Args: cobra.ExactArgs(1), 59 + RunE: func(cmd *cobra.Command, args []string) error { 60 + return opts.Run(cmd.Context(), args[0]) 61 + }, 62 + } 63 + 64 + cmd.Flags().StringArrayVar(&opts.FromLiteral, "from-literal", nil, "Specify a key and literal value to insert in secret (i.e. --from-literal key=value)") 65 + cmd.Flags().StringArrayVar(&opts.FromFile, "from-file", nil, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used") 66 + cmd.Flags().BoolVar(&opts.Interactive, "interactive", false, "Prompt for secret values interactively") 67 + cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace") 68 + cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)") 69 + cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details") 70 + 71 + return cmd 72 + } 73 + 74 + // Run executes the create command 75 + func (opts *CreateOptions) Run(ctx context.Context, secretName string) error { 76 + // Validate secret name 77 + if secretName == "" { 78 + return fmt.Errorf("secret name is required") 79 + } 80 + 81 + // Validate options 82 + methods := 0 83 + if len(opts.FromLiteral) > 0 { 84 + methods++ 85 + } 86 + if len(opts.FromFile) > 0 { 87 + methods++ 88 + } 89 + if opts.Interactive { 90 + methods++ 91 + } 92 + 93 + if methods == 0 { 94 + return fmt.Errorf("must specify one of --from-literal, --from-file, or --interactive") 95 + } 96 + if methods > 1 { 97 + return fmt.Errorf("cannot specify multiple input methods (--from-literal, --from-file, --interactive)") 98 + } 99 + 100 + // Collect secret data 101 + var secretData map[string]any 102 + var err error 103 + 104 + if len(opts.FromLiteral) > 0 { 105 + secretData, err = parseFromLiteral(opts.FromLiteral) 106 + if err != nil { 107 + return err 108 + } 109 + } else if len(opts.FromFile) > 0 { 110 + secretData = make(map[string]any) 111 + for _, fileSpec := range opts.FromFile { 112 + parts := strings.SplitN(fileSpec, "=", 2) 113 + var key, filename string 114 + if len(parts) == 2 { 115 + key = parts[0] 116 + filename = parts[1] 117 + } else { 118 + filename = parts[0] 119 + } 120 + 121 + fileData, err := readFromFile(key, filename) 122 + if err != nil { 123 + return err 124 + } 125 + 126 + // Merge file data 127 + for k, v := range fileData { 128 + secretData[k] = v 129 + } 130 + } 131 + } else if opts.Interactive { 132 + secretData, err = promptForInteractiveInput() 133 + if err != nil { 134 + return err 135 + } 136 + } 137 + 138 + // Create client manager 139 + cm, err := NewClientManager(opts.Namespace, opts.Verbose) 140 + if err != nil { 141 + return err 142 + } 143 + defer cm.Close() 144 + 145 + // Get HSM client 146 + hsmClient, err := cm.GetClient(ctx) 147 + if err != nil { 148 + return err 149 + } 150 + 151 + // Create the secret 152 + fmt.Printf("Creating secret '%s' in namespace '%s'...\n", secretName, cm.GetCurrentNamespace()) 153 + if err := hsmClient.CreateSecret(ctx, secretName, secretData); err != nil { 154 + return fmt.Errorf("failed to create secret: %w", err) 155 + } 156 + 157 + fmt.Printf("Secret '%s' created successfully.\n", secretName) 158 + 159 + // Show how to retrieve the secret 160 + fmt.Printf("\nTo view the secret:\n") 161 + fmt.Printf(" kubectl hsm get %s\n", secretName) 162 + if opts.Namespace != "" { 163 + fmt.Printf(" kubectl hsm get %s -n %s\n", secretName, opts.Namespace) 164 + } 165 + 166 + return nil 167 + }
+136
kubectl-hsm/pkg/commands/delete.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 commands 18 + 19 + import ( 20 + "bufio" 21 + "context" 22 + "fmt" 23 + "os" 24 + "strings" 25 + 26 + "github.com/spf13/cobra" 27 + ) 28 + 29 + // DeleteOptions holds options for the delete command 30 + type DeleteOptions struct { 31 + CommonOptions 32 + Key string 33 + Force bool 34 + } 35 + 36 + // NewDeleteCmd creates the delete command 37 + func NewDeleteCmd() *cobra.Command { 38 + opts := &DeleteOptions{} 39 + 40 + cmd := &cobra.Command{ 41 + Use: "delete SECRET_NAME [flags]", 42 + Short: "Delete an HSM secret", 43 + Long: `Delete an HSM secret or a specific key within a secret. 44 + 45 + By default, deletes the entire secret. Use --key to delete only a specific key. 46 + 47 + Examples: 48 + # Delete an entire secret (with confirmation) 49 + kubectl hsm delete database-creds 50 + 51 + # Delete a specific key from a secret 52 + kubectl hsm delete database-creds --key password 53 + 54 + # Force delete without confirmation 55 + kubectl hsm delete api-keys --force`, 56 + Args: cobra.ExactArgs(1), 57 + RunE: func(cmd *cobra.Command, args []string) error { 58 + return opts.Run(cmd.Context(), args[0]) 59 + }, 60 + } 61 + 62 + cmd.Flags().StringVar(&opts.Key, "key", "", "Delete only the specified key from the secret") 63 + cmd.Flags().BoolVar(&opts.Force, "force", false, "Skip confirmation prompt") 64 + cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace") 65 + cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details") 66 + 67 + return cmd 68 + } 69 + 70 + // Run executes the delete command 71 + func (opts *DeleteOptions) Run(ctx context.Context, secretName string) error { 72 + // Validate secret name 73 + if secretName == "" { 74 + return fmt.Errorf("secret name is required") 75 + } 76 + 77 + // Create client manager 78 + cm, err := NewClientManager(opts.Namespace, opts.Verbose) 79 + if err != nil { 80 + return err 81 + } 82 + defer cm.Close() 83 + 84 + // Get HSM client 85 + hsmClient, err := cm.GetClient(ctx) 86 + if err != nil { 87 + return err 88 + } 89 + 90 + if opts.Key != "" { 91 + return fmt.Errorf("deleting individual keys is not yet supported - please delete the entire secret and recreate it") 92 + } 93 + 94 + // Confirm deletion unless force is specified 95 + if !opts.Force { 96 + if err := opts.confirmDeletion(secretName); err != nil { 97 + return err 98 + } 99 + } 100 + 101 + // Delete the secret 102 + fmt.Printf("Deleting secret '%s' from namespace '%s'...\n", secretName, cm.GetCurrentNamespace()) 103 + if err := hsmClient.DeleteSecret(ctx, secretName); err != nil { 104 + return fmt.Errorf("failed to delete secret: %w", err) 105 + } 106 + 107 + fmt.Printf("Secret '%s' deleted successfully.\n", secretName) 108 + return nil 109 + } 110 + 111 + // confirmDeletion prompts the user to confirm the deletion 112 + func (opts *DeleteOptions) confirmDeletion(secretName string) error { 113 + var target string 114 + if opts.Key != "" { 115 + target = fmt.Sprintf("key '%s' from secret '%s'", opts.Key, secretName) 116 + } else { 117 + target = fmt.Sprintf("secret '%s'", secretName) 118 + } 119 + 120 + fmt.Printf("⚠️ You are about to delete %s.\n", target) 121 + fmt.Printf("This action cannot be undone.\n\n") 122 + fmt.Printf("Type the secret name '%s' to confirm: ", secretName) 123 + 124 + reader := bufio.NewReader(os.Stdin) 125 + input, err := reader.ReadString('\n') 126 + if err != nil { 127 + return fmt.Errorf("failed to read confirmation: %w", err) 128 + } 129 + 130 + input = strings.TrimSpace(input) 131 + if input != secretName { 132 + return fmt.Errorf("confirmation failed - deletion cancelled") 133 + } 134 + 135 + return nil 136 + }
+264
kubectl-hsm/pkg/commands/get.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 commands 18 + 19 + import ( 20 + "context" 21 + "encoding/base64" 22 + "encoding/json" 23 + "fmt" 24 + "sort" 25 + "strings" 26 + 27 + "github.com/spf13/cobra" 28 + "sigs.k8s.io/yaml" 29 + 30 + "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client" 31 + ) 32 + 33 + // GetOptions holds options for the get command 34 + type GetOptions struct { 35 + CommonOptions 36 + Key string 37 + } 38 + 39 + // NewGetCmd creates the get command 40 + func NewGetCmd() *cobra.Command { 41 + opts := &GetOptions{} 42 + 43 + cmd := &cobra.Command{ 44 + Use: "get SECRET_NAME [flags]", 45 + Short: "Get an HSM secret", 46 + Long: `Retrieve and display an HSM secret. 47 + 48 + By default, displays all keys in the secret. Use --key to display only a specific key. 49 + 50 + Examples: 51 + # Get all keys in a secret 52 + kubectl hsm get database-creds 53 + 54 + # Get a specific key from a secret 55 + kubectl hsm get database-creds --key password 56 + 57 + # Output in different formats 58 + kubectl hsm get api-keys -o json 59 + kubectl hsm get api-keys -o yaml`, 60 + Args: cobra.ExactArgs(1), 61 + RunE: func(cmd *cobra.Command, args []string) error { 62 + return opts.Run(cmd.Context(), args[0]) 63 + }, 64 + } 65 + 66 + cmd.Flags().StringVar(&opts.Key, "key", "", "Show only the specified key") 67 + cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace") 68 + cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)") 69 + cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details") 70 + 71 + return cmd 72 + } 73 + 74 + // Run executes the get command 75 + func (opts *GetOptions) Run(ctx context.Context, secretName string) error { 76 + // Validate secret name 77 + if secretName == "" { 78 + return fmt.Errorf("secret name is required") 79 + } 80 + 81 + // Create client manager 82 + cm, err := NewClientManager(opts.Namespace, opts.Verbose) 83 + if err != nil { 84 + return err 85 + } 86 + defer cm.Close() 87 + 88 + // Get HSM client 89 + hsmClient, err := cm.GetClient(ctx) 90 + if err != nil { 91 + return err 92 + } 93 + 94 + // Retrieve the secret 95 + secretData, err := hsmClient.GetSecret(ctx, secretName) 96 + if err != nil { 97 + return fmt.Errorf("failed to get secret: %w", err) 98 + } 99 + 100 + // Handle specific key request 101 + if opts.Key != "" { 102 + value, exists := secretData.Data[opts.Key] 103 + if !exists { 104 + return fmt.Errorf("key '%s' not found in secret '%s'", opts.Key, secretName) 105 + } 106 + 107 + switch opts.Output { 108 + case "json": 109 + keyData := map[string]any{opts.Key: value} 110 + jsonBytes, err := json.MarshalIndent(keyData, "", " ") 111 + if err != nil { 112 + return fmt.Errorf("failed to marshal key data to JSON: %w", err) 113 + } 114 + fmt.Println(string(jsonBytes)) 115 + case "yaml": 116 + keyData := map[string]any{opts.Key: value} 117 + yamlBytes, err := yaml.Marshal(keyData) 118 + if err != nil { 119 + return fmt.Errorf("failed to marshal key data to YAML: %w", err) 120 + } 121 + fmt.Print(string(yamlBytes)) 122 + default: 123 + fmt.Printf("%s\n", value) 124 + } 125 + return nil 126 + } 127 + 128 + // Display full secret 129 + switch opts.Output { 130 + case "json": 131 + jsonBytes, err := json.MarshalIndent(secretData, "", " ") 132 + if err != nil { 133 + return fmt.Errorf("failed to marshal secret data to JSON: %w", err) 134 + } 135 + fmt.Println(string(jsonBytes)) 136 + case "yaml": 137 + yamlBytes, err := yaml.Marshal(secretData) 138 + if err != nil { 139 + return fmt.Errorf("failed to marshal secret data to YAML: %w", err) 140 + } 141 + fmt.Print(string(yamlBytes)) 142 + default: 143 + return opts.displaySecretText(secretName, secretData, cm.GetCurrentNamespace()) 144 + } 145 + 146 + return nil 147 + } 148 + 149 + // displaySecretText displays the secret in a human-readable text format 150 + func (opts *GetOptions) displaySecretText(secretName string, secretData *client.SecretData, namespace string) error { 151 + fmt.Printf("Name: %s\n", secretName) 152 + fmt.Printf("Namespace: %s\n", namespace) 153 + 154 + 155 + // Parse metadata from _metadata key if present 156 + if metadataValue, hasMetadata := secretData.Data["_metadata"]; hasMetadata { 157 + if err := opts.parseAndDisplayMetadata(metadataValue); err != nil { 158 + fmt.Printf("Metadata: <parse error: %v>\n", err) 159 + } 160 + } 161 + 162 + // Display data keys (but not values for security, and exclude _metadata) 163 + var keys []string 164 + for k := range secretData.Data { 165 + if k != "_metadata" { 166 + keys = append(keys, k) 167 + } 168 + } 169 + 170 + if len(keys) > 0 { 171 + sort.Strings(keys) 172 + fmt.Printf("Keys: %s\n", strings.Join(keys, ", ")) 173 + } else { 174 + fmt.Printf("Keys: <none>\n") 175 + } 176 + 177 + return nil 178 + } 179 + 180 + // parseAndDisplayMetadata parses and displays metadata from the _metadata key 181 + func (opts *GetOptions) parseAndDisplayMetadata(metadataValue any) error { 182 + var metadataMap map[string]any 183 + 184 + switch v := metadataValue.(type) { 185 + case string: 186 + // First try to decode as base64, then parse JSON 187 + decoded, err := base64.StdEncoding.DecodeString(v) 188 + if err != nil { 189 + // If base64 decode fails, try direct JSON parsing 190 + if jsonErr := json.Unmarshal([]byte(v), &metadataMap); jsonErr != nil { 191 + return fmt.Errorf("failed to parse metadata (not base64: %v, not JSON: %v)", err, jsonErr) 192 + } 193 + } else { 194 + // Parse the decoded base64 as JSON 195 + if err := json.Unmarshal(decoded, &metadataMap); err != nil { 196 + return fmt.Errorf("failed to parse base64-decoded metadata JSON: %w", err) 197 + } 198 + } 199 + case map[string]any: 200 + metadataMap = v 201 + default: 202 + return fmt.Errorf("unexpected metadata type: %T", v) 203 + } 204 + 205 + // Display all metadata fields with proper formatting 206 + opts.displayMetadataFields(metadataMap, "") 207 + 208 + return nil 209 + } 210 + 211 + // displayMetadataFields displays metadata fields with proper key formatting 212 + func (opts *GetOptions) displayMetadataFields(data map[string]any, indent string) { 213 + // Get all keys and sort them for consistent output 214 + keys := make([]string, 0, len(data)) 215 + for k := range data { 216 + keys = append(keys, k) 217 + } 218 + sort.Strings(keys) 219 + 220 + for _, key := range keys { 221 + value := data[key] 222 + 223 + // Handle nested objects (like labels) 224 + if nested, ok := value.(map[string]any); ok { 225 + // Display the parent key with capitalization 226 + displayKey := opts.formatMetadataKey(key) 227 + fmt.Printf("%s%-13s\n", indent, displayKey+":") 228 + 229 + // Display nested items with indentation, keeping original keys 230 + nestedKeys := make([]string, 0, len(nested)) 231 + for k := range nested { 232 + nestedKeys = append(nestedKeys, k) 233 + } 234 + sort.Strings(nestedKeys) 235 + 236 + for _, nestedKey := range nestedKeys { 237 + fmt.Printf("%s %-11s %v\n", indent, nestedKey+":", nested[nestedKey]) 238 + } 239 + continue 240 + } 241 + 242 + // Format the display key (capitalize first letter, replace underscores with spaces) 243 + displayKey := opts.formatMetadataKey(key) 244 + 245 + // Display the key-value pair 246 + fmt.Printf("%s%-13s %v\n", indent, displayKey+":", value) 247 + } 248 + } 249 + 250 + // formatMetadataKey formats a metadata key for display 251 + func (opts *GetOptions) formatMetadataKey(key string) string { 252 + // Replace underscores with spaces 253 + formatted := strings.ReplaceAll(key, "_", " ") 254 + 255 + // Split into words and capitalize each word 256 + words := strings.Fields(formatted) 257 + for i, word := range words { 258 + if len(word) > 0 { 259 + words[i] = strings.ToUpper(word[:1]) + word[1:] 260 + } 261 + } 262 + 263 + return strings.Join(words, " ") 264 + }
+165
kubectl-hsm/pkg/commands/health.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 commands 18 + 19 + import ( 20 + "context" 21 + "encoding/json" 22 + "fmt" 23 + 24 + "github.com/spf13/cobra" 25 + "sigs.k8s.io/yaml" 26 + 27 + "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client" 28 + ) 29 + 30 + // HealthOptions holds options for the health command 31 + type HealthOptions struct { 32 + CommonOptions 33 + } 34 + 35 + // NewHealthCmd creates the health command 36 + func NewHealthCmd() *cobra.Command { 37 + opts := &HealthOptions{} 38 + 39 + cmd := &cobra.Command{ 40 + Use: "health [flags]", 41 + Short: "Check HSM operator health", 42 + Long: `Check the health status of the HSM operator and connected devices. 43 + 44 + This command verifies: 45 + - Connection to the HSM operator API 46 + - HSM device connectivity 47 + - Replication status 48 + - Active agent nodes 49 + 50 + Examples: 51 + # Check health status 52 + kubectl hsm health 53 + 54 + # Check health with JSON output 55 + kubectl hsm health -o json`, 56 + Args: cobra.NoArgs, 57 + RunE: func(cmd *cobra.Command, args []string) error { 58 + return opts.Run(cmd.Context()) 59 + }, 60 + } 61 + 62 + cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace") 63 + cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)") 64 + cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details") 65 + 66 + return cmd 67 + } 68 + 69 + // Run executes the health command 70 + func (opts *HealthOptions) Run(ctx context.Context) error { 71 + // Create client manager 72 + cm, err := NewClientManager(opts.Namespace, opts.Verbose) 73 + if err != nil { 74 + return err 75 + } 76 + defer cm.Close() 77 + 78 + // Get HSM client 79 + hsmClient, err := cm.GetClient(ctx) 80 + if err != nil { 81 + return fmt.Errorf("failed to connect to HSM operator: %w", err) 82 + } 83 + 84 + // Get health status 85 + health, err := hsmClient.GetHealth(ctx) 86 + if err != nil { 87 + return fmt.Errorf("failed to get health status: %w", err) 88 + } 89 + 90 + // Handle output formatting 91 + switch opts.Output { 92 + case "json": 93 + jsonBytes, err := json.MarshalIndent(health, "", " ") 94 + if err != nil { 95 + return fmt.Errorf("failed to marshal health status to JSON: %w", err) 96 + } 97 + fmt.Println(string(jsonBytes)) 98 + case "yaml": 99 + yamlBytes, err := yaml.Marshal(health) 100 + if err != nil { 101 + return fmt.Errorf("failed to marshal health status to YAML: %w", err) 102 + } 103 + fmt.Print(string(yamlBytes)) 104 + default: 105 + return opts.displayHealthText(health, cm.GetCurrentNamespace()) 106 + } 107 + 108 + return nil 109 + } 110 + 111 + // displayHealthText displays the health status in a human-readable format 112 + func (opts *HealthOptions) displayHealthText(health *client.HealthStatus, namespace string) error { 113 + fmt.Printf("HSM Operator Health Status\n") 114 + fmt.Printf("==========================\n\n") 115 + 116 + // Overall status with emoji 117 + statusEmoji := "✅" 118 + if health.Status == "degraded" { 119 + statusEmoji = "⚠️" 120 + } else if health.Status == "unhealthy" { 121 + statusEmoji = "❌" 122 + } 123 + 124 + fmt.Printf("Overall Status: %s %s\n", statusEmoji, health.Status) 125 + fmt.Printf("Namespace: %s\n", namespace) 126 + fmt.Printf("Check Time: %s\n", health.Timestamp.Format("2006-01-02 15:04:05 UTC")) 127 + fmt.Printf("\n") 128 + 129 + // HSM connectivity 130 + hsmEmoji := "✅" 131 + if !health.HSMConnected { 132 + hsmEmoji = "❌" 133 + } 134 + fmt.Printf("HSM Connected: %s %t\n", hsmEmoji, health.HSMConnected) 135 + 136 + // Replication status 137 + replicationEmoji := "✅" 138 + if !health.ReplicationEnabled { 139 + replicationEmoji = "⚠️" 140 + } 141 + fmt.Printf("Replication: %s %t\n", replicationEmoji, health.ReplicationEnabled) 142 + fmt.Printf("Active Nodes: %d\n", health.ActiveNodes) 143 + fmt.Printf("\n") 144 + 145 + // Recommendations 146 + if !health.HSMConnected { 147 + fmt.Printf("⚠️ Recommendations:\n") 148 + fmt.Printf(" • Check if HSM devices are connected and accessible\n") 149 + fmt.Printf(" • Verify HSM agent pods are running: kubectl get pods -l app.kubernetes.io/component=agent\n") 150 + fmt.Printf(" • Check agent logs for connection errors\n") 151 + } 152 + 153 + if !health.ReplicationEnabled && health.ActiveNodes <= 1 { 154 + fmt.Printf("💡 Recommendations:\n") 155 + fmt.Printf(" • Consider adding more HSM devices for high availability\n") 156 + fmt.Printf(" • Multiple devices enable automatic replication and failover\n") 157 + } 158 + 159 + // Overall assessment 160 + if health.Status == "healthy" { 161 + fmt.Printf("🎉 All systems operational!\n") 162 + } 163 + 164 + return nil 165 + }
+220
kubectl-hsm/pkg/commands/list.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 commands 18 + 19 + import ( 20 + "context" 21 + "encoding/json" 22 + "fmt" 23 + "os" 24 + "sort" 25 + "text/tabwriter" 26 + 27 + "github.com/spf13/cobra" 28 + "sigs.k8s.io/yaml" 29 + 30 + "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client" 31 + ) 32 + 33 + // ListOptions holds options for the list command 34 + type ListOptions struct { 35 + CommonOptions 36 + AllNamespaces bool 37 + } 38 + 39 + // NewListCmd creates the list command 40 + func NewListCmd() *cobra.Command { 41 + opts := &ListOptions{} 42 + 43 + cmd := &cobra.Command{ 44 + Use: "list [flags]", 45 + Short: "List HSM secrets", 46 + Long: `List all secrets stored in the HSM. 47 + 48 + Examples: 49 + # List secrets in current namespace 50 + kubectl hsm list 51 + 52 + # List secrets in specific namespace 53 + kubectl hsm list -n hsm-secrets-operator-system 54 + 55 + # List secrets in all namespaces (Note: HSM secrets are global, namespace is for display only) 56 + kubectl hsm list --all-namespaces 57 + 58 + # Output in different formats 59 + kubectl hsm list -o json 60 + kubectl hsm list -o yaml`, 61 + Args: cobra.NoArgs, 62 + RunE: func(cmd *cobra.Command, args []string) error { 63 + return opts.Run(cmd.Context()) 64 + }, 65 + } 66 + 67 + cmd.Flags().BoolVar(&opts.AllNamespaces, "all-namespaces", false, "List secrets from all namespaces") 68 + cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace") 69 + cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)") 70 + cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details") 71 + 72 + return cmd 73 + } 74 + 75 + // Run executes the list command 76 + func (opts *ListOptions) Run(ctx context.Context) error { 77 + // Create client manager 78 + cm, err := NewClientManager(opts.Namespace, opts.Verbose) 79 + if err != nil { 80 + return err 81 + } 82 + defer cm.Close() 83 + 84 + // Get HSM client 85 + hsmClient, err := cm.GetClient(ctx) 86 + if err != nil { 87 + return err 88 + } 89 + 90 + // List secrets (HSM secrets are global, but we display namespace context) 91 + secretList, err := hsmClient.ListSecrets(ctx, 0, 0) // No pagination for now 92 + if err != nil { 93 + return fmt.Errorf("failed to list secrets: %w", err) 94 + } 95 + 96 + // Handle output formatting 97 + switch opts.Output { 98 + case "json": 99 + jsonBytes, err := json.MarshalIndent(secretList, "", " ") 100 + if err != nil { 101 + return fmt.Errorf("failed to marshal secrets to JSON: %w", err) 102 + } 103 + fmt.Println(string(jsonBytes)) 104 + case "yaml": 105 + yamlBytes, err := yaml.Marshal(secretList) 106 + if err != nil { 107 + return fmt.Errorf("failed to marshal secrets to YAML: %w", err) 108 + } 109 + fmt.Print(string(yamlBytes)) 110 + default: 111 + return opts.displaySecretsText(secretList, cm.GetCurrentNamespace()) 112 + } 113 + 114 + return nil 115 + } 116 + 117 + // displaySecretsText displays the secrets in a human-readable table format 118 + func (opts *ListOptions) displaySecretsText(secretList *client.SecretList, currentNamespace string) error { 119 + if secretList == nil { 120 + fmt.Println("No secrets found") 121 + return nil 122 + } 123 + 124 + // The API returns secret names as strings, not full SecretInfo objects 125 + if len(secretList.Secrets) == 0 { 126 + fmt.Println("No secrets found") 127 + return nil 128 + } 129 + 130 + // Sort secret names for consistent output 131 + secrets := make([]string, len(secretList.Secrets)) 132 + copy(secrets, secretList.Secrets) 133 + sort.Strings(secrets) 134 + 135 + // Create table writer 136 + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) 137 + 138 + // Print header 139 + if opts.AllNamespaces { 140 + fmt.Fprintln(w, "NAMESPACE\tNAME") 141 + } else { 142 + fmt.Fprintln(w, "NAME") 143 + } 144 + 145 + // Print each secret name 146 + for _, secret := range secrets { 147 + if opts.AllNamespaces { 148 + fmt.Fprintf(w, "%s\t%s\n", currentNamespace, secret) 149 + } else { 150 + fmt.Fprintf(w, "%s\n", secret) 151 + } 152 + } 153 + 154 + w.Flush() 155 + 156 + // Show summary 157 + fmt.Printf("\nTotal: %d secrets\n", secretList.Count) 158 + 159 + return nil 160 + } 161 + 162 + // displaySecretPaths displays just the secret paths when detailed info isn't available 163 + func (opts *ListOptions) displaySecretPaths(secretList *client.SecretList, currentNamespace string) error { 164 + if len(secretList.Paths) == 0 { 165 + fmt.Println("No secrets found") 166 + return nil 167 + } 168 + 169 + // Sort paths for consistent output 170 + paths := make([]string, len(secretList.Paths)) 171 + copy(paths, secretList.Paths) 172 + sort.Strings(paths) 173 + 174 + // Create table writer 175 + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) 176 + 177 + // Print header 178 + if opts.AllNamespaces { 179 + fmt.Fprintln(w, "NAMESPACE\tNAME") 180 + } else { 181 + fmt.Fprintln(w, "NAME") 182 + } 183 + 184 + // Print each path 185 + for _, path := range paths { 186 + if opts.AllNamespaces { 187 + fmt.Fprintf(w, "%s\t%s\n", currentNamespace, path) 188 + } else { 189 + fmt.Fprintf(w, "%s\n", path) 190 + } 191 + } 192 + 193 + w.Flush() 194 + 195 + // Show summary 196 + fmt.Printf("\nTotal: %d secrets\n", secretList.Count) 197 + 198 + return nil 199 + } 200 + 201 + // formatBytes formats a byte count in human-readable format 202 + func formatBytes(bytes int64) string { 203 + if bytes < 1024 { 204 + return fmt.Sprintf("%dB", bytes) 205 + } 206 + 207 + units := []string{"B", "KB", "MB", "GB"} 208 + size := float64(bytes) 209 + unitIndex := 0 210 + 211 + for unitIndex < len(units)-1 && size >= 1024 { 212 + size /= 1024 213 + unitIndex++ 214 + } 215 + 216 + if size == float64(int64(size)) { 217 + return fmt.Sprintf("%.0f%s", size, units[unitIndex]) 218 + } 219 + return fmt.Sprintf("%.1f%s", size, units[unitIndex]) 220 + }
+265
kubectl-hsm/pkg/util/kubectl.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 util 18 + 19 + import ( 20 + "context" 21 + "fmt" 22 + "io" 23 + "net/http" 24 + "os" 25 + "path/filepath" 26 + "time" 27 + 28 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 + "k8s.io/client-go/kubernetes" 30 + "k8s.io/client-go/rest" 31 + "k8s.io/client-go/tools/clientcmd" 32 + "k8s.io/client-go/tools/portforward" 33 + "k8s.io/client-go/transport/spdy" 34 + ) 35 + 36 + const ( 37 + operatorServiceName = "hsm-secrets-operator-api" 38 + operatorServicePort = 8090 39 + ) 40 + 41 + // KubectlUtil provides kubectl integration utilities 42 + type KubectlUtil struct { 43 + config *rest.Config 44 + clientset *kubernetes.Clientset 45 + namespace string 46 + } 47 + 48 + // NewKubectlUtil creates a new kubectl utility instance 49 + func NewKubectlUtil(namespace string) (*KubectlUtil, error) { 50 + config, err := getKubeConfig() 51 + if err != nil { 52 + return nil, fmt.Errorf("failed to get kubernetes config: %w", err) 53 + } 54 + 55 + clientset, err := kubernetes.NewForConfig(config) 56 + if err != nil { 57 + return nil, fmt.Errorf("failed to create kubernetes clientset: %w", err) 58 + } 59 + 60 + // Use provided namespace or get current namespace from kubeconfig 61 + if namespace == "" { 62 + namespace, err = getCurrentNamespace() 63 + if err != nil { 64 + return nil, fmt.Errorf("failed to get current namespace: %w", err) 65 + } 66 + } 67 + 68 + return &KubectlUtil{ 69 + config: config, 70 + clientset: clientset, 71 + namespace: namespace, 72 + }, nil 73 + } 74 + 75 + // GetCurrentNamespace returns the current namespace from kubeconfig 76 + func (k *KubectlUtil) GetCurrentNamespace() string { 77 + return k.namespace 78 + } 79 + 80 + // FindOperatorService finds the HSM operator service in the current namespace 81 + func (k *KubectlUtil) FindOperatorService(ctx context.Context) error { 82 + svc, err := k.clientset.CoreV1().Services(k.namespace).Get(ctx, operatorServiceName, metav1.GetOptions{}) 83 + if err != nil { 84 + return fmt.Errorf("HSM secrets operator service not found in namespace '%s': %w\n\nPlease check:\n - Is the operator installed? Try: kubectl get deploy -n %s\n - Are you in the correct namespace? Try: kubens <operator-namespace>", 85 + k.namespace, err, k.namespace) 86 + } 87 + 88 + // Check if service has the expected port 89 + found := false 90 + for _, port := range svc.Spec.Ports { 91 + if port.Port == operatorServicePort { 92 + found = true 93 + break 94 + } 95 + } 96 + 97 + if !found { 98 + return fmt.Errorf("operator service '%s' does not expose port %d", operatorServiceName, operatorServicePort) 99 + } 100 + 101 + return nil 102 + } 103 + 104 + // CreatePortForward creates a port forward to the operator service 105 + func (k *KubectlUtil) CreatePortForward(ctx context.Context, localPort int, verbose bool) (*PortForward, error) { 106 + // First check if the service exists 107 + if err := k.FindOperatorService(ctx); err != nil { 108 + return nil, err 109 + } 110 + 111 + // Get a pod from the operator deployment 112 + pods, err := k.clientset.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{ 113 + LabelSelector: "app.kubernetes.io/name=hsm-secrets-operator,control-plane=controller-manager", 114 + }) 115 + if err != nil { 116 + return nil, fmt.Errorf("failed to list operator pods: %w", err) 117 + } 118 + 119 + if len(pods.Items) == 0 { 120 + return nil, fmt.Errorf("no operator manager pods found in namespace '%s'", k.namespace) 121 + } 122 + 123 + pod := pods.Items[0] 124 + if pod.Status.Phase != "Running" { 125 + return nil, fmt.Errorf("operator pod '%s' is not running (status: %s)", pod.Name, pod.Status.Phase) 126 + } 127 + 128 + // Create port forward 129 + pf := &PortForward{ 130 + config: k.config, 131 + clientset: k.clientset, 132 + namespace: k.namespace, 133 + podName: pod.Name, 134 + localPort: localPort, 135 + remotePort: operatorServicePort, 136 + stopCh: make(chan struct{}), 137 + readyCh: make(chan struct{}), 138 + verbose: verbose, 139 + } 140 + 141 + if err := pf.Start(ctx); err != nil { 142 + return nil, fmt.Errorf("failed to start port forward: %w", err) 143 + } 144 + 145 + return pf, nil 146 + } 147 + 148 + // PortForward manages a port forward connection 149 + type PortForward struct { 150 + config *rest.Config 151 + clientset *kubernetes.Clientset 152 + namespace string 153 + podName string 154 + localPort int 155 + remotePort int 156 + stopCh chan struct{} 157 + readyCh chan struct{} 158 + verbose bool 159 + } 160 + 161 + // Start starts the port forward 162 + func (pf *PortForward) Start(ctx context.Context) error { 163 + req := pf.clientset.CoreV1().RESTClient().Post(). 164 + Resource("pods"). 165 + Namespace(pf.namespace). 166 + Name(pf.podName). 167 + SubResource("portforward") 168 + 169 + transport, upgrader, err := spdy.RoundTripperFor(pf.config) 170 + if err != nil { 171 + return fmt.Errorf("failed to create round tripper: %w", err) 172 + } 173 + 174 + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) 175 + 176 + ports := []string{fmt.Sprintf("%d:%d", pf.localPort, pf.remotePort)} 177 + 178 + // Control output based on verbose flag 179 + var stdout, stderr io.Writer 180 + if pf.verbose { 181 + stdout = os.Stdout 182 + stderr = os.Stderr 183 + } else { 184 + stdout = io.Discard 185 + stderr = io.Discard 186 + } 187 + 188 + forwarder, err := portforward.New(dialer, ports, pf.stopCh, pf.readyCh, stdout, stderr) 189 + if err != nil { 190 + return fmt.Errorf("failed to create port forwarder: %w", err) 191 + } 192 + 193 + go func() { 194 + if err := forwarder.ForwardPorts(); err != nil && pf.verbose { 195 + fmt.Fprintf(os.Stderr, "Port forward error: %v\n", err) 196 + } 197 + }() 198 + 199 + // Wait for port forward to be ready with timeout 200 + select { 201 + case <-pf.readyCh: 202 + return nil 203 + case <-time.After(10 * time.Second): 204 + pf.Stop() 205 + return fmt.Errorf("port forward did not become ready within 10 seconds") 206 + case <-ctx.Done(): 207 + pf.Stop() 208 + return ctx.Err() 209 + } 210 + } 211 + 212 + // Stop stops the port forward 213 + func (pf *PortForward) Stop() { 214 + close(pf.stopCh) 215 + } 216 + 217 + // GetLocalPort returns the local port being forwarded 218 + func (pf *PortForward) GetLocalPort() int { 219 + return pf.localPort 220 + } 221 + 222 + // getKubeConfig gets the Kubernetes client configuration 223 + func getKubeConfig() (*rest.Config, error) { 224 + // Try in-cluster config first (for when running in pod) 225 + if config, err := rest.InClusterConfig(); err == nil { 226 + return config, nil 227 + } 228 + 229 + // Fall back to kubeconfig file 230 + kubeconfig := os.Getenv("KUBECONFIG") 231 + if kubeconfig == "" { 232 + kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") 233 + } 234 + 235 + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 236 + if err != nil { 237 + return nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err) 238 + } 239 + 240 + return config, nil 241 + } 242 + 243 + // getCurrentNamespace gets the current namespace from kubeconfig 244 + func getCurrentNamespace() (string, error) { 245 + kubeconfig := os.Getenv("KUBECONFIG") 246 + if kubeconfig == "" { 247 + kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") 248 + } 249 + 250 + configLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 251 + &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, 252 + &clientcmd.ConfigOverrides{}, 253 + ) 254 + 255 + namespace, _, err := configLoader.Namespace() 256 + if err != nil { 257 + return "", fmt.Errorf("failed to get namespace from kubeconfig: %w", err) 258 + } 259 + 260 + if namespace == "" { 261 + namespace = "default" 262 + } 263 + 264 + return namespace, nil 265 + }