octocat from Chris

Passwordless Local Development: An SRE Guide to Azure Federated Credentials

by Chris on August 21, 2025

Local OIDC Flow


TL;DR

This blog post describes how to eliminate the need for local static credentials in development environments by adopting Azure Federated Credentials, mirroring the secure, passwordless authentication methods used in cloud-hosted Kubernetes clusters. It details a process for local machines and Minikube to authenticate with Azure AD using short-lived tokens and OpenID Connect (OIDC), effectively achieving dev-prod parity in credential management.

Here's what's going to happen:

  1. A JSON Web Token (JWT) is generated locally.

  2. This JWT is signed by a private key stored on the local machine.

  3. The JWT contains specific claims.

  4. The signed JWT is sent to Entra ID.

  5. Entra ID has a configured trust with a public OIDC endpoint.

  6. Entra ID retrieves the public key from this OIDC endpoint.

  7. Entra ID verifies the JWT's signature using the public key.

  8. Entra ID validates the JWT's claims.

  9. Upon successful validation, Entra ID issues an Entra ID access token.



Passwordless Local Development with Azure Federated Credentials

Working as SRE, you know the drill: client secrets, certs, and a never-ending cycle of password rotations for local development. It's a pain. Our goal is automation and standardization, so manual, error-prone credential management just doesn't cut it. That's why we're utilizing Azure Federated Credentials. Typically, you'd find these in sophisticated CI/CD pipelines. But we're bringing that same capability directly to our dev machines, even extending it to local Minikube clusters. This isn't just about bolstering security (though that's critical for M365 APIs); it's about eliminating credential sprawl and streamlining our daily workflows. No one wants another secret cluttering their .bashrc or, worse, checked into source control.
⚠️ One important thing before we start: this setup still relies on a local private key. That key can sign tokens Azure will trust — which means it is sensitive. Treat it like any other credential.



The Credential Headache: A Common Pain Point

We've all been there: setting up a new dev environment and immediately facing the question of which CLIENT_SECRET to use. Is it _PROD_VERY_SAFE_DONT_TOUCH or _DEV_SAFELY_IGNORE? The confusion alone is a liability. Passwords, and static secrets masquerading as them, are prone to being compromised, expiration, and general disruption to your day. Our aim is to achieve what large cloud providers offer: passwordless authentication, short-lived tokens, and cryptographic assurance that "yes, this is indeed my request, originating from a trusted source." Federated Credentials deliver precisely this.



Our Approach: Leveraging OIDC for a Cloud IdP

Consider this as establishing your own, entirely legitimate, cloud-based Identity Provider (IdP). Azure AD is remarkably flexible; it doesn't differentiate whether your OpenID Connect (OIDC) issuer is Google, GitHub, or even a custom blob storage bucket. The key requirements for Azure AD are:

  • Locating the .well-known/openid-configuration endpoint.
  • Retrieving the public keys necessary for authentication.
  • Verifying the JWT signature.
  • Confirming the claims (e.g., "I am the M365 Exporter in your development setup!").

By simply hosting a jwks.json file (containing our public key) in an blob storage bucket, we're effectively declaring to Azure, "Trust us! Our tokens are signed by this specific key!" It's a straightforward, secret-free method for establishing trust.



Step-by-Step Guide: Achieving Passwordless Authentication

For your convenience we have added our script snippets to this article. Please make sure you read them and the attendant instructions carefully before using them. You'll need your standard SRE toolkit for this: Azure CLI, jwt-cli (for JWT signing), jq (our JSON processing utility), openssl and curl.


1. Generating Your Keys: The Cryptographic Foundation

Start by generating your RSA key pair. Your private key should be safeguarded, while the public key will be exposed.


openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

2. Storage Account Deployment

Create required Storage Account and container:


RESOURCE_GROUP_NAME="demo-rg"
STORAGE_ACCOUNT_NAME="demostgrandom1312"
LOCATION="germanywestcentral"
CONTAINER_NAME="oidc"

# Create RG
az group create --name "${RESOURCE_GROUP_NAME}" --location "${LOCATION}"

# Create Stg. Account
az storage account create \
    --name "${STORAGE_ACCOUNT_NAME}" \
    --resource-group "${RESOURCE_GROUP_NAME}" \
    --location "${LOCATION}" \
    --sku Standard_LRS \
    --allow-blob-public-access \
    --kind StorageV2

ACCOUNT_KEY=$(az storage account keys list \
    --account-name "${STORAGE_ACCOUNT_NAME}" \
    --resource-group "${RESOURCE_GROUP_NAME}" \
    --query "[0].value" \
    --output tsv)

# --- Create Public Container ---
az storage container create \
    --name "${CONTAINER_NAME}" \
    --account-name "${STORAGE_ACCOUNT_NAME}" \
    --public-access container \
    --account-key "${ACCOUNT_KEY}"

# --- Get Public URL ---
BLOB_URL=$(az storage blob url \
    --container-name "${CONTAINER_NAME}" \
    --name "${FILE_TO_UPLOAD}" \
    --account-name "${STORAGE_ACCOUNT_NAME}" \
    --account-key "${ACCOUNT_KEY}" \
    --output tsv)

3. Publishing Your Identity: JWKS and OIDC Configuration

Convert your raw public key into JWKS format using jq. Then, create the .well-known/openid-configuration file for Azure AD. Upload both jwks.json and openid-configuration to the root of your storage bucket. Ensure they are publicly readable. As it's a public key, this is a secure practice.


# Extract modulus (n) and exponent (e) for JWKS
MODULUS_HEX=$(openssl rsa -in public_key.pem -pubin -noout -modulus | sed -n 's/^Modulus=//p')
MODULUS_B64URL=$(echo "$MODULUS_HEX" | xxd -r -p | base64 | tr -d '=' | tr '+/' '-_')
EXPONENT_B64URL="AQAB" # The standard RSA exponent
KEY_ID=$(openssl rsa -in public_key.pem -pubin -outform DER | openssl dgst -sha256 -binary | openssl enc -base64 | tr -d '=' | tr '+/' '-_')

# Construct jwks.json using jq
jq -n \
  --arg kty "RSA" --arg use "sig" --arg alg "RS256" \
  --arg kid "$KEY_ID" --arg n "$MODULUS_B64URL" --arg e "$EXPONENT_B64URL" \
'{ "keys": [ { "kty": $kty, "use": $use, "alg": $alg, "kid": $kid, "n": $n, "e": $e } ] }' > jwks.json
echo "jwks.json generated:" && cat jwks.json && echo

# Construct openid-configuration using jq
jq -n \
  --arg issuer "${BLOB_URL}" \
  --arg jwks_uri "${BLOB_URL}openid/v1/jwks" \
'{ "issuer": $issuer, "jwks_uri": $jwks_uri, "response_types_supported": ["id_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"] }' > openid-configuration
echo "openid-configuration generated:" && cat openid-configuration && echo

# --- Upload File ---
az storage blob upload \
    --container-name "${CONTAINER_NAME}" \
    --file "openid-configuration" \
    --name ".well-known/openid-configuration" \
    --account-name "${STORAGE_ACCOUNT_NAME}" \
    --account-key "${ACCOUNT_KEY}" \
    --content-type "application/json" \
    --overwrite true

az storage blob upload \
    --container-name "${CONTAINER_NAME}" \
    --file "jwks.json" \
    --name "openid/v1/jwks" \
    --account-name "${STORAGE_ACCOUNT_NAME}" \
    --account-key "${ACCOUNT_KEY}" \
    --content-type "application/json" \
    --overwrite true

4. Azure AD Configuration: Establishing Trust

Create a User-Assigned Managed Identity, then configure Azure AD to trust tokens issued by your storage bucket.


MANAGED_IDENTITY_NAME="id-local-oidc-test"
az identity create --name "${MANAGED_IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP_NAME}"

az identity federated-credential create \
  --name "LocalDevOIDC1" \
  --identity-name "${MANAGED_IDENTITY_NAME}" \
  --resource-group "${RESOURCE_GROUP_NAME}" \
  --issuer "${BLOB_URL}" \
  --subject "dev-user-cloudeteer-exporter" \
  --audiences "api://AzureADTokenExchange"

# will be used in Powershell in the next step
az identity show --name "${MANAGED_IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP_NAME}" --query principalId -o tsv

Grant your Managed Identity the necessary permissions (e.g., Microsoft Graph Reader for the M365 Exporter). This part needs to be done by an Entra ID Administrator. This is done in Powershell!


Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes Application.Read.All, AppRoleAssignment.ReadWrite.All, RoleManagement.ReadWrite.Directory

$MANAGED_IDENTITY_OBJECT_ID="Object ID of managed Identity from previous step"
### graph ###
$roleNames = "ThreatHunting.Read.All,DeviceManagementConfiguration.Read.All,DeviceManagementManagedDevices.Read.All,Directory.Read.All,Files.Read.All,Organization.Read.All,SecurityEvents.Read.All,ServiceHealth.Read.All,Sites.Read.All,TeamSettings.Read.All,User.Read.All"
$roleNames = $roleNames.split(",");
$msgraph = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"

foreach ($roleName in $roleNames) {
    $role = $Msgraph.AppRoles| Where-Object {$_.Value -eq $roleName}
    New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $MANAGED_IDENTITY_OBJECT_ID -PrincipalId $MANAGED_IDENTITY_OBJECT_ID -ResourceId $msgraph.Id -AppRoleId $role.Id
}

5. Local Machine Authentication: jwt-cli in Action

Use jwt-cli to sign a JWT, then use curl to exchange it for an Azure AD access token.


# Set environment variables for token exchange
export MANAGED_IDENTITY_CLIENT_ID=$(az identity show --name "${MANAGED_IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP_NAME}" --query clientId -o tsv)
export TENANT_ID=$(az account show --query tenantId -o tsv)
export CUSTOM_OIDC_ISSUER="$BLOB_URL" # Your storage bucket as issuer
export JWT_SUBJECT="dev-user-cloudeteer-exporter" # Must match Azure AD configuration
export JWT_AUDIENCE="api://AzureADTokenExchange" # Standard for Azure AD
export JWT_KID=$(openssl rsa -in public_key.pem -pubin -outform DER | openssl dgst -sha256 -binary | openssl enc -base64 | tr -d '=' | tr '+/' '-_') # Must match KID in jwks.json

# Generate a short-lived JWT payload (5 minutes is generally sufficient for development)
EXP_TIME=$(($(date +%s) + 300))
PAYLOAD_JSON=$(cat <<EOF
{  "iss": "$CUSTOM_OIDC_ISSUER",
  "sub": "$JWT_SUBJECT",
  "aud": "$JWT_AUDIENCE",
  "jti": "jwt-$(date +%s)",
  "exp": $EXP_TIME,
  "nbf": $(date +%s),
  "iat": $(date +%s)
}
EOF
)

# Generate the ID Token using jwt-cli
# Use --alg RS256, --kid for the header, and --secret @private_key.pem for the private key file
ID_TOKEN=$(jwt encode \
  --alg RS256 \
  --kid "$JWT_KID" \
  --secret "@private_key.pem" \
  "$PAYLOAD_JSON")
echo "Generated ID Token (JWT): $ID_TOKEN"

# Exchange JWT for Azure AD Access Token
AZURE_AD_TOKEN_ENDPOINT="https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token"
export TOKEN=$(curl -X POST "$AZURE_AD_TOKEN_ENDPOINT" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$MANAGED_IDENTITY_CLIENT_ID" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=$ID_TOKEN" | jq -r .access_token)

# Test Token with basic Graph API Request
curl -v 'https://graph.microsoft.com/v1.0/users' \
  -H 'accept: application/json;odata.metadata=minimal' \
  -H 'odata-version: 4.0' \
  -H "authorization: bearer $TOKEN"

Integrating with Your Application: The AZURE_FEDERATED_TOKEN_FILE Variable

Azure SDKs are designed to pick up credentials from specific environment variables. Point them to your newly generated JWT.

Save the JWT and set the necessary environment variables:


echo "$ID_TOKEN" > /tmp/federated_token.jwt
export AZURE_CLIENT_ID="$MANAGED_IDENTITY_CLIENT_ID"
export AZURE_TENANT_ID="$TENANT_ID"
export AZURE_FEDERATED_TOKEN_FILE="/tmp/federated_token.jwt"

If you now run our M365 Exporter (or any other application that implements Azure SDK`s env schema), it will now leverage these environment variables, and securely authenticate. This provides a miniature AKS Workload Identity equivalent right on your laptop.



Minikube Integration: Passwordless within Your Local Cluster

For applications running in your Minikube environment, we can implement Workload Identity Federation with Azure AD. The core idea is to configure Minikube's API server to sign Service Account tokens using your private_key.pem, and then instruct Azure AD to look to your storage bucket (our custom OIDC issuer) for the corresponding public key.


The Minikube Workflow:

  • Minikube API Server: Configure Minikube to use your private_key.pem (the same key as before) for signing Service Account tokens, and to embed your storage bucket URL (https://<your-bucket-name>.blob.core.windows.net/) in the JWT's iss claim.
  • Azure AD Trust: Azure AD is already configured to trust our storage bucket as an OIDC issuer.
  • Kubernetes Service Account: Your deployment operates under a Kubernetes Service Account.
  • Projected Token: Kubernetes injects a short-lived JWT (signed by Minikube with your private key) into your pod.
  • Application Authentication: Your application reads this token and sends it to Azure AD.
  • Azure AD Validation: Azure AD retrieves your jwks.json from the blob endpoint, verifies the signature, and checks the claims.
  • Azure AD Access Token: Success! Your application receives an access token.

Minikube Example:

1. Minikube Setup: Key Re-use

Re-use your private_key.pem. Ensure Minikube can access it within its VM.


# Copy the private key into Minikube's accessible files directory
mkdir -p ~/.minikube/
cp private_key.pem ~/.minikube/private_key.pem
cp public_key.pem ~/.minikube/public_key.pem

minikube start --kubernetes-version=stable --container-runtime=containerd --mount \
  --mount-uid='root' --mount-gid='root' \
  --mount-string="$HOME/.minikube/:/var/lib/minikube/certs/host/" \
  --extra-config="apiserver.service-account-signing-key-file=/var/lib/minikube/certs/host/private_key.pem" \
  --extra-config="apiserver.service-account-issuer=$BLOB_URL" \
  --extra-config="apiserver.service-account-key-file=/var/lib/minikube/certs/host/public_key.pem"

2. Federated Credential for Kubernetes Service Account (If Not Yet Done):

Confirm that you have a Federated Credential with your storage bucket as the issuer and a subject that aligns with your Kubernetes Service Account (e.g., system:serviceaccount:default:m365-exporter-sa).


az identity federated-credential create \
  --name "LocalDevOIDC2" \
  --identity-name "${MANAGED_IDENTITY_NAME}" \
  --resource-group "${RESOURCE_GROUP_NAME}" \
  --issuer "${BLOB_URL}" \
  --subject "system:serviceaccount:default:m365-exporter-sa" \
  --audiences "api://AzureADTokenExchange"

3. Deploying Your Exporter to Minikube:

Your Kubernetes manifests will largely remain consistent with a standard Workload Identity setup.


# m365-exporter-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: m365-exporter-sa
  namespace: default
automountServiceAccountToken: true
---
# m365-exporter-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: m365-exporter
  labels: { app: m365-exporter }
spec:
  replicas: 1
  selector: { matchLabels: { app: m365-exporter } }
  template:
    metadata: { labels: { app: m365-exporter } }
    spec:
      serviceAccountName: m365-exporter-sa
      containers:
      - name: exporter
        image: ghcr.io/cloudeteer/m365-exporter:3.4.0
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        env:
        - name: AZURE_CLIENT_ID
          value: "$MANAGED_IDENTITY_CLIENT_ID"
        - name: AZURE_TENANT_ID
          value: "$TENANT_ID"
        - name: AZURE_FEDERATED_TOKEN_FILE
          value: "/var/run/secrets/tokens/azure-identity-token"
        volumeMounts:
        - name: azure-identity-token
          mountPath: "/var/run/secrets/tokens"
          readOnly: true
      volumes:
      - name: azure-identity-token
        projected:
          sources:
          - serviceAccountToken:
              path: azure-identity-token
              audience: api://AzureADTokenExchange
              expirationSeconds: 3600

kubectl apply -f m365-exporter.yaml

Key Advantages for SREs:

  • Azure Managed Identities: We don't need access at Entra ID level, just someone that can assign permissions to our managed Identity. Besides that, we can manage it using Lighthouse!
  • Dev-Prod Parity: Local development authentication mirrors actual AKS environments, minimizing surprises.
  • Automated Credential Management: Kubernetes handles tokens, and Azure AD handles trust – a seamless workflow.
  • Auditable Processes: Every token exchange is logged, enhancing visibility and security.
  • Technological Sophistication: Leveraging open standards and cryptography to enable a cloud provider to trust your local dev setup is robust and elegant.


Concluding Thoughts

This passwordless setup for local development, whether on your machine or within Minikube, represents a significant improvement in our daily workflow. It enhances our security posture without introducing additional artificial friction. The days of scrambling for secrets or dealing with expired certificates are over. What remains is a pure, securely authenticated engineering process. Go forth and build, fellow SREs, without having to care about expiring secrets. Hope you’ve learning a thing two about the inner workings of OIDC.