-
Notifications
You must be signed in to change notification settings - Fork 290
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
Open
ArLucaID
wants to merge
5
commits into
microsoft:main
Choose a base branch
from
ArLucaID:skill/credential-free-dev-mic
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
72a5710
feat: Add runtime token exchange, OBO, cross-tenant, and permission g…
ArLucaID b68f0ce
Add credential-free-dev skill
ArLucaID 2f041f5
feat: Replace DefaultAzureCredential with ManagedIdentityCredential f…
ArLucaID 12e3cf7
Address Scott Addie review feedback on credential-free-dev skill
ArLucaID 9d4b5c3
fix: apply scottaddie review suggestions for credential patterns
spboyer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(ManagedIdentityId.SystemAssigned); // system-assigned | ||
| // 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(require_envvar=True) | ||
| 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.
|
||
| | [references/acceptance-criteria.md](references/acceptance-criteria.md) | Correct/incorrect patterns for skill evaluation | | ||
197 changes: 197 additions & 0 deletions
197
.github/skills/credential-free-dev/references/acceptance-criteria.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(require_envvar=True) | ||
| 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 | ||
| ``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.