A Crossplane Configuration package that simplifies the creation and management of Azure Workload Identity resources, enabling secure authentication between Kubernetes workloads and Azure services using OIDC federation.
This configuration automates the setup of Azure Workload Identity by creating and managing:
- Azure User Assigned Managed Identity - The identity that will be used by your Kubernetes workloads
- Federated Identity Credentials - OIDC federation configuration linking the managed identity to Kubernetes service accounts
- Azure RBAC Role Assignments - Permissions granted to the managed identity
- Custom Role Definitions - Support for creating custom Azure roles when needed
- Kubernetes Service Accounts - Optionally creates service accounts with proper annotations
Before using this configuration, ensure you have:
-
Kubernetes Cluster (v1.24+) with:
- Crossplane v2.0.2 or higher installed
- OIDC issuer enabled (for AKS, EKS, or self-managed clusters)
-
Azure Subscription with:
- Appropriate permissions to create managed identities and role assignments
- Resource Group where resources will be created
-
Required Crossplane Providers:
provider-family-azurev2+provider-kubernetesv1+
-
Required Crossplane Functions:
function-go-templatingv0.10.0+function-auto-readyv0.5.0+
If you haven't already installed Crossplane:
kubectl create namespace crossplane-system
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane \
--namespace crossplane-system \
crossplane-stable/crossplane \
--version 2.0.2# Install providers
kubectl apply -f examples/providers.yaml
# Install functions
kubectl apply -f examples/functions.yaml
# Wait for providers to become healthy
kubectl wait --for=condition=Healthy \
--timeout=300s \
provider.pkg.crossplane.io --allCreate a secret with your Azure service principal credentials:
# Create a JSON file with your Azure credentials
cat > azure-credentials.json <<EOF
{
"clientId": "YOUR_CLIENT_ID",
"clientSecret": "YOUR_CLIENT_SECRET",
"subscriptionId": "YOUR_SUBSCRIPTION_ID",
"tenantId": "YOUR_TENANT_ID"
}
EOF
# Create the secret
kubectl create secret generic provider-azure \
--from-file=credential=./azure-credentials.json \
--namespace crossplane-system
# Clean up the credentials file
rm azure-credentials.jsonBuild and push the configuration package:
# Build the package
crossplane xpkg build
# Login to your registry (if using Upbound)
crossplane xpkg login
# Push the package
crossplane xpkg push -f *.xpkg livewyer-ops/crossplane-configuration-azure-workload-identity:v1.0.0Or install directly from the registry:
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
name: configuration-azure-workload-identity
spec:
package: xpkg.upbound.io/livewyer-ops/crossplane-configuration-azure-workload-identity:v1.0.0
EOFCreate a WorkloadIdentity resource with basic role assignments:
apiVersion: azure.livewyer.io/v1alpha1
kind: WorkloadIdentity
metadata:
name: my-app-identity
namespace: default
spec:
forProvider:
location: eastus
resourceGroupName: my-resource-group
serviceAccountName: my-app-sa
oidcIssuerURL: https://eastus.oic.prod-aks.azure.com/00000000-0000-0000-0000-000000000000/11111111-1111-1111-1111-111111111111/
roleAssignments:
- roleDefinitionName: Reader
scope: /subscriptions/YOUR_SUBSCRIPTION_ID
tags:
environment: production
app: my-appapiVersion: azure.livewyer.io/v1alpha1
kind: WorkloadIdentity
metadata:
name: storage-operator
namespace: storage-system
spec:
forProvider:
location: westeurope
resourceGroupName: storage-rg
serviceAccountName: storage-operator-sa
serviceAccountTokenExpiration: 3600 # 1 hour
oidcIssuerURL: https://oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
roleAssignments:
# Built-in role
- roleDefinitionName: "Storage Account Contributor"
scope: /subscriptions/SUB_ID/resourceGroups/storage-rg
description: "Manage storage accounts in storage-rg"
# Custom role with specific permissions
- roleDefinitionName: "CustomBlobReader"
scope: /subscriptions/SUB_ID
permissions:
- actions:
- "Microsoft.Storage/storageAccounts/listKeys/action"
- "Microsoft.Storage/storageAccounts/read"
dataActions:
- "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read"
- "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write"
notActions:
- "Microsoft.Storage/storageAccounts/delete"
tags:
team: platform
cost-center: engineeringOnce the WorkloadIdentity is created, you can use the service account in your pods:
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: default
spec:
serviceAccountName: my-app-sa # The service account created by WorkloadIdentity
containers:
- name: app
image: myapp:latest
env:
- name: AZURE_CLIENT_ID
valueFrom:
fieldRef:
fieldPath: metadata.annotations['azure.workload.identity/client-id']| Field | Type | Required | Description |
|---|---|---|---|
forProvider.location |
string | Yes | Azure region (e.g., eastus, westeurope) |
forProvider.resourceGroupName |
string | Yes | Name of the Azure Resource Group |
forProvider.oidcIssuerURL |
string | Yes | OIDC issuer URL of your Kubernetes cluster |
forProvider.roleAssignments |
array | Yes | List of role assignments to grant |
forProvider.serviceAccountName |
string | No | Kubernetes service account name to create |
forProvider.serviceAccountTokenExpiration |
integer | No | Token expiration in seconds (default: 86400) |
forProvider.tags |
object | No | Azure resource tags |
providerConfigRef |
object | No | Reference to provider configuration |
| Field | Type | Description |
|---|---|---|
roleDefinitionName |
string | Name of built-in or custom role |
scope |
string | Azure resource scope for the assignment |
permissions |
array | Permissions for custom role (optional) |
permissions[].actions |
array | Allowed management operations |
permissions[].dataActions |
array | Allowed data operations |
permissions[].notActions |
array | Denied management operations |
permissions[].notDataActions |
array | Denied data operations |
condition |
string | Condition expression for conditional access |
conditionVersion |
string | Version of condition syntax |
principalType |
string | Type of principal (default: ServicePrincipal) |
- Check that the configuration is installed:
kubectl get configurations
kubectl get xrds | grep workloadidentity
kubectl get compositions | grep workloadidentity- Create a test WorkloadIdentity:
kubectl apply -f examples/workload-identity.yaml- Verify resources are created:
# Check the composite resource
kubectl get workloadidentity -n test
# Check the managed resources
kubectl get managed -n test
# Check Azure resources
kubectl get userassignedidentity -n test
kubectl get federatedidentitycredential -n test
kubectl get roleassignment -n testUsing Azure CLI:
# List managed identities
az identity list --resource-group example-group
# Check role assignments
az role assignment list --assignee <PRINCIPAL_ID>
# Verify federated credentials
az identity federated-credential list \
--resource-group example-group \
--identity-name simple-exampleDeploy a test pod to verify authentication:
apiVersion: v1
kind: Pod
metadata:
name: test-workload-identity
namespace: test
spec:
serviceAccountName: simple-example
containers:
- name: azure-cli
image: mcr.microsoft.com/azure-cli:latest
command: ["/bin/bash", "-c", "--"]
args:
- |
# Login using workload identity
az login --federated-token $(cat $AZURE_FEDERATED_TOKEN_FILE) \
--service-principal -u $AZURE_CLIENT_ID -t $AZURE_TENANT_ID
# Test access
az account show
az group list# Get detailed status
kubectl describe workloadidentity -n test simple-example
# Check conditions
kubectl get workloadidentity -n test simple-example -o jsonpath='{.status.conditions}'# Crossplane pod logs
kubectl logs -n crossplane-system deployment/crossplane -f
# Provider logs
kubectl logs -n crossplane-system -l pkg.crossplane.io/provider=provider-azure-managedidentity-
OIDC Issuer URL Incorrect
- Verify the OIDC issuer URL matches your cluster configuration
- For AKS:
az aks show --name <cluster> --resource-group <rg> --query "oidcIssuerProfile.issuerUrl" - For EKS:
aws eks describe-cluster --name <cluster> --query "cluster.identity.oidc.issuer"
-
Role Assignment Failures
- Ensure the service principal has permissions to create role assignments
- Check that the scope exists and is correctly formatted
- Verify custom role permissions are valid
-
Service Account Not Created
- Check that the Kubernetes provider is healthy
- Verify provider configuration references are correct
- Check namespace exists
-
Resources Stuck in Creating State
- Review provider pod logs for errors
- Check Azure API quotas and limits
- Verify network connectivity to Azure
# Get all events
kubectl get events -n test --sort-by='.lastTimestamp'
# Check composition pipeline
kubectl get composite -n test simple-example -o yaml | less
# View rendered resources
kubectl get managed -n test -o yaml | grep -A 5 "forProvider"To remove a WorkloadIdentity and all its resources:
kubectl delete workloadidentity -n test simple-exampleTo uninstall the configuration:
kubectl delete configuration configuration-azure-workload-identityWe welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Update examples and documentation
- Test your changes
- Submit a pull request
# Build the package locally
crossplane xpkg build
# Install for testing
kubectl apply -f apis/definition.yaml
kubectl apply -f apis/composition.yaml
# Test with examples
kubectl apply -f examples/workload-identity.yaml- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Crossplane Docs
This project is licensed under the MIT License - see the LICENSE file for details.
- Livewyer Team [email protected]
- Crossplane for the amazing framework
- Upbound for provider implementations
- Azure Workload Identity documentation and examples