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
				
			
		
			
				
	
	
		
			164 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
			
		
		
	
	
			164 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
| # .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 }}"
 |