content/v2.0/guides/crossplane-with-workload-identity.md
When running Crossplane on managed Kubernetes clusters (EKS, AKS, GKE), you can use Kubernetes Workload Identity to grant Crossplane access to pull packages from private cloud container registries. This allows Crossplane to install providers, functions, and configurations from registries like AWS ECR, Azure ACR, and Google Artifact Registry without managing static credentials.
{{< hint "important" >}} This guide configures the Crossplane package manager to pull packages from private registries. Packages reference container images that run as separate pods (providers and functions).
Two-step image pull process:
This guide covers step 1. For step 2, ensure your Kubernetes nodes have permissions to pull images from the private registry. Typically configured at the cluster level:
AcrPull roleWithout node-level access, package installation succeeds but pods fail with ImagePullBackOff.
{{< /hint >}}
To enable Crossplane package manager access to private registries, configure service account annotations during installation. The crossplane service account in the crossplane-system namespace requires specific annotations for each cloud provider:
Select your cloud provider below for detailed setup instructions:
{{< tabs >}}
{{< tab "AWS EKS" >}}
Configure Crossplane to pull packages from Amazon ECR using IAM Roles for Service Accounts (IRSA).
kubectl configured to access your EKS clusterIf your EKS cluster doesn't have an OIDC provider, enable it:
eksctl utils associate-iam-oidc-provider \
--cluster=<CLUSTER_NAME> \
--approve
Verify the OIDC provider:
aws eks describe-cluster \
--name <CLUSTER_NAME> \
--query "cluster.identity.oidc.issuer" \
--output text
Create an IAM policy that grants permissions to pull images from ECR:
cat > crossplane-ecr-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "arn:aws:ecr:<REGION>:<ACCOUNT_ID>:repository/*"
}
]
}
EOF
aws iam create-policy \
--policy-name CrossplaneECRPolicy \
--policy-document file://crossplane-ecr-policy.json
{{< hint "note" >}}
Replace <REGION> and <ACCOUNT_ID> with your AWS region and account ID. You can restrict the Resource to specific repositories if needed.
{{< /hint >}}
Create an IAM role that the Crossplane service account can assume:
export CLUSTER_NAME=<your-cluster-name>
export AWS_REGION=<your-aws-region>
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export OIDC_PROVIDER=$(aws eks describe-cluster --name $CLUSTER_NAME --region $AWS_REGION --query "cluster.identity.oidc.issuer" --output text | sed -e "s/^https:\/\///")
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_PROVIDER}:sub": "system:serviceaccount:crossplane-system:crossplane",
"${OIDC_PROVIDER}:aud": "sts.amazonaws.com"
}
}
}
]
}
EOF
aws iam create-role \
--role-name CrossplaneECRRole \
--assume-role-policy-document file://trust-policy.json
Attach the ECR policy to the IAM role:
aws iam attach-role-policy \
--role-name CrossplaneECRRole \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/CrossplaneECRPolicy
Install Crossplane with the service account annotation:
helm upgrade --install crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--set "serviceAccount.customAnnotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:role/CrossplaneECRRole"
Check that the service account has the correct annotation:
kubectl get sa crossplane -n crossplane-system -o yaml
Expected output should include:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/CrossplaneECRRole
name: crossplane
namespace: crossplane-system
Once configured, you can install Crossplane packages (Providers, Functions, Configurations) from your ECR registry. Here's an example using a Provider:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws-s3
spec:
package: ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/crossplane/provider-aws-s3:v1.2.0
EOF
Check the provider installation status:
kubectl get provider provider-aws-s3
kubectl describe provider provider-aws-s3
Error:
failed to get authorization token: AccessDeniedException
Solution:
ecr:GetAuthorizationToken permissionError:
An error occurred (InvalidIdentityToken) when calling the AssumeRoleWithWebIdentity operation
Solution:
Error:
denied: User: arn:aws:sts::123456789012:assumed-role/CrossplaneECRRole is not authorized to perform: ecr:BatchGetImage
Solution:
ecr:BatchGetImage, ecr:BatchCheckLayerAvailability, and ecr:GetDownloadUrlForLayerResource includes your ECR repository ARNIf the service account doesn't have the annotation after installation:
# Update via Helm
helm upgrade crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--reuse-values \
--set "serviceAccount.customAnnotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:role/CrossplaneECRRole"
# Restart Crossplane
kubectl rollout restart deployment/crossplane -n crossplane-system
View logs for authentication issues:
kubectl logs -n crossplane-system deployment/crossplane --all-containers -f
Look for:
{{< /tab >}}
{{< tab "Azure AKS" >}}
Configure Crossplane to pull packages from Azure Container Registry (ACR) using Azure Workload Identity.
kubectl configured to access your AKS clusterIf your AKS cluster doesn't have Workload Identity enabled, update it:
export RESOURCE_GROUP=<your-resource-group>
export CLUSTER_NAME=<your-cluster-name>
az aks update \
--resource-group $RESOURCE_GROUP \
--name $CLUSTER_NAME \
--enable-oidc-issuer \
--enable-workload-identity
Get the OIDC issuer URL:
export AKS_OIDC_ISSUER=$(az aks show \
--resource-group $RESOURCE_GROUP \
--name $CLUSTER_NAME \
--query "oidcIssuerProfile.issuerUrl" \
--output tsv)
echo $AKS_OIDC_ISSUER
Create a managed identity for Crossplane:
export IDENTITY_NAME=crossplane-acr-identity
az identity create \
--name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP
export USER_ASSIGNED_CLIENT_ID=$(az identity show \
--name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--query 'clientId' \
--output tsv)
export USER_ASSIGNED_OBJECT_ID=$(az identity show \
--name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--query 'principalId' \
--output tsv)
echo "Client ID: $USER_ASSIGNED_CLIENT_ID"
echo "Object ID: $USER_ASSIGNED_OBJECT_ID"
Grant the managed identity permission to pull from ACR:
export ACR_NAME=<your-acr-name>
export ACR_ID=$(az acr show \
--name $ACR_NAME \
--query 'id' \
--output tsv)
az role assignment create \
--assignee-object-id $USER_ASSIGNED_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--role AcrPull \
--scope $ACR_ID
Create a federated identity credential that establishes trust between the managed identity and the Kubernetes service account:
az identity federated-credential create \
--name crossplane-federated-credential \
--identity-name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--issuer $AKS_OIDC_ISSUER \
--subject system:serviceaccount:crossplane-system:crossplane \
--audience api://AzureADTokenExchange
Get the tenant ID:
export AZURE_TENANT_ID=$(az account show --query tenantId --output tsv)
Install Crossplane with the workload identity annotations and label:
helm upgrade --install crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--set "serviceAccount.customAnnotations.azure\.workload\.identity/client-id=$USER_ASSIGNED_CLIENT_ID" \
--set "serviceAccount.customAnnotations.azure\.workload\.identity/tenant-id=$AZURE_TENANT_ID" \
--set-string 'customLabels.azure\.workload\.identity/use=true'
{{< hint "note" >}} Azure Workload Identity requires:
azure.workload.identity/use: "true" on pods (applied via customLabels)The customLabels setting applies the label to all Crossplane resources. The Azure Workload Identity webhook uses this label on pods to inject environment variables and token volumes. Use --set-string to treat the value as a string rather than a boolean.
{{< /hint >}}
Check that the service account has the correct annotations:
kubectl get sa crossplane -n crossplane-system -o yaml
Expected output should include:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
azure.workload.identity/client-id: <client-id>
azure.workload.identity/tenant-id: <tenant-id>
name: crossplane
namespace: crossplane-system
Check that the deployment has the required labels:
kubectl get deployment crossplane -n crossplane-system -o yaml
Expected output should include:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
azure.workload.identity/use: "true"
name: crossplane
namespace: crossplane-system
spec:
template:
metadata:
labels:
azure.workload.identity/use: "true"
Once configured, you can install Crossplane packages (Providers, Functions, Configurations) from your ACR. Here's an example using a Provider:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-azure-storage
spec:
package: ${ACR_NAME}.azurecr.io/crossplane/provider-azure-storage:v1.2.0
EOF
Check the provider installation status:
kubectl get provider provider-azure-storage
kubectl describe provider provider-azure-storage
Error:
unauthorized: authentication required
Solution:
AcrPull role on the ACRError:
failed to resolve reference: failed to fetch oauth token
Solution:
system:serviceaccount:crossplane-system:crossplaneError:
invalid federated token
Solution:
api://AzureADTokenExchangeIf the service account doesn't have the annotations after installation:
# Update via Helm
helm upgrade crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--reuse-values \
--set "serviceAccount.customAnnotations.azure\.workload\.identity/client-id=$USER_ASSIGNED_CLIENT_ID" \
--set "serviceAccount.customAnnotations.azure\.workload\.identity/tenant-id=$AZURE_TENANT_ID"
# Restart Crossplane
kubectl rollout restart deployment/crossplane -n crossplane-system
View logs for authentication issues:
kubectl logs -n crossplane-system deployment/crossplane --all-containers -f
Look for:
{{< /tab >}}
{{< tab "Google Cloud GKE" >}}
Configure Crossplane to pull packages from Google Artifact Registry using GKE Workload Identity.
gcloud CLI installed and configuredkubectl configured to access your GKE clusterIf your GKE cluster doesn't have Workload Identity enabled, create a new cluster with it enabled or update an existing cluster:
New cluster:
export PROJECT_ID=<your-project-id>
export CLUSTER_NAME=<your-cluster-name>
export REGION=<your-region>
gcloud container clusters create $CLUSTER_NAME \
--region=$REGION \
--workload-pool=${PROJECT_ID}.svc.id.goog
Existing cluster:
gcloud container clusters update $CLUSTER_NAME \
--region=$REGION \
--workload-pool=${PROJECT_ID}.svc.id.goog
Create a Google Cloud service account for Crossplane:
export GSA_NAME=crossplane-gar-sa
gcloud iam service-accounts create $GSA_NAME \
--display-name="Crossplane Artifact Registry Service Account" \
--project=$PROJECT_ID
Get the full service account email:
export GSA_EMAIL=${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
echo $GSA_EMAIL
Grant the service account permissions to read from Artifact Registry:
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${GSA_EMAIL}" \
--role="roles/artifactregistry.reader"
For specific repository access, use:
export REPOSITORY=<your-repository-name>
export REPOSITORY_LOCATION=<repository-location>
gcloud artifacts repositories add-iam-policy-binding $REPOSITORY \
--location=$REPOSITORY_LOCATION \
--member="serviceAccount:${GSA_EMAIL}" \
--role="roles/artifactregistry.reader"
Create an IAM policy binding between the Google service account and the Kubernetes service account:
gcloud iam service-accounts add-iam-policy-binding $GSA_EMAIL \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[crossplane-system/crossplane]"
Install Crossplane with the service account annotation:
helm upgrade --install crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--create-namespace \
--set "serviceAccount.customAnnotations.iam\.gke\.io/gcp-service-account=$GSA_EMAIL"
Check that the service account has the correct annotation:
kubectl get sa crossplane -n crossplane-system -o yaml
Expected output should include:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
iam.gke.io/gcp-service-account: [email protected]
name: crossplane
namespace: crossplane-system
Once configured, you can install Crossplane packages (Providers, Functions, Configurations) from your Artifact Registry. Here's an example using a Provider:
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-gcp-storage
spec:
package: us-docker.pkg.dev/${PROJECT_ID}/crossplane/provider-gcp-storage:v1.2.0
EOF
Check the provider installation status:
kubectl get provider provider-gcp-storage
kubectl describe provider provider-gcp-storage
Error:
PERMISSION_DENIED: Permission denied on resource
Solution:
roles/artifactregistry.reader roleError:
failed to fetch oauth token
Solution:
${PROJECT_ID}.svc.id.googError:
invalid identity token
Solution:
serviceAccount:${PROJECT_ID}.svc.id.goog[crossplane-system/crossplane]If the service account doesn't have the annotation after installation:
# Update via Helm
helm upgrade crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--reuse-values \
--set "serviceAccount.customAnnotations.iam\.gke\.io/gcp-service-account=$GSA_EMAIL"
# Restart Crossplane
kubectl rollout restart deployment/crossplane -n crossplane-system
Test the workload identity configuration:
# Check if workload identity is enabled on the cluster
gcloud container clusters describe $CLUSTER_NAME \
--region=$REGION \
--format="value(workloadIdentityConfig.workloadPool)"
# Verify IAM policy binding
gcloud iam service-accounts get-iam-policy $GSA_EMAIL
View logs for authentication issues:
kubectl logs -n crossplane-system deployment/crossplane --all-containers -f
Look for:
{{< /tab >}}
{{< /tabs >}}
If Crossplane is already installed, you can update the service account annotations using Helm upgrade with the appropriate --set flags shown in your cloud provider's tab. After updating, restart the Crossplane deployment:
kubectl rollout restart deployment/crossplane -n crossplane-system