···4455## Directory Structure
6677-- **[basic/](basic/)** - Basic usage examples for getting started
77+- **[basic/](basic/)** - Basic CRD resource examples for getting started
88- **[advanced/](advanced/)** - Advanced configurations and use cases
99-- **[api/](api/)** - REST API usage examples
99+- **[api/](api/)** - REST API usage examples and bulk operation scripts
1010+- **[deployment/](deployment/)** - Complete deployment configurations
1011- **[high-availability/](high-availability/)** - High availability and mirroring setups
11121213## Quick Start
13141515+### Method 1: kubectl-hsm Plugin (Recommended)
1616+14171. **Install the Operator**
1518 ```bash
1616- # Install CRDs
1717- kubectl apply -f config/crd/bases/
1818-1919- # Deploy the operator
1919+ # Install CRDs and deploy the operator
2020+ kubectl apply -f config/default/
2121+ ```
2222+2323+2. **Install kubectl-hsm plugin**
2424+ ```bash
2525+ cd kubectl-hsm && make install
2626+ ```
2727+2828+3. **Create your first secret**
2929+ ```bash
3030+ kubectl hsm create my-secret --from-literal=password=secret123
3131+ ```
3232+3333+4. **List and get secrets**
3434+ ```bash
3535+ kubectl hsm list
3636+ kubectl hsm get my-secret
3737+ ```
3838+3939+### Method 2: CRD Resources
4040+4141+1. **Install the Operator**
4242+ ```bash
2043 kubectl apply -f config/default/
2144 ```
2245···3053 kubectl apply -f examples/basic/database-secret.yaml
3154 ```
32553333-4. **Use the REST API**
3434- ```bash
3535- # Check health
3636- curl http://localhost:8090/api/v1/health
3737-3838- # Create a secret via API
3939- curl -X POST http://localhost:8090/api/v1/hsm/secrets \
4040- -H "Content-Type: application/json" \
4141- -d @examples/api/create-secret.json
4242- ```
5656+### Method 3: REST API (Advanced)
5757+5858+For automation and bulk operations:
5959+```bash
6060+# Port forward to API
6161+kubectl port-forward -n hsm-secrets-operator-system svc/hsm-secrets-operator-api 8090:8090
6262+6363+# Check health
6464+curl http://localhost:8090/api/v1/health
6565+6666+# Create a secret via API
6767+curl -X POST http://localhost:8090/api/v1/hsm/secrets \
6868+ -H "Content-Type: application/json" \
6969+ -d @examples/api/create-secret.json
7070+```
43714472## Prerequisites
4573
+7-4
examples/advanced/README.md
···2233This directory contains advanced configuration examples for complex use cases.
4455+> **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.
66+57## Examples Overview
6877-1. **[custom-discovery.yaml](custom-discovery.yaml)** - Custom USB device discovery
88-2. **[multi-environment.yaml](multi-environment.yaml)** - Multi-environment secret management
99-3. **[secret-rotation.yaml](secret-rotation.yaml)** - Automated secret rotation
1010-4. **[monitoring.yaml](monitoring.yaml)** - Prometheus monitoring setup
99+1. **[custom-pkcs11-library.yaml](custom-pkcs11-library.yaml)** - Custom PKCS#11 library configuration
1010+2. **[multi-environment.yaml](multi-environment.yaml)** - Multi-environment secret management
1111+3. **[custom-library-guide.md](custom-library-guide.md)** - Guide for using custom PKCS#11 libraries
1212+1313+> **Additional Examples:** See the [deployment](../deployment/) directory for monitoring configurations and [high-availability](../high-availability/) directory for advanced failover setups.
11141215## Advanced Use Cases
1316
+29-13
examples/advanced/custom-pkcs11-library.yaml
···4141spec:
4242 deviceType: Generic # Use Generic for custom devices
43434444- # Custom USB device
4545- usb:
4646- vendorId: "1234" # Your vendor ID
4747- productId: "5678" # Your product ID
4848- # serialNumber: "CUSTOM-001" # Optional specific device
4444+ # Discovery configuration
4545+ discovery:
4646+ # Custom USB device
4747+ usb:
4848+ vendorId: "1234" # Your vendor ID
4949+ productId: "5678" # Your product ID
5050+ # serialNumber: "CUSTOM-001" # Optional specific device
49515050- # Custom PKCS#11 library path
5151- pkcs11LibraryPath: "/opt/custom-hsm/lib/libcustomhsm-pkcs11.so"
5252+ # PKCS#11 configuration
5353+ pkcs11:
5454+ libraryPath: "/opt/custom-hsm/lib/libcustomhsm-pkcs11.so"
5555+ slotId: 0
5656+ pinSecret:
5757+ name: "custom-hsm-pin"
5858+ key: "pin"
5959+ tokenLabel: "CustomHSM"
52605361 nodeSelector:
5462 hsm.vendor: "custom-vendor" # Only deploy on nodes with this vendor
···6371spec:
6472 deviceType: Generic
65736666- # Use device path instead of USB discovery
6767- devicePath:
6868- path: "/dev/custom-hsm*"
6969- permissions: "0666"
7474+ # Discovery configuration
7575+ discovery:
7676+ # Use device path instead of USB discovery
7777+ devicePath:
7878+ path: "/dev/custom-hsm*"
7979+ permissions: "0666"
70807171- # Custom library path
7272- pkcs11LibraryPath: "/usr/local/lib/libcustomhsm.so"
8181+ # PKCS#11 configuration
8282+ pkcs11:
8383+ libraryPath: "/usr/local/lib/libcustomhsm.so"
8484+ slotId: 0
8585+ pinSecret:
8686+ name: "custom-hsm-pin"
8787+ key: "pin"
8888+ tokenLabel: "CustomHSM"
73897490 nodeSelector:
7591 custom-hsm.enabled: "true"
+16-3
examples/advanced/multi-environment.yaml
···2121 app: myapp
2222 environment: development
2323spec:
2424- secretName: "database-credentials"
2424+ # HSM path is automatically set to the metadata.name (database-credentials)
2525+ parentRef:
2626+ name: controller-manager
2727+ namespace: hsm-secrets-operator-system
2528 autoSync: true
2629 syncInterval: 300 # 5 minutes - frequent sync for dev
2730···4548 app: myapp
4649 environment: staging
4750spec:
4848- secretName: "database-credentials"
5151+ # HSM path is automatically set to the metadata.name (database-credentials)
5252+ parentRef:
5353+ name: controller-manager
5454+ namespace: hsm-secrets-operator-system
4955 autoSync: true
5056 syncInterval: 600 # 10 minutes
5157···7379 annotations:
7480 hsm.j5t.io/description: "Production database credentials - HIGH SECURITY"
7581spec:
7676- secretName: "database-credentials"
8282+ # HSM path is automatically set to the metadata.name (database-credentials)
8383+ parentRef:
8484+ name: controller-manager
8585+ namespace: hsm-secrets-operator-system
7786 autoSync: true
7887 syncInterval: 1800 # 30 minutes - less frequent for stability
7988···98107 type: tls
99108 scope: global
100109spec:
110110+ # HSM path is automatically set to the metadata.name (wildcard-tls-cert)
111111+ parentRef:
112112+ name: controller-manager
113113+ namespace: hsm-secrets-operator-system
101114 secretName: "wildcard-tls"
102115 autoSync: true
103116 syncInterval: 86400 # Daily sync for certificates
-82
examples/agent-deployment/README.md
···11-# HSM Agent Pod Architecture
22-33-This directory contains examples of how the HSM agent pod system works for distributed HSM operations.
44-55-## Architecture Overview
66-77-The HSM Secrets Operator now uses a **3-binary architecture**:
88-99-1. **Manager** (`/manager`) - Main operator that orchestrates everything
1010-2. **Discovery** (`/discovery`) - Lightweight USB device discovery (DaemonSet)
1111-3. **Agent** (`/agent`) - HSM operation execution pods (deployed on-demand)
1212-1313-## How It Works
1414-1515-```mermaid
1616-graph TB
1717- subgraph "Manager Pod (Any Node)"
1818- M[Manager Controller]
1919- end
2020-2121- subgraph "Node with HSM Hardware"
2222- D[Discovery Pod<br/>DaemonSet]
2323- A[Agent Pod<br/>On-demand]
2424- H[HSM Hardware]
2525- end
2626-2727- subgraph "Other Nodes"
2828- D2[Discovery Pod<br/>DaemonSet]
2929- end
3030-3131- M -->|1. Find HSMDevice| D
3232- M -->|2. Deploy Agent| A
3333- M -->|3. HTTP API calls| A
3434- A -->|4. PKCS#11| H
3535- D -->|USB discovery| H
3636-```
3737-3838-## Process Flow
3939-4040-1. **HSMSecret Created**: User creates an HSMSecret resource
4141-2. **Device Discovery**: Manager finds available HSMDevice (discovered by DaemonSet)
4242-3. **Agent Deployment**: Manager deploys HSM agent pod on node with hardware
4343-4. **Node Affinity**: Agent pod pinned to specific node via `kubernetes.io/hostname`
4444-5. **HTTP Communication**: Manager makes HTTP calls to agent for HSM operations
4545-6. **Hardware Access**: Agent executes PKCS#11 operations locally on HSM
4646-4747-## Key Benefits
4848-4949-✅ **Remote Execution**: Manager can be anywhere, agents run where hardware exists
5050-✅ **Resource Efficiency**: Agents only deployed when HSMSecrets exist
5151-✅ **Auto-cleanup**: Agents removed when no longer needed
5252-✅ **Node Targeting**: Perfect placement via HSMDevice discovery
5353-✅ **Clean Architecture**: Each component has single responsibility
5454-5555-## Example Deployment
5656-5757-See `agent-example.yaml` for a complete example of:
5858-- HSMDevice discovery configuration
5959-- HSMSecret that triggers agent deployment
6060-- Secret with PIN configuration
6161-6262-## Agent Pod Configuration
6363-6464-Agent pods are automatically configured with:
6565-- **Environment Variables**: PKCS#11 library path, slot ID, token label
6666-- **Secrets**: PIN from Kubernetes Secret reference
6767-- **Node Affinity**: Pinned to node with actual hardware
6868-- **Security**: Non-privileged execution with proper security contexts
6969-- **Health Checks**: Liveness and readiness probes
7070-- **Resources**: CPU/memory limits and requests
7171-7272-## API Endpoints
7373-7474-Each agent exposes:
7575-- `GET /api/v1/hsm/info` - HSM device information
7676-- `GET /api/v1/hsm/secrets/{path}` - Read secret
7777-- `POST /api/v1/hsm/secrets/{path}` - Write secret
7878-- `DELETE /api/v1/hsm/secrets/{path}` - Delete secret
7979-- `GET /api/v1/hsm/secrets` - List secrets
8080-- `GET /api/v1/hsm/checksum/{path}` - Get checksum
8181-- `GET /healthz` - Health check
8282-- `GET /readyz` - Readiness check
-140
examples/agent-deployment/agent-example.yaml
···11-# Complete example showing HSM agent pod deployment
22-# This demonstrates the full flow from HSMDevice discovery to agent-based secret operations
33-44----
55-# Step 1: Create PIN secret for HSM authentication
66-apiVersion: v1
77-kind: Secret
88-metadata:
99- name: pico-hsm-pin
1010- namespace: default
1111-type: Opaque
1212-data:
1313- pin: MTIzNDU2 # base64 encoded "123456"
1414-1515----
1616-# Step 2: Configure HSMDevice for discovery and agent deployment
1717-apiVersion: hsm.j5t.io/v1alpha1
1818-kind: HSMDevice
1919-metadata:
2020- name: pico-hsm-main
2121- namespace: default
2222- labels:
2323- hsm-type: pico
2424- environment: production
2525-spec:
2626- # Device type - uses well-known specifications
2727- deviceType: PicoHSM
2828-2929- # Discovery configuration
3030- discovery:
3131- usb:
3232- vendorId: "20a0"
3333- productId: "4230"
3434- # Optional: target specific device by serial number
3535- # serialNumber: "PICO123456"
3636-3737- # PKCS#11 configuration for agent pods
3838- pkcs11:
3939- libraryPath: "/usr/local/lib/libsc-hsm-pkcs11.so"
4040- slotId: 0
4141- pinSecret:
4242- name: "pico-hsm-pin"
4343- key: "pin"
4444- namespace: "default"
4545- tokenLabel: "PicoHSM-Production"
4646-4747- # Optional: restrict to specific nodes
4848- nodeSelector:
4949- hsm-enabled: "true"
5050- node-type: "worker"
5151-5252- # Maximum devices to discover (usually 1 for production)
5353- maxDevices: 1
5454-5555----
5656-# Step 3: Create HSMSecret that will trigger agent deployment
5757-apiVersion: hsm.j5t.io/v1alpha1
5858-kind: HSMSecret
5959-metadata:
6060- name: database-credentials
6161- namespace: default
6262- labels:
6363- app: myapp
6464- tier: database
6565-spec:
6666- # Path on HSM where secret is stored
6767-6868- # Name of Kubernetes Secret to create/sync
6969- secretName: "database-credentials"
7070-7171- # Enable automatic synchronization
7272- autoSync: true
7373-7474- # Sync every 5 minutes (300 seconds)
7575- syncInterval: 300
7676-7777- # Secret type (default: Opaque)
7878- secretType: "Opaque"
7979-8080----
8181-# Step 4: Example application using the synced secret
8282-apiVersion: apps/v1
8383-kind: Deployment
8484-metadata:
8585- name: database-app
8686- namespace: default
8787-spec:
8888- replicas: 1
8989- selector:
9090- matchLabels:
9191- app: database-app
9292- template:
9393- metadata:
9494- labels:
9595- app: database-app
9696- spec:
9797- containers:
9898- - name: app
9999- image: nginx:1.21
100100- env:
101101- - name: DB_USERNAME
102102- valueFrom:
103103- secretKeyRef:
104104- name: database-credentials # Synced from HSM
105105- key: username
106106- - name: DB_PASSWORD
107107- valueFrom:
108108- secretKeyRef:
109109- name: database-credentials # Synced from HSM
110110- key: password
111111- - name: DB_HOST
112112- valueFrom:
113113- secretKeyRef:
114114- name: database-credentials # Synced from HSM
115115- key: host
116116- ports:
117117- - containerPort: 80
118118- resources:
119119- requests:
120120- cpu: 100m
121121- memory: 128Mi
122122- limits:
123123- cpu: 200m
124124- memory: 256Mi
125125-126126----
127127-# Optional: Service to expose the application
128128-apiVersion: v1
129129-kind: Service
130130-metadata:
131131- name: database-app-service
132132- namespace: default
133133-spec:
134134- selector:
135135- app: database-app
136136- ports:
137137- - name: http
138138- port: 80
139139- targetPort: 80
140140- type: ClusterIP
+53-23
examples/api/README.md
···2323- Kubernetes ServiceAccount tokens (when deployed in cluster)
2424- Future: OAuth2, API keys, mTLS
25252626-## Examples
2626+## kubectl-hsm Plugin (Recommended)
27272828-1. **[health-check.sh](health-check.sh)** - Check API and HSM health
2929-2. **[create-secret.json](create-secret.json)** - Create a new secret via API
3030-3. **[create-secret.sh](create-secret.sh)** - Script to create secrets
3131-4. **[import-from-k8s.sh](import-from-k8s.sh)** - Import existing Kubernetes secrets
3232-5. **[list-secrets.sh](list-secrets.sh)** - List all HSM secrets
3333-6. **[update-secret.sh](update-secret.sh)** - Update existing secrets
3434-7. **[bulk-operations.sh](bulk-operations.sh)** - Bulk secret operations
2828+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.
35293636-## Quick Start
3030+### Quick Start with kubectl-hsm
37313838-1. **Start the API server** (if running locally):
3232+1. **Install the plugin**:
3933 ```bash
4040- ./bin/manager --enable-api=true --api-port=8090
3434+ cd kubectl-hsm && make install
3535+ ```
3636+3737+2. **Check health**:
3838+ ```bash
3939+ kubectl hsm health
4040+ ```
4141+4242+3. **Create a secret**:
4343+ ```bash
4444+ kubectl hsm create my-secret --from-literal=password=secret123
4545+ ```
4646+4747+4. **List secrets**:
4848+ ```bash
4949+ kubectl hsm list
5050+ ```
5151+5252+5. **Get a secret**:
5353+ ```bash
5454+ kubectl hsm get my-secret
5555+ ```
5656+5757+See the [kubectl-hsm documentation](../../kubectl-hsm/README.md) for full usage.
5858+5959+## REST API Examples
6060+6161+For advanced use cases or automation that requires direct API access, the following scripts demonstrate REST API usage:
6262+6363+1. **[import-from-k8s.sh](import-from-k8s.sh)** - Import existing Kubernetes secrets
6464+2. **[bulk-operations.sh](bulk-operations.sh)** - Bulk secret operations (auto-detects kubectl-hsm)
6565+3. **[advanced-bulk-import.sh](advanced-bulk-import.sh)** - Advanced import with validation and rollback
6666+4. **[create-secret.json](create-secret.json)** - Sample secret data structure
6767+5. **[production-import.json](production-import.json)** - Production-ready import configuration
6868+6969+## Quick Start with REST API
7070+7171+1. **Port forward to API server**:
7272+ ```bash
7373+ kubectl port-forward -n hsm-secrets-operator-system svc/hsm-secrets-operator-api 8090:8090
4174 ```
427543762. **Check health**:
···91124## Common Use Cases
9212593126### 1. Development Workflow
9494-- Create secrets during development
9595-- Import from existing sources
9696-- Test secret rotation
127127+- **kubectl-hsm**: Interactive secret management during development
128128+- **REST API**: Automated testing and CI/CD integration
9712998130### 2. CI/CD Integration
9999-- Automated secret provisioning
100100-- Environment-specific deployments
101101-- Secret validation and testing
131131+- **kubectl-hsm**: Simple command-line operations in pipelines
132132+- **REST API**: Complex automation and bulk operations
102133103134### 3. Secret Migration
104104-- Import from Kubernetes Secrets
105105-- Migrate from other secret stores
106106-- Bulk operations for large environments
135135+- **bulk-operations.sh**: Mass import/export operations
136136+- **advanced-bulk-import.sh**: Production migrations with validation
137137+- **import-from-k8s.sh**: Migrate existing Kubernetes secrets
107138108139### 4. Monitoring and Operations
109109-- Health monitoring
110110-- Secret inventory management
111111-- Troubleshooting sync issues
140140+- **kubectl-hsm health**: Quick health checks
141141+- **REST API**: Detailed monitoring and troubleshooting
112142113143## Error Handling
114144
+18-1
examples/api/advanced-bulk-import.sh
···11#!/bin/bash
2233# Advanced bulk import script with validation and rollback
44+# Automatically uses kubectl-hsm plugin if available, falls back to REST API
45# Usage: ./advanced-bulk-import.sh [config-file] [options]
5667set -e
···187188 local conflicts=()
188189189190 while IFS= read -r label; do
191191+ # Try kubectl-hsm first if available
192192+ if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
193193+ if kubectl hsm get "$label" >/dev/null 2>&1; then
194194+ conflicts+=("$label")
195195+ continue
196196+ fi
197197+ fi
198198+199199+ # Fallback to API
190200 response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label")
191201 success=$(echo "$response" | jq -r '.success')
192202···259269260270 for label in "${imported_secrets[@]}"; do
261271 log "Rolling back: $label"
262262- curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null
272272+273273+ # Try kubectl-hsm first if available
274274+ if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
275275+ kubectl hsm delete "$label" --force >/dev/null 2>&1 || \
276276+ curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null
277277+ else
278278+ curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null
279279+ fi
263280 done
264281265282 warning "Rollback completed"
+41-3
examples/api/bulk-operations.sh
···11#!/bin/bash
2233-# Bulk operations for HSM secrets via REST API
33+# Bulk operations for HSM secrets
44+# Automatically uses kubectl-hsm plugin if available, falls back to REST API
45# Usage: ./bulk-operations.sh [operation] [config-file]
5667set -e
···74757576 echo " Creating secret: $label"
76777878+ # Try using kubectl-hsm first if available, fallback to API
7979+ if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
8080+ # Convert secret_data to create command format
8181+ local temp_file=$(mktemp)
8282+ echo "$secret_data" | jq '.data' > "$temp_file"
8383+8484+ if kubectl hsm create "$label" --from-file="$temp_file" >/dev/null 2>&1; then
8585+ rm "$temp_file"
8686+ echo " ✅ Created successfully (kubectl-hsm)"
8787+ return 0
8888+ else
8989+ rm "$temp_file"
9090+ echo " ⚠️ kubectl-hsm failed, trying API..."
9191+ fi
9292+ fi
9393+9494+ # Fallback to API
7795 response=$(curl -s -X POST \
7896 -H "Content-Type: application/json" \
7997 -d "$secret_data" \
···819982100 success=$(echo "$response" | jq -r '.success')
83101 if [ "$success" = "true" ]; then
8484- echo " ✅ Created successfully"
102102+ echo " ✅ Created successfully (API)"
85103 return 0
86104 else
87105 error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"')
···9611497115 echo " Deleting secret: $label"
98116117117+ # Try using kubectl-hsm first if available, fallback to API
118118+ if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
119119+ if kubectl hsm delete "$label" --force >/dev/null 2>&1; then
120120+ echo " ✅ Deleted successfully (kubectl-hsm)"
121121+ return 0
122122+ else
123123+ echo " ⚠️ kubectl-hsm failed, trying API..."
124124+ fi
125125+ fi
126126+127127+ # Fallback to API
99128 response=$(curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label")
100129101130 success=$(echo "$response" | jq -r '.success')
102131 if [ "$success" = "true" ]; then
103103- echo " ✅ Deleted successfully"
132132+ echo " ✅ Deleted successfully (API)"
104133 return 0
105134 else
106135 error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"')
···113142get_secret() {
114143 local label="$1"
115144145145+ # Try using kubectl-hsm first if available, fallback to API
146146+ if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
147147+ if kubectl hsm get "$label" >/dev/null 2>&1; then
148148+ echo " ✅ $label (available via kubectl-hsm)"
149149+ return 0
150150+ fi
151151+ fi
152152+153153+ # Fallback to API for detailed status
116154 response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label")
117155 success=$(echo "$response" | jq -r '.success')
118156
-90
examples/api/create-secret.sh
···11-#!/bin/bash
22-33-# Create HSM Secret via REST API
44-# Usage: ./create-secret.sh [secret-name] [secret-id]
55-66-set -e
77-88-API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"}
99-SECRET_NAME=${1:-"example-secret"}
1010-SECRET_ID=${2:-"$(date +%s)"} # Use timestamp as default ID
1111-1212-echo "🔐 Creating HSM Secret via API..."
1313-echo "Secret Name: $SECRET_NAME"
1414-echo "Secret ID: $SECRET_ID"
1515-echo "API Base URL: $API_BASE_URL"
1616-echo ""
1717-1818-# Create the JSON payload for agent API (path-based)
1919-# The agent expects the path in the URL and data directly in the request body
2020-payload=$(cat <<EOF
2121-{
2222- "data": {
2323- "api_key": "sk_test_$(openssl rand -hex 16)",
2424- "webhook_secret": "whsec_$(openssl rand -hex 20)",
2525- "database_url": "postgresql://user:$(openssl rand -hex 12)@localhost:5432/testdb",
2626- "redis_url": "redis://localhost:6379/0",
2727- "created_timestamp": "$(date +%s)",
2828- "label": "$SECRET_NAME",
2929- "id": "$SECRET_ID",
3030- "description": "Secret created via API on $(date)",
3131- "created_by": "api-script",
3232- "environment": "development",
3333- "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
3434- }
3535-}
3636-EOF
3737-)
3838-3939-echo "📝 Request Payload:"
4040-echo "$payload" | jq '.'
4141-echo ""
4242-4343-# Create HSM path from secret name (just use the secret name as path)
4444-HSM_PATH="$SECRET_NAME"
4545-4646-# Make the API call - using path-based endpoint
4747-echo "📤 Sending create request to path: $HSM_PATH"
4848-response=$(curl -s -X POST \
4949- -H "Content-Type: application/json" \
5050- -d "$payload" \
5151- "$API_BASE_URL/api/v1/hsm/secrets/$HSM_PATH")
5252-5353-echo "📥 Response:"
5454-echo "$response" | jq '.'
5555-5656-# Check if the request was successful
5757-success=$(echo "$response" | jq -r '.success')
5858-if [ "$success" = "true" ]; then
5959- echo ""
6060- echo "✅ Secret created successfully!"
6161-6262- # Extract created secret info from agent response
6363- checksum=$(echo "$response" | jq -r '.data.checksum // "unknown"')
6464- path=$(echo "$response" | jq -r '.data.path // "unknown"')
6565-6666- echo " Secret Name: $SECRET_NAME"
6767- echo " HSM Path: $HSM_PATH"
6868- echo " Checksum: ${checksum:0:16}..."
6969-7070- echo ""
7171- echo "🔍 To retrieve this secret:"
7272- echo " curl $API_BASE_URL/api/v1/hsm/secrets/$HSM_PATH"
7373-7474- echo ""
7575- echo "📋 To list all secrets:"
7676- echo " curl $API_BASE_URL/api/v1/hsm/secrets"
7777-7878- echo ""
7979- echo "🗑️ To delete this secret:"
8080- echo " curl -X DELETE $API_BASE_URL/api/v1/hsm/secrets/$HSM_PATH"
8181-8282-else
8383- echo ""
8484- echo "❌ Failed to create secret!"
8585- error_code=$(echo "$response" | jq -r '.error.code // "unknown"')
8686- error_message=$(echo "$response" | jq -r '.error.message // "No error message"')
8787- echo " Error Code: $error_code"
8888- echo " Error Message: $error_message"
8989- exit 1
9090-fi
···11-#!/bin/bash
22-33-# List all HSM secrets via REST API
44-# Usage: ./list-secrets.sh [page] [page_size]
55-66-set -e
77-88-API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"}
99-PAGE=${1:-1}
1010-PAGE_SIZE=${2:-10}
1111-1212-echo "📋 Listing HSM Secrets via API..."
1313-echo "API Base URL: $API_BASE_URL"
1414-echo "Page: $PAGE, Page Size: $PAGE_SIZE"
1515-echo ""
1616-1717-# Make the API call
1818-response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets?page=$PAGE&page_size=$PAGE_SIZE")
1919-2020-# Check if the request was successful
2121-success=$(echo "$response" | jq -r '.success')
2222-if [ "$success" = "true" ]; then
2323- # Extract secret info
2424- count=$(echo "$response" | jq -r '.data.count')
2525- prefix=$(echo "$response" | jq -r '.data.prefix // ""')
2626-2727- echo "📊 Summary:"
2828- echo " Total Secrets: $count"
2929- if [ -n "$prefix" ] && [ "$prefix" != "" ]; then
3030- echo " Prefix Filter: $prefix"
3131- fi
3232- echo ""
3333-3434- # List secrets
3535- echo "🔐 Secrets:"
3636- if [ "$count" -gt 0 ]; then
3737- echo "$response" | jq -r '.data.paths[] | " • \(.)"'
3838- else
3939- echo " No secrets found"
4040- fi
4141-else
4242- echo "❌ Failed to list secrets!"
4343- error_code=$(echo "$response" | jq -r '.error.code // "unknown"')
4444- error_message=$(echo "$response" | jq -r '.error.message // "No error message"')
4545- echo " Error Code: $error_code"
4646- echo " Error Message: $error_message"
4747- exit 1
4848-fi
+20-3
examples/basic/README.md
···27272828### Step 2: Create Your First Secret
29293030-Create a database secret stored on the HSM:
3030+**Option A: Using kubectl-hsm plugin (recommended for interactive use):**
3131+```bash
3232+kubectl hsm create database-credentials \
3333+ --from-literal=database_url="postgresql://user:pass@db:5432/mydb" \
3434+ --from-literal=username="dbuser" \
3535+ --from-literal=password="secret123"
3636+```
31373838+**Option B: Using CRD resources (recommended for GitOps):**
3239```bash
3340kubectl apply -f database-secret.yaml
3441```
35423643Verify the secret was created:
3744```bash
4545+# Using kubectl-hsm
4646+kubectl hsm get database-credentials
4747+kubectl hsm list
4848+4949+# Using standard kubectl
3850kubectl get hsmsecret database-credentials
3951kubectl get secret database-credentials
4052```
···104116Update secrets directly on the HSM, and they'll automatically sync:
105117106118```bash
107107-# The operator detects HSM changes and updates Kubernetes Secrets
108108-# No manual intervention required
119119+# Option 1: Update via kubectl-hsm (writes to HSM, then syncs to K8s)
120120+kubectl hsm create database-credentials \
121121+ --from-literal=password="new-secret123" \
122122+ --dry-run=false
123123+124124+# Option 2: Direct HSM update (via pkcs11-tool or HSM tools)
125125+# The operator detects HSM changes and updates Kubernetes Secrets automatically
109126```
110127111128### Multiple Applications
+5-6
examples/basic/api-keys.yaml
···99 annotations:
1010 hsm.j5t.io/description: "API keys for external services (Stripe, AWS, etc.)"
1111spec:
1212- # Path on the HSM where API keys are stored
1212+ # HSM path is automatically set to the metadata.name (external-api-keys)
13131414- # Name of the Secret containing API keys
1515- secretName: "external-api-keys"
1414+ # ParentRef identifies which operator instance should handle this HSMSecret
1515+ parentRef:
1616+ name: controller-manager
1717+ namespace: hsm-secrets-operator-system
16181719 # Enable automatic synchronization
1820 autoSync: true
19212022 # Sync every 10 minutes (API keys might rotate frequently)
2123 syncInterval: 600
2222-2323- # Standard opaque secret type
2424- secretType: Opaque
25242625---
2726# Example application using the API keys
+5-6
examples/basic/database-secret.yaml
···1010 annotations:
1111 hsm.j5t.io/description: "PostgreSQL database credentials for production"
1212spec:
1313- # Path on the HSM where the secret is stored
1313+ # HSM path is automatically set to the metadata.name (database-credentials)
14141515- # Name of the Kubernetes Secret to create/maintain
1616- secretName: "database-credentials"
1515+ # ParentRef identifies which operator instance should handle this HSMSecret
1616+ parentRef:
1717+ name: controller-manager
1818+ namespace: hsm-secrets-operator-system
17191820 # Enable automatic sync from HSM to Kubernetes
1921 autoSync: true
20222123 # Check for changes every 5 minutes (300 seconds)
2224 syncInterval: 300
2323-2424- # Type of Kubernetes Secret to create
2525- secretType: Opaque
26252726---
2827# Example of how to use the secret in a deployment
+20-12
examples/basic/pico-hsm-device.yaml
···1010 # Device type for auto-discovery
1111 deviceType: PicoHSM
12121313- # USB device specifications for Pico HSM
1414- usb:
1515- vendorId: "20a0"
1616- productId: "4230"
1717- # serialNumber: "12345" # Optional: specific device serial
1313+ # Discovery configuration
1414+ discovery:
1515+ # USB device specifications for Pico HSM
1616+ usb:
1717+ vendorId: "20a0"
1818+ productId: "4230"
1919+ # serialNumber: "12345" # Optional: specific device serial
2020+2121+ # Alternative: Manual path specification
2222+ # devicePath:
2323+ # path: "/dev/sc-hsm*"
2424+ # permissions: "0666"
18251919- # Alternative: Manual path specification
2020- # devicePath:
2121- # path: "/dev/sc-hsm*"
2222- # permissions: "0666"
2626+ # PKCS#11 configuration
2727+ pkcs11:
2828+ libraryPath: "/usr/lib/opensc-pkcs11.so" # Use OpenSC for Pico HSM
2929+ slotId: 0
3030+ pinSecret:
3131+ name: "pico-hsm-pin"
3232+ key: "pin"
3333+ tokenLabel: "PicoHSM"
23342435 # Node selection (optional - runs on all nodes if not specified)
2536 nodeSelector:
2637 # kubernetes.io/hostname: "worker-node-1"
2738 hsm.j5t.io/enabled: "true"
2828-2929- # PKCS#11 library path (auto-detected for known devices)
3030- pkcs11LibraryPath: "/usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so"
31393240 # Maximum number of devices to discover
3341 maxDevices: 2
+7-2
examples/basic/tls-certificate.yaml
···99 annotations:
1010 hsm.j5t.io/description: "TLS certificate and key for webapp.example.com"
1111spec:
1212- # Path on the HSM where the TLS cert/key is stored
1212+ # HSM path is automatically set to the metadata.name (webapp-tls-cert)
1313+1414+ # ParentRef identifies which operator instance should handle this HSMSecret
1515+ parentRef:
1616+ name: controller-manager
1717+ namespace: hsm-secrets-operator-system
13181414- # Name of the TLS Secret to create
1919+ # Name of the TLS Secret to create (optional, defaults to metadata.name)
1520 secretName: "webapp-tls"
16211722 # Enable automatic sync
+23-7
examples/deployment/README.md
···55## Files
6677- **[complete-setup.yaml](complete-setup.yaml)** - Full production deployment with all components
88-- **[operator-deployment.yaml](operator-deployment.yaml)** - Just the operator deployment
99-- **[monitoring-setup.yaml](monitoring-setup.yaml)** - Prometheus monitoring configuration
88+99+> **Additional Configurations:** See the [config](../../config/) directory for CRDs, RBAC, and operator deployments, or the [helm](../../helm/) directory for Helm chart deployments.
10101111## Complete Setup
1212···7575# Check HSM devices
7676kubectl get hsmdevice
77777878-# Check secrets
7979-kubectl get hsmsecret
8080-kubectl get secret
7878+# Check secrets (multiple ways)
7979+kubectl hsm list # via kubectl-hsm plugin
8080+kubectl get hsmsecret # via CRDs
8181+kubectl get secret # via K8s secrets
8182```
82838383-### 5. Test the API
8484+### 5. Test Secret Operations
84858585-If API is enabled:
8686+**Option A: Using kubectl-hsm plugin (recommended):**
8787+```bash
8888+# Check health
8989+kubectl hsm health
9090+9191+# Create a test secret
9292+kubectl hsm create test-secret --from-literal=key=value
9393+9494+# List secrets
9595+kubectl hsm list
9696+9797+# Get secret details
9898+kubectl hsm get test-secret
9999+```
100100+101101+**Option B: Using REST API (advanced):**
86102```bash
87103# Port forward to access API locally
88104kubectl port-forward -n hsm-secrets-operator-system service/hsm-secrets-operator-api 8090:8090
···99## Examples
101011111. **[mirrored-hsm-device.yaml](mirrored-hsm-device.yaml)** - HSM device with mirroring enabled
1212-2. **[ha-deployment.yaml](ha-deployment.yaml)** - Complete HA deployment example
1313-3. **[multi-region.yaml](multi-region.yaml)** - Multi-region deployment with mirroring
1414-4. **[failover-testing.yaml](failover-testing.yaml)** - Failover testing scenarios
1212+1313+> **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.
15141615## Architecture
1716···152151# Review mirroring status
153152kubectl describe hsmdevice hsm-primary
154153155155-# Check secret sync status
154154+# Check secret sync status (multiple options)
155155+kubectl hsm health # via kubectl-hsm plugin
156156+kubectl hsm list # shows all secrets with status
156157kubectl get hsmsecret -o custom-columns=NAME:.metadata.name,STATUS:.status.syncStatus,LAST-SYNC:.status.lastSyncTime
157158158159# Monitor failover events
···11+# kubectl-hsm Plugin Makefile
22+# Copyright 2025. Licensed under the Apache License, Version 2.0.
33+44+# Build variables
55+PLUGIN_NAME=kubectl-hsm
66+VERSION?=dev
77+BUILD_DIR=bin
88+DIST_DIR=dist
99+1010+# Go build settings
1111+GO_VERSION=1.24
1212+CGO_ENABLED=0
1313+LDFLAGS=-ldflags "-s -w -X main.version=$(VERSION)"
1414+1515+# Supported platforms for cross-compilation
1616+PLATFORMS = \
1717+ linux/amd64 \
1818+ linux/arm64 \
1919+ darwin/amd64 \
2020+ darwin/arm64 \
2121+ windows/amd64
2222+2323+# Default target
2424+.PHONY: all
2525+all: build
2626+2727+# Build for current platform
2828+.PHONY: build
2929+build:
3030+ @echo "Building kubectl-hsm for current platform..."
3131+ @mkdir -p $(BUILD_DIR)
3232+ CGO_ENABLED=$(CGO_ENABLED) go build $(LDFLAGS) -o $(BUILD_DIR)/$(PLUGIN_NAME) ./cmd
3333+3434+# Build for all supported platforms
3535+.PHONY: build-all
3636+build-all: clean-dist
3737+ @echo "Building kubectl-hsm for all platforms..."
3838+ @mkdir -p $(DIST_DIR)
3939+ @$(foreach platform,$(PLATFORMS), \
4040+ echo "Building for $(platform)..."; \
4141+ GOOS=$(shell echo $(platform) | cut -d'/' -f1) \
4242+ GOARCH=$(shell echo $(platform) | cut -d'/' -f2) \
4343+ CGO_ENABLED=$(CGO_ENABLED) go build $(LDFLAGS) \
4444+ -o $(DIST_DIR)/$(PLUGIN_NAME)-$(shell echo $(platform) | sed 's/\//-/')$(shell [ "$(shell echo $(platform) | cut -d'/' -f1)" = "windows" ] && echo ".exe" || echo "") ./cmd \
4545+ && echo "✅ Built $(DIST_DIR)/$(PLUGIN_NAME)-$(shell echo $(platform) | sed 's/\//-/')$(shell [ "$(shell echo $(platform) | cut -d'/' -f1)" = "windows" ] && echo ".exe" || echo "")";)
4646+4747+# Install to local bin directory (for testing)
4848+.PHONY: install
4949+install: build
5050+ @echo "Installing kubectl-hsm to ~/bin/..."
5151+ @mkdir -p ~/bin
5252+ @cp $(BUILD_DIR)/$(PLUGIN_NAME) ~/bin/
5353+ @echo "✅ Installed to ~/bin/$(PLUGIN_NAME)"
5454+ @echo ""
5555+ @echo "To use the plugin:"
5656+ @echo " 1. Ensure ~/bin is in your PATH"
5757+ @echo " 2. Run: kubectl hsm --help"
5858+5959+# Install to system-wide location (requires sudo)
6060+.PHONY: install-system
6161+install-system: build
6262+ @echo "Installing kubectl-hsm to /usr/local/bin/..."
6363+ @sudo cp $(BUILD_DIR)/$(PLUGIN_NAME) /usr/local/bin/
6464+ @echo "✅ Installed to /usr/local/bin/$(PLUGIN_NAME)"
6565+ @echo ""
6666+ @echo "Plugin is now available system-wide:"
6767+ @echo " kubectl hsm --help"
6868+6969+# Create installation script
7070+.PHONY: install-script
7171+install-script:
7272+ @echo "Creating installation script..."
7373+ @mkdir -p $(DIST_DIR)
7474+ @echo '#!/bin/bash' > $(DIST_DIR)/install.sh
7575+ @echo 'set -e' >> $(DIST_DIR)/install.sh
7676+ @echo '' >> $(DIST_DIR)/install.sh
7777+ @echo '# kubectl-hsm Installation Script' >> $(DIST_DIR)/install.sh
7878+ @echo '# This script downloads and installs the kubectl-hsm plugin' >> $(DIST_DIR)/install.sh
7979+ @echo '' >> $(DIST_DIR)/install.sh
8080+ @echo 'PLUGIN_NAME="kubectl-hsm"' >> $(DIST_DIR)/install.sh
8181+ @echo 'VERSION="$${VERSION:-latest}"' >> $(DIST_DIR)/install.sh
8282+ @echo 'INSTALL_DIR="$${INSTALL_DIR:-/usr/local/bin}"' >> $(DIST_DIR)/install.sh
8383+ @echo '' >> $(DIST_DIR)/install.sh
8484+ @echo 'echo "Installing kubectl-hsm..."' >> $(DIST_DIR)/install.sh
8585+ @echo 'echo "✅ kubectl-hsm installation script created"' >> $(DIST_DIR)/install.sh
8686+ @chmod +x $(DIST_DIR)/install.sh
8787+ @echo "✅ Created installation script: $(DIST_DIR)/install.sh"
8888+8989+# Create archive packages for distribution
9090+.PHONY: package
9191+package: build-all install-script
9292+ @echo "Creating distribution packages..."
9393+ @$(foreach platform,$(PLATFORMS), \
9494+ os=$$(echo $(platform) | cut -d'/' -f1); \
9595+ arch=$$(echo $(platform) | cut -d'/' -f2); \
9696+ binary="$(PLUGIN_NAME)-$$os-$$arch"; \
9797+ [ "$$os" = "windows" ] && binary="$$binary.exe"; \
9898+ archive_name="$(PLUGIN_NAME)-$(VERSION)-$$os-$$arch"; \
9999+ if [ "$$os" = "windows" ]; then \
100100+ (cd $(DIST_DIR) && zip "$$archive_name.zip" "$$binary" install.sh README.md 2>/dev/null || zip "$$archive_name.zip" "$$binary" install.sh); \
101101+ else \
102102+ (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); \
103103+ fi; \
104104+ echo "📦 Created $$archive_name archive";)
105105+106106+# Generate checksums for release artifacts
107107+.PHONY: checksums
108108+checksums: package
109109+ @echo "Generating checksums..."
110110+ @cd $(DIST_DIR) && find . -name "*.tar.gz" -o -name "*.zip" | xargs shasum -a 256 > SHA256SUMS
111111+ @echo "✅ Generated checksums: $(DIST_DIR)/SHA256SUMS"
112112+113113+# Test the built binary
114114+.PHONY: test-binary
115115+test-binary: build
116116+ @echo "Testing built binary..."
117117+ @$(BUILD_DIR)/$(PLUGIN_NAME) version
118118+ @echo "✅ Binary test passed"
119119+120120+# Clean build artifacts
121121+.PHONY: clean
122122+clean:
123123+ @echo "Cleaning build directory..."
124124+ @rm -rf $(BUILD_DIR)
125125+126126+.PHONY: clean-dist
127127+clean-dist:
128128+ @echo "Cleaning distribution directory..."
129129+ @rm -rf $(DIST_DIR)
130130+131131+.PHONY: clean-all
132132+clean-all: clean clean-dist
133133+134134+# Development targets
135135+.PHONY: dev
136136+dev: build test-binary
137137+ @echo "Development build complete"
138138+139139+.PHONY: dev-install
140140+dev-install: dev install
141141+ @echo "Development installation complete"
142142+143143+# Help target
144144+.PHONY: help
145145+help:
146146+ @echo "kubectl-hsm Plugin Build System"
147147+ @echo ""
148148+ @echo "Targets:"
149149+ @echo " build - Build for current platform"
150150+ @echo " build-all - Build for all supported platforms"
151151+ @echo " install - Install to ~/bin/"
152152+ @echo " install-system - Install to /usr/local/bin/ (requires sudo)"
153153+ @echo " install-script - Generate installation script"
154154+ @echo " package - Create distribution packages"
155155+ @echo " checksums - Generate checksums for packages"
156156+ @echo " test-binary - Test the built binary"
157157+ @echo " dev - Development build and test"
158158+ @echo " dev-install - Development build, test, and install"
159159+ @echo " clean - Clean build artifacts"
160160+ @echo " clean-all - Clean all artifacts"
161161+ @echo " help - Show this help"
162162+ @echo ""
163163+ @echo "Variables:"
164164+ @echo " VERSION - Plugin version (default: dev)"
165165+ @echo " INSTALL_DIR - Installation directory (default: /usr/local/bin)"
166166+ @echo ""
167167+ @echo "Examples:"
168168+ @echo " make build"
169169+ @echo " make build-all VERSION=v1.0.0"
170170+ @echo " make install"
171171+ @echo " make package VERSION=v1.0.0"
172172+173173+# Show supported platforms
174174+.PHONY: platforms
175175+platforms:
176176+ @echo "Supported platforms:"
177177+ @$(foreach platform,$(PLATFORMS),echo " $(platform)";)
+241
kubectl-hsm/README.md
···11+# kubectl-hsm Plugin
22+33+A kubectl plugin that provides a Kubernetes-native command-line interface for Hardware Security Module (HSM) secret management.
44+55+## Overview
66+77+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.
88+99+## Features
1010+1111+- **Kubernetes-native**: Works seamlessly with kubectl and respects namespace context
1212+- **Secure**: All secrets stored in HSM hardware for maximum security
1313+- **Interactive**: Support for secure password input and interactive secret creation
1414+- **Flexible**: Multiple input methods (literals, files, interactive prompts)
1515+- **Cross-platform**: Supports Linux, macOS, and Windows
1616+1717+## Installation
1818+1919+### Quick Install (Recommended)
2020+2121+```bash
2222+curl -fsSL https://github.com/evanjarrett/hsm-secrets-operator/releases/latest/download/install.sh | bash
2323+```
2424+2525+### Manual Installation
2626+2727+1. Download the binary for your platform from the [releases page](https://github.com/evanjarrett/hsm-secrets-operator/releases)
2828+2. Rename it to `kubectl-hsm`
2929+3. Make it executable: `chmod +x kubectl-hsm`
3030+4. Move it to a directory in your PATH (e.g., `/usr/local/bin/`)
3131+3232+### Build from Source
3333+3434+```bash
3535+git clone https://github.com/evanjarrett/hsm-secrets-operator.git
3636+cd hsm-secrets-operator/cmd/kubectl-hsm
3737+make build
3838+make install
3939+```
4040+4141+## Prerequisites
4242+4343+- kubectl installed and configured
4444+- HSM Secrets Operator deployed in your cluster
4545+- Access to the operator's API service
4646+4747+## Usage
4848+4949+### Basic Commands
5050+5151+```bash
5252+# Check plugin version
5353+kubectl hsm version
5454+5555+# Check operator health
5656+kubectl hsm health
5757+5858+# List all secrets
5959+kubectl hsm list
6060+6161+# Create a secret interactively (recommended for sensitive data)
6262+kubectl hsm create database-creds --interactive
6363+6464+# Create a secret with literal values
6565+kubectl hsm create api-config \
6666+ --from-literal api_key=sk_test_123 \
6767+ --from-literal endpoint=https://api.example.com
6868+6969+# Load secret values from files
7070+kubectl hsm create tls-cert \
7171+ --from-file cert=server.crt \
7272+ --from-file key=server.key
7373+7474+# Get a secret (shows metadata, not values)
7575+kubectl hsm get database-creds
7676+7777+# Get a specific key value
7878+kubectl hsm get database-creds --key password
7979+8080+# Delete a secret (with confirmation)
8181+kubectl hsm delete old-credentials
8282+8383+# Force delete without confirmation
8484+kubectl hsm delete old-credentials --force
8585+```
8686+8787+### Namespace Handling
8888+8989+The plugin respects your current kubectl namespace context:
9090+9191+```bash
9292+# Use current namespace context (set by kubens, kubectl config, etc.)
9393+kubectl hsm list
9494+9595+# Override namespace for a single command
9696+kubectl hsm list -n hsm-secrets-operator-system
9797+9898+# Switch namespace context (affects all kubectl commands)
9999+kubens hsm-secrets-operator-system
100100+kubectl hsm list
101101+```
102102+103103+### Output Formats
104104+105105+```bash
106106+# Human-readable output (default)
107107+kubectl hsm get my-secret
108108+109109+# JSON output
110110+kubectl hsm get my-secret -o json
111111+112112+# YAML output
113113+kubectl hsm get my-secret -o yaml
114114+```
115115+116116+### Interactive Mode
117117+118118+Interactive mode is recommended for creating secrets with sensitive data:
119119+120120+```bash
121121+kubectl hsm create database-creds --interactive
122122+```
123123+124124+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.
125125+126126+## How It Works
127127+128128+1. **Service Discovery**: The plugin automatically discovers the HSM operator API service in your current namespace
129129+2. **Port Forwarding**: Creates a secure port-forward connection to the operator
130130+3. **API Proxy**: Routes commands through the operator's REST API
131131+4. **HSM Integration**: The operator handles the actual HSM hardware communication
132132+133133+## Error Handling
134134+135135+The plugin provides helpful error messages and suggestions:
136136+137137+```bash
138138+# If operator not found
139139+❌ HSM secrets operator service not found in namespace 'default'
140140+141141+Please check:
142142+ - Is the operator installed? Try: kubectl get deploy -n default
143143+ - Are you in the correct namespace? Try: kubens <operator-namespace>
144144+145145+# If connection fails
146146+❌ Failed to connect to HSM operator: connection refused
147147+148148+Please check:
149149+ - Operator pods are running: kubectl get pods -l app.kubernetes.io/name=hsm-secrets-operator
150150+ - Service is available: kubectl get svc hsm-secrets-operator-api
151151+```
152152+153153+## Security Considerations
154154+155155+- **No Secret Values in Transit**: The plugin never logs or exposes secret values
156156+- **Secure Communication**: All communication uses port-forwarded connections
157157+- **HSM Storage**: Secrets are stored in HSM hardware, not Kubernetes etcd
158158+- **Interactive Input**: Passwords are hidden during interactive input
159159+- **Confirmation Required**: Destructive operations require explicit confirmation
160160+161161+## Configuration
162162+163163+The plugin uses standard kubectl configuration:
164164+165165+- **Kubeconfig**: Reads from `~/.kube/config` or `$KUBECONFIG`
166166+- **Current Context**: Respects the current kubectl context
167167+- **Namespace**: Uses current namespace or explicit `-n` flag
168168+169169+## Examples
170170+171171+### Database Credentials
172172+173173+```bash
174174+# Create database credentials interactively
175175+kubectl hsm create database-creds --interactive
176176+# Prompts for: username, password, host, port, database
177177+178178+# Retrieve for use in deployment
179179+kubectl hsm get database-creds --key username
180180+kubectl hsm get database-creds --key password -o json | jq -r '.password'
181181+```
182182+183183+### API Keys and Tokens
184184+185185+```bash
186186+# Create API configuration
187187+kubectl hsm create stripe-config \
188188+ --from-literal publishable_key=pk_live_123 \
189189+ --from-literal secret_key=sk_live_456 \
190190+ --from-literal webhook_secret=whsec_789
191191+192192+# View configuration (metadata only)
193193+kubectl hsm get stripe-config
194194+```
195195+196196+### TLS Certificates
197197+198198+```bash
199199+# Load certificate files
200200+kubectl hsm create tls-server \
201201+ --from-file tls.crt=server.crt \
202202+ --from-file tls.key=server.key \
203203+ --from-file ca.crt=ca.crt
204204+205205+# Check certificate info
206206+kubectl hsm get tls-server
207207+```
208208+209209+## Troubleshooting
210210+211211+### Plugin Not Found
212212+213213+If kubectl can't find the plugin:
214214+215215+1. Ensure the binary is named `kubectl-hsm`
216216+2. Verify it's in your PATH: `which kubectl-hsm`
217217+3. Check permissions: `ls -la $(which kubectl-hsm)`
218218+219219+### Connection Issues
220220+221221+If you can't connect to the operator:
222222+223223+1. Check if the operator is running: `kubectl get pods -n hsm-secrets-operator-system`
224224+2. Verify the service exists: `kubectl get svc hsm-secrets-operator-api -n hsm-secrets-operator-system`
225225+3. Test direct connection: `kubectl port-forward -n hsm-secrets-operator-system svc/hsm-secrets-operator-api 8090:8090`
226226+227227+### Permission Errors
228228+229229+If you get permission errors:
230230+231231+1. Verify your kubectl access: `kubectl auth can-i get services`
232232+2. Check if you're in the right namespace: `kubectl config get-contexts`
233233+3. Ensure the operator is accessible from your namespace
234234+235235+## Contributing
236236+237237+Contributions are welcome! Please see the main [repository](https://github.com/evanjarrett/hsm-secrets-operator) for contribution guidelines.
238238+239239+## License
240240+241241+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
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package main
1818+1919+import (
2020+ "fmt"
2121+ "os"
2222+2323+ "github.com/spf13/cobra"
2424+2525+ "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/commands"
2626+)
2727+2828+var version = "dev" // Set during build
2929+3030+func main() {
3131+ rootCmd := newRootCmd()
3232+ if err := rootCmd.Execute(); err != nil {
3333+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
3434+ os.Exit(1)
3535+ }
3636+}
3737+3838+func newRootCmd() *cobra.Command {
3939+ cmd := &cobra.Command{
4040+ Use: "kubectl-hsm",
4141+ Short: "Kubernetes plugin for HSM secret management",
4242+ Long: `kubectl-hsm is a kubectl plugin that provides a Kubernetes-native
4343+command-line interface for Hardware Security Module (HSM) secret management.
4444+4545+This plugin integrates with the HSM Secrets Operator to provide secure
4646+secret storage using hardware-based security modules while maintaining
4747+seamless integration with Kubernetes workflows.
4848+4949+Examples:
5050+ # Create a new secret interactively (recommended for sensitive data)
5151+ kubectl hsm create database-creds --interactive
5252+5353+ # Create a secret with literal values
5454+ kubectl hsm create api-config --from-literal api_key=abc123 --from-literal endpoint=https://api.example.com
5555+5656+ # List all secrets
5757+ kubectl hsm list
5858+5959+ # Get a specific secret
6060+ kubectl hsm get database-creds
6161+6262+ # Delete a secret (with confirmation)
6363+ kubectl hsm delete old-credentials
6464+6565+ # Check operator health
6666+ kubectl hsm health
6767+6868+For more information about the HSM Secrets Operator, visit:
6969+https://github.com/evanjarrett/hsm-secrets-operator`,
7070+ SilenceUsage: true,
7171+ SilenceErrors: true,
7272+ }
7373+7474+ // Add version command
7575+ cmd.AddCommand(newVersionCmd())
7676+7777+ // Add core secret management commands
7878+ cmd.AddCommand(commands.NewCreateCmd())
7979+ cmd.AddCommand(commands.NewGetCmd())
8080+ cmd.AddCommand(commands.NewListCmd())
8181+ cmd.AddCommand(commands.NewDeleteCmd())
8282+8383+ // Add operational commands
8484+ cmd.AddCommand(commands.NewHealthCmd())
8585+8686+ return cmd
8787+}
8888+8989+func newVersionCmd() *cobra.Command {
9090+ return &cobra.Command{
9191+ Use: "version",
9292+ Short: "Show version information",
9393+ Long: "Display the version of kubectl-hsm plugin and related information.",
9494+ Run: func(cmd *cobra.Command, args []string) {
9595+ fmt.Printf("kubectl-hsm version: %s\n", version)
9696+ fmt.Printf("Compatible with: HSM Secrets Operator v1alpha1\n")
9797+ fmt.Printf("Plugin type: kubectl plugin\n")
9898+ },
9999+ }
100100+}
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package client
1818+1919+import (
2020+ "bytes"
2121+ "context"
2222+ "encoding/json"
2323+ "fmt"
2424+ "io"
2525+ "net/http"
2626+ "net/url"
2727+ "strconv"
2828+ "time"
2929+)
3030+3131+// Client provides methods for interacting with the HSM operator API
3232+type Client struct {
3333+ baseURL string
3434+ httpClient *http.Client
3535+}
3636+3737+// NewClient creates a new HSM API client
3838+func NewClient(baseURL string) *Client {
3939+ return &Client{
4040+ baseURL: baseURL,
4141+ httpClient: &http.Client{
4242+ Timeout: 30 * time.Second,
4343+ },
4444+ }
4545+}
4646+4747+// CreateSecret creates a new secret in the HSM
4848+func (c *Client) CreateSecret(ctx context.Context, name string, data map[string]any) error {
4949+ req := CreateSecretRequest{
5050+ Data: data,
5151+ }
5252+5353+ return c.doRequest(ctx, "POST", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), req, nil)
5454+}
5555+5656+// GetSecret retrieves a secret from the HSM
5757+func (c *Client) GetSecret(ctx context.Context, name string) (*SecretData, error) {
5858+ var result SecretData
5959+ err := c.doRequest(ctx, "GET", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), nil, &result)
6060+ if err != nil {
6161+ return nil, err
6262+ }
6363+ return &result, nil
6464+}
6565+6666+// ListSecrets lists all secrets in the HSM
6767+func (c *Client) ListSecrets(ctx context.Context, page, pageSize int) (*SecretList, error) {
6868+ path := "/api/v1/hsm/secrets"
6969+7070+ // Add pagination parameters if specified
7171+ if page > 0 || pageSize > 0 {
7272+ params := url.Values{}
7373+ if page > 0 {
7474+ params.Add("page", strconv.Itoa(page))
7575+ }
7676+ if pageSize > 0 {
7777+ params.Add("page_size", strconv.Itoa(pageSize))
7878+ }
7979+ if len(params) > 0 {
8080+ path += "?" + params.Encode()
8181+ }
8282+ }
8383+8484+ var result SecretList
8585+ err := c.doRequest(ctx, "GET", path, nil, &result)
8686+ if err != nil {
8787+ return nil, err
8888+ }
8989+ return &result, nil
9090+}
9191+9292+// DeleteSecret deletes a secret from the HSM
9393+func (c *Client) DeleteSecret(ctx context.Context, name string) error {
9494+ return c.doRequest(ctx, "DELETE", fmt.Sprintf("/api/v1/hsm/secrets/%s", name), nil, nil)
9595+}
9696+9797+// GetHealth checks the health status of the HSM operator
9898+func (c *Client) GetHealth(ctx context.Context) (*HealthStatus, error) {
9999+ var result HealthStatus
100100+ err := c.doRequest(ctx, "GET", "/api/v1/health", nil, &result)
101101+ if err != nil {
102102+ return nil, err
103103+ }
104104+ return &result, nil
105105+}
106106+107107+// doRequest performs an HTTP request and handles the standard API response format
108108+func (c *Client) doRequest(ctx context.Context, method, path string, requestBody any, responseData any) error {
109109+ url := c.baseURL + path
110110+111111+ var body io.Reader
112112+ if requestBody != nil {
113113+ jsonData, err := json.Marshal(requestBody)
114114+ if err != nil {
115115+ return fmt.Errorf("failed to marshal request body: %w", err)
116116+ }
117117+ body = bytes.NewBuffer(jsonData)
118118+ }
119119+120120+ req, err := http.NewRequestWithContext(ctx, method, url, body)
121121+ if err != nil {
122122+ return fmt.Errorf("failed to create request: %w", err)
123123+ }
124124+125125+ if requestBody != nil {
126126+ req.Header.Set("Content-Type", "application/json")
127127+ }
128128+129129+ resp, err := c.httpClient.Do(req)
130130+ if err != nil {
131131+ return fmt.Errorf("request failed: %w", err)
132132+ }
133133+ defer resp.Body.Close()
134134+135135+ respBody, err := io.ReadAll(resp.Body)
136136+ if err != nil {
137137+ return fmt.Errorf("failed to read response body: %w", err)
138138+ }
139139+140140+ var apiResp APIResponse
141141+ if err := json.Unmarshal(respBody, &apiResp); err != nil {
142142+ return fmt.Errorf("failed to parse API response: %w", err)
143143+ }
144144+145145+ // Check if the API reported an error
146146+ if !apiResp.Success {
147147+ if apiResp.Error != nil {
148148+ return fmt.Errorf("API error (%s): %s", apiResp.Error.Code, apiResp.Error.Message)
149149+ }
150150+ return fmt.Errorf("API request failed: %s", apiResp.Message)
151151+ }
152152+153153+ // If we need to extract specific data from the response
154154+ if responseData != nil && apiResp.Data != nil {
155155+ dataBytes, err := json.Marshal(apiResp.Data)
156156+ if err != nil {
157157+ return fmt.Errorf("failed to marshal response data: %w", err)
158158+ }
159159+160160+ if err := json.Unmarshal(dataBytes, responseData); err != nil {
161161+ return fmt.Errorf("failed to unmarshal response data: %w", err)
162162+ }
163163+ }
164164+165165+ return nil
166166+}
+83
kubectl-hsm/pkg/client/types.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package client
1818+1919+import (
2020+ "time"
2121+)
2222+2323+// APIResponse represents a standard API response from HSM operator
2424+type APIResponse struct {
2525+ Success bool `json:"success"`
2626+ Message string `json:"message,omitempty"`
2727+ Data any `json:"data,omitempty"`
2828+ Error *APIError `json:"error,omitempty"`
2929+}
3030+3131+// APIError represents an API error response
3232+type APIError struct {
3333+ Code string `json:"code"`
3434+ Message string `json:"message"`
3535+ Details map[string]any `json:"details,omitempty"`
3636+}
3737+3838+// SecretData represents the actual secret data
3939+type SecretData struct {
4040+ Data map[string]any `json:"data"`
4141+ Metadata *SecretInfo `json:"metadata,omitempty"`
4242+}
4343+4444+// SecretInfo represents information about a secret
4545+type SecretInfo struct {
4646+ Label string `json:"label"`
4747+ ID uint32 `json:"id"`
4848+ Format string `json:"format"`
4949+ Description string `json:"description,omitempty"`
5050+ Tags map[string]string `json:"tags,omitempty"`
5151+ CreatedAt time.Time `json:"created_at"`
5252+ UpdatedAt time.Time `json:"updated_at"`
5353+ Size int64 `json:"size"`
5454+ Checksum string `json:"checksum"`
5555+ IsReplicated bool `json:"is_replicated"`
5656+}
5757+5858+// SecretList represents a list of secrets
5959+type SecretList struct {
6060+ Secrets []string `json:"secrets,omitempty"`
6161+ Paths []string `json:"paths,omitempty"`
6262+ Count int `json:"count"`
6363+ Total int `json:"total"`
6464+ Page int `json:"page,omitempty"`
6565+ PageSize int `json:"page_size,omitempty"`
6666+ Prefix string `json:"prefix,omitempty"`
6767+}
6868+6969+// CreateSecretRequest represents a request to create a secret
7070+type CreateSecretRequest struct {
7171+ Data map[string]any `json:"data"`
7272+ Description string `json:"description,omitempty"`
7373+ Tags map[string]string `json:"tags,omitempty"`
7474+}
7575+7676+// HealthStatus represents the health status of the HSM operator
7777+type HealthStatus struct {
7878+ Status string `json:"status"`
7979+ HSMConnected bool `json:"hsm_connected"`
8080+ ReplicationEnabled bool `json:"replication_enabled"`
8181+ ActiveNodes int `json:"active_nodes"`
8282+ Timestamp time.Time `json:"timestamp"`
8383+}
+206
kubectl-hsm/pkg/commands/common.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package commands
1818+1919+import (
2020+ "context"
2121+ "fmt"
2222+ "os"
2323+ "path/filepath"
2424+ "strings"
2525+ "syscall"
2626+ "time"
2727+2828+ "golang.org/x/term"
2929+3030+ "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client"
3131+ "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/util"
3232+)
3333+3434+// CommonOptions holds common options for all commands
3535+type CommonOptions struct {
3636+ Namespace string
3737+ Output string
3838+ Verbose bool
3939+}
4040+4141+// ClientManager manages the connection to the HSM operator API
4242+type ClientManager struct {
4343+ kubectl *util.KubectlUtil
4444+ portForward *util.PortForward
4545+ hsmClient *client.Client
4646+ verbose bool
4747+}
4848+4949+// NewClientManager creates a new client manager
5050+func NewClientManager(namespace string, verbose bool) (*ClientManager, error) {
5151+ kubectl, err := util.NewKubectlUtil(namespace)
5252+ if err != nil {
5353+ return nil, fmt.Errorf("failed to initialize kubectl utilities: %w", err)
5454+ }
5555+5656+ return &ClientManager{
5757+ kubectl: kubectl,
5858+ verbose: verbose,
5959+ }, nil
6060+}
6161+6262+// GetClient returns an HSM API client, setting up port forwarding if necessary
6363+func (cm *ClientManager) GetClient(ctx context.Context) (*client.Client, error) {
6464+ if cm.hsmClient != nil {
6565+ return cm.hsmClient, nil
6666+ }
6767+6868+ // Set up port forwarding to the operator
6969+ localPort := 8090 // Default port, could be made configurable
7070+ pf, err := cm.kubectl.CreatePortForward(ctx, localPort, cm.verbose)
7171+ if err != nil {
7272+ return nil, err
7373+ }
7474+7575+ cm.portForward = pf
7676+7777+ // Create HSM client pointing to the forwarded port
7878+ baseURL := fmt.Sprintf("http://localhost:%d", pf.GetLocalPort())
7979+ cm.hsmClient = client.NewClient(baseURL)
8080+8181+ return cm.hsmClient, nil
8282+}
8383+8484+// Close cleans up the client manager resources
8585+func (cm *ClientManager) Close() {
8686+ if cm.portForward != nil {
8787+ cm.portForward.Stop()
8888+ }
8989+}
9090+9191+// GetCurrentNamespace returns the current namespace
9292+func (cm *ClientManager) GetCurrentNamespace() string {
9393+ return cm.kubectl.GetCurrentNamespace()
9494+}
9595+9696+// readSecretValue reads a secret value, optionally hiding input for passwords
9797+func readSecretValue(prompt string, hidden bool) (string, error) {
9898+ fmt.Print(prompt)
9999+100100+ if hidden {
101101+ // Read password without echoing
102102+ byteValue, err := term.ReadPassword(int(syscall.Stdin))
103103+ fmt.Println() // Add newline after hidden input
104104+ if err != nil {
105105+ return "", fmt.Errorf("failed to read hidden input: %w", err)
106106+ }
107107+ return string(byteValue), nil
108108+ }
109109+110110+ // Read normal input
111111+ var value string
112112+ if _, err := fmt.Scanln(&value); err != nil {
113113+ return "", fmt.Errorf("failed to read input: %w", err)
114114+ }
115115+ return value, nil
116116+}
117117+118118+// parseFromLiteral parses key=value pairs from --from-literal flags
119119+func parseFromLiteral(literals []string) (map[string]any, error) {
120120+ data := make(map[string]any)
121121+122122+ for _, literal := range literals {
123123+ parts := strings.SplitN(literal, "=", 2)
124124+ if len(parts) != 2 {
125125+ return nil, fmt.Errorf("invalid --from-literal format: %s (expected key=value)", literal)
126126+ }
127127+ data[parts[0]] = parts[1]
128128+ }
129129+130130+ return data, nil
131131+}
132132+133133+// readFromFile reads content from a file for --from-file flags
134134+func readFromFile(key, filename string) (map[string]any, error) {
135135+ // Handle both "key=file" and "file" formats
136136+ if filename == "" {
137137+ filename = key
138138+ key = filepath.Base(filename)
139139+ // Remove file extension for the key
140140+ if ext := filepath.Ext(key); ext != "" {
141141+ key = strings.TrimSuffix(key, ext)
142142+ }
143143+ }
144144+145145+ content, err := os.ReadFile(filename)
146146+ if err != nil {
147147+ return nil, fmt.Errorf("failed to read file %s: %w", filename, err)
148148+ }
149149+150150+ data := map[string]any{
151151+ key: string(content),
152152+ }
153153+154154+ return data, nil
155155+}
156156+157157+// promptForInteractiveInput prompts the user for secret values interactively
158158+func promptForInteractiveInput() (map[string]any, error) {
159159+ data := make(map[string]any)
160160+161161+ fmt.Println("Enter secret data (press Enter with empty key to finish):")
162162+163163+ for {
164164+ key, err := readSecretValue("Key: ", false)
165165+ if err != nil {
166166+ return nil, err
167167+ }
168168+169169+ if key == "" {
170170+ break
171171+ }
172172+173173+ // Determine if this looks like a password field
174174+ isPassword := strings.Contains(strings.ToLower(key), "password") ||
175175+ strings.Contains(strings.ToLower(key), "secret") ||
176176+ strings.Contains(strings.ToLower(key), "token") ||
177177+ strings.Contains(strings.ToLower(key), "key")
178178+179179+ value, err := readSecretValue(fmt.Sprintf("Value for '%s': ", key), isPassword)
180180+ if err != nil {
181181+ return nil, err
182182+ }
183183+184184+ data[key] = value
185185+ }
186186+187187+ if len(data) == 0 {
188188+ return nil, fmt.Errorf("no secret data provided")
189189+ }
190190+191191+ return data, nil
192192+}
193193+194194+// formatDuration formats a time duration in a human-readable way
195195+func formatDuration(d time.Duration) string {
196196+ if d < time.Minute {
197197+ return fmt.Sprintf("%ds ago", int(d.Seconds()))
198198+ }
199199+ if d < time.Hour {
200200+ return fmt.Sprintf("%dm ago", int(d.Minutes()))
201201+ }
202202+ if d < 24*time.Hour {
203203+ return fmt.Sprintf("%dh ago", int(d.Hours()))
204204+ }
205205+ return fmt.Sprintf("%dd ago", int(d.Hours()/24))
206206+}
+167
kubectl-hsm/pkg/commands/create.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package commands
1818+1919+import (
2020+ "context"
2121+ "fmt"
2222+ "strings"
2323+2424+ "github.com/spf13/cobra"
2525+)
2626+2727+// CreateOptions holds options for the create command
2828+type CreateOptions struct {
2929+ CommonOptions
3030+ FromLiteral []string
3131+ FromFile []string
3232+ Interactive bool
3333+}
3434+3535+// NewCreateCmd creates the create command
3636+func NewCreateCmd() *cobra.Command {
3737+ opts := &CreateOptions{}
3838+3939+ cmd := &cobra.Command{
4040+ Use: "create SECRET_NAME [flags]",
4141+ Short: "Create a new HSM secret",
4242+ Long: `Create a new secret in the HSM.
4343+4444+The secret data can be provided in several ways:
4545+- --from-literal key=value: Specify key-value pairs directly
4646+- --from-file key=path: Load values from files
4747+- --interactive: Prompt for values interactively (recommended for passwords)
4848+4949+Examples:
5050+ # Create secret with literal values
5151+ kubectl hsm create database-creds --from-literal username=admin --from-literal password=secret123
5252+5353+ # Load values from files
5454+ kubectl hsm create tls-cert --from-file cert=server.crt --from-file key=server.key
5555+5656+ # Interactive creation (prompts for input, hides passwords)
5757+ kubectl hsm create api-keys --interactive`,
5858+ Args: cobra.ExactArgs(1),
5959+ RunE: func(cmd *cobra.Command, args []string) error {
6060+ return opts.Run(cmd.Context(), args[0])
6161+ },
6262+ }
6363+6464+ cmd.Flags().StringArrayVar(&opts.FromLiteral, "from-literal", nil, "Specify a key and literal value to insert in secret (i.e. --from-literal key=value)")
6565+ 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")
6666+ cmd.Flags().BoolVar(&opts.Interactive, "interactive", false, "Prompt for secret values interactively")
6767+ cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace")
6868+ cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)")
6969+ cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details")
7070+7171+ return cmd
7272+}
7373+7474+// Run executes the create command
7575+func (opts *CreateOptions) Run(ctx context.Context, secretName string) error {
7676+ // Validate secret name
7777+ if secretName == "" {
7878+ return fmt.Errorf("secret name is required")
7979+ }
8080+8181+ // Validate options
8282+ methods := 0
8383+ if len(opts.FromLiteral) > 0 {
8484+ methods++
8585+ }
8686+ if len(opts.FromFile) > 0 {
8787+ methods++
8888+ }
8989+ if opts.Interactive {
9090+ methods++
9191+ }
9292+9393+ if methods == 0 {
9494+ return fmt.Errorf("must specify one of --from-literal, --from-file, or --interactive")
9595+ }
9696+ if methods > 1 {
9797+ return fmt.Errorf("cannot specify multiple input methods (--from-literal, --from-file, --interactive)")
9898+ }
9999+100100+ // Collect secret data
101101+ var secretData map[string]any
102102+ var err error
103103+104104+ if len(opts.FromLiteral) > 0 {
105105+ secretData, err = parseFromLiteral(opts.FromLiteral)
106106+ if err != nil {
107107+ return err
108108+ }
109109+ } else if len(opts.FromFile) > 0 {
110110+ secretData = make(map[string]any)
111111+ for _, fileSpec := range opts.FromFile {
112112+ parts := strings.SplitN(fileSpec, "=", 2)
113113+ var key, filename string
114114+ if len(parts) == 2 {
115115+ key = parts[0]
116116+ filename = parts[1]
117117+ } else {
118118+ filename = parts[0]
119119+ }
120120+121121+ fileData, err := readFromFile(key, filename)
122122+ if err != nil {
123123+ return err
124124+ }
125125+126126+ // Merge file data
127127+ for k, v := range fileData {
128128+ secretData[k] = v
129129+ }
130130+ }
131131+ } else if opts.Interactive {
132132+ secretData, err = promptForInteractiveInput()
133133+ if err != nil {
134134+ return err
135135+ }
136136+ }
137137+138138+ // Create client manager
139139+ cm, err := NewClientManager(opts.Namespace, opts.Verbose)
140140+ if err != nil {
141141+ return err
142142+ }
143143+ defer cm.Close()
144144+145145+ // Get HSM client
146146+ hsmClient, err := cm.GetClient(ctx)
147147+ if err != nil {
148148+ return err
149149+ }
150150+151151+ // Create the secret
152152+ fmt.Printf("Creating secret '%s' in namespace '%s'...\n", secretName, cm.GetCurrentNamespace())
153153+ if err := hsmClient.CreateSecret(ctx, secretName, secretData); err != nil {
154154+ return fmt.Errorf("failed to create secret: %w", err)
155155+ }
156156+157157+ fmt.Printf("Secret '%s' created successfully.\n", secretName)
158158+159159+ // Show how to retrieve the secret
160160+ fmt.Printf("\nTo view the secret:\n")
161161+ fmt.Printf(" kubectl hsm get %s\n", secretName)
162162+ if opts.Namespace != "" {
163163+ fmt.Printf(" kubectl hsm get %s -n %s\n", secretName, opts.Namespace)
164164+ }
165165+166166+ return nil
167167+}
+136
kubectl-hsm/pkg/commands/delete.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package commands
1818+1919+import (
2020+ "bufio"
2121+ "context"
2222+ "fmt"
2323+ "os"
2424+ "strings"
2525+2626+ "github.com/spf13/cobra"
2727+)
2828+2929+// DeleteOptions holds options for the delete command
3030+type DeleteOptions struct {
3131+ CommonOptions
3232+ Key string
3333+ Force bool
3434+}
3535+3636+// NewDeleteCmd creates the delete command
3737+func NewDeleteCmd() *cobra.Command {
3838+ opts := &DeleteOptions{}
3939+4040+ cmd := &cobra.Command{
4141+ Use: "delete SECRET_NAME [flags]",
4242+ Short: "Delete an HSM secret",
4343+ Long: `Delete an HSM secret or a specific key within a secret.
4444+4545+By default, deletes the entire secret. Use --key to delete only a specific key.
4646+4747+Examples:
4848+ # Delete an entire secret (with confirmation)
4949+ kubectl hsm delete database-creds
5050+5151+ # Delete a specific key from a secret
5252+ kubectl hsm delete database-creds --key password
5353+5454+ # Force delete without confirmation
5555+ kubectl hsm delete api-keys --force`,
5656+ Args: cobra.ExactArgs(1),
5757+ RunE: func(cmd *cobra.Command, args []string) error {
5858+ return opts.Run(cmd.Context(), args[0])
5959+ },
6060+ }
6161+6262+ cmd.Flags().StringVar(&opts.Key, "key", "", "Delete only the specified key from the secret")
6363+ cmd.Flags().BoolVar(&opts.Force, "force", false, "Skip confirmation prompt")
6464+ cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace")
6565+ cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details")
6666+6767+ return cmd
6868+}
6969+7070+// Run executes the delete command
7171+func (opts *DeleteOptions) Run(ctx context.Context, secretName string) error {
7272+ // Validate secret name
7373+ if secretName == "" {
7474+ return fmt.Errorf("secret name is required")
7575+ }
7676+7777+ // Create client manager
7878+ cm, err := NewClientManager(opts.Namespace, opts.Verbose)
7979+ if err != nil {
8080+ return err
8181+ }
8282+ defer cm.Close()
8383+8484+ // Get HSM client
8585+ hsmClient, err := cm.GetClient(ctx)
8686+ if err != nil {
8787+ return err
8888+ }
8989+9090+ if opts.Key != "" {
9191+ return fmt.Errorf("deleting individual keys is not yet supported - please delete the entire secret and recreate it")
9292+ }
9393+9494+ // Confirm deletion unless force is specified
9595+ if !opts.Force {
9696+ if err := opts.confirmDeletion(secretName); err != nil {
9797+ return err
9898+ }
9999+ }
100100+101101+ // Delete the secret
102102+ fmt.Printf("Deleting secret '%s' from namespace '%s'...\n", secretName, cm.GetCurrentNamespace())
103103+ if err := hsmClient.DeleteSecret(ctx, secretName); err != nil {
104104+ return fmt.Errorf("failed to delete secret: %w", err)
105105+ }
106106+107107+ fmt.Printf("Secret '%s' deleted successfully.\n", secretName)
108108+ return nil
109109+}
110110+111111+// confirmDeletion prompts the user to confirm the deletion
112112+func (opts *DeleteOptions) confirmDeletion(secretName string) error {
113113+ var target string
114114+ if opts.Key != "" {
115115+ target = fmt.Sprintf("key '%s' from secret '%s'", opts.Key, secretName)
116116+ } else {
117117+ target = fmt.Sprintf("secret '%s'", secretName)
118118+ }
119119+120120+ fmt.Printf("⚠️ You are about to delete %s.\n", target)
121121+ fmt.Printf("This action cannot be undone.\n\n")
122122+ fmt.Printf("Type the secret name '%s' to confirm: ", secretName)
123123+124124+ reader := bufio.NewReader(os.Stdin)
125125+ input, err := reader.ReadString('\n')
126126+ if err != nil {
127127+ return fmt.Errorf("failed to read confirmation: %w", err)
128128+ }
129129+130130+ input = strings.TrimSpace(input)
131131+ if input != secretName {
132132+ return fmt.Errorf("confirmation failed - deletion cancelled")
133133+ }
134134+135135+ return nil
136136+}
+264
kubectl-hsm/pkg/commands/get.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package commands
1818+1919+import (
2020+ "context"
2121+ "encoding/base64"
2222+ "encoding/json"
2323+ "fmt"
2424+ "sort"
2525+ "strings"
2626+2727+ "github.com/spf13/cobra"
2828+ "sigs.k8s.io/yaml"
2929+3030+ "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client"
3131+)
3232+3333+// GetOptions holds options for the get command
3434+type GetOptions struct {
3535+ CommonOptions
3636+ Key string
3737+}
3838+3939+// NewGetCmd creates the get command
4040+func NewGetCmd() *cobra.Command {
4141+ opts := &GetOptions{}
4242+4343+ cmd := &cobra.Command{
4444+ Use: "get SECRET_NAME [flags]",
4545+ Short: "Get an HSM secret",
4646+ Long: `Retrieve and display an HSM secret.
4747+4848+By default, displays all keys in the secret. Use --key to display only a specific key.
4949+5050+Examples:
5151+ # Get all keys in a secret
5252+ kubectl hsm get database-creds
5353+5454+ # Get a specific key from a secret
5555+ kubectl hsm get database-creds --key password
5656+5757+ # Output in different formats
5858+ kubectl hsm get api-keys -o json
5959+ kubectl hsm get api-keys -o yaml`,
6060+ Args: cobra.ExactArgs(1),
6161+ RunE: func(cmd *cobra.Command, args []string) error {
6262+ return opts.Run(cmd.Context(), args[0])
6363+ },
6464+ }
6565+6666+ cmd.Flags().StringVar(&opts.Key, "key", "", "Show only the specified key")
6767+ cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace")
6868+ cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)")
6969+ cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details")
7070+7171+ return cmd
7272+}
7373+7474+// Run executes the get command
7575+func (opts *GetOptions) Run(ctx context.Context, secretName string) error {
7676+ // Validate secret name
7777+ if secretName == "" {
7878+ return fmt.Errorf("secret name is required")
7979+ }
8080+8181+ // Create client manager
8282+ cm, err := NewClientManager(opts.Namespace, opts.Verbose)
8383+ if err != nil {
8484+ return err
8585+ }
8686+ defer cm.Close()
8787+8888+ // Get HSM client
8989+ hsmClient, err := cm.GetClient(ctx)
9090+ if err != nil {
9191+ return err
9292+ }
9393+9494+ // Retrieve the secret
9595+ secretData, err := hsmClient.GetSecret(ctx, secretName)
9696+ if err != nil {
9797+ return fmt.Errorf("failed to get secret: %w", err)
9898+ }
9999+100100+ // Handle specific key request
101101+ if opts.Key != "" {
102102+ value, exists := secretData.Data[opts.Key]
103103+ if !exists {
104104+ return fmt.Errorf("key '%s' not found in secret '%s'", opts.Key, secretName)
105105+ }
106106+107107+ switch opts.Output {
108108+ case "json":
109109+ keyData := map[string]any{opts.Key: value}
110110+ jsonBytes, err := json.MarshalIndent(keyData, "", " ")
111111+ if err != nil {
112112+ return fmt.Errorf("failed to marshal key data to JSON: %w", err)
113113+ }
114114+ fmt.Println(string(jsonBytes))
115115+ case "yaml":
116116+ keyData := map[string]any{opts.Key: value}
117117+ yamlBytes, err := yaml.Marshal(keyData)
118118+ if err != nil {
119119+ return fmt.Errorf("failed to marshal key data to YAML: %w", err)
120120+ }
121121+ fmt.Print(string(yamlBytes))
122122+ default:
123123+ fmt.Printf("%s\n", value)
124124+ }
125125+ return nil
126126+ }
127127+128128+ // Display full secret
129129+ switch opts.Output {
130130+ case "json":
131131+ jsonBytes, err := json.MarshalIndent(secretData, "", " ")
132132+ if err != nil {
133133+ return fmt.Errorf("failed to marshal secret data to JSON: %w", err)
134134+ }
135135+ fmt.Println(string(jsonBytes))
136136+ case "yaml":
137137+ yamlBytes, err := yaml.Marshal(secretData)
138138+ if err != nil {
139139+ return fmt.Errorf("failed to marshal secret data to YAML: %w", err)
140140+ }
141141+ fmt.Print(string(yamlBytes))
142142+ default:
143143+ return opts.displaySecretText(secretName, secretData, cm.GetCurrentNamespace())
144144+ }
145145+146146+ return nil
147147+}
148148+149149+// displaySecretText displays the secret in a human-readable text format
150150+func (opts *GetOptions) displaySecretText(secretName string, secretData *client.SecretData, namespace string) error {
151151+ fmt.Printf("Name: %s\n", secretName)
152152+ fmt.Printf("Namespace: %s\n", namespace)
153153+154154+155155+ // Parse metadata from _metadata key if present
156156+ if metadataValue, hasMetadata := secretData.Data["_metadata"]; hasMetadata {
157157+ if err := opts.parseAndDisplayMetadata(metadataValue); err != nil {
158158+ fmt.Printf("Metadata: <parse error: %v>\n", err)
159159+ }
160160+ }
161161+162162+ // Display data keys (but not values for security, and exclude _metadata)
163163+ var keys []string
164164+ for k := range secretData.Data {
165165+ if k != "_metadata" {
166166+ keys = append(keys, k)
167167+ }
168168+ }
169169+170170+ if len(keys) > 0 {
171171+ sort.Strings(keys)
172172+ fmt.Printf("Keys: %s\n", strings.Join(keys, ", "))
173173+ } else {
174174+ fmt.Printf("Keys: <none>\n")
175175+ }
176176+177177+ return nil
178178+}
179179+180180+// parseAndDisplayMetadata parses and displays metadata from the _metadata key
181181+func (opts *GetOptions) parseAndDisplayMetadata(metadataValue any) error {
182182+ var metadataMap map[string]any
183183+184184+ switch v := metadataValue.(type) {
185185+ case string:
186186+ // First try to decode as base64, then parse JSON
187187+ decoded, err := base64.StdEncoding.DecodeString(v)
188188+ if err != nil {
189189+ // If base64 decode fails, try direct JSON parsing
190190+ if jsonErr := json.Unmarshal([]byte(v), &metadataMap); jsonErr != nil {
191191+ return fmt.Errorf("failed to parse metadata (not base64: %v, not JSON: %v)", err, jsonErr)
192192+ }
193193+ } else {
194194+ // Parse the decoded base64 as JSON
195195+ if err := json.Unmarshal(decoded, &metadataMap); err != nil {
196196+ return fmt.Errorf("failed to parse base64-decoded metadata JSON: %w", err)
197197+ }
198198+ }
199199+ case map[string]any:
200200+ metadataMap = v
201201+ default:
202202+ return fmt.Errorf("unexpected metadata type: %T", v)
203203+ }
204204+205205+ // Display all metadata fields with proper formatting
206206+ opts.displayMetadataFields(metadataMap, "")
207207+208208+ return nil
209209+}
210210+211211+// displayMetadataFields displays metadata fields with proper key formatting
212212+func (opts *GetOptions) displayMetadataFields(data map[string]any, indent string) {
213213+ // Get all keys and sort them for consistent output
214214+ keys := make([]string, 0, len(data))
215215+ for k := range data {
216216+ keys = append(keys, k)
217217+ }
218218+ sort.Strings(keys)
219219+220220+ for _, key := range keys {
221221+ value := data[key]
222222+223223+ // Handle nested objects (like labels)
224224+ if nested, ok := value.(map[string]any); ok {
225225+ // Display the parent key with capitalization
226226+ displayKey := opts.formatMetadataKey(key)
227227+ fmt.Printf("%s%-13s\n", indent, displayKey+":")
228228+229229+ // Display nested items with indentation, keeping original keys
230230+ nestedKeys := make([]string, 0, len(nested))
231231+ for k := range nested {
232232+ nestedKeys = append(nestedKeys, k)
233233+ }
234234+ sort.Strings(nestedKeys)
235235+236236+ for _, nestedKey := range nestedKeys {
237237+ fmt.Printf("%s %-11s %v\n", indent, nestedKey+":", nested[nestedKey])
238238+ }
239239+ continue
240240+ }
241241+242242+ // Format the display key (capitalize first letter, replace underscores with spaces)
243243+ displayKey := opts.formatMetadataKey(key)
244244+245245+ // Display the key-value pair
246246+ fmt.Printf("%s%-13s %v\n", indent, displayKey+":", value)
247247+ }
248248+}
249249+250250+// formatMetadataKey formats a metadata key for display
251251+func (opts *GetOptions) formatMetadataKey(key string) string {
252252+ // Replace underscores with spaces
253253+ formatted := strings.ReplaceAll(key, "_", " ")
254254+255255+ // Split into words and capitalize each word
256256+ words := strings.Fields(formatted)
257257+ for i, word := range words {
258258+ if len(word) > 0 {
259259+ words[i] = strings.ToUpper(word[:1]) + word[1:]
260260+ }
261261+ }
262262+263263+ return strings.Join(words, " ")
264264+}
+165
kubectl-hsm/pkg/commands/health.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package commands
1818+1919+import (
2020+ "context"
2121+ "encoding/json"
2222+ "fmt"
2323+2424+ "github.com/spf13/cobra"
2525+ "sigs.k8s.io/yaml"
2626+2727+ "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client"
2828+)
2929+3030+// HealthOptions holds options for the health command
3131+type HealthOptions struct {
3232+ CommonOptions
3333+}
3434+3535+// NewHealthCmd creates the health command
3636+func NewHealthCmd() *cobra.Command {
3737+ opts := &HealthOptions{}
3838+3939+ cmd := &cobra.Command{
4040+ Use: "health [flags]",
4141+ Short: "Check HSM operator health",
4242+ Long: `Check the health status of the HSM operator and connected devices.
4343+4444+This command verifies:
4545+- Connection to the HSM operator API
4646+- HSM device connectivity
4747+- Replication status
4848+- Active agent nodes
4949+5050+Examples:
5151+ # Check health status
5252+ kubectl hsm health
5353+5454+ # Check health with JSON output
5555+ kubectl hsm health -o json`,
5656+ Args: cobra.NoArgs,
5757+ RunE: func(cmd *cobra.Command, args []string) error {
5858+ return opts.Run(cmd.Context())
5959+ },
6060+ }
6161+6262+ cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace")
6363+ cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)")
6464+ cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details")
6565+6666+ return cmd
6767+}
6868+6969+// Run executes the health command
7070+func (opts *HealthOptions) Run(ctx context.Context) error {
7171+ // Create client manager
7272+ cm, err := NewClientManager(opts.Namespace, opts.Verbose)
7373+ if err != nil {
7474+ return err
7575+ }
7676+ defer cm.Close()
7777+7878+ // Get HSM client
7979+ hsmClient, err := cm.GetClient(ctx)
8080+ if err != nil {
8181+ return fmt.Errorf("failed to connect to HSM operator: %w", err)
8282+ }
8383+8484+ // Get health status
8585+ health, err := hsmClient.GetHealth(ctx)
8686+ if err != nil {
8787+ return fmt.Errorf("failed to get health status: %w", err)
8888+ }
8989+9090+ // Handle output formatting
9191+ switch opts.Output {
9292+ case "json":
9393+ jsonBytes, err := json.MarshalIndent(health, "", " ")
9494+ if err != nil {
9595+ return fmt.Errorf("failed to marshal health status to JSON: %w", err)
9696+ }
9797+ fmt.Println(string(jsonBytes))
9898+ case "yaml":
9999+ yamlBytes, err := yaml.Marshal(health)
100100+ if err != nil {
101101+ return fmt.Errorf("failed to marshal health status to YAML: %w", err)
102102+ }
103103+ fmt.Print(string(yamlBytes))
104104+ default:
105105+ return opts.displayHealthText(health, cm.GetCurrentNamespace())
106106+ }
107107+108108+ return nil
109109+}
110110+111111+// displayHealthText displays the health status in a human-readable format
112112+func (opts *HealthOptions) displayHealthText(health *client.HealthStatus, namespace string) error {
113113+ fmt.Printf("HSM Operator Health Status\n")
114114+ fmt.Printf("==========================\n\n")
115115+116116+ // Overall status with emoji
117117+ statusEmoji := "✅"
118118+ if health.Status == "degraded" {
119119+ statusEmoji = "⚠️"
120120+ } else if health.Status == "unhealthy" {
121121+ statusEmoji = "❌"
122122+ }
123123+124124+ fmt.Printf("Overall Status: %s %s\n", statusEmoji, health.Status)
125125+ fmt.Printf("Namespace: %s\n", namespace)
126126+ fmt.Printf("Check Time: %s\n", health.Timestamp.Format("2006-01-02 15:04:05 UTC"))
127127+ fmt.Printf("\n")
128128+129129+ // HSM connectivity
130130+ hsmEmoji := "✅"
131131+ if !health.HSMConnected {
132132+ hsmEmoji = "❌"
133133+ }
134134+ fmt.Printf("HSM Connected: %s %t\n", hsmEmoji, health.HSMConnected)
135135+136136+ // Replication status
137137+ replicationEmoji := "✅"
138138+ if !health.ReplicationEnabled {
139139+ replicationEmoji = "⚠️"
140140+ }
141141+ fmt.Printf("Replication: %s %t\n", replicationEmoji, health.ReplicationEnabled)
142142+ fmt.Printf("Active Nodes: %d\n", health.ActiveNodes)
143143+ fmt.Printf("\n")
144144+145145+ // Recommendations
146146+ if !health.HSMConnected {
147147+ fmt.Printf("⚠️ Recommendations:\n")
148148+ fmt.Printf(" • Check if HSM devices are connected and accessible\n")
149149+ fmt.Printf(" • Verify HSM agent pods are running: kubectl get pods -l app.kubernetes.io/component=agent\n")
150150+ fmt.Printf(" • Check agent logs for connection errors\n")
151151+ }
152152+153153+ if !health.ReplicationEnabled && health.ActiveNodes <= 1 {
154154+ fmt.Printf("💡 Recommendations:\n")
155155+ fmt.Printf(" • Consider adding more HSM devices for high availability\n")
156156+ fmt.Printf(" • Multiple devices enable automatic replication and failover\n")
157157+ }
158158+159159+ // Overall assessment
160160+ if health.Status == "healthy" {
161161+ fmt.Printf("🎉 All systems operational!\n")
162162+ }
163163+164164+ return nil
165165+}
+220
kubectl-hsm/pkg/commands/list.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package commands
1818+1919+import (
2020+ "context"
2121+ "encoding/json"
2222+ "fmt"
2323+ "os"
2424+ "sort"
2525+ "text/tabwriter"
2626+2727+ "github.com/spf13/cobra"
2828+ "sigs.k8s.io/yaml"
2929+3030+ "github.com/evanjarrett/hsm-secrets-operator/kubectl-hsm/pkg/client"
3131+)
3232+3333+// ListOptions holds options for the list command
3434+type ListOptions struct {
3535+ CommonOptions
3636+ AllNamespaces bool
3737+}
3838+3939+// NewListCmd creates the list command
4040+func NewListCmd() *cobra.Command {
4141+ opts := &ListOptions{}
4242+4343+ cmd := &cobra.Command{
4444+ Use: "list [flags]",
4545+ Short: "List HSM secrets",
4646+ Long: `List all secrets stored in the HSM.
4747+4848+Examples:
4949+ # List secrets in current namespace
5050+ kubectl hsm list
5151+5252+ # List secrets in specific namespace
5353+ kubectl hsm list -n hsm-secrets-operator-system
5454+5555+ # List secrets in all namespaces (Note: HSM secrets are global, namespace is for display only)
5656+ kubectl hsm list --all-namespaces
5757+5858+ # Output in different formats
5959+ kubectl hsm list -o json
6060+ kubectl hsm list -o yaml`,
6161+ Args: cobra.NoArgs,
6262+ RunE: func(cmd *cobra.Command, args []string) error {
6363+ return opts.Run(cmd.Context())
6464+ },
6565+ }
6666+6767+ cmd.Flags().BoolVar(&opts.AllNamespaces, "all-namespaces", false, "List secrets from all namespaces")
6868+ cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "", "Override the default namespace")
6969+ cmd.Flags().StringVarP(&opts.Output, "output", "o", "text", "Output format (text, json, yaml)")
7070+ cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including port forward details")
7171+7272+ return cmd
7373+}
7474+7575+// Run executes the list command
7676+func (opts *ListOptions) Run(ctx context.Context) error {
7777+ // Create client manager
7878+ cm, err := NewClientManager(opts.Namespace, opts.Verbose)
7979+ if err != nil {
8080+ return err
8181+ }
8282+ defer cm.Close()
8383+8484+ // Get HSM client
8585+ hsmClient, err := cm.GetClient(ctx)
8686+ if err != nil {
8787+ return err
8888+ }
8989+9090+ // List secrets (HSM secrets are global, but we display namespace context)
9191+ secretList, err := hsmClient.ListSecrets(ctx, 0, 0) // No pagination for now
9292+ if err != nil {
9393+ return fmt.Errorf("failed to list secrets: %w", err)
9494+ }
9595+9696+ // Handle output formatting
9797+ switch opts.Output {
9898+ case "json":
9999+ jsonBytes, err := json.MarshalIndent(secretList, "", " ")
100100+ if err != nil {
101101+ return fmt.Errorf("failed to marshal secrets to JSON: %w", err)
102102+ }
103103+ fmt.Println(string(jsonBytes))
104104+ case "yaml":
105105+ yamlBytes, err := yaml.Marshal(secretList)
106106+ if err != nil {
107107+ return fmt.Errorf("failed to marshal secrets to YAML: %w", err)
108108+ }
109109+ fmt.Print(string(yamlBytes))
110110+ default:
111111+ return opts.displaySecretsText(secretList, cm.GetCurrentNamespace())
112112+ }
113113+114114+ return nil
115115+}
116116+117117+// displaySecretsText displays the secrets in a human-readable table format
118118+func (opts *ListOptions) displaySecretsText(secretList *client.SecretList, currentNamespace string) error {
119119+ if secretList == nil {
120120+ fmt.Println("No secrets found")
121121+ return nil
122122+ }
123123+124124+ // The API returns secret names as strings, not full SecretInfo objects
125125+ if len(secretList.Secrets) == 0 {
126126+ fmt.Println("No secrets found")
127127+ return nil
128128+ }
129129+130130+ // Sort secret names for consistent output
131131+ secrets := make([]string, len(secretList.Secrets))
132132+ copy(secrets, secretList.Secrets)
133133+ sort.Strings(secrets)
134134+135135+ // Create table writer
136136+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
137137+138138+ // Print header
139139+ if opts.AllNamespaces {
140140+ fmt.Fprintln(w, "NAMESPACE\tNAME")
141141+ } else {
142142+ fmt.Fprintln(w, "NAME")
143143+ }
144144+145145+ // Print each secret name
146146+ for _, secret := range secrets {
147147+ if opts.AllNamespaces {
148148+ fmt.Fprintf(w, "%s\t%s\n", currentNamespace, secret)
149149+ } else {
150150+ fmt.Fprintf(w, "%s\n", secret)
151151+ }
152152+ }
153153+154154+ w.Flush()
155155+156156+ // Show summary
157157+ fmt.Printf("\nTotal: %d secrets\n", secretList.Count)
158158+159159+ return nil
160160+}
161161+162162+// displaySecretPaths displays just the secret paths when detailed info isn't available
163163+func (opts *ListOptions) displaySecretPaths(secretList *client.SecretList, currentNamespace string) error {
164164+ if len(secretList.Paths) == 0 {
165165+ fmt.Println("No secrets found")
166166+ return nil
167167+ }
168168+169169+ // Sort paths for consistent output
170170+ paths := make([]string, len(secretList.Paths))
171171+ copy(paths, secretList.Paths)
172172+ sort.Strings(paths)
173173+174174+ // Create table writer
175175+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
176176+177177+ // Print header
178178+ if opts.AllNamespaces {
179179+ fmt.Fprintln(w, "NAMESPACE\tNAME")
180180+ } else {
181181+ fmt.Fprintln(w, "NAME")
182182+ }
183183+184184+ // Print each path
185185+ for _, path := range paths {
186186+ if opts.AllNamespaces {
187187+ fmt.Fprintf(w, "%s\t%s\n", currentNamespace, path)
188188+ } else {
189189+ fmt.Fprintf(w, "%s\n", path)
190190+ }
191191+ }
192192+193193+ w.Flush()
194194+195195+ // Show summary
196196+ fmt.Printf("\nTotal: %d secrets\n", secretList.Count)
197197+198198+ return nil
199199+}
200200+201201+// formatBytes formats a byte count in human-readable format
202202+func formatBytes(bytes int64) string {
203203+ if bytes < 1024 {
204204+ return fmt.Sprintf("%dB", bytes)
205205+ }
206206+207207+ units := []string{"B", "KB", "MB", "GB"}
208208+ size := float64(bytes)
209209+ unitIndex := 0
210210+211211+ for unitIndex < len(units)-1 && size >= 1024 {
212212+ size /= 1024
213213+ unitIndex++
214214+ }
215215+216216+ if size == float64(int64(size)) {
217217+ return fmt.Sprintf("%.0f%s", size, units[unitIndex])
218218+ }
219219+ return fmt.Sprintf("%.1f%s", size, units[unitIndex])
220220+}
+265
kubectl-hsm/pkg/util/kubectl.go
···11+/*
22+Copyright 2025.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+1717+package util
1818+1919+import (
2020+ "context"
2121+ "fmt"
2222+ "io"
2323+ "net/http"
2424+ "os"
2525+ "path/filepath"
2626+ "time"
2727+2828+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2929+ "k8s.io/client-go/kubernetes"
3030+ "k8s.io/client-go/rest"
3131+ "k8s.io/client-go/tools/clientcmd"
3232+ "k8s.io/client-go/tools/portforward"
3333+ "k8s.io/client-go/transport/spdy"
3434+)
3535+3636+const (
3737+ operatorServiceName = "hsm-secrets-operator-api"
3838+ operatorServicePort = 8090
3939+)
4040+4141+// KubectlUtil provides kubectl integration utilities
4242+type KubectlUtil struct {
4343+ config *rest.Config
4444+ clientset *kubernetes.Clientset
4545+ namespace string
4646+}
4747+4848+// NewKubectlUtil creates a new kubectl utility instance
4949+func NewKubectlUtil(namespace string) (*KubectlUtil, error) {
5050+ config, err := getKubeConfig()
5151+ if err != nil {
5252+ return nil, fmt.Errorf("failed to get kubernetes config: %w", err)
5353+ }
5454+5555+ clientset, err := kubernetes.NewForConfig(config)
5656+ if err != nil {
5757+ return nil, fmt.Errorf("failed to create kubernetes clientset: %w", err)
5858+ }
5959+6060+ // Use provided namespace or get current namespace from kubeconfig
6161+ if namespace == "" {
6262+ namespace, err = getCurrentNamespace()
6363+ if err != nil {
6464+ return nil, fmt.Errorf("failed to get current namespace: %w", err)
6565+ }
6666+ }
6767+6868+ return &KubectlUtil{
6969+ config: config,
7070+ clientset: clientset,
7171+ namespace: namespace,
7272+ }, nil
7373+}
7474+7575+// GetCurrentNamespace returns the current namespace from kubeconfig
7676+func (k *KubectlUtil) GetCurrentNamespace() string {
7777+ return k.namespace
7878+}
7979+8080+// FindOperatorService finds the HSM operator service in the current namespace
8181+func (k *KubectlUtil) FindOperatorService(ctx context.Context) error {
8282+ svc, err := k.clientset.CoreV1().Services(k.namespace).Get(ctx, operatorServiceName, metav1.GetOptions{})
8383+ if err != nil {
8484+ 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>",
8585+ k.namespace, err, k.namespace)
8686+ }
8787+8888+ // Check if service has the expected port
8989+ found := false
9090+ for _, port := range svc.Spec.Ports {
9191+ if port.Port == operatorServicePort {
9292+ found = true
9393+ break
9494+ }
9595+ }
9696+9797+ if !found {
9898+ return fmt.Errorf("operator service '%s' does not expose port %d", operatorServiceName, operatorServicePort)
9999+ }
100100+101101+ return nil
102102+}
103103+104104+// CreatePortForward creates a port forward to the operator service
105105+func (k *KubectlUtil) CreatePortForward(ctx context.Context, localPort int, verbose bool) (*PortForward, error) {
106106+ // First check if the service exists
107107+ if err := k.FindOperatorService(ctx); err != nil {
108108+ return nil, err
109109+ }
110110+111111+ // Get a pod from the operator deployment
112112+ pods, err := k.clientset.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{
113113+ LabelSelector: "app.kubernetes.io/name=hsm-secrets-operator,control-plane=controller-manager",
114114+ })
115115+ if err != nil {
116116+ return nil, fmt.Errorf("failed to list operator pods: %w", err)
117117+ }
118118+119119+ if len(pods.Items) == 0 {
120120+ return nil, fmt.Errorf("no operator manager pods found in namespace '%s'", k.namespace)
121121+ }
122122+123123+ pod := pods.Items[0]
124124+ if pod.Status.Phase != "Running" {
125125+ return nil, fmt.Errorf("operator pod '%s' is not running (status: %s)", pod.Name, pod.Status.Phase)
126126+ }
127127+128128+ // Create port forward
129129+ pf := &PortForward{
130130+ config: k.config,
131131+ clientset: k.clientset,
132132+ namespace: k.namespace,
133133+ podName: pod.Name,
134134+ localPort: localPort,
135135+ remotePort: operatorServicePort,
136136+ stopCh: make(chan struct{}),
137137+ readyCh: make(chan struct{}),
138138+ verbose: verbose,
139139+ }
140140+141141+ if err := pf.Start(ctx); err != nil {
142142+ return nil, fmt.Errorf("failed to start port forward: %w", err)
143143+ }
144144+145145+ return pf, nil
146146+}
147147+148148+// PortForward manages a port forward connection
149149+type PortForward struct {
150150+ config *rest.Config
151151+ clientset *kubernetes.Clientset
152152+ namespace string
153153+ podName string
154154+ localPort int
155155+ remotePort int
156156+ stopCh chan struct{}
157157+ readyCh chan struct{}
158158+ verbose bool
159159+}
160160+161161+// Start starts the port forward
162162+func (pf *PortForward) Start(ctx context.Context) error {
163163+ req := pf.clientset.CoreV1().RESTClient().Post().
164164+ Resource("pods").
165165+ Namespace(pf.namespace).
166166+ Name(pf.podName).
167167+ SubResource("portforward")
168168+169169+ transport, upgrader, err := spdy.RoundTripperFor(pf.config)
170170+ if err != nil {
171171+ return fmt.Errorf("failed to create round tripper: %w", err)
172172+ }
173173+174174+ dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
175175+176176+ ports := []string{fmt.Sprintf("%d:%d", pf.localPort, pf.remotePort)}
177177+178178+ // Control output based on verbose flag
179179+ var stdout, stderr io.Writer
180180+ if pf.verbose {
181181+ stdout = os.Stdout
182182+ stderr = os.Stderr
183183+ } else {
184184+ stdout = io.Discard
185185+ stderr = io.Discard
186186+ }
187187+188188+ forwarder, err := portforward.New(dialer, ports, pf.stopCh, pf.readyCh, stdout, stderr)
189189+ if err != nil {
190190+ return fmt.Errorf("failed to create port forwarder: %w", err)
191191+ }
192192+193193+ go func() {
194194+ if err := forwarder.ForwardPorts(); err != nil && pf.verbose {
195195+ fmt.Fprintf(os.Stderr, "Port forward error: %v\n", err)
196196+ }
197197+ }()
198198+199199+ // Wait for port forward to be ready with timeout
200200+ select {
201201+ case <-pf.readyCh:
202202+ return nil
203203+ case <-time.After(10 * time.Second):
204204+ pf.Stop()
205205+ return fmt.Errorf("port forward did not become ready within 10 seconds")
206206+ case <-ctx.Done():
207207+ pf.Stop()
208208+ return ctx.Err()
209209+ }
210210+}
211211+212212+// Stop stops the port forward
213213+func (pf *PortForward) Stop() {
214214+ close(pf.stopCh)
215215+}
216216+217217+// GetLocalPort returns the local port being forwarded
218218+func (pf *PortForward) GetLocalPort() int {
219219+ return pf.localPort
220220+}
221221+222222+// getKubeConfig gets the Kubernetes client configuration
223223+func getKubeConfig() (*rest.Config, error) {
224224+ // Try in-cluster config first (for when running in pod)
225225+ if config, err := rest.InClusterConfig(); err == nil {
226226+ return config, nil
227227+ }
228228+229229+ // Fall back to kubeconfig file
230230+ kubeconfig := os.Getenv("KUBECONFIG")
231231+ if kubeconfig == "" {
232232+ kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config")
233233+ }
234234+235235+ config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
236236+ if err != nil {
237237+ return nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
238238+ }
239239+240240+ return config, nil
241241+}
242242+243243+// getCurrentNamespace gets the current namespace from kubeconfig
244244+func getCurrentNamespace() (string, error) {
245245+ kubeconfig := os.Getenv("KUBECONFIG")
246246+ if kubeconfig == "" {
247247+ kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config")
248248+ }
249249+250250+ configLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
251251+ &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
252252+ &clientcmd.ConfigOverrides{},
253253+ )
254254+255255+ namespace, _, err := configLoader.Namespace()
256256+ if err != nil {
257257+ return "", fmt.Errorf("failed to get namespace from kubeconfig: %w", err)
258258+ }
259259+260260+ if namespace == "" {
261261+ namespace = "default"
262262+ }
263263+264264+ return namespace, nil
265265+}