azure-oidc
helps configure and implement GitHub OIDC authentication with Azure for secure, keyless access from GitHub Actions to Azure resources
GitHub OIDC Authentication for Azure
Overview
OpenID Connect (OIDC) allows your GitHub Actions workflows to access resources in Microsoft Azure without needing to store Azure credentials as long-lived GitHub secrets. This eliminates the security risks associated with static credentials and provides temporary, scoped access tokens through Azure Active Directory (Azure AD) / Microsoft Entra ID.
Core Concepts
What is OIDC?
- Federated Identity: Azure trusts GitHub's OIDC provider to authenticate workflows
- Temporary Tokens: Short-lived tokens replace static credentials
- Scoped Access: Fine-grained permissions based on repository, branch, and environment
- Security: No secrets stored in GitHub; tokens are generated on-demand
What is Federated Identity Credential?
Azure's Federated Identity Credential feature allows external identity providers (like GitHub Actions) to access Azure resources without managing secrets or certificates. This is Azure's implementation of workload identity federation.
Benefits:
- Improved Security: Eliminates the need to store and rotate service principal secrets
- Automatic Token Management: Azure AD handles token lifecycle automatically
- Simplified Management: Centralized identity and access management through Azure AD
- Fine-grained Access Control: Azure RBAC provides granular permissions
Key Components
- Azure AD App Registration: Application identity in Azure Active Directory
- Service Principal: Enterprise application that represents the GitHub Actions identity
- Federated Identity Credential: Configuration that trusts GitHub's OIDC provider
- Azure RBAC: Role assignments that define what actions the identity can perform
- Token Exchange: GitHub token is exchanged for Azure AD access token
Authentication Flow
- GitHub Actions workflow requests OIDC token from
https://token.actions.githubusercontent.com - GitHub issues JWT token with workflow identity claims
- Token is sent to Azure AD for validation
- Azure AD validates token against Federated Identity Credential configuration
- Azure AD issues access token with appropriate permissions
- Workflow uses access token to access Azure resources
Setup Methods
Method 1: Manual Configuration via Azure Portal & CLI
Prerequisites
- Azure subscription with appropriate permissions
- Azure CLI installed and configured
- GitHub repository for Actions workflows
- Owner or Application Administrator role in Azure AD
Step 1: Authenticate with Azure
# Login to Azure
az login
# Set your subscription
az account set --subscription "Your-Subscription-Name-or-ID"
# Verify current subscription
az account show
Step 2: Create Azure AD App Registration
# Create App Registration
APP_NAME="github-actions-app"
az ad app create --display-name "$APP_NAME"
# Get the Application (client) ID
APP_ID=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv)
echo "Application ID: $APP_ID"
# Get the Tenant ID
TENANT_ID=$(az account show --query tenantId -o tsv)
echo "Tenant ID: $TENANT_ID"
Step 3: Create Service Principal
# Create service principal for the app
az ad sp create --id $APP_ID
# Get the Service Principal Object ID
SP_OBJECT_ID=$(az ad sp list --filter "appId eq '$APP_ID'" --query "[0].id" -o tsv)
echo "Service Principal Object ID: $SP_OBJECT_ID"
Step 4: Configure Federated Identity Credential
# Define your GitHub organization, repository, and environment
GITHUB_ORG="your-org"
GITHUB_REPO="your-repo"
ENVIRONMENT="production" # Optional: for environment-specific access
# For main branch access
cat <<EOF > federated-credential.json
{
"name": "github-actions-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/main",
"description": "GitHub Actions access for main branch",
"audiences": [
"api://AzureADTokenExchange"
]
}
EOF
# Create the federated credential
az ad app federated-credential create \
--id $APP_ID \
--parameters @federated-credential.json
# For environment-based access (alternative)
cat <<EOF > federated-credential-env.json
{
"name": "github-actions-${ENVIRONMENT}",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:${ENVIRONMENT}",
"description": "GitHub Actions access for ${ENVIRONMENT} environment",
"audiences": [
"api://AzureADTokenExchange"
]
}
EOF
az ad app federated-credential create \
--id $APP_ID \
--parameters @federated-credential-env.json
# For pull request access (optional)
cat <<EOF > federated-credential-pr.json
{
"name": "github-actions-pr",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:pull_request",
"description": "GitHub Actions access for pull requests",
"audiences": [
"api://AzureADTokenExchange"
]
}
EOF
az ad app federated-credential create \
--id $APP_ID \
--parameters @federated-credential-pr.json
Step 5: Assign Azure RBAC Roles
# Get your subscription ID
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Assign Contributor role at subscription level (adjust scope as needed)
az role assignment create \
--assignee $APP_ID \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID"
# For resource group scope (more restrictive)
RESOURCE_GROUP="your-resource-group"
az role assignment create \
--assignee $APP_ID \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP"
# For specific resource scope (most restrictive)
RESOURCE_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/yourstorageaccount"
az role assignment create \
--assignee $APP_ID \
--role "Storage Blob Data Contributor" \
--scope "$RESOURCE_ID"
Step 6: Verify Configuration
# List federated credentials
az ad app federated-credential list --id $APP_ID
# List role assignments
az role assignment list --assignee $APP_ID --output table
Note the following values for GitHub Actions configuration:
- Client ID:
$APP_ID(Application ID) - Tenant ID:
$TENANT_ID - Subscription ID:
$SUBSCRIPTION_ID
Method 2: Terraform Infrastructure as Code
Terraform provides a repeatable, version-controlled way to configure Azure OIDC for multiple projects and environments.
Prerequisites
- Terraform installed (version >= 1.3.0)
- Azure CLI authenticated
- Azure subscription created
Repository Structure
terraform/
├── providers.tf # Provider configuration
├── main.tf # App registration and federated credentials
├── rbac.tf # Role assignments
├── variables.tf # Input variables
├── outputs.tf # Output values
└── tfvars/
├── dev.tfvars # Development environment
├── staging.tfvars # Staging environment
└── prod.tfvars # Production environment
Key Terraform Resources
1. Provider Configuration (providers.tf)
terraform {
required_version = ">= 1.3.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.80"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 2.45"
}
}
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstate"
container_name = "tfstate"
key = "github-oidc.tfstate"
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
provider "azuread" {
tenant_id = var.tenant_id
}
2. App Registration and Federated Credentials (main.tf)
# Data source for current Azure AD configuration
data "azuread_client_config" "current" {}
# Create Azure AD Application
resource "azuread_application" "github_actions" {
display_name = "${var.app_name}-${var.environment}"
owners = [data.azuread_client_config.current.object_id]
tags = [
"Environment:${var.environment}",
"ManagedBy:Terraform",
"Purpose:GitHubActions"
]
}
# Create Service Principal
resource "azuread_service_principal" "github_actions" {
application_id = azuread_application.github_actions.application_id
app_role_assignment_required = false
owners = [data.azuread_client_config.current.object_id]
tags = [
"Environment:${var.environment}",
"ManagedBy:Terraform"
]
}
# Federated Credential for main branch
resource "azuread_application_federated_identity_credential" "main_branch" {
application_object_id = azuread_application.github_actions.object_id
display_name = "github-actions-main-${var.environment}"
description = "GitHub Actions access for main branch in ${var.environment}"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main"
}
# Federated Credential for develop branch (optional)
resource "azuread_application_federated_identity_credential" "develop_branch" {
count = var.enable_develop_branch ? 1 : 0
application_object_id = azuread_application.github_actions.object_id
display_name = "github-actions-develop-${var.environment}"
description = "GitHub Actions access for develop branch in ${var.environment}"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/develop"
}
# Federated Credential for environment-based access
resource "azuread_application_federated_identity_credential" "environment" {
count = var.enable_environment_credential ? 1 : 0
application_object_id = azuread_application.github_actions.object_id
display_name = "github-actions-env-${var.environment}"
description = "GitHub Actions access for ${var.environment} environment"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:${var.github_org}/${var.github_repo}:environment:${var.environment}"
}
# Federated Credential for pull requests (optional)
resource "azuread_application_federated_identity_credential" "pull_request" {
count = var.enable_pr_access ? 1 : 0
application_object_id = azuread_application.github_actions.object_id
display_name = "github-actions-pr-${var.environment}"
description = "GitHub Actions access for pull requests in ${var.environment}"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:${var.github_org}/${var.github_repo}:pull_request"
}
3. RBAC Role Assignments (rbac.tf)
# Data source for subscription
data "azurerm_subscription" "current" {}
# Data source for resource group (if scope is resource group)
data "azurerm_resource_group" "target" {
count = var.rbac_scope == "resource_group" ? 1 : 0
name = var.target_resource_group
}
# Built-in role assignments
resource "azurerm_role_assignment" "builtin_roles" {
for_each = toset(var.builtin_roles)
scope = local.rbac_scope
role_definition_name = each.value
principal_id = azuread_service_principal.github_actions.object_id
depends_on = [azuread_service_principal.github_actions]
}
# Custom role definition (if needed)
resource "azurerm_role_definition" "custom" {
count = var.create_custom_role ? 1 : 0
name = "${var.app_name}-custom-role-${var.environment}"
scope = data.azurerm_subscription.current.id
description = "Custom role for GitHub Actions in ${var.environment}"
permissions {
actions = var.custom_role_actions
not_actions = var.custom_role_not_actions
data_actions = var.custom_role_data_actions
not_data_actions = var.custom_role_not_data_actions
}
assignable_scopes = [
local.rbac_scope
]
}
# Assign custom role
resource "azurerm_role_assignment" "custom" {
count = var.create_custom_role ? 1 : 0
scope = local.rbac_scope
role_definition_id = azurerm_role_definition.custom[0].role_definition_resource_id
principal_id = azuread_service_principal.github_actions.object_id
depends_on = [
azuread_service_principal.github_actions,
azurerm_role_definition.custom
]
}
# Local values for scope determination
locals {
rbac_scope = var.rbac_scope == "subscription" ? data.azurerm_subscription.current.id : (
var.rbac_scope == "resource_group" ? data.azurerm_resource_group.target[0].id :
var.custom_rbac_scope
)
}
4. Variables (variables.tf)
variable "subscription_id" {
type = string
description = "Azure subscription ID"
}
variable "tenant_id" {
type = string
description = "Azure AD tenant ID"
}
variable "environment" {
type = string
description = "Environment name (dev, staging, prod)"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "app_name" {
type = string
description = "Base name for the Azure AD application"
default = "github-actions"
}
variable "github_org" {
type = string
description = "GitHub organization name"
}
variable "github_repo" {
type = string
description = "GitHub repository name"
}
variable "enable_develop_branch" {
type = bool
description = "Enable federated credential for develop branch"
default = false
}
variable "enable_environment_credential" {
type = bool
description = "Enable environment-based federated credential"
default = true
}
variable "enable_pr_access" {
type = bool
description = "Enable federated credential for pull requests"
default = false
}
variable "rbac_scope" {
type = string
description = "Scope for RBAC assignment: subscription, resource_group, or custom"
default = "subscription"
validation {
condition = contains(["subscription", "resource_group", "custom"], var.rbac_scope)
error_message = "RBAC scope must be subscription, resource_group, or custom."
}
}
variable "target_resource_group" {
type = string
description = "Target resource group name (required if rbac_scope is resource_group)"
default = null
}
variable "custom_rbac_scope" {
type = string
description = "Custom RBAC scope (full resource ID)"
default = null
}
variable "builtin_roles" {
type = list(string)
description = "List of built-in Azure roles to assign"
default = ["Contributor"]
}
variable "create_custom_role" {
type = bool
description = "Create and assign a custom role"
default = false
}
variable "custom_role_actions" {
type = list(string)
description = "Actions allowed in custom role"
default = []
}
variable "custom_role_not_actions" {
type = list(string)
description = "Actions denied in custom role"
default = []
}
variable "custom_role_data_actions" {
type = list(string)
description = "Data actions allowed in custom role"
default = []
}
variable "custom_role_not_data_actions" {
type = list(string)
description = "Data actions denied in custom role"
default = []
}
5. Outputs (outputs.tf)
output "application_id" {
description = "Azure AD Application (Client) ID"
value = azuread_application.github_actions.application_id
}
output "tenant_id" {
description = "Azure AD Tenant ID"
value = var.tenant_id
}
output "subscription_id" {
description = "Azure Subscription ID"
value = var.subscription_id
}
output "service_principal_object_id" {
description = "Service Principal Object ID"
value = azuread_service_principal.github_actions.object_id
}
output "federated_credentials" {
description = "List of federated credential subjects"
value = {
main_branch = azuread_application_federated_identity_credential.main_branch.subject
develop_branch = var.enable_develop_branch ? azuread_application_federated_identity_credential.develop_branch[0].subject : null
environment = var.enable_environment_credential ? azuread_application_federated_identity_credential.environment[0].subject : null
pull_request = var.enable_pr_access ? azuread_application_federated_identity_credential.pull_request[0].subject : null
}
}
output "rbac_scope" {
description = "RBAC assignment scope"
value = local.rbac_scope
}
Example Configuration (tfvars/dev.tfvars)
subscription_id = "12345678-1234-1234-1234-123456789012"
tenant_id = "87654321-4321-4321-4321-210987654321"
environment = "dev"
app_name = "github-actions"
github_org = "OptumInsight-Platform"
github_repo = "my-application"
enable_develop_branch = true
enable_environment_credential = true
enable_pr_access = true
rbac_scope = "resource_group"
target_resource_group = "my-app-dev-rg"
builtin_roles = [
"Contributor",
"Storage Blob Data Contributor"
]
create_custom_role = false
Deployment Commands
# Navigate to terraform directory
cd terraform
# Initialize Terraform
terraform init
# Validate configuration
terraform validate
# Preview changes
terraform plan -var-file=tfvars/dev.tfvars -out=tfplan
# Apply configuration
terraform apply tfplan
# View outputs
terraform output
# Save outputs for GitHub Actions secrets
terraform output -raw application_id > client_id.txt
terraform output -raw tenant_id > tenant_id.txt
terraform output -raw subscription_id > subscription_id.txt
GitHub Actions Integration
Required Permissions
permissions:
id-token: write # Required for OIDC token generation
contents: read # Required for repository access
Basic Workflow Example
name: Deploy to Azure
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: uhg-runner
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Azure Login with OIDC
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Verify authentication
run: |
az account show
az group list --output table
Advanced Workflow with Multiple Environments
name: Multi-Environment Deployment
on:
push:
branches:
- main
- develop
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: uhg-runner
strategy:
matrix:
environment: [dev, staging, prod]
include:
- environment: dev
azure-client-id: ${{ secrets.AZURE_CLIENT_ID_DEV }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_DEV }}
condition: github.ref == 'refs/heads/develop'
- environment: staging
azure-client-id: ${{ secrets.AZURE_CLIENT_ID_STAGING }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_STAGING }}
condition: github.ref == 'refs/heads/main'
- environment: prod
azure-client-id: ${{ secrets.AZURE_CLIENT_ID_PROD }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_PROD }}
condition: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
if: ${{ matrix.condition }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ matrix.azure-client-id }}
tenant-id: ${{ matrix.azure-tenant-id }}
subscription-id: ${{ matrix.azure-subscription-id }}
- name: Deploy to ${{ matrix.environment }}
run: |
echo "Deploying to ${{ matrix.environment }}"
az account show
Integration with Azure Storage
name: Deploy Static Website
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: uhg-runner
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Upload to Storage
run: |
az storage blob upload-batch \
--account-name ${{ secrets.STORAGE_ACCOUNT }} \
--destination '$web' \
--source ./dist \
--overwrite
Integration with Azure Container Registry
name: Build and Push Container
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
build-push:
runs-on: uhg-runner
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Login to ACR
run: |
az acr login --name ${{ secrets.ACR_NAME }}
- name: Build and Push
run: |
docker build -t ${{ secrets.ACR_NAME }}.azurecr.io/myapp:${{ github.sha }} .
docker push ${{ secrets.ACR_NAME }}.azurecr.io/myapp:${{ github.sha }}
Integration with Optum Artifactory
name: Build, Scan, and Deploy to Azure
on:
push:
branches: [main]
permissions:
actions: read
contents: write
pull-requests: write
security-events: write
checks: write
id-token: write
jobs:
build-and-deploy:
runs-on: [uhg-runner]
steps:
- uses: actions/checkout@v4
# Configure Artifactory
- name: Configure Artifactory Connection
id: artifactory-setup
uses: uhg-pipelines/epl-jf/configure-saas-connection@latest
with:
jfrog-project-key: your-project-key
npm-setup: true
# Build and publish to Artifactory
- name: Build and Scan
uses: optum-eeps/epl-actions/node-build-scan@v1
with:
jfrog-project-key: your-project-key
jfrog-build-name: ${{ steps.artifactory-setup.outputs.jfrog-build-name }}
jfrog-build-number: ${{ steps.artifactory-setup.outputs.jfrog-build-number }}
npm-publish: true
# Authenticate to Azure
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# Deploy to Azure
- name: Deploy to Azure App Service
run: |
az webapp deployment source config-zip \
--resource-group ${{ secrets.RESOURCE_GROUP }} \
--name ${{ secrets.APP_NAME }} \
--src ./dist.zip
Security Best Practices
1. Least Privilege Access
Grant only the minimum permissions required:
# Instead of Contributor at subscription level
# Use specific roles at resource level
az role assignment create \
--assignee $APP_ID \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$STORAGE"
2. Restrict Repository Access
Use specific subject patterns in federated credentials:
# Specific branch only
subject = "repo:OptumInsight-Platform/my-repo:ref:refs/heads/main"
# Specific environment only
subject = "repo:OptumInsight-Platform/my-repo:environment:production"
# Specific tag pattern
subject = "repo:OptumInsight-Platform/my-repo:ref:refs/tags/v*"
3. Environment-Specific Credentials
Create separate app registrations for each environment:
# Development
az ad app create --display-name "github-actions-dev"
# Staging
az ad app create --display-name "github-actions-staging"
# Production
az ad app create --display-name "github-actions-prod"
4. Conditional Access Policies
Implement Azure AD Conditional Access for additional security:
# Require specific IP ranges
# Configure through Azure Portal > Azure AD > Security > Conditional Access
# Create policy requiring corporate network or VPN
5. Audit and Monitoring
Enable logging and monitoring:
# Enable diagnostic logging for App Registration
az monitor diagnostic-settings create \
--resource $APP_ID \
--name "github-actions-audit" \
--logs '[{"category": "AuditLogs", "enabled": true}]' \
--workspace $LOG_ANALYTICS_WORKSPACE_ID
Troubleshooting
Common Issues
1. "AADSTS700016: Application not found"
# Verify app registration exists
az ad app list --display-name "your-app-name"
# Check if service principal was created
az ad sp list --filter "appId eq 'YOUR_CLIENT_ID'"
# Recreate service principal if missing
az ad sp create --id $APP_ID
2. "AADSTS70021: No matching federated identity"
# Verify federated credential configuration
az ad app federated-credential list --id $APP_ID
# Check the subject claim matches your repository
# Subject format: repo:ORG/REPO:ref:refs/heads/BRANCH
# or: repo:ORG/REPO:environment:ENVIRONMENT
# Verify issuer is correct
# Should be: https://token.actions.githubusercontent.com
# Verify audience is correct
# Should be: api://AzureADTokenExchange
3. "Authorization failed"
# Check role assignments
az role assignment list --assignee $APP_ID --output table
# Verify subscription ID matches
az account show
# Check if role has propagated (can take a few minutes)
sleep 60
az role assignment list --assignee $APP_ID --output table
4. "Token validation failed"
- Verify workflow has
id-token: writepermission - Check that audience is
api://AzureADTokenExchange - Ensure repository name and branch match the subject claim exactly
Debugging Steps
Debug Federated Credential Match
- name: Debug OIDC Token Claims
env:
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ${{ env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }}
ACTIONS_ID_TOKEN_REQUEST_URL: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }}
run: |
# Get the OIDC token
OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r .value)
# Decode JWT payload
echo $OIDC_TOKEN | cut -d'.' -f2 | base64 -d | jq .
Verify Azure CLI Authentication
# Check current authentication
az account show
# List accessible subscriptions
az account list --output table
# Verify service principal access
az login --service-principal \
-u $APP_ID \
-p <certificate-or-secret> \
--tenant $TENANT_ID
Test Role Permissions
# Try to list resources with the service principal
az resource list --output table
# Test specific permission
az storage account list --output table
Advanced Patterns
1. Cross-Subscription Access
# Assign role in different subscription
resource "azurerm_role_assignment" "cross_sub" {
scope = "/subscriptions/${var.target_subscription_id}"
role_definition_name = "Reader"
principal_id = azuread_service_principal.github_actions.object_id
}
2. Conditional Access Based on PR Labels
- name: Deploy to production
if: contains(github.event.pull_request.labels.*.name, 'deploy-prod')
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID_PROD }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_PROD }}
3. Multi-Region Deployment
strategy:
matrix:
region: [eastus, westus2, northeurope]
steps:
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to ${{ matrix.region }}
run: |
az deployment group create \
--resource-group rg-${{ matrix.region }} \
--template-file deploy.json \
--parameters location=${{ matrix.region }}
4. Managed Identity Integration
- name: Azure Login with OIDC
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Access Key Vault using Managed Identity
run: |
# Service principal can access Key Vault
az keyvault secret show \
--vault-name ${{ secrets.KEY_VAULT_NAME }} \
--name my-secret
Getting Latest Versions
GitHub CLI Commands
# Get latest azure/login action version
gh api repos/azure/login/releases/latest --jq '.tag_name'
# Check Azure CLI version
az version
# Update Azure CLI
# macOS
brew upgrade azure-cli
# Linux
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
Azure CLI Commands
# List all app registrations
az ad app list --output table
# List all federated credentials for an app
az ad app federated-credential list --id $APP_ID --output table
# List role assignments
az role assignment list --assignee $APP_ID --output table
# Check role definition details
az role definition list --name "Contributor" --output json
Implementation Checklist
Pre-Setup
- Azure subscription with appropriate permissions
- Azure CLI installed and authenticated
- Terraform installed (if using IaC approach)
- GitHub repository in organization
- Owner or Application Administrator role in Azure AD
Azure AD Configuration
- App registration created
- Service principal created
- Federated identity credentials configured
- Appropriate audience set (
api://AzureADTokenExchange) - Subject claims match repository/branch pattern
- Issuer set to
https://token.actions.githubusercontent.com
RBAC Configuration
- Appropriate roles assigned to service principal
- Scope correctly set (subscription, resource group, or resource)
- Least privilege principle applied
- Role assignments propagated (wait ~60 seconds)
GitHub Actions
- Workflow includes
id-token: writepermission - Client ID configured as repository secret
- Tenant ID configured as repository secret
- Subscription ID configured as repository secret
- Test workflow validates authentication
- Error handling implemented
Security Validation
- Least privilege principle applied
- Repository access properly restricted
- Environment-based credentials configured
- Monitoring and alerting configured
- Separate app registrations per environment
Documentation
- Team training on OIDC and Azure AD concepts
- Runbooks for troubleshooting
- Security policies documented
- Regular review schedule established
Support Resources
- Azure Documentation: Workload identity federation
- GitHub Actions: Azure Login Action
- GitHub Documentation: About security hardening with OpenID Connect
- Azure Best Practices: Best practices for Azure AD
- Terraform AzureRM Provider: Terraform Azure Provider
- Terraform AzureAD Provider: Terraform AzureAD Provider
Remember: Federated Identity Credentials eliminate the need for long-lived secrets and certificates for Azure authentication, significantly improving security posture while maintaining automation capabilities. Always use the principle of least privilege and restrict access to specific repositories, branches, or environments.

