Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save shaposhnikoff/42160e0df7a6d498fd37f31b180e869d to your computer and use it in GitHub Desktop.

Select an option

Save shaposhnikoff/42160e0df7a6d498fd37f31b180e869d to your computer and use it in GitHub Desktop.
Secure Azure App Registration and Service Principal Patterns for Python Automation

Secure Azure App Registration and Service Principal Patterns for Python Automation

Executive summary

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. citeturn27search1turn27search0turn3search4

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. citeturn35view0turn35view1turn35view3

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. citeturn14search5turn2search10turn11search5turn15search6

Identity model and end-to-end workflow

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. citeturn27search0turn27search1turn3search4turn35view1

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]
Loading

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. citeturn15search0turn27search1turn28search1turn35view1turn14search5turn8search0turn8search8

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 citeturn15search0turn15search7turn12search0
client_id Copy stable app identifier App Overview az ad app show --id <appId-or-objectId> GET /applications/{id} citeturn28search13turn3search2
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 citeturn35view1turn12search2turn12search3turn12search13turn29search2
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 citeturn27search0turn28search1turn3search2turn3search1turn26search0
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 citeturn2search10turn2search2turn16search0turn16search1turn14search5
Python obtains token Confidential client flow or Azure Identity N/A MSAL / Azure Identity POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token citeturn8search0turn33view0
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 citeturn34search5turn34search4turn17view4turn17view0turn17view5turn17view2
Azure enforces authorization Resource evaluates token + permissions Resource-specific N/A Resource service / Graph / ARM citeturn19search0turn20view0turn32search5turn11search8

The step-by-step workflow in practical form

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. citeturn15search0turn15search7turn12search0turn4search11

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. citeturn28search13turn3search2turn27search0

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. citeturn35view1turn35view0turn12search2turn12search3turn12search13turn29search2

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. citeturn27search1turn28search1turn3search2turn3search1turn3search3turn26search0

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. citeturn2search10turn17view3turn16search0turn16search1turn16search8

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. citeturn8search0turn33view0turn1search14turn8search8turn18view0turn20view0turn8search3

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}. citeturn34search5turn34search4turn17view4turn17view5turn17view0turn17view2turn8search1

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. citeturn19search0turn20view0turn32search5

Credential choices and lifecycle

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. citeturn35view0turn35view1turn35view3

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 citeturn35view1turn15search18turn35view0
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 citeturn35view1turn35view0turn12search3
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 citeturn35view2turn35view0turn12search13
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 citeturn35view3turn35view0turn7search6

Practical recommendations by environment

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. citeturn35view3turn7search6turn7search9

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. citeturn35view0turn30view0turn13search4

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. citeturn35view1turn35view0

Finding or creating service principals and assigning least privilege

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. citeturn27search0turn3search4turn35view1turn14search5

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"
Loading

How to find or create the service principal

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. citeturn28search1turn27search0

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. citeturn3search3turn3search5turn3search8

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 $appId

Az.Resources documents Get-AzADServicePrincipal -ApplicationId and New-AzADServicePrincipal -ApplicationId. citeturn26search0turn26search6

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. citeturn3search2turn3search1

Least-privilege assignment patterns for common scenarios

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. citeturn19search0turn2search10turn11search5

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 citeturn2search3turn18view1turn34search5
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 citeturn2search3turn18view0turn34search2
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 citeturn2search0turn1search14turn17view4
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> citeturn2search1turn20view0turn10search15
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> citeturn2search1turn20view0turn17view2
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 citeturn22view0turn11search5turn17view5
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 citeturn22view4turn24search1
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 citeturn23search0turn23search11
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 citeturn23search0turn23search11

Important least-privilege cautions

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. citeturn19search0turn20view0

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. citeturn11search5turn24search1turn22view4

Python implementation patterns

MSAL client-credential flow with token caching and REST calls

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. citeturn33view0turn33view1

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 usage with Azure Identity

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. citeturn34search5turn34search4turn7search1turn31search5

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
    raise
import 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
    raise

Certificate-based authentication

Microsoft 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. citeturn5search0turn33view2

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.")

Federated identity with GitHub Actions OIDC

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. citeturn30view0turn35view2turn13search4

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.json

Azure CLI exposes az ad app federated-credential create, while PowerShell alternatives include New-AzADAppFederatedCredential and Graph PowerShell New-MgApplicationFederatedIdentityCredential. citeturn29search2turn29search0turn29search9

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-kv

Python 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. citeturn30view0turn32search2turn31search5

Operations and security checklist

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. citeturn35view0turn35view3turn6search3turn36view0

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. citeturn35view0turn35view3
  • If the workload runs outside Azure, prefer federated credentials to eliminate stored secrets; GitHub Actions OIDC is a first-class Microsoft-documented example. citeturn35view0turn30view0turn35view2
  • If federation is unavailable, use certificates, not client secrets, and keep private keys in Azure Key Vault or equivalent protected storage. citeturn35view1turn35view0
  • Keep role scopes narrow. For Azure RBAC, assign at a resource or container scope whenever possible, because broader scopes inherit downward. citeturn2search10turn19search0
  • 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. citeturn19search0turn20view0turn17view2
  • 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.OwnedBy is purposely narrower than Application.ReadWrite.All. citeturn24search12turn22view4
  • Review application owners regularly. Microsoft recommends regular ownership review because owners can manage all aspects of the application. citeturn35view0
  • 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. citeturn6search0turn6search10
  • 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. citeturn6search3turn6search7turn6search19
  • 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. citeturn36view0
  • 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. citeturn35view0

Useful endpoint and SDK reference map

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 citeturn1search14turn17view4turn34search4
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 citeturn8search8turn17view5turn11search5
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 citeturn18view0turn18view1turn34search5
Blob Storage https://storage.azure.com/.default BlobClient, BlobServiceClient https://{account}.blob.core.windows.net/... Use blob data roles, not just management roles citeturn20view0turn17view2turn19search0

Open questions and limitations

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. citeturn30view0turn15search6

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. citeturn14search15turn2search12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment