Some checks failed
		
		
	
	Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Build Test Deploy / build-test (push) Has been cancelled
				
			Build Test Deploy / authority-container (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
			
				
	
	
		
			190 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
| #!/usr/bin/env bash
 | |
| 
 | |
| set -euo pipefail
 | |
| 
 | |
| usage() {
 | |
|   cat <<'USAGE'
 | |
| Usage: key-rotation.sh --authority-url URL --api-key TOKEN --key-id ID --key-path PATH [options]
 | |
| 
 | |
| Required flags:
 | |
|   -u, --authority-url   Base Authority URL (e.g. https://authority.example.com)
 | |
|   -k, --api-key         Bootstrap API key (x-stellaops-bootstrap-key header)
 | |
|   -i, --key-id          Identifier (kid) for the new signing key
 | |
|   -p, --key-path        Path (relative to Authority content root or absolute) where the PEM key lives
 | |
| 
 | |
| Optional flags:
 | |
|   -s, --source          Key source loader identifier (default: file)
 | |
|   -a, --algorithm       Signing algorithm (default: ES256)
 | |
|       --provider        Preferred crypto provider name
 | |
|   -m, --meta key=value  Additional metadata entries for the rotation record (repeatable)
 | |
|       --dry-run         Print the JSON payload instead of invoking the API
 | |
|   -h, --help            Show this help
 | |
| 
 | |
| Environment fallbacks:
 | |
|   AUTHORITY_URL, AUTHORITY_BOOTSTRAP_KEY, AUTHORITY_KEY_SOURCE, AUTHORITY_KEY_PROVIDER
 | |
| 
 | |
| Example:
 | |
|   AUTHORITY_BOOTSTRAP_KEY=$(cat key.txt) \\
 | |
|     ./key-rotation.sh -u https://authority.local \\
 | |
|       -i authority-signing-2025 \\
 | |
|       -p ../certificates/authority-signing-2025.pem \\
 | |
|       -m rotatedBy=pipeline -m ticket=OPS-1234
 | |
| USAGE
 | |
| }
 | |
| 
 | |
| require_python() {
 | |
|   if command -v python3 >/dev/null 2>&1; then
 | |
|     PYTHON_BIN=python3
 | |
|   elif command -v python >/dev/null 2>&1; then
 | |
|     PYTHON_BIN=python
 | |
|   else
 | |
|     echo "error: python3 (or python) is required for JSON encoding" >&2
 | |
|     exit 1
 | |
|   fi
 | |
| }
 | |
| 
 | |
| json_quote() {
 | |
|   "$PYTHON_BIN" - "$1" <<'PY'
 | |
| import json, sys
 | |
| print(json.dumps(sys.argv[1]))
 | |
| PY
 | |
| }
 | |
| 
 | |
| AUTHORITY_URL="${AUTHORITY_URL:-}"
 | |
| API_KEY="${AUTHORITY_BOOTSTRAP_KEY:-}"
 | |
| KEY_ID=""
 | |
| KEY_PATH=""
 | |
| SOURCE="${AUTHORITY_KEY_SOURCE:-file}"
 | |
| ALGORITHM="ES256"
 | |
| PROVIDER="${AUTHORITY_KEY_PROVIDER:-}"
 | |
| DRY_RUN=false
 | |
| declare -a METADATA=()
 | |
| 
 | |
| while [[ $# -gt 0 ]]; do
 | |
|   case "$1" in
 | |
|     -u|--authority-url)
 | |
|       AUTHORITY_URL="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     -k|--api-key)
 | |
|       API_KEY="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     -i|--key-id)
 | |
|       KEY_ID="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     -p|--key-path)
 | |
|       KEY_PATH="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     -s|--source)
 | |
|       SOURCE="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     -a|--algorithm)
 | |
|       ALGORITHM="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     --provider)
 | |
|       PROVIDER="$2"
 | |
|       shift 2
 | |
|       ;;
 | |
|     -m|--meta)
 | |
|       METADATA+=("$2")
 | |
|       shift 2
 | |
|       ;;
 | |
|     --dry-run)
 | |
|       DRY_RUN=true
 | |
|       shift
 | |
|       ;;
 | |
|     -h|--help)
 | |
|       usage
 | |
|       exit 0
 | |
|       ;;
 | |
|     *)
 | |
|       echo "Unknown option: $1" >&2
 | |
|       usage
 | |
|       exit 1
 | |
|       ;;
 | |
|   esac
 | |
| done
 | |
| 
 | |
| if [[ -z "$AUTHORITY_URL" || -z "$API_KEY" || -z "$KEY_ID" || -z "$KEY_PATH" ]]; then
 | |
|   echo "error: missing required arguments" >&2
 | |
|   usage
 | |
|   exit 1
 | |
| fi
 | |
| 
 | |
| case "$AUTHORITY_URL" in
 | |
|   http://*|https://*) ;;
 | |
|   *)
 | |
|     echo "error: --authority-url must include scheme (http/https)" >&2
 | |
|     exit 1
 | |
|     ;;
 | |
| esac
 | |
| 
 | |
| require_python
 | |
| 
 | |
| payload="{"
 | |
| payload+="\"keyId\":$(json_quote "$KEY_ID"),"
 | |
| payload+="\"location\":$(json_quote "$KEY_PATH"),"
 | |
| payload+="\"source\":$(json_quote "$SOURCE"),"
 | |
| payload+="\"algorithm\":$(json_quote "$ALGORITHM"),"
 | |
| if [[ -n "$PROVIDER" ]]; then
 | |
|   payload+="\"provider\":$(json_quote "$PROVIDER"),"
 | |
| fi
 | |
| 
 | |
| if [[ ${#METADATA[@]} -gt 0 ]]; then
 | |
|   payload+="\"metadata\":{"
 | |
|   for entry in "${METADATA[@]}"; do
 | |
|     if [[ "$entry" != *=* ]]; then
 | |
|       echo "warning: ignoring metadata entry '$entry' (expected key=value)" >&2
 | |
|       continue
 | |
|     fi
 | |
|     key="${entry%%=*}"
 | |
|     value="${entry#*=}"
 | |
|     payload+="$(json_quote "$key"):$(json_quote "$value"),"
 | |
|   done
 | |
|   if [[ "${payload: -1}" == "," ]]; then
 | |
|     payload="${payload::-1}"
 | |
|   fi
 | |
|   payload+="},"
 | |
| fi
 | |
| 
 | |
| if [[ "${payload: -1}" == "," ]]; then
 | |
|   payload="${payload::-1}"
 | |
| fi
 | |
| payload+="}"
 | |
| 
 | |
| if [[ "$DRY_RUN" == true ]]; then
 | |
|   echo "# Dry run payload:"
 | |
|   echo "$payload"
 | |
|   exit 0
 | |
| fi
 | |
| 
 | |
| tmp_response="$(mktemp)"
 | |
| cleanup() { rm -f "$tmp_response"; }
 | |
| trap cleanup EXIT
 | |
| 
 | |
| http_code=$(curl -sS -o "$tmp_response" -w "%{http_code}" \
 | |
|   -X POST "${AUTHORITY_URL%/}/internal/signing/rotate" \
 | |
|   -H "Content-Type: application/json" \
 | |
|   -H "x-stellaops-bootstrap-key: $API_KEY" \
 | |
|   --data "$payload")
 | |
| 
 | |
| if [[ "$http_code" != "200" && "$http_code" != "201" ]]; then
 | |
|   echo "error: rotation API returned HTTP $http_code" >&2
 | |
|   cat "$tmp_response" >&2 || true
 | |
|   exit 1
 | |
| fi
 | |
| 
 | |
| echo "Rotation request accepted (HTTP $http_code). Response:"
 | |
| cat "$tmp_response"
 | |
| 
 | |
| echo
 | |
| echo "Fetching JWKS to confirm active key..."
 | |
| curl -sS "${AUTHORITY_URL%/}/jwks" || true
 | |
| echo
 | |
| echo "Done. Remember to update authority.yaml with the new key metadata to keep restarts consistent."
 |