From ba9f84a460870dedf9a3689d8d6c47735ba8eb26 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Thu, 20 Aug 2026 11:43:56 +0200 Subject: [PATCH] fix(flux): authenticate to ACR explicitly instead of by discovery Altinn/info.altinn.no hit this when moving a workflow that uses flux/build-push-image onto self-hosted runners: error during login with provider: failed to create provider access token for the controller: DefaultAzureCredential: failed to acquire a token ... ManagedIdentityCredential authentication failed RESPONSE 400: Unable to load the proper Managed Identity flux --provider=azure authenticates through the Azure SDK's DefaultAzureCredential, which resolves a credential by discovery rather than using the session azure/login just established, and stops at the first credential that fails outright. On GitHub-hosted runners nothing earlier in the chain is available, so it falls through to the CLI credential and picks up that session. On self-hosted runners, which are Azure Container App Jobs, a managed identity endpoint is present, so ManagedIdentityCredential is attempted instead. It returns HTTP 400 because the job has a user-assigned identity and flux supplies no client id, and the chain aborts before the CLI credential is reached. Every workflow in this repository that uses this action runs on ubuntu-latest, which is why it has not surfaced here. Self-hosted jobs that reach altinncr do so through the az CLI, which uses its own session and never enters this chain. Any team moving a flux workflow onto the self-hosted runners will hit it. Handing the SDK the same federated credentials azure/login uses makes WorkloadIdentityCredential resolve first, so flux authenticates as the same application regardless of runner type. This requires 'permissions: id-token: write' on the calling job, which OIDC-based azure/login already required. Verified against flux 2.6.4 with a mock Container Apps identity endpoint: without these variables the chain stops at ManagedIdentityCredential with the 400 above, and with them set the chain resolves WorkloadIdentityCredential first and never reaches managed identity. Co-Authored-By: Claude Opus 5 (1M context) --- actions/flux/setup-flux-acr/action.yaml | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/actions/flux/setup-flux-acr/action.yaml b/actions/flux/setup-flux-acr/action.yaml index 862df8744..aab1a4677 100644 --- a/actions/flux/setup-flux-acr/action.yaml +++ b/actions/flux/setup-flux-acr/action.yaml @@ -37,3 +37,46 @@ runs: client-id: ${{ inputs.azure_app_id }} subscription-id: ${{ inputs.azure_subscription_id }} tenant-id: ${{ inputs.azure_tenant_id }} + - name: Expose federated credentials to the Azure SDK + shell: bash + env: + AZURE_APP_ID: ${{ inputs.azure_app_id }} + AZURE_TENANT: ${{ inputs.azure_tenant_id }} + run: | + set -euo pipefail + + # `flux --provider=azure` authenticates through the Azure SDK's + # DefaultAzureCredential, which resolves a credential by discovery rather + # than using the session `az login` just established. The chain stops at + # the first credential that fails outright. + # + # On GitHub-hosted runners nothing earlier in the chain is available, so it + # falls through to the CLI credential and picks up that session. On + # self-hosted runners backed by Azure Container App Jobs a managed identity + # endpoint is present, so ManagedIdentityCredential is attempted instead. It + # returns HTTP 400 ("Unable to load the proper Managed Identity") because + # the job has a user-assigned identity and flux supplies no client id, and + # the chain aborts before the CLI credential is ever reached. + # + # Handing the SDK the same federated credentials azure/login uses makes + # WorkloadIdentityCredential resolve first, so flux authenticates as the + # same application on every runner type instead of depending on where the + # job happens to land. + + if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then + echo "::error::No OIDC token endpoint available. This action needs 'permissions: id-token: write' on the calling job." + exit 1 + fi + + token_file="${RUNNER_TEMP}/azure-federated-token" + install -m 600 /dev/null "${token_file}" + curl -sSf \ + -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=api%3A%2F%2FAzureADTokenExchange" \ + | jq -er '.value' > "${token_file}" + + { + echo "AZURE_CLIENT_ID=${AZURE_APP_ID}" + echo "AZURE_TENANT_ID=${AZURE_TENANT}" + echo "AZURE_FEDERATED_TOKEN_FILE=${token_file}" + } >> "${GITHUB_ENV}"