From 96ca40c7d1d47399078e7b99fcf73c2fd18e6d62 Mon Sep 17 00:00:00 2001 From: Youssef Omar Bouden Date: Wed, 28 Oct 2026 09:22:50 +0100 Subject: [PATCH] feat: make the credentials Secret optional for workload identity The AKS workload identity webhook injects AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE and the token volume, and the Azure SDK falls back to them when the options are empty, but the function still required the azure-creds Secret to exist. It is now optional for identity.type AzureWorkloadIdentityCredentials and the README describes the Secret, its keys and the DeploymentRuntimeConfig as the webhook leaves them. Signed-off-by: Youssef Omar Bouden --- README.md | 44 +++++++++++++++--------------- fn.go | 10 ++++--- fn_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 76aa89b..243964d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,15 @@ stringData: AKS cluster needs to have workload identity enabled. The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html. -##### Credentials secret: +##### Credentials secret (optional): +The workload identity webhook injects `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, +`AZURE_FEDERATED_TOKEN_FILE` and the projected token volume into every pod +that carries the `azure.workload.identity/use: "true"` label and runs with an +annotated service account, and the function reads those variables when the +corresponding key is absent. A credentials Secret is therefore only needed to +override them, for example to use a different client id than the one on the +service account: + ```yaml apiVersion: v1 kind: Secret @@ -68,12 +76,14 @@ type: Opaque stringData: credentials: | { - "clientId": "your-client-id", # optional - "tenantId": "your-tenant-id", # optional - "federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token" + "clientId": "your-client-id", # optional, defaults to AZURE_CLIENT_ID + "tenantId": "your-tenant-id", # optional, defaults to AZURE_TENANT_ID + "federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token" # optional, defaults to AZURE_FEDERATED_TOKEN_FILE } ``` +Without a Secret, leave `credentials` out of the pipeline step. + ##### Function ```yaml apiVersion: pkg.crossplane.io/v1 @@ -89,39 +99,27 @@ spec: ``` ##### DeploymentRuntimeConfig +The label on the pod template and the annotation on the service account are +all the webhook needs; it adds the environment variables and the token volume +itself. + ```yaml apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig metadata: name: upbound-function-msgraph -spec: +spec: deploymentTemplate: spec: - selector: - matchLabels: - azure.workload.identity/use: "true" - pkg.crossplane.io/function: "upbound-function-msgraph" + selector: {} template: metadata: labels: azure.workload.identity/use: "true" - pkg.crossplane.io/function: "upbound-function-msgraph" spec: + serviceAccountName: "upbound-function-msgraph" containers: - name: package-runtime - volumeMounts: - - mountPath: /var/run/secrets/azure/tokens - name: azure-identity-token - readOnly: true - serviceAccountName: "upbound-function-msgraph" - volumes: - - name: azure-identity-token - projected: - sources: - - serviceAccountToken: - audience: api://AzureADTokenExchange - expirationSeconds: 3600 - path: azure-identity-token serviceAccountTemplate: metadata: annotations: diff --git a/fn.go b/fn.go index 1830c1f..8908348 100644 --- a/fn.go +++ b/fn.go @@ -359,7 +359,11 @@ func (f *Function) parseInputAndCredentials(req *fnv1.RunFunctionRequest, rsp *f return nil, nil, err } - azureCreds, err := getCreds(req) + // Workload identity can run on what the AKS webhook injects into the pod + // (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE), so its + // credentials Secret is optional. + required := in.Identity == nil || in.Identity.Type != v1beta1.IdentityTypeAzureWorkloadIdentityCredentials + azureCreds, err := getCreds(req, required) if err != nil { response.Fatal(rsp, err) return nil, nil, err @@ -596,7 +600,7 @@ func (f *Function) processResults(req *fnv1.RunFunctionRequest, in *v1beta1.Inpu return nil } -func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { +func getCreds(req *fnv1.RunFunctionRequest, required bool) (map[string]string, error) { var azureCreds map[string]string rawCreds := req.GetCredentials() @@ -608,7 +612,7 @@ func getCreds(req *fnv1.RunFunctionRequest) (map[string]string, error) { return nil, errors.Wrap(err, "cannot parse json credentials") } } - } else { + } else if required { return nil, errors.New("failed to get azure-creds credentials") } diff --git a/fn_test.go b/fn_test.go index debf402..b720bb1 100644 --- a/fn_test.go +++ b/fn_test.go @@ -4212,6 +4212,85 @@ func TestIdentityType(t *testing.T) { }, }, }, + "AzureWorkloadIdentityCredentialsWithoutSecret": { + reason: "The Function should not require the azure-creds Secret for Workload Identity, which the pod environment can provide entirely", + args: args{ + ctx: context.Background(), + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: testRequestTag}, + Input: resource.MustStructJSON(`{ + "apiVersion": "msgraph.fn.crossplane.io/v1alpha1", + "kind": "Input", + "queryType": "GroupObjectIDs", + "groupsRef": "status.groups", + "target": "status.groupObjectIDs", + "identity": { + "type": "AzureWorkloadIdentityCredentials" + } + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: testRequestTag, Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: `failed to initialize workload identity provider: failed to obtain workloadidentity credentials`, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "AzureServicePrincipalCredentialsWithoutSecret": { + reason: "The Function should still require the azure-creds Secret for service principal credentials", + args: args{ + ctx: context.Background(), + req: &fnv1.RunFunctionRequest{ + Meta: &fnv1.RequestMeta{Tag: testRequestTag}, + Input: resource.MustStructJSON(`{ + "apiVersion": "msgraph.fn.crossplane.io/v1alpha1", + "kind": "Input", + "queryType": "GroupObjectIDs", + "groupsRef": "status.groups", + "target": "status.groupObjectIDs" + }`), + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1.RunFunctionResponse{ + Meta: &fnv1.ResponseMeta{Tag: testRequestTag, Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1.Result{ + { + Severity: fnv1.Severity_SEVERITY_FATAL, + Message: `failed to get azure-creds credentials`, + Target: fnv1.Target_TARGET_COMPOSITE.Enum(), + }, + }, + Desired: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, "AzureWorkloadIdentityCredentials": { reason: "The Function should use Workload Identity credentials if identity.type is AzureWorkloadIdentityCredentials", args: args{