Update JWT Validation Policy #3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update JWT Validation Policy | |
| permissions: | |
| id-token: write | |
| contents: read | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| type: choice | |
| description: "Target environment (UAT or Production)" | |
| options: | |
| - UAT | |
| - Production | |
| default: Production | |
| deployment_name: | |
| type: string | |
| description: "Name of the deployment to update JWT policy for" | |
| default: "depa-inferencing-kms-prod-cin" | |
| kms_url: | |
| type: string | |
| description: "KMS URL" | |
| default: "https://depa-inferencing-kms-azure.ispirt.in" | |
| trust_type: | |
| type: choice | |
| description: "Type of JWT trust to configure" | |
| options: | |
| - managed-identity | |
| - aad | |
| - token | |
| - gcp-workload | |
| default: "managed-identity" | |
| managed_identity_resource_id: | |
| type: string | |
| description: "Resource ID of the managed identity (e.g., /subscriptions/.../resourceGroups/.../providers/Microsoft.ManagedIdentity/userAssignedIdentities/...). Required when trust_type is 'managed-identity'" | |
| required: false | |
| jwt_token: | |
| type: string | |
| description: "JWT token to extract trust policy from. Required when trust_type is 'token'. For 'gcp-workload', a SAMPLE Confidential Space token whose signature is verified before its service account is allowlisted." | |
| required: false | |
| operation: | |
| type: choice | |
| description: "For trust_type 'gcp-workload': add or remove the workload's service account from the JWT validation policy allowlist (Gate 1)" | |
| options: | |
| - add | |
| - remove | |
| default: add | |
| workflow_call: | |
| inputs: | |
| environment: | |
| type: string | |
| description: "Target environment (UAT or Production)" | |
| default: Production | |
| deployment_name: | |
| type: string | |
| description: "Name of the deployment to update JWT policy for" | |
| default: "depa-inferencing-kms-prod-cin" | |
| kms_url: | |
| type: string | |
| description: "KMS URL" | |
| default: "https://depa-inferencing-kms-azure.ispirt.in" | |
| trust_type: | |
| type: string | |
| description: "Type of JWT trust to configure (managed-identity, aad, token, or gcp-workload)" | |
| default: "managed-identity" | |
| managed_identity_resource_id: | |
| type: string | |
| description: "Resource ID of the managed identity. Required when trust_type is 'managed-identity'" | |
| required: false | |
| jwt_token: | |
| type: string | |
| description: "JWT token to extract trust policy from. Required when trust_type is 'token' or 'gcp-workload'" | |
| required: false | |
| operation: | |
| type: string | |
| description: "For trust_type 'gcp-workload': add or remove the service account (add or remove)" | |
| default: add | |
| jobs: | |
| update-jwt-policy: | |
| name: Update JWT Policy for ${{ inputs.deployment_name }} | |
| runs-on: self-hosted | |
| environment: ${{ inputs.environment }} | |
| continue-on-error: false | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Python 3.12 | |
| # Needs sudo (apt). The gcp-workload path runs on a pre-provisioned | |
| # self-hosted runner that already has Python 3 + venv, so skip it there. | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y python3.12 python3.12-venv | |
| - name: Setup Python virtual environment | |
| run: | | |
| # Create virtual environment | |
| python3 -m venv ccf_env | |
| # Activate virtual environment and install CCF | |
| source ccf_env/bin/activate | |
| pip install --upgrade pip | |
| pip install ccf==6.0.10 | |
| # Add virtual environment to PATH for subsequent steps | |
| echo "ccf_env/bin" >> $GITHUB_PATH | |
| - name: Install Dependencies | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: pip3 install -r requirements.txt | |
| - name: Install Azure CLI | |
| # Azure trust types only. gcp-workload signs locally with on-disk member | |
| # certs and never touches Azure Key Vault, so it needs no Azure CLI/login. | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| run: | | |
| curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash | |
| - name: Log into Azure | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| uses: azure/login@v2 | |
| with: | |
| # Use a managed identity to authenticate to Azure | |
| # Use properties such as client-id, tenant-id, and subscription-id in the secrets and vars | |
| # The managed identity should have a federated credential with subject identifier repo:<organization>/<repo>:pull_request | |
| # Use subject identifier repo:<organization>/<repo>:ref:refs/heads/<branch> for running manual CI's | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Install Azure CLI confidentialledger extension | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| run: | | |
| az version | |
| az extension add --name confidentialledger --yes | |
| - name: Setup environment variables | |
| env: | |
| DEPLOYMENT_NAME: ${{ inputs.deployment_name || 'depa-inferencing-kms-prod-cin' }} | |
| KMS_URL: ${{ inputs.kms_url }} | |
| run: | | |
| export KMS_WORKSPACE=~/$DEPLOYMENT_NAME.aclworkspace | |
| mkdir -p $KMS_WORKSPACE/proposals | |
| echo "KMS_WORKSPACE=$KMS_WORKSPACE" >> $GITHUB_ENV | |
| export KMS_SERVICE_CERT_PATH=${GITHUB_WORKSPACE}/service_cert.pem | |
| echo "KMS_SERVICE_CERT_PATH=$KMS_SERVICE_CERT_PATH" >> $GITHUB_ENV | |
| export KMS_MEMBER_CERT_PATH=${KMS_WORKSPACE}/member_cert.pem | |
| echo "KMS_MEMBER_CERT_PATH=${KMS_MEMBER_CERT_PATH}" >> $GITHUB_ENV | |
| - name: Get service certificate | |
| env: | |
| DEPLOYMENT_NAME: ${{ inputs.deployment_name || 'depa-inferencing-kms-prod-cin' }} | |
| KMS_SERVICE_CERT_PATH: ${{ env.KMS_SERVICE_CERT_PATH }} | |
| run: | | |
| curl https://identity.confidential-ledger.core.azure.com/ledgerIdentity/$DEPLOYMENT_NAME \ | |
| | jq -r '.ledgerTlsCertificate' > ${KMS_SERVICE_CERT_PATH} | |
| - name: Download member certificate from Key Vault | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| env: | |
| DEPLOYMENT_NAME: ${{ inputs.deployment_name || 'depa-inferencing-kms-prod-cin' }} | |
| KMS_WORKSPACE: ${{ env.KMS_WORKSPACE }} | |
| AKV_VAULT_NAME: ${{ secrets.AZURE_KEY_VAULT_NAME }} | |
| KMS_MEMBER_CERT_PATH: ${{ env.KMS_MEMBER_CERT_PATH }} | |
| run: | | |
| az keyvault certificate download \ | |
| --file ${KMS_MEMBER_CERT_PATH} \ | |
| --vault-name $AKV_VAULT_NAME \ | |
| --name ${DEPLOYMENT_NAME}-member-cert | |
| - name: Validate inputs | |
| env: | |
| TRUST_TYPE: ${{ inputs.trust_type || 'managed-identity' }} | |
| MANAGED_IDENTITY_RESOURCE_ID: ${{ inputs.managed_identity_resource_id }} | |
| JWT_TOKEN: ${{ inputs.jwt_token }} | |
| run: | | |
| if [ "$TRUST_TYPE" = "managed-identity" ] && [ -z "$MANAGED_IDENTITY_RESOURCE_ID" ]; then | |
| echo "Error: managed_identity_resource_id is required when trust_type is 'managed-identity'" | |
| exit 1 | |
| fi | |
| if [ "$TRUST_TYPE" = "token" ] && [ -z "$JWT_TOKEN" ]; then | |
| echo "Error: jwt_token is required when trust_type is 'token'" | |
| exit 1 | |
| fi | |
| if [ "$TRUST_TYPE" = "gcp-workload" ] && [ -z "$JWT_TOKEN" ]; then | |
| echo "Error: jwt_token (a sample Confidential Space token) is required when trust_type is 'gcp-workload'" | |
| exit 1 | |
| fi | |
| # --------------------------------------------------------------------- | |
| # Azure trust types (managed-identity / aad / token) configure the JWT | |
| # VALIDATION policy (authentication / Gate 1) via jwt-issuer-trust. | |
| # --------------------------------------------------------------------- | |
| - name: Update JWT validation policy | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| env: | |
| DEPLOYMENT_NAME: ${{ inputs.deployment_name || 'depa-inferencing-kms-prod-cin' }} | |
| KMS_URL: ${{ env.KMS_URL }} | |
| KMS_WORKSPACE: ${{ env.KMS_WORKSPACE }} | |
| USE_AKV: "true" | |
| AKV_VAULT_NAME: ${{ secrets.AZURE_KEY_VAULT_NAME }} | |
| AKV_KEY_NAME: ${{ inputs.deployment_name }}-member-cert | |
| KMS_MEMBER_CERT_PATH: ${{ env.KMS_MEMBER_CERT_PATH }} | |
| KMS_SERVICE_CERT_PATH: ${{ env.KMS_SERVICE_CERT_PATH }} | |
| TRUST_TYPE: ${{ inputs.trust_type || 'managed-identity' }} | |
| MANAGED_IDENTITY_RESOURCE_ID: ${{ inputs.managed_identity_resource_id }} | |
| JWT_TOKEN: ${{ inputs.jwt_token }} | |
| run: | | |
| if [ "$TRUST_TYPE" = "aad" ]; then | |
| echo "Updating JWT validation policy with AAD (current Azure user)" | |
| export JWT_TRUST_TYPE="--aad" | |
| export JWT_TRUST_ARGS="" | |
| elif [ "$TRUST_TYPE" = "token" ]; then | |
| echo "Updating JWT validation policy from JWT token" | |
| export JWT_TRUST_TYPE="--token" | |
| export JWT_TRUST_ARGS="${JWT_TOKEN}" | |
| else | |
| echo "Updating JWT validation policy with managed identity: $MANAGED_IDENTITY_RESOURCE_ID" | |
| export JWT_TRUST_TYPE="--managed-identity" | |
| export JWT_TRUST_ARGS="${MANAGED_IDENTITY_RESOURCE_ID}" | |
| fi | |
| make jwt-issuer-trust | |
| - name: Print updated JWT validation policy | |
| if: ${{ inputs.trust_type != 'gcp-workload' }} | |
| env: | |
| KMS_URL: ${{ env.KMS_URL }} | |
| KMS_SERVICE_CERT_PATH: ${{ env.KMS_SERVICE_CERT_PATH }} | |
| run: | | |
| echo "=== Updated JWT Validation Policy ===" | |
| echo "" | |
| curl ${KMS_URL}/app/jwtValidationPolicy \ | |
| --cacert ${KMS_SERVICE_CERT_PATH} \ | |
| -H "Content-Type: application/json" \ | |
| -w '\n' | jq . | |
| # --------------------------------------------------------------------- | |
| # GCP workload registration (Gate 1 - AUTHENTICATION). | |
| # | |
| # A GCP workload's IDENTITY is its IAM service account, which is the | |
| # analog of the Azure managed identity's sub/oid. Like the managed | |
| # identity, it is registered in the JWT VALIDATION policy | |
| # (public:policies.jwt_validation), NOT the key release policy. The code | |
| # MEASUREMENT (image_digest) is the hostdata analog and is handled | |
| # separately at Gate 2 ("Update KMS policy"). | |
| # | |
| # The google_service_accounts entry in the validation policy is an | |
| # ALLOWLIST that any number of service accounts can be added to (and | |
| # removed from). set_jwt_validation_policy overwrites the whole per-issuer | |
| # policy, so we read the current policy, merge the operator's SA in/out, | |
| # preserve iss/swname, and resubmit - the same additive pattern the Azure | |
| # hostdata flow uses. The member signature is the trust anchor; the JWT | |
| # signature check assures the operator the SA belongs to a real workload. | |
| # --------------------------------------------------------------------- | |
| - name: Verify sample JWT and extract service accounts | |
| if: ${{ inputs.trust_type == 'gcp-workload' }} | |
| env: | |
| JWT_TOKEN: ${{ inputs.jwt_token }} | |
| run: | | |
| # Keep the raw token out of the logs. | |
| echo "::add-mask::$JWT_TOKEN" | |
| # Verifies Google's signature + issuer + swname, then prints the | |
| # google_service_accounts as a JSON array. Fails the job if the token | |
| # is forged, not a Confidential Space token, or carries no service | |
| # account - so nothing gets proposed in that case. | |
| SERVICE_ACCOUNTS=$(python scripts/kms/gcp_extract_workload_sa.py "$JWT_TOKEN") | |
| echo "Verified service account(s): $SERVICE_ACCOUNTS" | |
| echo "SERVICE_ACCOUNTS=$SERVICE_ACCOUNTS" >> $GITHUB_ENV | |
| - name: Register service account(s) in the JWT validation policy | |
| if: ${{ inputs.trust_type == 'gcp-workload' }} | |
| env: | |
| OPERATION: ${{ inputs.operation || 'add' }} | |
| SERVICE_ACCOUNTS: ${{ env.SERVICE_ACCOUNTS }} | |
| KMS_URL: ${{ env.KMS_URL }} | |
| KMS_WORKSPACE: ${{ env.KMS_WORKSPACE }} | |
| KMS_SERVICE_CERT_PATH: ${{ env.KMS_SERVICE_CERT_PATH }} | |
| # Local signing on the self-hosted runner: the member cert and private | |
| # key stay on the runner's disk (never uploaded anywhere). USE_AKV=false | |
| # so ccf-sign uses the on-disk key directly instead of Azure Key Vault. | |
| # vars.KMS_KEYS_DIR points at the local dev_common directory. | |
| KMS_MEMBER_CERT_PATH: ${{ vars.KMS_KEYS_DIR }}/member0_cert.pem | |
| KMS_MEMBER_PRIVK_PATH: ${{ vars.KMS_KEYS_DIR }}/member0_privk.pem | |
| USE_AKV: "false" | |
| run: | | |
| set -euo pipefail | |
| ISSUER="https://confidentialcomputing.googleapis.com" | |
| # READ: fetch the current per-issuer validation policy (public read). | |
| # Fall back to the base policy if the issuer has none yet. | |
| CURRENT=$(curl -sS "${KMS_URL}/app/jwtValidationPolicy" \ | |
| --cacert "${KMS_SERVICE_CERT_PATH}" -H "Content-Type: application/json") | |
| CURRENT_POLICY=$(echo "$CURRENT" | jq --arg iss "$ISSUER" \ | |
| '.[$iss] // {iss: $iss, swname: "CONFIDENTIAL_SPACE", google_service_accounts: []}') | |
| # MODIFY: add or remove the operator's SA(s) in the allowlist while | |
| # preserving iss/swname (and any other existing claims). Unbounded: | |
| # any number of service accounts can accumulate here. | |
| MERGED=$(echo "$CURRENT_POLICY" | jq \ | |
| --argjson sas "$SERVICE_ACCOUNTS" --arg op "$OPERATION" ' | |
| .google_service_accounts = ((.google_service_accounts // []) | |
| | if type == "array" then . else [.] end) | |
| | if $op == "add" | |
| then .google_service_accounts = (.google_service_accounts + $sas | unique) | |
| else .google_service_accounts = (.google_service_accounts - $sas) | |
| end | |
| ') | |
| # WRITE: build the set_jwt_validation_policy proposal (repo-relative, | |
| # release_policy_set.sh resolves it under $REPO_ROOT). | |
| PROPOSAL_PATH="${GITHUB_WORKSPACE}/governance/proposals/gcp_jwt_validation_policy_update.json" | |
| jq -n --arg iss "$ISSUER" --argjson policy "$MERGED" \ | |
| '{ actions: [ { name: "set_jwt_validation_policy", | |
| args: { issuer: $iss, validation_policy: $policy } } ] }' \ | |
| > "${PROPOSAL_PATH}" | |
| echo "Generated proposal (Gate 1 JWT validation policy):" | |
| cat "${PROPOSAL_PATH}" | jq . | |
| # release_policy_set.sh is generic: it signs and submits whatever | |
| # proposal file it is handed (here a set_jwt_validation_policy action). | |
| make release-policy-set \ | |
| release-policy-proposal=governance/proposals/gcp_jwt_validation_policy_update.json | |
| - name: Print updated JWT validation policy | |
| if: ${{ inputs.trust_type == 'gcp-workload' }} | |
| env: | |
| KMS_URL: ${{ env.KMS_URL }} | |
| KMS_SERVICE_CERT_PATH: ${{ env.KMS_SERVICE_CERT_PATH }} | |
| run: | | |
| echo "=== Updated JWT Validation Policy (GCP issuer, Gate 1) ===" | |
| echo "" | |
| curl -sS "${KMS_URL}/app/jwtValidationPolicy" \ | |
| --cacert "${KMS_SERVICE_CERT_PATH}" \ | |
| -H "Content-Type: application/json" \ | |
| -w '\n' | jq '.["https://confidentialcomputing.googleapis.com"]' |