up
	
		
			
	
		
	
	
		
	
		
			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
				
			
		
		
	
	
				
					
				
			
		
			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
				
			This commit is contained in:
		
							
								
								
									
										163
									
								
								.gitea/workflows/authority-key-rotation.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								.gitea/workflows/authority-key-rotation.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| # .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 }}" | ||||
		Reference in New Issue
	
	Block a user