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