A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1#!/bin/bash
2
3# Advanced bulk import script with validation and rollback
4# Automatically uses kubectl-hsm plugin if available, falls back to REST API
5# Usage: ./advanced-bulk-import.sh [config-file] [options]
6
7set -e
8
9API_BASE_URL=${API_BASE_URL:-"http://localhost:8090"}
10CONFIG_FILE=${1:-"production-import.json"}
11DRY_RUN=${DRY_RUN:-false}
12ROLLBACK_ON_FAILURE=${ROLLBACK_ON_FAILURE:-true}
13MAX_PARALLEL=${MAX_PARALLEL:-5}
14
15# Colors for output
16RED='\033[0;31m'
17GREEN='\033[0;32m'
18YELLOW='\033[1;33m'
19BLUE='\033[0;34m'
20NC='\033[0m' # No Color
21
22log() {
23 echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
24}
25
26success() {
27 echo -e "${GREEN}✅${NC} $1"
28}
29
30error() {
31 echo -e "${RED}❌${NC} $1"
32}
33
34warning() {
35 echo -e "${YELLOW}⚠️${NC} $1"
36}
37
38# Convert Bitwarden vault format to HSM format
39convert_bitwarden_vault() {
40 local input_file="$1"
41 local output_file="${input_file%.json}-hsm.json"
42
43 log "Converting Bitwarden vault format to HSM format..."
44
45 # Check if this is a Bitwarden vault file
46 if jq -e '.projects and .secrets' "$input_file" > /dev/null 2>&1; then
47 log "Detected Bitwarden vault format"
48
49 # Build project name mapping
50 local project_map=$(mktemp)
51 jq -r '.projects[] | "\(.id) \(.name)"' "$input_file" > "$project_map"
52
53 # Convert secrets format
54 jq --slurpfile projects <(jq '.projects' "$input_file") '
55 {
56 "secrets": [
57 .secrets[] | {
58 "label": .key,
59 "id": (.id | gsub("-"; "") | .[0:8]),
60 "format": "text",
61 "description": (if .note != "" then .note else "Imported from Bitwarden vault" end),
62 "tags": {
63 "source": "bitwarden",
64 "projects": [.projectIds[] as $pid | $projects[0][] | select(.id == $pid) | .name]
65 },
66 "metadata": {
67 "label": .key,
68 "description": (if .note != "" then .note else "Imported from Bitwarden vault" end),
69 "tags": {
70 "source": "bitwarden",
71 "projects": [.projectIds[] as $pid | $projects[0][] | select(.id == $pid) | .name]
72 },
73 "format": "text",
74 "dataType": "plaintext",
75 "createdAt": now | strftime("%Y-%m-%dT%H:%M:%SZ"),
76 "source": "bitwarden"
77 },
78 "data": {
79 "value": .value
80 }
81 }
82 ]
83 }' "$input_file" > "$output_file"
84
85 rm "$project_map"
86 success "Converted Bitwarden vault to: $output_file"
87 CONFIG_FILE="$output_file"
88 else
89 log "Not a Bitwarden vault format, proceeding with original file"
90 fi
91}
92
93# Validate prerequisites
94validate_prerequisites() {
95 log "Validating prerequisites..."
96
97 if ! command -v jq &> /dev/null; then
98 error "jq is required but not installed"
99 exit 1
100 fi
101
102 if ! command -v curl &> /dev/null; then
103 error "curl is required but not installed"
104 exit 1
105 fi
106
107 if [ ! -f "$CONFIG_FILE" ]; then
108 error "Config file not found: $CONFIG_FILE"
109 exit 1
110 fi
111
112 if ! jq empty "$CONFIG_FILE" 2>/dev/null; then
113 error "Invalid JSON in config file: $CONFIG_FILE"
114 exit 1
115 fi
116
117 # Convert Bitwarden format if detected
118 convert_bitwarden_vault "$CONFIG_FILE"
119
120 # Test API connectivity
121 if ! curl -s --connect-timeout 5 "$API_BASE_URL/api/v1/health" > /dev/null; then
122 error "Cannot connect to API at: $API_BASE_URL"
123 exit 1
124 fi
125
126 success "Prerequisites validated"
127}
128
129# Pre-import validation
130validate_config() {
131 log "Validating configuration..."
132
133 local issues=0
134
135 # Check for duplicate labels
136 duplicate_labels=$(jq -r '.secrets[].label' "$CONFIG_FILE" | sort | uniq -d)
137 if [ -n "$duplicate_labels" ]; then
138 error "Duplicate labels found:"
139 echo "$duplicate_labels" | while IFS= read -r label; do
140 echo " - $label"
141 done
142 ((issues++))
143 fi
144
145 # Check for duplicate IDs
146 duplicate_ids=$(jq -r '.secrets[].id' "$CONFIG_FILE" | sort | uniq -d)
147 if [ -n "$duplicate_ids" ]; then
148 error "Duplicate IDs found:"
149 echo "$duplicate_ids" | while IFS= read -r id; do
150 echo " - $id"
151 done
152 ((issues++))
153 fi
154
155 # Validate required fields
156 jq -c '.secrets[]' "$CONFIG_FILE" | while IFS= read -r secret; do
157 label=$(echo "$secret" | jq -r '.label')
158 id=$(echo "$secret" | jq -r '.id')
159
160 if [ "$label" = "null" ] || [ -z "$label" ]; then
161 error "Secret missing label"
162 ((issues++))
163 fi
164
165 if [ "$id" = "null" ] || [ -z "$id" ]; then
166 error "Secret '$label' missing ID"
167 ((issues++))
168 fi
169
170 if ! echo "$secret" | jq -e '.data' > /dev/null; then
171 error "Secret '$label' missing data"
172 ((issues++))
173 fi
174 done
175
176 if [ $issues -gt 0 ]; then
177 error "Configuration validation failed with $issues issues"
178 exit 1
179 fi
180
181 success "Configuration validated"
182}
183
184# Check for existing secrets
185check_existing_secrets() {
186 log "Checking for existing secrets..."
187
188 local conflicts=()
189
190 while IFS= read -r label; do
191 # Try kubectl-hsm first if available
192 if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
193 if kubectl hsm get "$label" >/dev/null 2>&1; then
194 conflicts+=("$label")
195 continue
196 fi
197 fi
198
199 # Fallback to API
200 response=$(curl -s "$API_BASE_URL/api/v1/hsm/secrets/$label")
201 success=$(echo "$response" | jq -r '.success')
202
203 if [ "$success" = "true" ]; then
204 conflicts+=("$label")
205 fi
206 done <<< "$(jq -r '.secrets[].label' "$CONFIG_FILE")"
207
208 if [ ${#conflicts[@]} -gt 0 ]; then
209 warning "Found ${#conflicts[@]} existing secrets that will be overwritten:"
210 for conflict in "${conflicts[@]}"; do
211 echo " - $conflict"
212 done
213
214 if [ "$DRY_RUN" = "false" ]; then
215 read -p "Continue? (y/N): " -n 1 -r
216 echo
217 if [[ ! $REPLY =~ ^[Yy]$ ]]; then
218 log "Import cancelled by user"
219 exit 0
220 fi
221 fi
222 else
223 success "No conflicts found"
224 fi
225}
226
227# Import a single secret
228import_secret() {
229 local secret_data="$1"
230 local label=$(echo "$secret_data" | jq -r '.label')
231
232 if [ "$DRY_RUN" = "true" ]; then
233 echo "[DRY RUN] Would import: $label"
234 return 0
235 fi
236
237 log "Importing: $label"
238
239 response=$(curl -s -X POST \
240 -H "Content-Type: application/json" \
241 -d "$secret_data" \
242 "$API_BASE_URL/api/v1/hsm/secrets/$label" 2>/dev/null)
243
244 if [ $? -ne 0 ]; then
245 error "Failed to connect to API for $label"
246 return 1
247 fi
248
249 success_status=$(echo "$response" | jq -r '.success')
250 if [ "$success_status" = "true" ]; then
251 success "Imported: $label"
252 return 0
253 else
254 error_message=$(echo "$response" | jq -r '.error.message // "Unknown error"')
255 error "Failed to import $label: $error_message"
256 return 1
257 fi
258}
259
260# Rollback imported secrets
261rollback_secrets() {
262 local imported_secrets=("$@")
263
264 if [ ${#imported_secrets[@]} -eq 0 ]; then
265 return 0
266 fi
267
268 warning "Rolling back ${#imported_secrets[@]} imported secrets..."
269
270 for label in "${imported_secrets[@]}"; do
271 log "Rolling back: $label"
272
273 # Try kubectl-hsm first if available
274 if command -v kubectl >/dev/null && kubectl hsm --help >/dev/null 2>&1; then
275 kubectl hsm delete "$label" --force >/dev/null 2>&1 || \
276 curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null
277 else
278 curl -s -X DELETE "$API_BASE_URL/api/v1/hsm/secrets/$label" > /dev/null
279 fi
280 done
281
282 warning "Rollback completed"
283}
284
285# Main import process
286perform_import() {
287 log "Starting bulk import..."
288
289 local total_secrets=$(jq '.secrets | length' "$CONFIG_FILE")
290 local imported_secrets=()
291 local failed_secrets=()
292 local success_count=0
293 local failure_count=0
294
295 log "Importing $total_secrets secrets..."
296
297 # Process secrets sequentially for better error handling
298 while IFS= read -r secret_json; do
299 label=$(echo "$secret_json" | jq -r '.label')
300
301 if import_secret "$secret_json"; then
302 imported_secrets+=("$label")
303 ((success_count++))
304 else
305 failed_secrets+=("$label")
306 ((failure_count++))
307
308 # Rollback on first failure if enabled
309 if [ "$ROLLBACK_ON_FAILURE" = "true" ] && [ $failure_count -eq 1 ]; then
310 error "First failure detected, initiating rollback..."
311 rollback_secrets "${imported_secrets[@]}"
312 exit 1
313 fi
314 fi
315
316 # Progress indicator
317 local current=$((success_count + failure_count))
318 log "Progress: $current/$total_secrets"
319
320 done < <(jq -c '.secrets[]' "$CONFIG_FILE")
321
322 # Summary
323 echo ""
324 log "Import Summary:"
325 success "Successfully imported: $success_count secrets"
326 if [ $failure_count -gt 0 ]; then
327 error "Failed to import: $failure_count secrets"
328 if [ ${#failed_secrets[@]} -gt 0 ]; then
329 echo "Failed secrets:"
330 for failed in "${failed_secrets[@]}"; do
331 echo " - $failed"
332 done
333 fi
334 fi
335
336 # Generate import report
337 local report_file="import-report-$(date +%Y%m%d-%H%M%S).json"
338 cat > "$report_file" <<EOF
339{
340 "timestamp": "$(date -Iseconds)",
341 "config_file": "$CONFIG_FILE",
342 "api_url": "$API_BASE_URL",
343 "total_secrets": $total_secrets,
344 "successful_imports": $success_count,
345 "failed_imports": $failure_count,
346 "imported_secrets": $(printf '%s\n' "${imported_secrets[@]}" | jq -R . | jq -s .),
347 "failed_secrets": $(printf '%s\n' "${failed_secrets[@]}" | jq -R . | jq -s .)
348}
349EOF
350
351 log "Import report saved to: $report_file"
352
353 if [ $failure_count -eq 0 ]; then
354 success "All secrets imported successfully!"
355 exit 0
356 else
357 error "Some secrets failed to import"
358 exit 1
359 fi
360}
361
362# Parse command line options
363while [[ $# -gt 0 ]]; do
364 case $1 in
365 --dry-run)
366 DRY_RUN=true
367 shift
368 ;;
369 --no-rollback)
370 ROLLBACK_ON_FAILURE=false
371 shift
372 ;;
373 --api-url)
374 API_BASE_URL="$2"
375 shift 2
376 ;;
377 --help)
378 echo "Usage: $0 [config-file] [options]"
379 echo ""
380 echo "Supports both HSM format and Bitwarden vault format (auto-detected)."
381 echo ""
382 echo "Options:"
383 echo " --dry-run Show what would be imported without making changes"
384 echo " --no-rollback Don't rollback on failure"
385 echo " --api-url URL Override API base URL"
386 echo " --help Show this help message"
387 echo ""
388 echo "Config file formats:"
389 echo " HSM format: Standard format with 'secrets' array"
390 echo " Bitwarden: Vault export with 'projects' and 'secrets' arrays"
391 echo ""
392 echo "Environment variables:"
393 echo " API_BASE_URL API endpoint (default: http://localhost:8090)"
394 echo " DRY_RUN Enable dry run mode (default: false)"
395 echo " ROLLBACK_ON_FAILURE Enable rollback on failure (default: true)"
396 exit 0
397 ;;
398 *)
399 CONFIG_FILE="$1"
400 shift
401 ;;
402 esac
403done
404
405# Main execution
406echo "🔐 Advanced HSM Secrets Bulk Import"
407echo "====================================="
408echo "Config file: $CONFIG_FILE"
409echo "API URL: $API_BASE_URL"
410echo "Dry run: $DRY_RUN"
411echo "Rollback on failure: $ROLLBACK_ON_FAILURE"
412echo ""
413
414validate_prerequisites
415validate_config
416check_existing_secrets
417perform_import