Skip to content

Update JWT Validation Policy #3

Update JWT Validation Policy

Update JWT Validation Policy #3

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"]'