-
Notifications
You must be signed in to change notification settings - Fork 221
feat: Replace DefaultAzureCredential with ManagedIdentityCredential for production #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
72a5710
b68f0ce
2f041f5
12e3cf7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,197 @@ | ||||||
| --- | ||||||
| name: credential-free-dev | ||||||
| description: | | ||||||
| Eliminate secrets and credentials from Azure applications using managed identities, | ||||||
| workload identity federation, and the Azure Identity SDK. Covers migration patterns | ||||||
| from connection strings and keys to credential-free authentication across Azure services. | ||||||
| Triggers: "credential-free", "managed identity", "workload identity federation", | ||||||
| "remove secrets", "connection string to managed identity", "DefaultAzureCredential", | ||||||
| "ManagedIdentityCredential", "passwordless", "keyless authentication", "eliminate keys". | ||||||
| --- | ||||||
|
|
||||||
| # Azure Credential-Free Development | ||||||
|
|
||||||
| Eliminate secrets and credentials from Azure applications using managed identities, workload identity federation, and the Azure Identity SDK. | ||||||
|
|
||||||
| ## Before Implementation | ||||||
|
|
||||||
| Search `microsoft-docs` MCP for current patterns: | ||||||
| - Query: "credential-free development Azure managed identity" | ||||||
| - Query: "ManagedIdentityCredential [target service] [language]" | ||||||
| - Verify: RBAC role names match current documentation | ||||||
|
|
||||||
| ## Core Principles | ||||||
|
|
||||||
| 1. **No secrets in code, config, or environment variables.** Use managed identities for Azure-hosted workloads. Use workload identity federation for non-Azure or CI/CD workloads. | ||||||
| 2. **ManagedIdentityCredential for production. DefaultAzureCredential for local dev only.** In production, use `ManagedIdentityCredential` explicitly — `DefaultAzureCredential`'s credential chain probing can cause subtle failures, latency, and silent fallback to unintended credentials. `DefaultAzureCredential` remains convenient for local development (falls through to Azure CLI / VS credentials). | ||||||
| 3. **System-assigned managed identity** for single-purpose resources. **User-assigned managed identity** when multiple resources share a credential or when you need pre-provisioned RBAC. | ||||||
| 4. **Connection strings with keys are legacy.** Every Azure service that supports Entra auth should use it. If a service requires a key, treat it as a gap to escalate, not a pattern to accept. | ||||||
| 5. **Least privilege always.** Grant the narrowest RBAC role that works. `Storage Blob Data Reader`, not `Storage Account Key Operator`. `db_datareader`, not `db_owner`. | ||||||
|
|
||||||
| ## When to Use Each Credential Type | ||||||
|
|
||||||
| | Scenario | Use This | Why | | ||||||
| |---|---|---| | ||||||
| | App on Azure (App Service, Container Apps, Functions, VMs, AKS) | **ManagedIdentityCredential** (system or user-assigned) | Zero secrets, platform-managed rotation, Entra RBAC. Use explicit credential, not DefaultAzureCredential. | | ||||||
| | CI/CD pipeline (GitHub Actions, Azure DevOps) | **Workload Identity Federation** | No secrets stored. OIDC token exchange with Entra. | | ||||||
| | App on AWS/GCP calling Azure | **Workload Identity Federation** | Cross-cloud trust without shared secrets | | ||||||
| | Local development | **DefaultAzureCredential** | Developer's own identity via CLI/VS fallback, no shared dev secrets | | ||||||
| | Service-to-service (no Azure hosting) | **App registration + certificate** | When MI/WIF aren't possible. Certificates over secrets. | | ||||||
| | Legacy app that can't change auth code | **Key Vault references** (stepping stone) | Secrets in Key Vault, not in config. Not the end state. | | ||||||
|
|
||||||
| ## Production Authentication: ManagedIdentityCredential | ||||||
|
|
||||||
| > **Do not use `DefaultAzureCredential` in production.** Its credential chain probing | ||||||
| > can cause subtle issues or silent failures. Replace it with a specific | ||||||
| > `TokenCredential` implementation such as `ManagedIdentityCredential`. | ||||||
| > — [Authentication best practices with the Azure Identity library](https://learn.microsoft.com/dotnet/azure/sdk/authentication/best-practices) | ||||||
|
|
||||||
| Use `ManagedIdentityCredential` directly for Azure-hosted workloads. For user-assigned | ||||||
| managed identities, pass the `client_id` (or object ID / resource ID in constrained | ||||||
| environments where client ID isn't available) explicitly. | ||||||
|
|
||||||
| ## DefaultAzureCredential (Local Development Only) | ||||||
|
|
||||||
| `DefaultAzureCredential` is convenient for local development because it automatically | ||||||
| falls through to developer tool credentials. **Do not use in production.** | ||||||
|
|
||||||
| The credential chain order and included credentials vary by language. See the authoritative per-language references: | ||||||
|
|
||||||
| - [.NET](https://aka.ms/azsdk/net/identity/credential-chains#defaultazurecredential-overview) | ||||||
| - [C++](https://aka.ms/azsdk/cpp/identity/credential-chains#defaultazurecredential-overview) | ||||||
| - [Go](https://aka.ms/azsdk/go/identity/credential-chains#defaultazurecredential-overview) | ||||||
| - [Java](https://aka.ms/azsdk/java/identity/credential-chains#defaultazurecredential-overview) | ||||||
| - [JavaScript](https://aka.ms/azsdk/js/identity/credential-chains#defaultazurecredential-overview) | ||||||
| - [Python](https://aka.ms/azsdk/python/identity/credential-chains#defaultazurecredential-overview) | ||||||
|
|
||||||
| In production this chain introduces latency and unpredictable fallback — use `ManagedIdentityCredential` instead. | ||||||
|
|
||||||
| ### Optimize for Local Dev with `AZURE_TOKEN_CREDENTIALS` | ||||||
|
|
||||||
| Set `AZURE_TOKEN_CREDENTIALS=dev` to disable production-grade credentials (MI, WIF, Environment) | ||||||
| and only keep developer tool credentials in the chain. This prevents accidental use of | ||||||
| deployed-service credentials during local development. | ||||||
|
|
||||||
| > **Minimum SDK versions:** Python `azure-identity` 1.23.0+, .NET `Azure.Identity` 1.15.0+, | ||||||
| > Java `azure-identity` 1.16.1+, JavaScript `@azure/identity` 4.11.0+, Go `azidentity` 1.10.0+, | ||||||
| > C++ `azure-identity-cpp` 1.13.1+. | ||||||
| > | ||||||
| > See [Exclude a credential type category](https://learn.microsoft.com/azure/developer/python/sdk/authentication/credential-chains?tabs=dac#exclude-a-credential-type-category) for details. | ||||||
|
|
||||||
| ### SDK Packages | ||||||
|
|
||||||
| | Language | Package | Install | | ||||||
| |----------|---------|---------| | ||||||
| | Python | `azure-identity` | `pip install azure-identity` | | ||||||
| | .NET | `Azure.Identity` | `dotnet add package Azure.Identity` | | ||||||
| | Java | `azure-identity` | Maven: `com.azure:azure-identity` | | ||||||
| | TypeScript | `@azure/identity` | `npm install @azure/identity` | | ||||||
| | Go | `azidentity` | `go get github.com/Azure/azure-sdk-for-go/sdk/azidentity` | | ||||||
| | C++ | `azure-identity-cpp` | `vcpkg add port azure-identity-cpp` | | ||||||
|
|
||||||
| ### Production Pattern (All Languages) | ||||||
|
|
||||||
| ```python | ||||||
| # Python — production | ||||||
| from azure.identity import ManagedIdentityCredential | ||||||
| credential = ManagedIdentityCredential() # system-assigned | ||||||
| # credential = ManagedIdentityCredential(client_id="<user-assigned-mi-client-id>") # user-assigned | ||||||
| client = ServiceClient(endpoint, credential=credential) | ||||||
| ``` | ||||||
|
|
||||||
| ```csharp | ||||||
| // C# — production | ||||||
| var credential = new ManagedIdentityCredential(); // system-assigned | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This parameterless ctor is obsolete in newer versions of the library. Recommend the following pattern instead:
Suggested change
|
||||||
| // var credential = new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedClientId("<user-assigned-mi-client-id>")); // user-assigned | ||||||
| var client = new ServiceClient(new Uri(endpoint), credential); | ||||||
| ``` | ||||||
|
|
||||||
| ```java | ||||||
| // Java — production | ||||||
| ManagedIdentityCredential credential = new ManagedIdentityCredentialBuilder().build(); // system-assigned | ||||||
| // ManagedIdentityCredential credential = new ManagedIdentityCredentialBuilder() | ||||||
| // .clientId("<user-assigned-mi-client-id>").build(); // user-assigned | ||||||
| ServiceClient client = new ServiceClientBuilder() | ||||||
| .endpoint(endpoint) | ||||||
| .credential(credential) | ||||||
| .buildClient(); | ||||||
| ``` | ||||||
|
|
||||||
| ```typescript | ||||||
| // TypeScript — production | ||||||
| import { ManagedIdentityCredential } from "@azure/identity"; | ||||||
| const credential = new ManagedIdentityCredential(); // system-assigned | ||||||
| // const credential = new ManagedIdentityCredential({ clientId: "<user-assigned-mi-client-id>" }); // user-assigned | ||||||
| const client = new ServiceClient(endpoint, credential); | ||||||
| ``` | ||||||
|
|
||||||
| ### Local Development Pattern | ||||||
|
|
||||||
| ```python | ||||||
| # Python — local dev only | ||||||
| from azure.identity import DefaultAzureCredential | ||||||
| credential = DefaultAzureCredential() | ||||||
ArLucaID marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| client = ServiceClient(endpoint, credential=credential) | ||||||
| ``` | ||||||
|
|
||||||
| ## Migration Pattern: Keys to Credential-Free | ||||||
|
|
||||||
| Every migration follows the same 4 steps: | ||||||
|
|
||||||
| 1. **Enable managed identity** on your compute resource | ||||||
| 2. **Assign the right RBAC role** on the target resource | ||||||
| 3. **Replace** connection string / key with endpoint URL + `ManagedIdentityCredential` (production) or `DefaultAzureCredential` (local dev) | ||||||
| 4. **Remove** the key/secret from all config | ||||||
|
|
||||||
| ### Quick Reference: Service RBAC Roles | ||||||
|
|
||||||
| | Service | Read Role | Write Role | | ||||||
| |---------|-----------|------------| | ||||||
| | Azure Storage (Blob) | `Storage Blob Data Reader` | `Storage Blob Data Contributor` | | ||||||
| | Azure SQL Database | `db_datareader` (SQL role) | `db_datawriter` (SQL role) | | ||||||
| | Azure Cosmos DB | `Cosmos DB Built-in Data Reader` | `Cosmos DB Built-in Data Contributor` | | ||||||
| | Azure Service Bus | `Azure Service Bus Data Receiver` | `Azure Service Bus Data Sender` | | ||||||
| | Azure Event Hubs | `Azure Event Hubs Data Receiver` | `Azure Event Hubs Data Sender` | | ||||||
| | Azure Key Vault | `Key Vault Secrets User` | `Key Vault Secrets Officer` | | ||||||
| | Azure App Configuration | `App Configuration Data Reader` | `App Configuration Data Owner` | | ||||||
|
|
||||||
| > **Detailed migration code for each service:** See [references/migration-patterns.md](references/migration-patterns.md) | ||||||
|
|
||||||
| ## Workload Identity Federation (CI/CD and Cross-Cloud) | ||||||
|
|
||||||
| When managed identity isn't available (GitHub Actions, external clouds, on-prem): | ||||||
|
|
||||||
| 1. Create an app registration in Entra | ||||||
| 2. Add a federated identity credential (OIDC issuer + subject) | ||||||
| 3. External workload exchanges its native token for an Entra access token | ||||||
| 4. No secrets stored, rotated, or transmitted | ||||||
|
|
||||||
| ### GitHub Actions Example | ||||||
|
|
||||||
| ```yaml | ||||||
| - uses: azure/login@v2 | ||||||
| with: | ||||||
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | ||||||
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | ||||||
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | ||||||
| ``` | ||||||
|
|
||||||
| No `AZURE_CLIENT_SECRET` needed. The GitHub OIDC token is exchanged directly. | ||||||
|
|
||||||
| ## Common Pitfalls | ||||||
|
|
||||||
| 1. **Forgetting RBAC roles.** Enabling MI is step 1. Assigning the right role on the target resource is step 2. Most "MI doesn't work" issues are missing role assignments. | ||||||
| 2. **Overly broad roles.** `Contributor` on a resource group when you need `Storage Blob Data Reader` on one account. | ||||||
| 3. **Not testing locally.** `DefaultAzureCredential` falls through to Azure CLI. Make sure `az login` is used with the right subscription. | ||||||
| 4. **Using `DefaultAzureCredential` in production.** Its credential chain probing adds latency, can fall back to unintended credentials, and masks configuration errors. Use `ManagedIdentityCredential` explicitly for Azure-hosted production apps. | ||||||
| 5. **Mixing key and MI auth.** Some SDKs behave differently when both a connection string and a credential are provided. Pick one. | ||||||
| 6. **Assuming all services support MI.** Most do. Some legacy or partner services don't yet. Check the [services that support managed identities](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/managed-identities-status) list. | ||||||
| 7. **Not handling token refresh.** `ManagedIdentityCredential` handles refresh automatically. If caching tokens manually, respect expiry. | ||||||
| 8. **System-assigned when user-assigned is better.** Multiple resources needing same permissions = user-assigned MI to avoid duplicating RBAC assignments. | ||||||
|
|
||||||
| ## Reference Files | ||||||
|
|
||||||
| | File | Contents | | ||||||
| |------|----------| | ||||||
| | [references/migration-patterns.md](references/migration-patterns.md) | Detailed before/after code for SQL, Storage, Cosmos DB, Service Bus, Event Hubs, Key Vault | | ||||||
ArLucaID marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| | [references/acceptance-criteria.md](references/acceptance-criteria.md) | Correct/incorrect patterns for skill evaluation | | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,197 @@ | ||||||
| # Acceptance Criteria: credential-free-dev | ||||||
|
|
||||||
| **Packages**: `azure-identity` (Python), `Azure.Identity` (.NET), `@azure/identity` (TypeScript), `com.azure:azure-identity` (Java) | ||||||
| **Purpose**: Skill testing acceptance criteria for credential-free development patterns | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## 1. Authentication Patterns | ||||||
|
|
||||||
| ### 1.1 Production Credential Selection | ||||||
|
|
||||||
| #### ✅ CORRECT: ManagedIdentityCredential for production | ||||||
| ```python | ||||||
| from azure.identity import ManagedIdentityCredential | ||||||
| from azure.storage.blob import BlobServiceClient | ||||||
|
|
||||||
| credential = ManagedIdentityCredential() | ||||||
| client = BlobServiceClient( | ||||||
| account_url="https://mystorageaccount.blob.core.windows.net", | ||||||
| credential=credential | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ✅ CORRECT: DefaultAzureCredential for local development | ||||||
| ```python | ||||||
| from azure.identity import DefaultAzureCredential | ||||||
| from azure.storage.blob import BlobServiceClient | ||||||
|
|
||||||
| credential = DefaultAzureCredential() | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this local dev scenario, we recommend setting env var
Suggested change
|
||||||
| client = BlobServiceClient( | ||||||
| account_url="https://mystorageaccount.blob.core.windows.net", | ||||||
| credential=credential | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: DefaultAzureCredential in production | ||||||
| ```python | ||||||
| # WRONG — DefaultAzureCredential's credential chain probing causes subtle failures, | ||||||
| # latency, and silent fallback to unintended credentials in production. | ||||||
| # Use ManagedIdentityCredential explicitly. | ||||||
| from azure.identity import DefaultAzureCredential | ||||||
| credential = DefaultAzureCredential() # Don't use in production | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Hardcoded key in code | ||||||
| ```python | ||||||
| from azure.storage.blob import BlobServiceClient | ||||||
| client = BlobServiceClient.from_connection_string( | ||||||
| "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=abc123..." | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Hardcoded client secret | ||||||
| ```python | ||||||
| from azure.identity import ClientSecretCredential | ||||||
| credential = ClientSecretCredential( | ||||||
| tenant_id="...", | ||||||
| client_id="...", | ||||||
| client_secret="hardcoded-secret-value" # Never hardcode | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| ### 1.2 Environment Variables for Credentials | ||||||
|
|
||||||
| #### ✅ CORRECT: Read from environment | ||||||
| ```python | ||||||
| import os | ||||||
| from azure.identity import ClientSecretCredential | ||||||
|
|
||||||
| credential = ClientSecretCredential( | ||||||
| tenant_id=os.environ["AZURE_TENANT_ID"], | ||||||
| client_id=os.environ["AZURE_CLIENT_ID"], | ||||||
| client_secret=os.environ["AZURE_CLIENT_SECRET"], | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Inline secrets | ||||||
| ```python | ||||||
| credential = ClientSecretCredential( | ||||||
| tenant_id="12345-abcde", | ||||||
| client_id="67890-fghij", | ||||||
| client_secret="super-secret-value", | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## 2. Service Client Initialization | ||||||
|
|
||||||
| ### 2.1 Storage Blob | ||||||
|
|
||||||
| #### ✅ CORRECT: Endpoint URL + ManagedIdentityCredential | ||||||
| ```python | ||||||
| client = BlobServiceClient( | ||||||
| account_url="https://myaccount.blob.core.windows.net", | ||||||
| credential=ManagedIdentityCredential() | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Connection string with key | ||||||
| ```python | ||||||
| client = BlobServiceClient.from_connection_string("...AccountKey=...") | ||||||
| ``` | ||||||
|
|
||||||
| ### 2.2 Service Bus | ||||||
|
|
||||||
| #### ✅ CORRECT: Namespace + ManagedIdentityCredential | ||||||
| ```python | ||||||
| client = ServiceBusClient( | ||||||
| fully_qualified_namespace="my-namespace.servicebus.windows.net", | ||||||
| credential=ManagedIdentityCredential() | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Connection string with SAS | ||||||
| ```python | ||||||
| client = ServiceBusClient.from_connection_string("...SharedAccessKey=...") | ||||||
| ``` | ||||||
|
|
||||||
| ### 2.3 Cosmos DB | ||||||
|
|
||||||
| #### ✅ CORRECT: URL + ManagedIdentityCredential | ||||||
| ```python | ||||||
| client = CosmosClient( | ||||||
| "https://myaccount.documents.azure.com:443/", | ||||||
| credential=ManagedIdentityCredential() | ||||||
| ) | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: URL + primary key | ||||||
| ```python | ||||||
| client = CosmosClient("https://myaccount.documents.azure.com:443/", "primary-key-here") | ||||||
| ``` | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## 3. RBAC Role Guidance | ||||||
|
|
||||||
| ### 3.1 Least Privilege | ||||||
|
|
||||||
| #### ✅ CORRECT: Narrow role scoped to resource | ||||||
| ``` | ||||||
| Assign "Storage Blob Data Reader" on the specific storage account | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Overly broad role | ||||||
| ``` | ||||||
| Assign "Contributor" on the resource group | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Key operator instead of data role | ||||||
| ``` | ||||||
| Assign "Storage Account Key Operator Service Role" — this grants key access, not data access | ||||||
| ``` | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## 4. Managed Identity Selection | ||||||
|
|
||||||
| ### 4.1 System vs User-Assigned | ||||||
|
|
||||||
| #### ✅ CORRECT: System-assigned for single-purpose | ||||||
| ``` | ||||||
| Single App Service accessing one storage account → system-assigned MI | ||||||
| ``` | ||||||
|
|
||||||
| #### ✅ CORRECT: User-assigned for shared permissions | ||||||
| ``` | ||||||
| Three Container Apps needing same Cosmos DB access → one user-assigned MI, one RBAC assignment | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: System-assigned with duplicated RBAC | ||||||
| ``` | ||||||
| Three Container Apps each with system-assigned MI, each needing identical RBAC → 3x role assignments to maintain | ||||||
| ``` | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## 5. Workload Identity Federation | ||||||
|
|
||||||
| ### 5.1 GitHub Actions | ||||||
|
|
||||||
| #### ✅ CORRECT: OIDC login without secret | ||||||
| ```yaml | ||||||
| - uses: azure/login@v2 | ||||||
| with: | ||||||
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | ||||||
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | ||||||
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | ||||||
| ``` | ||||||
|
|
||||||
| #### ❌ INCORRECT: Client secret in GitHub Actions | ||||||
| ```yaml | ||||||
| - uses: azure/login@v2 | ||||||
| with: | ||||||
| creds: ${{ secrets.AZURE_CREDENTIALS }} # Contains client_secret — use WIF instead | ||||||
| ``` | ||||||
Uh oh!
There was an error while loading. Please reload this page.