# .gitea/workflows/authority-key-rotation.yml # Manual workflow to push a new Authority signing key using OPS3 tooling name: Authority Key Rotation on: workflow_dispatch: inputs: environment: description: 'Target environment name (used to select secrets/vars)' required: true default: 'staging' type: choice options: - staging - production authority_url: description: 'Override Authority URL (leave blank to use env-specific secret)' required: false default: '' type: string key_id: description: 'New signing key identifier (kid)' required: true type: string key_path: description: 'Path (as Authority sees it) to the PEM key' required: true type: string source: description: 'Signing key source loader (default: file)' required: false default: 'file' type: string algorithm: description: 'Signing algorithm (default: ES256)' required: false default: 'ES256' type: string provider: description: 'Preferred crypto provider hint' required: false default: '' type: string metadata: description: 'Optional key=value metadata entries (comma-separated)' required: false default: '' type: string jobs: rotate: runs-on: ubuntu-22.04 environment: ${{ inputs.environment }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Resolve Authority configuration id: config run: | set -euo pipefail env_name=${{ inputs.environment }} echo "Environment: $env_name" bootstrap_key="" authority_url="${{ inputs.authority_url }}" # Helper to prefer secrets over variables and fall back to shared defaults resolve_var() { local name="$1" local default="$2" local value="${{ secrets[name] }}" if [ -z "$value" ]; then value="${{ vars[name] }}"; fi if [ -z "$value" ]; then value="$default"; fi printf '%s' "$value" } key_name="${env_name^^}_AUTHORITY_BOOTSTRAP_KEY" bootstrap_key="$(resolve_var "$key_name" "")" if [ -z "$bootstrap_key" ]; then bootstrap_key="$(resolve_var "AUTHORITY_BOOTSTRAP_KEY" "")" fi if [ -z "$bootstrap_key" ]; then echo "::error::Missing bootstrap key secret (expected $key_name or AUTHORITY_BOOTSTRAP_KEY)" exit 1 fi if [ -z "$authority_url" ]; then url_name="${env_name^^}_AUTHORITY_URL" authority_url="$(resolve_var "$url_name" "")" if [ -z "$authority_url" ]; then authority_url="$(resolve_var "AUTHORITY_URL" "")" fi fi if [ -z "$authority_url" ]; then echo "::error::Authority URL not provided and no secret/var found" exit 1 fi key_file="${RUNNER_TEMP}/authority-bootstrap-key" printf '%s\n' "$bootstrap_key" > "$key_file" chmod 600 "$key_file" echo "bootstrap-key-file=$key_file" >> "$GITHUB_OUTPUT" echo "authority-url=$authority_url" >> "$GITHUB_OUTPUT" - name: Execute key rotation id: rotate shell: bash env: AUTHORITY_BOOTSTRAP_KEY_FILE: ${{ steps.config.outputs['bootstrap-key-file'] }} AUTHORITY_URL: ${{ steps.config.outputs['authority-url'] }} KEY_ID: ${{ inputs.key_id }} KEY_PATH: ${{ inputs.key_path }} KEY_SOURCE: ${{ inputs.source }} KEY_ALGORITHM: ${{ inputs.algorithm }} KEY_PROVIDER: ${{ inputs.provider }} KEY_METADATA: ${{ inputs.metadata }} run: | set -euo pipefail bootstrap_key=$(cat "$AUTHORITY_BOOTSTRAP_KEY_FILE") metadata_args=() if [ -n "$KEY_METADATA" ]; then IFS=',' read -ra META <<< "$KEY_METADATA" for entry in "${META[@]}"; do trimmed="$(echo "$entry" | xargs)" [ -z "$trimmed" ] && continue metadata_args+=(-m "$trimmed") done fi provider_args=() if [ -n "$KEY_PROVIDER" ]; then provider_args+=(--provider "$KEY_PROVIDER") fi ./ops/authority/key-rotation.sh \ --authority-url "$AUTHORITY_URL" \ --api-key "$bootstrap_key" \ --key-id "$KEY_ID" \ --key-path "$KEY_PATH" \ --source "$KEY_SOURCE" \ --algorithm "$KEY_ALGORITHM" \ "${provider_args[@]}" \ "${metadata_args[@]}" - name: JWKS summary run: | echo "✅ Rotation complete" echo "Environment: ${{ inputs.environment }}" echo "Authority: ${{ steps.config.outputs['authority-url'] }}" echo "Key ID: ${{ inputs.key_id }}" echo "Key Path: ${{ inputs.key_path }}" echo "Source: ${{ inputs.source }}" echo "Algorithm: ${{ inputs.algorithm }}"