Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions .github/skills/credential-free-dev/SKILL.md
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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(); // system-assigned
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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
credential = 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 |
| [references/acceptance-criteria.md](references/acceptance-criteria.md) | Correct/incorrect patterns for skill evaluation |
197 changes: 197 additions & 0 deletions .github/skills/credential-free-dev/references/acceptance-criteria.md
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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this local dev scenario, we recommend setting env var AZURE_TOKEN_CREDENTIALS to dev. The DefaultAzureCredential code should then look as follows:

Suggested change
credential = DefaultAzureCredential()
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
```
Loading