A Kubernetes operator that bridges Hardware Security Module (HSM) data storage with Kubernetes Secrets, providing true secret portability th
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 417 lines 12 kB view raw
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