For Python automation against Azure or Microsoft Graph, the secure baseline is: create an App Registration as the application definition, use its Application (client) ID as the stable identifier in code, let Microsoft Entra create or associate a service principal as the tenant-local security principal, grant only the minimum Azure RBAC roles or Graph application permissions needed at the narrowest possible scope, obtain tokens with a supported library such as MSAL or Azure Identity, and let the target resource enforce authorization using the token’s app identity plus the assigned roles and permissions. In Microsoft Entra, the application object and service principal are distinct objects: the app object defines the application globally in its home tenant, while the service principal is the concrete instance used in a tenant. Microsoft documents that when you register a new application in Entra ID, a service principal is automatically created for that app registration in the tenant. citeturn27search1turn27search0turn3search4
For credentials, Microsoft’s current direction is clear. If the workload runs on Azure, prefer managed identities. If it runs outside Azure but can use a trusted external identity provider, prefer federated identity credentials such as GitHub Actions OIDC. If neither is possible, use certificates. Client secrets are the weakest option and Microsoft explicitly recommends certificates or federated credentials for production, while also recommending policies and monitoring to shorten or eliminate secret use. citeturn35view0turn35view1turn35view3
For least privilege, separate the two axes of authorization. Azure resources use Azure RBAC with scope inheritance, so the practical rule is to assign the narrowest built-in or custom role at the smallest viable scope. Microsoft Graph uses delegated or application permissions, and for daemon-style Python automation the relevant model is almost always application permissions with admin consent where required. Microsoft also notes that some Graph scenarios require both Graph permissions and RBAC, so treat Graph and Azure RBAC as complementary, not interchangeable. citeturn14search5turn2search10turn11search5turn15search6
The core object relationships are shown below. The diagram is based on Microsoft’s application-object and service-principal object model, the Entra portal creation flow, the Graph servicePrincipal resource definition, and the documented credential types supported on the application object. citeturn27search0turn27search1turn3search4turn35view1
flowchart LR
A[App Registration<br/>application object] --> B[client_id<br/>appId]
A --> C[Credentials<br/>secret | certificate | federated credential]
A --> D[Service Principal<br/>tenant-local enterprise app]
D --> E[Azure RBAC role assignments<br/>subscription | RG | resource]
D --> F[Graph app role assignments<br/>application permissions]
B --> G[Python app]
C --> G
G --> H[Microsoft identity platform token endpoint]
H --> I[Access token for resource]
I --> J[Azure SDK or REST call]
J --> K[Azure resource or Microsoft Graph]
K --> L[Authorization enforced<br/>by RBAC / app roles / resource policy]
A concise step map for the user’s requested eight-stage flow is below. Each row includes the main portal action, a CLI or PowerShell example, and the principal REST surface. Sources are in the last column. citeturn15search0turn27search1turn28search1turn35view1turn14search5turn8search0turn8search8
| Step | What happens | Portal path | CLI / PowerShell example | REST surface | Sources |
|---|---|---|---|---|---|
| App Registration | Create application object | Entra ID → App registrations → New registration | az ad app create --display-name "my-python-automation" |
POST /applications |
citeturn15search0turn15search7turn12search0 |
| client_id | Copy stable app identifier | App Overview | az ad app show --id <appId-or-objectId> |
GET /applications/{id} |
citeturn28search13turn3search2 |
| Credentials | Add secret, cert, or federated credential | Certificates & secrets | az ad app credential reset, az ad app federated-credential create |
POST /applications/{id}/addPassword, POST /applications/{id}/addKey, POST /applications/{id}/federatedIdentityCredentials |
citeturn35view1turn12search2turn12search3turn12search13turn29search2 |
| Locate or use SP | Find the tenant-local service principal | Enterprise applications or Managed application in local directory | az ad sp show --id <appId> / Get-AzADServicePrincipal -ApplicationId $appId |
GET /servicePrincipals(appId='{appId}') or POST /servicePrincipals |
citeturn27search0turn28search1turn3search2turn3search1turn26search0 |
| Assign minimal rights | Bind Azure RBAC or Graph app roles | Resource Access control (IAM) or app API permissions | az role assignment create ... / az ad app permission add ... |
ARM PUT .../roleAssignments/{guid} or Graph app role assignment endpoints |
citeturn2search10turn2search2turn16search0turn16search1turn14search5 |
| Python obtains token | Confidential client flow or Azure Identity | N/A | MSAL / Azure Identity | POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token |
citeturn8search0turn33view0 |
| Python calls SDK or REST | Use SDK client or raw HTTP | N/A | SecretClient, ResourceManagementClient, requests |
Resource endpoints such as ARM, Graph, Key Vault, Storage | citeturn34search5turn34search4turn17view4turn17view0turn17view5turn17view2 |
| Azure enforces authorization | Resource evaluates token + permissions | Resource-specific | N/A | Resource service / Graph / ARM | citeturn19search0turn20view0turn32search5turn11search8 |
Step one — register the app.
In the portal, go to Entra ID → App registrations → New registration, choose a display name, choose the supported account types that match your scenario, and register the app. The Overview page then exposes the Application (client) ID and Directory (tenant) ID. With Graph, the corresponding REST create call is POST /applications. With Azure CLI, az ad app create --display-name "my-python-automation" creates the application. With PowerShell, New-AzADApplication is the Az.Resources cmdlet family for creating application objects. citeturn15search0turn15search7turn12search0turn4search11
Step two — capture the client ID.
The client_id used by Python code is the application’s appId, shown in the portal as Application (client) ID. Microsoft Graph distinguishes this from the service principal’s object ID; GET /servicePrincipals/{id} targets the service principal object ID, while GET /servicePrincipals(appId='{appId}') targets the application/client ID. This distinction matters because app object IDs, service principal object IDs, and app IDs are different values with different uses. citeturn28search13turn3search2turn27search0
Step three — choose and add credentials.
In the portal, use Certificates & secrets and pick one of the three credential families: client secret, certificate, or federated credential. Microsoft recommends certificates over secrets for confidential clients, recommends federated credentials when you can trust an external IdP such as GitHub Actions, and treats client secrets as less secure and unsuitable for production where stronger alternatives are available. The corresponding Graph endpoints are POST /applications/{id}/addPassword, POST /applications/{id}/addKey, and POST /applications/{id}/federatedIdentityCredentials. CLI equivalents include az ad app credential reset --id <appId> --append for secrets or certificates and az ad app federated-credential create --id <appObjectId> --parameters @fic.json for federation. citeturn35view1turn35view0turn12search2turn12search3turn12search13turn29search2
Step four — locate or create the service principal and map it back to the app.
In the home tenant, a new app registration automatically gets a service principal. You can find it either in Enterprise applications or from the app registration’s Managed application in local directory link. Programmatically, az ad sp show --id <appId> or Get-AzADServicePrincipal -ApplicationId $appId resolves the service principal from the app’s client ID. If you have an application object but need to instantiate or repair the service principal explicitly, Graph supports POST /servicePrincipals with the app’s appId, and Azure CLI supports az ad sp create --id <appId>. The mapping key between the app registration and its service principal is the shared appId. citeturn27search1turn28search1turn3search2turn3search1turn3search3turn26search0
Step five — grant the minimum rights.
For Azure resources, assign RBAC at the smallest scope that works: resource, resource group, subscription, or management group. Microsoft’s role assignment guidance for CLI is az role assignment create --assignee "{assignee}" --role "{roleNameOrId}" --scope "{scope}", and the REST equivalent is PUT https://management.azure.com/{scope}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}?api-version=2022-04-01 with principalId and roleDefinitionId. For Graph application permissions, add only the app roles you need, and then obtain admin consent where required. Azure CLI exposes az ad app permission add and az ad app permission admin-consent; Graph exposes service-principal app role assignment endpoints. citeturn2search10turn17view3turn16search0turn16search1turn16search8
Step six — Python obtains an access token.
For daemon automation, use the OAuth 2.0 client credentials flow against https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token. Microsoft documents that client credentials with MSAL use acquire_token_for_client, and since MSAL Python 1.23 this call automatically checks the token cache before requesting a new token. Use the resource’s .default scope, for example https://management.azure.com/.default for ARM, https://graph.microsoft.com/.default for Microsoft Graph, and https://vault.azure.net/.default for Key Vault. For Azure Storage, Azure documents the resource ID https://storage.azure.com/; when using the v2 endpoint’s client-credentials pattern, that resource ID becomes the .default scope. citeturn8search0turn33view0turn1search14turn8search8turn18view0turn20view0turn8search3
Step seven — Python calls Azure SDKs or REST APIs.
With Azure SDKs, pass a TokenCredential implementation such as DefaultAzureCredential, ClientSecretCredential, or CertificateCredential into the client object. Microsoft’s Python docs show this pattern for SecretClient and ResourceManagementClient. With raw REST, send the bearer token in Authorization: Bearer <token> to the resource endpoint. Example endpoints include ARM VM list at GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines?api-version=2026-03-01, Graph users at GET https://graph.microsoft.com/v1.0/users, Key Vault secret set at PUT {vaultBaseUrl}/secrets/{secret-name}?api-version=2025-07-01, and Blob Storage PUT https://{account}.blob.core.windows.net/{container}/{blob}. citeturn34search5turn34search4turn17view4turn17view5turn17view0turn17view2turn8search1
Step eight — Azure enforces authorization.
The token only proves who the calling workload is. The actual allow/deny decision is made by the target resource using Azure RBAC, Graph application permissions, and service-specific authorization rules. Microsoft’s Storage docs explicitly describe authorization as a two-step process—authenticate the principal, then authorize the request using RBAC—and the Python Azure SDK authorization overview shows the same pattern for SDK calls returning 403 Forbidden when role assignments are insufficient. citeturn19search0turn20view0turn32search5
The following comparison reflects Microsoft’s guidance on credential strength, operational burden, and production suitability. The strategic order is: managed identity where available, then federated credential, then certificate, and only then client secret as a last resort. citeturn35view0turn35view1turn35view3
| Credential type | Security profile | Rotation model | Storage model | Recommended use | Main caveats | Sources |
|---|---|---|---|---|---|---|
| Client secret | Weakest of the four; easier to leak, copy, or mismanage | Manual or scripted rotation; secret value is shown only once when created | Must be stored securely outside code, ideally in a vault | Local dev or constrained legacy automation only | Microsoft says secrets are less secure than certificates or federated credentials and shouldn’t be used in production | citeturn35view1turn15search18turn35view0 |
| Certificate | Stronger than secrets; private key can stay in HSM or Key Vault | Roll by adding new key, validating, then removing old key | Store private key in Key Vault or equivalent secure store | Production headless automation when federation or MI is unavailable | Certificate lifecycle management still exists; private key custody remains your responsibility | citeturn35view1turn35view0turn12search3 |
| Federated credential | Secretless; avoids password or cert distribution | Typically rotate trust metadata or external IdP policy, not a stored secret | No app secret to store; trust is based on issuer/subject/audience match | CI/CD, GitHub Actions, external platforms, Kubernetes | issuer, subject, and audience must match case-sensitively; supported scenarios depend on platform and flow |
citeturn35view2turn35view0turn12search13 |
| Managed identity | Best default when workload runs on Azure; credentials not exposed to you | Azure-managed | No secret or certificate management for the app team | Azure-hosted automation | Limited to Azure-supported hosting patterns and identity attachment model | citeturn35view3turn35view0turn7search6 |
For Azure-hosted Python such as App Service, Functions, Azure Container Apps, VMs, or AKS, the recommended pattern is to avoid app-registration credentials entirely and use managed identities, because Microsoft describes them as secure by default, low-maintenance, and preferred for Azure-hosted service-to-service access. If you need a reusable identity across multiple compute resources, user-assigned managed identities are the recommended managed identity type for Microsoft services. citeturn35view3turn7search6turn7search9
For external CI/CD such as GitHub Actions, use OIDC workload identity federation instead of storing a secret in GitHub. Microsoft explicitly recommends trusted external identities such as GitHub Actions workflows rather than static credentials, and the Azure Login OIDC flow is the official path for GitHub → Azure authentication. citeturn35view0turn30view0turn13search4
For non-Azure long-running automation where federation is not available, use a certificate and keep the private key in Key Vault or another secure store. Microsoft’s Graph and identity docs specifically call certificates the recommended credential type for confidential clients and state that a certificate is more secure than a client secret. citeturn35view1turn35view0
The entity relationship below captures the app-to-SP mapping and where credentials and permissions live. It is synthesized from Microsoft’s Entra object model, Graph resource definitions, and credential/role assignment documentation. citeturn27search0turn3search4turn35view1turn14search5
erDiagram
APPLICATION ||--o{ SERVICE_PRINCIPAL : "instantiated as"
APPLICATION {
string objectId
string appId
string displayName
string signInAudience
}
SERVICE_PRINCIPAL {
string objectId
string appId
string displayName
string tenantId
}
APPLICATION ||--o{ PASSWORD_CREDENTIAL : "has"
APPLICATION ||--o{ KEY_CREDENTIAL : "has"
APPLICATION ||--o{ FEDERATED_IDENTITY_CREDENTIAL : "has"
SERVICE_PRINCIPAL ||--o{ RBAC_ROLE_ASSIGNMENT : "receives"
SERVICE_PRINCIPAL ||--o{ GRAPH_APP_ROLE_ASSIGNMENT : "receives"
Portal path.
Go to Entra ID → App registrations → [your app] → Overview and use Managed application in local directory to jump to the enterprise application, which is the service principal. You can also search directly in Entra ID → Enterprise applications → All applications. Microsoft’s app/service-principal documentation explicitly says the App registrations page manages application objects while the Enterprise applications page manages service principals. citeturn28search1turn27search0
Azure CLI.
# Find app registration
az ad app list --display-name "my-python-automation"
# Show the app registration
az ad app show --id <appId-or-objectId>
# Show the matching service principal by appId/clientId
az ad sp show --id <appId>
# Create the service principal explicitly if needed
az ad sp create --id <appId>These commands align with Azure CLI’s az ad app and az ad sp command sets, and az ad sp create --id {appId} is the documented way to create the corresponding service principal for an existing app. citeturn3search3turn3search5turn3search8
Azure PowerShell.
# Find the application object
Get-AzADApplication -DisplayName "my-python-automation"
# Resolve the service principal from the appId
Get-AzADServicePrincipal -ApplicationId $appId
# Create the service principal explicitly if needed
New-AzADServicePrincipal -ApplicationId $appIdAz.Resources documents Get-AzADServicePrincipal -ApplicationId and New-AzADServicePrincipal -ApplicationId. citeturn26search0turn26search6
Microsoft Graph REST.
GET https://graph.microsoft.com/v1.0/servicePrincipals(appId='{appId}')POST https://graph.microsoft.com/v1.0/servicePrincipals
Content-Type: application/json
{
"appId": "00001111-aaaa-2222-bbbb-3333cccc4444"
}Graph explicitly supports both retrieval by appId and creation using appId. citeturn3search2turn3search1
The table below uses Microsoft-built role and permission references to show concrete role names, example scopes, the token audience or OAuth scope to request, and the typical SDK or REST surface. A key design rule is to scope each assignment as narrowly as possible, because Azure RBAC roles assigned on a broader scope are inherited by lower scopes. citeturn19search0turn2search10turn11search5
| Scenario | Minimum practical permission pattern | Example assignment scope | Token audience / scope | Typical SDK / method | REST endpoint example | Sources |
|---|---|---|---|---|---|---|
| Read Key Vault secrets | Key Vault Secrets User | /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<kv> |
https://vault.azure.net/.default |
SecretClient.get_secret |
GET {vaultBaseUrl}/secrets?api-version=2025-07-01 or get specific secret |
citeturn2search3turn18view1turn34search5 |
| Write Key Vault secrets | Key Vault Secrets Officer | Same vault scope | https://vault.azure.net/.default |
SecretClient.set_secret |
PUT {vaultBaseUrl}/secrets/{secret-name}?api-version=2025-07-01 |
citeturn2search3turn18view0turn34search2 |
| Manage VMs in a resource group | Virtual Machine Contributor | /subscriptions/<sub>/resourceGroups/<rg> |
https://management.azure.com/.default |
ComputeManagementClient.virtual_machines.* |
GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines?api-version=2026-03-01 |
citeturn2search0turn1search14turn17view4 |
| Read blob data | Storage Blob Data Reader | /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<sa>/blobServices/default/containers/<container> |
https://storage.azure.com/.default |
BlobClient.download_blob |
GET https://<account>.blob.core.windows.net/<container>/<blob> |
citeturn2search1turn20view0turn10search15 |
| Write blob data | Storage Blob Data Contributor | Same container scope | https://storage.azure.com/.default |
BlobClient.upload_blob |
PUT https://<account>.blob.core.windows.net/<container>/<blob> |
citeturn2search1turn20view0turn17view2 |
| Graph: list users | User.Read.All application permission | Tenant-wide app permission + admin consent | https://graph.microsoft.com/.default |
Graph SDK or requests |
GET https://graph.microsoft.com/v1.0/users |
citeturn22view0turn11search5turn17view5 |
| Graph: manage only apps the automation owns | Application.ReadWrite.OwnedBy application permission | Tenant-wide app permission + admin consent | https://graph.microsoft.com/.default |
Graph SDK or requests |
GET /applications, GET /servicePrincipals, modify owned app objects |
citeturn22view4turn24search1 |
| Graph: SharePoint / OneDrive read all site items | Sites.Read.All application permission | Tenant-wide app permission + admin consent | https://graph.microsoft.com/.default |
Graph SDK or requests |
Graph site/list/document endpoints | citeturn23search0turn23search11 |
| Graph: SharePoint / OneDrive read-write all site items | Sites.ReadWrite.All application permission | Tenant-wide app permission + admin consent | https://graph.microsoft.com/.default |
Graph SDK or requests |
Graph site/list/document endpoints | citeturn23search0turn23search11 |
For Azure Storage, Microsoft explicitly warns that management roles such as Owner, Contributor, and Storage Account Contributor do not directly provide blob data access via Entra ID, although roles that include listKeys can still allow data access through Shared Key. For secure automation, this is why you should prefer the Blob Data roles and disable Shared Key authorization where feasible. citeturn19search0turn20view0
For Graph, Microsoft notes that app-only permissions are broad and tenant-wide by nature, require admin consent, and in some cases might also need RBAC in the underlying workload. That makes Application.ReadWrite.OwnedBy a strong default when the automation only needs to manage applications it owns, instead of the broader Application.ReadWrite.All. citeturn11search5turn24search1turn22view4
Microsoft recommends using MSAL rather than implementing token acquisition yourself. MSAL Python’s acquire_token_for_client supports the confidential-client client-credentials flow, and MSAL documents both in-memory caching and file-backed serialization through SerializableTokenCache, while also pointing to MSAL Extensions for encrypted persistent caching. citeturn33view0turn33view1
import atexit
import json
import os
from pathlib import Path
import msal
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
TENANT_ID = os.environ["AZURE_TENANT_ID"]
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["https://management.azure.com/.default"] # ARM
CACHE_PATH = Path(os.environ.get("MSAL_CACHE_PATH", ".msal_cache.bin"))
def load_cache() -> msal.SerializableTokenCache:
cache = msal.SerializableTokenCache()
if CACHE_PATH.exists():
cache.deserialize(CACHE_PATH.read_text(encoding="utf-8"))
def _save():
if cache.has_state_changed:
CACHE_PATH.write_text(cache.serialize(), encoding="utf-8")
atexit.register(_save)
return cache
def build_app() -> msal.ConfidentialClientApplication:
return msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
authority=AUTHORITY,
client_credential=CLIENT_SECRET,
token_cache=load_cache(),
)
def get_access_token(app: msal.ConfidentialClientApplication) -> str:
result = app.acquire_token_for_client(scopes=SCOPES)
if "access_token" not in result:
raise RuntimeError(
f"Token acquisition failed: {result.get('error')} "
f"{result.get('error_description')} "
f"correlation_id={result.get('correlation_id')}"
)
return result["access_token"]
def build_session(token: str) -> requests.Session:
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {token}",
"Accept": "application/json",
})
retries = Retry(
total=5,
backoff_factor=1.0,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
)
session.mount("https://", HTTPAdapter(max_retries=retries))
return session
def list_vms(subscription_id: str, resource_group: str) -> dict:
app = build_app()
token = get_access_token(app)
session = build_session(token)
url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
f"/resourceGroups/{resource_group}"
f"/providers/Microsoft.Compute/virtualMachines"
f"?api-version=2026-03-01"
)
response = session.get(url, timeout=30)
if response.status_code == 401:
raise RuntimeError("Unauthorized. Check tenant, credential, and token audience.")
if response.status_code == 403:
raise RuntimeError("Forbidden. The service principal likely lacks RBAC at this scope.")
response.raise_for_status()
return response.json()
if __name__ == "__main__":
sub = os.environ["AZURE_SUBSCRIPTION_ID"]
rg = os.environ["AZURE_RESOURCE_GROUP"]
print(json.dumps(list_vms(sub, rg), indent=2))Azure SDK clients accept a TokenCredential, and Microsoft’s Python docs show DefaultAzureCredential as the general recommendation for most scenarios. In production on Azure, Microsoft recommends managed identity; in local or external automation, DefaultAzureCredential can resolve an environment-based service principal or another supported credential in the chain. citeturn34search5turn34search4turn7search1turn31search5
import os
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
vault_name = os.environ["KEY_VAULT_NAME"]
vault_url = f"https://{vault_name}.vault.azure.net"
credential = DefaultAzureCredential(
exclude_interactive_browser_credential=True
)
client = SecretClient(vault_url=vault_url, credential=credential)
try:
secret = client.get_secret("db-password")
print("Secret version:", secret.properties.version)
except ClientAuthenticationError as exc:
raise RuntimeError(f"Authentication failed: {exc}") from exc
except HttpResponseError as exc:
if exc.status_code == 403:
raise RuntimeError("Authenticated, but not authorized. Check Key Vault RBAC.") from exc
raiseimport os
from azure.identity import DefaultAzureCredential
from azure.mgmt.resource import ResourceManagementClient
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
credential = DefaultAzureCredential(exclude_interactive_browser_credential=True)
client = ResourceManagementClient(credential=credential, subscription_id=subscription_id)
try:
for rg in client.resource_groups.list():
print(rg.name, rg.location)
except ClientAuthenticationError as exc:
raise RuntimeError(f"Authentication failed: {exc}") from exc
except HttpResponseError as exc:
if exc.status_code == 403:
raise RuntimeError("Authenticated, but missing Reader or higher at this scope.") from exc
raiseMicrosoft documents two strong certificate paths for Python automation: Azure Identity’s CertificateCredential and MSAL confidential client with a PFX or PEM-backed private key. Azure Identity requires an RSA private key for CertificateCredential. MSAL Python supports PEM and, in newer versions, PFX input and SHA-256 thumbprints. citeturn5search0turn33view2
import os
from azure.identity import CertificateCredential
from azure.keyvault.secrets import SecretClient
tenant_id = os.environ["AZURE_TENANT_ID"]
client_id = os.environ["AZURE_CLIENT_ID"]
certificate_path = os.environ["AZURE_CLIENT_CERTIFICATE_PATH"] # PEM or compatible path
credential = CertificateCredential(
tenant_id=tenant_id,
client_id=client_id,
certificate_path=certificate_path,
)
client = SecretClient(
vault_url=f"https://{os.environ['KEY_VAULT_NAME']}.vault.azure.net",
credential=credential,
)
print(client.get_secret("db-password").value[:4] + "...")import msal
import os
tenant_id = os.environ["AZURE_TENANT_ID"]
client_id = os.environ["AZURE_CLIENT_ID"]
authority = f"https://login.microsoftonline.com/{tenant_id}"
app = msal.ConfidentialClientApplication(
client_id=client_id,
authority=authority,
client_credential={
"private_key_pfx_path": os.environ["AZURE_CLIENT_CERT_PFX_PATH"],
"passphrase": os.environ.get("AZURE_CLIENT_CERT_PFX_PASSPHRASE"),
# Set to True when you want x5c / subject-name-issuer behavior
"public_certificate": True,
},
)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" not in result:
raise RuntimeError(result)
print("Token acquired for Graph.")The clean GitHub pattern is: configure a federated identity credential on the Entra application, let azure/login@v2 perform the OIDC-based sign-in, and then let Python use the resulting authenticated Azure CLI context via DefaultAzureCredential or AzureCliCredential. Microsoft’s GitHub OIDC guidance requires id-token: write, the Entra app’s client ID, tenant ID, and subscription ID, and a federated credential that trusts the repository/branch/environment subject. citeturn30view0turn35view2turn13search4
CLI creation of the federated credential
cat > fic.json <<'JSON'
{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:ORG/REPO:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"],
"description": "GitHub Actions main branch OIDC trust"
}
JSON
az ad app federated-credential create \
--id <APPLICATION_OBJECT_ID> \
--parameters @fic.jsonAzure CLI exposes az ad app federated-credential create, while PowerShell alternatives include New-AzADAppFederatedCredential and Graph PowerShell New-MgApplicationFederatedIdentityCredential. citeturn29search2turn29search0turn29search9
GitHub Actions workflow
name: python-azure-automation
on:
push:
branches: [main]
jobs:
run:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: pip install azure-identity azure-keyvault-secrets
- run: python automation.py
env:
KEY_VAULT_NAME: my-kvPython inside the GitHub runner
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
import os
credential = DefaultAzureCredential(
exclude_interactive_browser_credential=True
)
client = SecretClient(
vault_url=f"https://{os.environ['KEY_VAULT_NAME']}.vault.azure.net",
credential=credential,
)
print(client.get_secret("db-password").value)In this pattern, the federation happens in azure/login, and DefaultAzureCredential consumes the already-authenticated runner context, typically via Azure CLI in the credential chain. That is the most operationally stable and officially documented GitHub Actions path. citeturn30view0turn32search2turn31search5
A secure implementation is not finished when the first token works. Microsoft’s identity, workload identity, and monitoring docs point to an operational baseline that is easy to audit and defend. citeturn35view0turn35view3turn6search3turn36view0
Use this checklist:
- Prefer managed identities whenever the Python workload runs on Azure. Microsoft describes them as the most secure option with little to no ongoing credential maintenance. citeturn35view0turn35view3
- If the workload runs outside Azure, prefer federated credentials to eliminate stored secrets; GitHub Actions OIDC is a first-class Microsoft-documented example. citeturn35view0turn30view0turn35view2
- If federation is unavailable, use certificates, not client secrets, and keep private keys in Azure Key Vault or equivalent protected storage. citeturn35view1turn35view0
- Keep role scopes narrow. For Azure RBAC, assign at a resource or container scope whenever possible, because broader scopes inherit downward. citeturn2search10turn19search0
- For Storage, prefer data-plane roles like Storage Blob Data Reader/Contributor and avoid Shared Key. Microsoft explicitly warns that Shared Key is less secure and recommends Entra ID or user-delegation SAS instead. citeturn19search0turn20view0turn17view2
- For Graph, choose the least powerful application permission that satisfies the use case. Microsoft’s Graph best-practices guidance specifically calls out least-privilege permission selection, and
Application.ReadWrite.OwnedByis purposely narrower thanApplication.ReadWrite.All. citeturn24search12turn22view4 - Review application owners regularly. Microsoft recommends regular ownership review because owners can manage all aspects of the application. citeturn35view0
- Use Conditional Access for workload identities where licensing and scenario fit permit it. Microsoft documents CA for workload identities and shows evaluation details in Sign-in logs → Service principal sign-ins. citeturn6search0turn6search10
- Monitor service principal sign-in logs and, where available, risky workload identity signals. Microsoft Entra has dedicated service principal sign-in logs and workload identity risk telemetry. citeturn6search3turn6search7turn6search19
- Rotate credentials before expiry and validate the new credential in sign-in logs before removing the old one. Microsoft’s Entra recommendation flow for expiring credentials explicitly calls for add-new, validate, then remove-old. citeturn36view0
- For code, never hard-code secrets or certificates, never commit them to source control, and monitor build pipelines for accidental credential exposure. Microsoft specifically calls out scanning production pipelines and repositories for credentials. citeturn35view0
| Target | .default scope |
Python SDK | REST example | Notes | Sources |
|---|---|---|---|---|---|
| Azure Resource Manager | https://management.azure.com/.default |
azure.mgmt.* clients such as ResourceManagementClient, ComputeManagementClient |
ARM endpoints under https://management.azure.com/... |
Standard audience for Azure management plane | citeturn1search14turn17view4turn34search4 |
| Microsoft Graph | https://graph.microsoft.com/.default |
Microsoft Graph SDK or requests |
https://graph.microsoft.com/v1.0/... |
App-only requires application permissions and admin consent | citeturn8search8turn17view5turn11search5 |
| Key Vault | https://vault.azure.net/.default |
SecretClient from azure-keyvault-secrets |
https://{vault}.vault.azure.net/secrets/... |
RBAC roles differ for read versus write | citeturn18view0turn18view1turn34search5 |
| Blob Storage | https://storage.azure.com/.default |
BlobClient, BlobServiceClient |
https://{account}.blob.core.windows.net/... |
Use blob data roles, not just management roles | citeturn20view0turn17view2turn19search0 |
This report assumes no tenant-specific or subscription-specific details, so all scope examples use placeholders. Exact role-assignment scopes, whether you need a resource-group scope versus a resource scope, and whether Graph app permissions must be combined with workload-specific RBAC depend on the concrete automation scenario and service topology. Microsoft also has cloud-specific differences for national and sovereign clouds, especially around token audiences and environment selection, which were not expanded here beyond the official GitHub OIDC note about non-public cloud audiences. citeturn30view0turn15search6
A second limitation is that VM management can require adjacent permissions outside the Compute provider if the automation also creates or modifies dependent NICs, disks, public IPs, extensions, or role assignments. In practice, “Virtual Machine Contributor” is often enough for VM-only operations but may still be too broad or insufficient depending on the full workflow; for strict least-privilege implementations, a custom role is often the cleanest end state after validating the exact ARM operations used by the Python code. Azure RBAC supports custom roles when built-in roles are not a precise fit. citeturn14search15turn2search12