Files
git.stella-ops.org/ops/authority/key-rotation.sh
master 607e72e2a1
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
up
2025-10-12 20:37:18 +03:00

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."