Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 21 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
10 changes: 7 additions & 3 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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")
}

Expand Down
79 changes: 79 additions & 0 deletions fn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down