
Intro
Did you know that over 23 millions secrets were publicly exposed in GitHub in 2024 alone? and even 70% of the secrets leaked in 2022 are still valid? This is additional evidence that leaked secrets are still the number one biggest threat to your business. The worst thing to do is make it easy for hackers to infiltrate your company’s system.
That’s where workload identity federation—like OIDC—steps in to make cloud authentication seamless, secure, and secret-free. In this post we’ll help you setup an Azure managed Identity (New) to authenticate your GitHub workflows using OIDC to deploy in Azure cloud.
What is OIDC ?
OpenID Connect (OIDC) is an authentication protocol built on top of OAuth 2.0 which adds an ID layer (ID token) to enable user authentication and Single Sign-On on top of OAUTH’s access delegation. If you want to learn more about both OAuth and OIDC check out our post: What is OIDC?
You can also check out my deep dive around OIDC authentication in the cloud.
Key Benefits
How Does It Work?

We use OpenID Connect (OIDC) to create a trust relationship between your GitHub pipeline and your Azure(Entra). Meaning when you push code to your branch, GitHub Actions will authenticate itself to Azure without requiring static credentials (service principal secrets).
OIDC Workflow
There are 2 pillars to the OIDC authentication (see details here):
- First, App registration in Azure to set up the OIDC trust and allow pipeline authentication via ID token(JWT).
- Then you can update your GitHub actions workflow with azure action info, to authenticate using tokens.

Getting started
What Are We Doing ?
We’re setting up a Terraform pipeline to deploy an Azure RG & VNET in AZURE without any service principal secret. Instead, we’ll use GitHub Actions workload identity to assume an Azure role directly via OIDC.
There are two ways to do it:
- Option 1: Microsoft Entra application (service principal)
- Option 2: User-assigned managed identity
Access Token lifetime
- Azure User managed identity method: 24hs
- Azure Service principal(entra app) method: 1hr
I. My GitHub Terraform Pipeline
To demonstrate how to configure OIDC in Azure, I’ve set up a GitHub repository with a Terraform pipeline (see below):
Repository: github.com/brokedba/terraform-examples
Branch: github_actions
Environment: az-labs
II. Configuring OIDC in Azure
Before using Azure Login Action with OIDC, you need to configure a federated identity credential on Azure Entra.
Step 1: Setup a managed identity (OIDC Trust)
Create user-assigned managed identity (i.e
oidc-managed-gitaction
).
- On the Azure portal Search box, enter
Managed Identities
, then click +create button. - Specify following fields:
Subscription
|Resource Group
|Name
: oidc-managed-gitaction |Region
- Copy
Client ID
,Subscription ID
, andTENANT_ID
for later use.

Step 2: Create an IAM Role and assign it to the managed Identity
1. Use built-in roles (Contributor, VM Contributor ) or create a custom role for least privilege. See tutorial for my CLI-based custom role (vm-vnet-rg-role
) creation using a json role definition.
# Create a role "vm-vnet-rg-role" with permissions on [Resource-Groups/VMs/Vnets]
az role definition create --role-definition @vm-vnet-role-all-rgs-scoped.json
2. Assign the role to the managed identity (Subscription level):
- Go to managed identity>
oidc-managed-gitaction
>Azure role assignment- Select the scope [
subscription
] and add the role from the list
- Select the scope [

Azure CLI command
# Run by a privileged user/SPN
TF_MI_OBJECT_ID="{your-terraform-managed-identity-object-id}"
SUB_ID="{yourSubscriptionId}"
ROLE_NAME="vm-vnet-role" # Or whatever you named the updated role
az role assignment create --assignee-object-id $TF_MI_OBJECT_ID \
--role "$ROLE_NAME" \
--scope "/subscriptions/$SUB_ID"
My terraform config creates a Resource Group, hence the need for a higher scope. But you can lower the scope.
Step 3: Add a federated identity credential to the managed identity
- Go to managed identity>
oidc-managed-gitaction
>Settings>federated credentials and click add credentials

- Select [GitHub Action deploying Azure resource] Scenario
- In the GitHub claim, select the right
Organization
|Repo
|Entity type : Value
- The entity can be either: {
branch
|environment
|pull-request
|tag}
- We will choose
environment
with value"az-labs"
(see my repo )
- In the GitHub claim, select the right
repo:yourRepo/terraform-examples:environment:az-labs
] .
Step 4: Configure GitHub Actions Workflow
Once OIDC trust is set, create a workflow where the azure-login
action will receive a JWT from the GitHub ID provider, then requests an access token from Azure.
🚀 Action :
Azure/login

The 3 input parameters aren’t real secrets but we defined them as secrets in the az-labs
environment

- Create a workflow triggered by push on the “git_actions” branch from tf files under create-vnet.
- Prerequisites: Set
id-token permission = write
to allow this action to generate the JWT Token.
name: 'Terraform_azure_vnet'
on:
push:
branches: [ "git_actions" ]
paths:
- 'terraform-provider-azure/create-vnet/*tf' # tf files changes
env:
TF_VAR_az_location: "${{ vars.az_location }}"
TF_VAR_prefix: "${{ vars.TF_APP_PREFIX }}"
TF_VAR_az_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} # azurerm v4.0
STACK_DIR: ${{ vars.TF_STACK_DIR }}
permissions:
id-token: write # Can also be put under the azure login job
- Add the
azure/login
action info to authenticate to Azure from the pipeline.
# Authenticate with Azure using OIDC Workload Federated Identiry
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
link: Terraform_azure_vnet
name: 'Terraform_azure_vnet'
on:
push:
branches: [ "git_actions" ]
paths:
- 'terraform-provider-azure/create-vnet/*tf'
env:
#ARM_USE_MSI: true
#ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
#ARM_TENANT_ID: "${{ secrets.AZURE_TENANT_ID }}"
#ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
TF_VAR_az_location: "${{ vars.az_location }}"
TF_VAR_prefix: "${{ vars.TF_APP_PREFIX }}"
TF_VAR_az_subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
STACK_DIR: ${{ vars.TF_STACK_DIR }}
permissions:
id-token: write
jobs:
# ############
# INIT
# ############
terraform_setup:
name: 'Terraform Init-Validate'
runs-on: ubuntu-latest
environment: az-labs
# Use default shell and working directory regardless of the os of the GitHub Actions runner
defaults:
run:
shell: bash
working-directory: ${{ env.STACK_DIR }}
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3
# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.3
terraform_wrapper: false
# cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} --> Terraform cloud
# Create a cache for the terraform pluggin and copy tf binary
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
mkdir --parents ~/.terraform.d/plugin-cache
terra_bin=`which terraform`
cp $terra_bin .
# Initialize a new or existing Terraform working directory(creating initial files, loading any remote state, downloading modules..)
- name: Terraform Init
id: init
run: |
echo ====== INITIALIZE terraform provider plugins : $GITHUB_WORKSPACE/${{ vars.TF_STACK_DIR }} ======
pwd
echo terra_bin=$terra_bin >> "$GITHUB_OUTPUT"
# echo the temp directory path is : $RUNNER_TEMP
terraform init
terraform -v
- name: Terraform format
run: |
echo ====== FORMAT the Terraform configuration in ${{ env.STACK_DIR }} ======
terraform fmt
- name: Terraform Validate
run: |
echo ====== VALIDATE the Terraform configuration in ${{ env.STACK_DIR }} ======
terraform validate
# Authenticate with Azure using OIDC Workload Federated Identiry (i.e User Manged Identity)
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# PLAN
- name: Terraform Plan
id: plan
run: |
echo ====== PLAN execution for the Terraform configuration in ${{ env.STACK_DIR }} ======
terraform plan -input=false -no-color -out tf.plan
# Save all plugin files and working Directory in a cache
- name: Cache Terraform
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
./*
key: ${{ runner.os }}-terraform-${{ env.STACK_DIR }}
restore-keys: |
${{ runner.os }}-terraform-${{ env.STACK_DIR }}
# ${{ hashFiles('**/.terraform.lock.hcl') }}
outputs:
terra_path: ${{ steps.init.outputs.terra_bin }}
# ############
# APPLY
# ############
Terraform_Apply:
name: 'Terraform Apply'
runs-on: ubuntu-latest
environment: az-labs
needs: [terraform_setup]
# Use default shell and working directory regardless of the os of the GitHub Actions runner
defaults:
run:
shell: bash
working-directory: ${{ env.STACK_DIR }}
steps:
# Checkout the repository to the GitHub Actions runner not necessary. The cache has it
- name: Cache Terraform
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
./*
key: ${{ runner.os }}-terraform-${{ env.STACK_DIR }}
restore-keys: |
${{ runner.os }}-terraform-${{ env.STACK_DIR }}
# Configure terraform pluggin in the new runner
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
# terraform init not needed here . int files are already in the cache
# TERRAPATH="${{ needs.terraform_setup.outputs.terra_path }}"
# echo old terraform binary location: $TERRAPATH
# Authenticate with Azure using OIDC Workload Federated Identiry (i.e User Manged Identity)
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# APPLY
- name: Terraform Apply
id: plan
if: github.event_name == 'push'
run: |
echo ====== APPLY execution for the Terraform configuration in ${{ env.STACK_DIR }} ======
echo "== Reusing cached version of terraform =="
sudo cp ./terraform /usr/local/bin/
terraform -v
terraform plan -input=false -no-color -out tf.plan
terraform apply --auto-approve tf.plan
# Create a cache for the terraform state file
- name: Cache Terraform statefile
uses: actions/cache@v3
with:
path: |
${{ env.STACK_DIR }}/terraform.tfstate
key: ${{ runner.os }}-terraform-apply-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-terraform-apply-${{ github.run_id }}
# ############
# DESTROY
# ############
Terraform_Destroy:
name: 'Terraform Destroy'
runs-on: ubuntu-latest
environment: az-labs
permissions: write-all
needs: [Terraform_Apply]
# Use default shell and working directory regardless of the os of the GitHub Actions runner
defaults:
run:
shell: bash
working-directory: ${{ env.STACK_DIR }}
steps:
# Checkout the repository to the GitHub Actions runner not necessary. The cache has it
- name: Cache Terraform
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
./*
key: ${{ runner.os }}-terraform-${{ env.STACK_DIR }}
restore-keys: |
${{ runner.os }}-terraform-${{ env.STACK_DIR }}
# Configure terraform pluggin in the new runner
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
# Restore a cache for the terraform state file
- name: Cache Terraform statefile
uses: actions/cache@v3
with:
path: |
${{ env.STACK_DIR }}/terraform.tfstate
key: ${{ runner.os }}-terraform-apply-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-terraform-apply
# terraform init not needed here . int files are already in the cache
# ls terraform.tfstate
# Authenticate with Azure using OIDC Workload Federated Identiry (i.e User Manged Identity)
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# clean terraform cache after destroy completion
# DESTROY
- name: Terraform Destroy
id: destroy
run: |
echo ====== Destroy the Terraform configuration in ${{ env.STACK_DIR }} ======
echo "== Reusing cached version of terraform =="
sudo cp ./terraform /usr/local/bin/
terraform -v
terraform destroy --auto-approve
# clean terraform cache after destroy completion
- name: clean cache
id: cache_deletion
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh extension install actions/gh-actions-cache
echo " deleting tfstate caches"
gh actions-cache list
gh actions-cache delete ${{ runner.os }}-terraform-apply-${{ github.run_id }} --confirm
gh actions-cache delete ${{ runner.os }}-terraform-${{ env.STACK_DIR }} --confirm
How to check your git actions claims?
In case of errors, the actions-oidc-debugger action is the perfect tool to validate your ID token (JWT) claims.
Here’s an excerpt of my repo claims:
Example: OIDC Token claims for “brokedba”s github actions workflow
{ "actor": "brokedba",
"aud": "https://github.com/brokedba",
"environment": "gcp-labs",
"event_name": "push",
"iss": "https://token.actions.githubusercontent.com",
"job_workflow_ref": "brokedba/terraform-examples/.github/workflows/..",
"ref": "refs/heads/git_actions",
"ref_type": "branch",
"repository": "brokedba/terraform-examples",
"repository_owner": "brokedba",
snip ...
"sub": "repo:brokedba/terraform-examples:environment:gcp-labs", <<---
"workflow": "Terraform_gcp_vpc",
snip ...
}
Step5 (Final) : Test the Pipeline
Once the configuration is complete, the last step is to test the pipeline to ensure everything works as expected. Our workflow has 3 different Jobs :
1. Terraform init-validate
2. Terraform Apply
3. Terraform Destroy

Push changes to the github_actions
branch to trigger the GitHub pipeline.

Go to Actions tab in the GitHub repo & select the latest run. Check the log.

check the portal quickly as terraform apply job is followed by a destroy

III. How to Fork My Repo to Try OIDC
- Visit the Repository: Go to https://github.com/brokedba/terraform-examples.
- Fork the Repository:
- Create an environment called az_labs linkedin to git_actions branch
- Follow the Azure OIDC configuration steps in this article
- Add scoped secrets and variable accordingly under the az_labs environment
- secret:
AZURE_CLIENT_ID
(from your managed identity) - secret:
AZURE_SUBSCRIPTION_ID
(from your managed identity) - secret:
AZURE_TENANT_ID
(from your Entra overview page) - Variables
TF_STACK_DIR
: terraform-provider-aws/create-vpcAZ_LOCATION
: i.e eastusTF_AP_PREFIX
: Prefix for the application used to name the resource Group.
- secret:
- Switch to the git_actions Branch
- Commit any tf file modification under
terraform-provider-azure/create-vnet
folder
Conclusion
Integrating GitHub Actions with Azure managed identity using OIDC provides a seamless, secure, and keyless authentication workflow. By leveraging OIDC trust, you eliminate the need for long-term credentials, reducing secret management overhead (storage, duplication, rotation) and simplifying cloud access.
The below best practices should already be in your baseline.
Best Practices
Don’t let your workflow be part of the 2025 secret sprawl report ;).
🙋🏻♀️If you like this content please subscribe to our blog newsletter ❤️.