| title | Crossplane with Workload Identity |
|---|---|
| weight | 205 |
| description | Configure Crossplane to pull packages from cloud provider container registries using workload identity |
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:
- Crossplane package manager pulls the package, extracts the package contents (CRDs, XRDs) and creates deployments
- Kubernetes nodes pull the runtime container images when creating provider/function pods
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:
- AWS EKS: Node IAM role with ECR pull permissions
- Azure AKS: Kubelet managed identity with
AcrPullrole - GCP GKE: Node service account with Artifact Registry reader role
Without 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:
- AWS EKS: IAM Roles for Service Accounts (IRSA)
- Azure AKS: Azure Workload Identity
- Google Cloud GKE: GKE Workload Identity
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).
- An Amazon EKS cluster with OIDC provider enabled
- AWS CLI installed and configured
kubectlconfigured to access your EKS cluster- Permissions to create IAM roles and policies
If your EKS cluster doesn't have an OIDC provider, enable it:
eksctl utils associate-iam-oidc-provider \
--cluster=<CLUSTER_NAME> \
--approveVerify the OIDC provider:
aws eks describe-cluster \
--name <CLUSTER_NAME> \
--query "cluster.identity.oidc.issuer" \
--output textCreate 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.jsonAttach the ECR policy to the IAM role:
aws iam attach-role-policy \
--role-name CrossplaneECRRole \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/CrossplaneECRPolicyInstall 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 yamlExpected output should include:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/CrossplaneECRRole
name: crossplane
namespace: crossplane-systemOnce 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
EOFCheck the provider installation status:
kubectl get provider provider-aws-s3
kubectl describe provider provider-aws-s3Error:
failed to get authorization token: AccessDeniedException
Solution:
- Verify the IAM role has the
ecr:GetAuthorizationTokenpermission - Check the trust policy allows the service account to assume the role
- Confirm the service account annotation matches the IAM role ARN
Error:
An error occurred (InvalidIdentityToken) when calling the AssumeRoleWithWebIdentity operation
Solution:
- Confirm the OIDC provider is active on your EKS cluster
- Check the trust policy condition matches your cluster's OIDC provider
- Verify the service account namespace and name in the condition are correct
Error:
denied: User: arn:aws:sts::123456789012:assumed-role/CrossplaneECRRole is not authorized to perform: ecr:BatchGetImage
Solution:
- Verify the IAM policy includes
ecr:BatchGetImage,ecr:BatchCheckLayerAvailability, andecr:GetDownloadUrlForLayer - Check the policy's
Resourceincludes your ECR repository ARN - Confirm the IAM role has the policy attached
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.eks\.amazonaws\.com/role-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:role/CrossplaneECRRole"
# Restart Crossplane
kubectl rollout restart deployment/crossplane -n crossplane-systemView logs for authentication issues:
kubectl logs -n crossplane-system deployment/crossplane --all-containers -fLook for:
- AWS credential errors
- ECR authentication failures
- Image pull errors with specific repository paths
{{< /tab >}}
{{< tab "Azure AKS" >}}
Configure Crossplane to pull packages from Azure Container Registry (ACR) using Azure Workload Identity.
- An AKS cluster with Workload Identity enabled
- Azure CLI installed and configured
kubectlconfigured to access your AKS cluster- Permissions to create Azure managed identities and role assignments
If 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-identityGet 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_ISSUERCreate 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_IDCreate 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://AzureADTokenExchangeGet 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:
- Service account annotations for the client ID and tenant ID
- Label
azure.workload.identity/use: "true"on pods (applied viacustomLabels)
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 yamlExpected 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-systemCheck that the deployment has the required labels:
kubectl get deployment crossplane -n crossplane-system -o yamlExpected 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
EOFCheck the provider installation status:
kubectl get provider provider-azure-storage
kubectl describe provider provider-azure-storageError:
unauthorized: authentication required
Solution:
- Verify the managed identity has
AcrPullrole on the ACR - Check the federated credential configuration
- Confirm the service account annotations match the managed identity client ID and tenant ID
Error:
failed to resolve reference: failed to fetch oauth token
Solution:
- Confirm workload identity is active on the AKS cluster
- Check the OIDC issuer URL in the federated credential matches your cluster's OIDC issuer
- Verify the subject in the federated credential matches
system:serviceaccount:crossplane-system:crossplane
Error:
invalid federated token
Solution:
- Verify the federated credential audience uses
api://AzureADTokenExchange - Check that the OIDC issuer URL is correct
- Confirm the service account namespace and name match the federated credential subject
If 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-systemView logs for authentication issues:
kubectl logs -n crossplane-system deployment/crossplane --all-containers -fLook for:
- Azure authentication errors
- ACR authentication failures
- Image pull errors with specific repository paths
- Azure Workload Identity Documentation
- AKS Workload Identity Overview
- Azure Container Registry Authentication
{{< /tab >}}
{{< tab "Google Cloud GKE" >}}
Configure Crossplane to pull packages from Google Artifact Registry using GKE Workload Identity.
- A GKE cluster with Workload Identity enabled
gcloudCLI installed and configuredkubectlconfigured to access your GKE cluster- Permissions to create service accounts and IAM bindings
If 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.googExisting cluster:
gcloud container clusters update $CLUSTER_NAME \
--region=$REGION \
--workload-pool=${PROJECT_ID}.svc.id.googCreate 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_IDGet the full service account email:
export GSA_EMAIL=${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
echo $GSA_EMAILGrant 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 yamlExpected output should include:
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
iam.gke.io/gcp-service-account: crossplane-gar-sa@project-id.iam.gserviceaccount.com
name: crossplane
namespace: crossplane-systemBy default, each provider creates its own ServiceAccount with a random name. To use Workload Identity for providers, create a DeploymentRuntimeConfig to assign a specific ServiceAccount.
Create a DeploymentRuntimeConfig to force providers to use this ServiceAccount:
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: gcp-runtime-config
spec:
deploymentTemplate:
spec:
template:
spec:
serviceAccountName: crossplane
EOFOnce configured, you can install Crossplane packages (Providers, Functions, Configurations) from your Artifact Registry. Reference the DeploymentRuntimeConfig in the 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
runtimeConfigRef:
name: gcp-runtime-config
EOFCheck the provider installation status:
kubectl get provider provider-gcp-storage
kubectl describe provider provider-gcp-storageError:
PERMISSION_DENIED: Permission denied on resource
Solution:
- Verify the Google service account has
roles/artifactregistry.readerrole - Check the IAM policy binding exists between the Google and Kubernetes service accounts
- Confirm the service account annotation matches the Google service account email
Error:
failed to fetch oauth token
Solution:
- Confirm workload identity is active on the GKE cluster
- Check the IAM policy binding allows the Kubernetes service account to impersonate the Google service account
- Verify the workload pool matches your project:
${PROJECT_ID}.svc.id.goog
Error:
invalid identity token
Solution:
- Verify the IAM policy binding member format:
serviceAccount:${PROJECT_ID}.svc.id.goog[crossplane-system/crossplane] - Check that workload identity is active on the node pool
- Confirm the service account annotation is correct
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-systemTest 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_EMAILView logs for authentication issues:
kubectl logs -n crossplane-system deployment/crossplane --all-containers -fLook for:
- Google Cloud authentication errors
- Artifact Registry authentication failures
- Image pull errors with specific repository paths
- GKE Workload Identity Documentation
- Artifact Registry Authentication
- IAM Service Account Permissions
{{< /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