up
This commit is contained in:
@@ -37,3 +37,22 @@ Key environment variables (mirroring `StellaOpsAuthorityOptions`):
|
||||
| `STELLAOPS_AUTHORITY__PLUGINS__CONFIGURATIONDIRECTORY` | Path to plugin manifest directory |
|
||||
|
||||
For additional options, see `etc/authority.yaml.sample`.
|
||||
|
||||
## Key rotation automation (OPS3)
|
||||
|
||||
The `key-rotation.sh` helper wraps the `/internal/signing/rotate` endpoint delivered with CORE10. It can run in CI/CD once the new PEM key is staged on the Authority host volume.
|
||||
|
||||
```bash
|
||||
AUTHORITY_BOOTSTRAP_KEY=$(cat ~/.secrets/authority-bootstrap.key) \
|
||||
./key-rotation.sh \
|
||||
--authority-url https://authority.stella-ops.local \
|
||||
--key-id authority-signing-2025 \
|
||||
--key-path ../certificates/authority-signing-2025.pem \
|
||||
--meta rotatedBy=pipeline --meta changeTicket=OPS-1234
|
||||
```
|
||||
|
||||
- `--key-path` should resolve from the Authority content root (same as `docs/11_AUTHORITY.md` SOP).
|
||||
- Provide `--source`/`--provider` if the key loader differs from the default file-based provider.
|
||||
- Pass `--dry-run` during rehearsals to inspect the JSON payload without invoking the API.
|
||||
|
||||
After rotation, export a fresh revocation bundle (`stellaops-cli auth revoke export`) so downstream mirrors consume signatures from the new `kid`. The canonical operational steps live in `docs/11_AUTHORITY.md` – make sure any local automation keeps that guide as source of truth.
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| OPS3.KEY-ROTATION | BLOCKED | DevOps Crew, Authority Core | CORE10.JWKS | Implement key rotation tooling + pipeline hook once rotating JWKS lands. Document SOP and secret handling. | ✅ CLI/script rotates keys + updates JWKS; ✅ Pipeline job documented; ✅ docs/ops runbook updated. |
|
||||
| OPS3.KEY-ROTATION | DONE (2025-10-12) | DevOps Crew, Authority Core | CORE10.JWKS | Implement key rotation tooling + pipeline hook once rotating JWKS lands. Document SOP and secret handling. | ✅ CLI/script rotates keys + updates JWKS; ✅ Pipeline job documented; ✅ docs/ops runbook updated. |
|
||||
|
||||
189
ops/authority/key-rotation.sh
Normal file
189
ops/authority/key-rotation.sh
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/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."
|
||||
Reference in New Issue
Block a user