Skip to content

Commit 1ddbae4

Browse files
authored
Implement support for workload identity (#99)
* Implement support for workload identity * Review comment: add logging for authentication method
1 parent 7c623db commit 1ddbae4

File tree

7 files changed

+531
-19
lines changed

7 files changed

+531
-19
lines changed

README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,109 @@ With the secret containing:
203203
[azresourcegraph]: https://learn.microsoft.com/en-us/azure/governance/resource-graph/
204204
[azop]: https://marketplace.upbound.io/providers/upbound/provider-family-azure/latest
205205
[examples]: ./example
206+
207+
## Workload Identity Authentication
208+
AKS cluster needs to have workload identity enabled.
209+
The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html.
210+
211+
⚠️
212+
Does not support Multiple Service Principals with Round-Robin
213+
214+
### Credentials secret:
215+
```yaml
216+
apiVersion: v1
217+
kind: Secret
218+
metadata:
219+
name: azure-account-creds
220+
namespace: crossplane-system
221+
type: Opaque
222+
stringData:
223+
credentials: |
224+
{
225+
"clientId": "your-client-id", # optional, defaults to AZURE_CLIENT_ID injected by Azure Workload Identity
226+
"tenantId": "your-tenant-id", # optional, defaults to AZURE_TENANT_ID injected by Azure Workload Identity
227+
"subscriptionId": "your-subscription-id" # optional, if both subscriptionId and Explicit Subscriptions scope is not defined defaults to tenant-scope search
228+
"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token"
229+
}
230+
```
231+
232+
#### Function
233+
```yaml
234+
apiVersion: pkg.crossplane.io/v1
235+
kind: Function
236+
metadata:
237+
name: upbound-function-azresourcegraph
238+
spec:
239+
package: xpkg.upbound.io/upbound/function-azresourcegraph:v0.10.0
240+
runtimeConfigRef:
241+
apiVersion: pkg.crossplane.io/v1beta1
242+
kind: DeploymentRuntimeConfig
243+
name: upbound-function-azresourcegraph
244+
```
245+
246+
#### DeploymentRuntimeConfig
247+
```yaml
248+
apiVersion: pkg.crossplane.io/v1beta1
249+
kind: DeploymentRuntimeConfig
250+
metadata:
251+
name: upbound-function-azresourcegraph
252+
spec:
253+
deploymentTemplate:
254+
spec:
255+
selector:
256+
matchLabels:
257+
azure.workload.identity/use: "true"
258+
pkg.crossplane.io/function: "upbound-function-azresourcegraph"
259+
template:
260+
metadata:
261+
labels:
262+
azure.workload.identity/use: "true"
263+
pkg.crossplane.io/function: "upbound-function-azresourcegraph"
264+
spec:
265+
containers:
266+
- name: package-runtime
267+
volumeMounts:
268+
- mountPath: /var/run/secrets/azure/tokens
269+
name: azure-identity-token
270+
readOnly: true
271+
serviceAccountName: "upbound-function-azresourcegraph"
272+
volumes:
273+
- name: azure-identity-token
274+
projected:
275+
sources:
276+
- serviceAccountToken:
277+
audience: api://AzureADTokenExchange
278+
expirationSeconds: 3600
279+
path: azure-identity-token
280+
serviceAccountTemplate:
281+
metadata:
282+
annotations:
283+
azure.workload.identity/client-id: "your-client-id"
284+
name: "upbound-function-azresourcegraph"
285+
```
286+
287+
## Using Different Credentials
288+
289+
### Using ServicePrincipal credentials
290+
291+
#### Explicitly
292+
```yaml
293+
apiVersion: azresourcegraph.fn.crossplane.io/v1beta1
294+
kind: Input
295+
identity:
296+
type: AzureServicePrincipalCredentials
297+
```
298+
299+
#### Default
300+
```yaml
301+
apiVersion: azresourcegraph.fn.crossplane.io/v1beta1
302+
kind: Input
303+
```
304+
305+
### Using Workload Identity Credentials
306+
```yaml
307+
apiVersion: azresourcegraph.fn.crossplane.io/v1beta1
308+
kind: Input
309+
identity:
310+
type: AzureWorkloadIdentityCredentials
311+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: v1
2+
stringData:
3+
credentials: |
4+
{
5+
"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token",
6+
"subscriptionId": "your-subscription-id"
7+
}
8+
kind: Secret
9+
metadata:
10+
name: azure-workload-identity-creds
11+
namespace: upbound-system
12+
type: Opaque

fn.go

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ import (
2727
// Round-robin counter for service principal selection
2828
var servicePrincipalCounter uint64
2929

30+
const (
31+
// SubscriptionID defines the azure credentials key for subscription id
32+
SubscriptionID = "subscriptionId"
33+
// TenantID defines the azure credentials key for tenant id
34+
TenantID = "tenantId"
35+
// ClientID defines the azure credentials key for client id
36+
ClientID = "clientId"
37+
// ClientSecret defines the azure credentials key for client secret
38+
ClientSecret = "clientSecret"
39+
// WorkloadIdentityCredentialPath defines the azure credentials key for federated token file path
40+
WorkloadIdentityCredentialPath = "federatedTokenFile"
41+
)
42+
3043
// AzureQueryInterface defines the methods required for querying Azure resources.
3144
type AzureQueryInterface interface {
3245
azQuery(ctx context.Context, azureCreds interface{}, in *v1beta1.Input, log logging.Logger) (armresourcegraph.ClientResourcesResponse, error)
@@ -371,7 +384,7 @@ func (a *AzureQuery) handleSingleServicePrincipal(creds map[string]string, log l
371384
allSubscriptionIDs := []string{}
372385

373386
// Extract subscription ID if present
374-
if subID, exists := creds["subscriptionId"]; exists && subID != "" {
387+
if subID, exists := creds[SubscriptionID]; exists && subID != "" {
375388
allSubscriptionIDs = append(allSubscriptionIDs, subID)
376389
}
377390

@@ -392,7 +405,7 @@ func (a *AzureQuery) handleMultipleServicePrincipals(creds []map[string]string,
392405

393406
// Extract subscription IDs from all service principals
394407
for _, cred := range creds {
395-
if subID, exists := cred["subscriptionId"]; exists && subID != "" {
408+
if subID, exists := cred[SubscriptionID]; exists && subID != "" {
396409
allSubscriptionIDs = append(allSubscriptionIDs, subID)
397410
}
398411
}
@@ -436,16 +449,24 @@ func (a *AzureQuery) setupQueryRequest(in *v1beta1.Input, allSubscriptionIDs []s
436449
}
437450

438451
// azQuery is a concrete implementation that interacts with Azure Resource Graph API.
439-
func (a *AzureQuery) azQuery(ctx context.Context, azureCreds interface{}, in *v1beta1.Input, log logging.Logger) (armresourcegraph.ClientResourcesResponse, error) {
452+
func (a *AzureQuery) azQuery(ctx context.Context, azureCreds interface{}, in *v1beta1.Input, log logging.Logger) (results armresourcegraph.ClientResourcesResponse, err error) { //nolint:gocyclo // complexity can not be reduced as it's a result of choosing correct identity and credentials for it
440453
var selectedCreds map[string]string
441454
var allSubscriptionIDs []string
455+
var client *armresourcegraph.Client
456+
identityType := v1beta1.IdentityTypeAzureServicePrincipalCredentials
457+
458+
if in.Identity != nil && in.Identity.Type != "" {
459+
identityType = in.Identity.Type
460+
}
442461

443462
// Handle different credential formats and extract subscription IDs
444463
switch v := azureCreds.(type) {
445464
case map[string]string:
446465
selectedCreds, allSubscriptionIDs, _ = a.handleSingleServicePrincipal(v, log)
447466
case []map[string]string:
448-
var err error
467+
if identityType == v1beta1.IdentityTypeAzureWorkloadIdentityCredentials {
468+
return armresourcegraph.ClientResourcesResponse{}, errors.New("invalid credential format: workload identity support only one credentials entry")
469+
}
449470
selectedCreds, allSubscriptionIDs, _, err = a.handleMultipleServicePrincipals(v, log)
450471
if err != nil {
451472
return armresourcegraph.ClientResourcesResponse{}, err
@@ -454,35 +475,91 @@ func (a *AzureQuery) azQuery(ctx context.Context, azureCreds interface{}, in *v1
454475
return armresourcegraph.ClientResourcesResponse{}, errors.New("invalid credential format")
455476
}
456477

457-
tenantID := selectedCreds["tenantId"]
458-
clientID := selectedCreds["clientId"]
459-
clientSecret := selectedCreds["clientSecret"]
478+
switch identityType {
479+
case v1beta1.IdentityTypeAzureServicePrincipalCredentials:
480+
log.Info("Using authentication method", "identityType", v1beta1.IdentityTypeAzureServicePrincipalCredentials)
481+
client, err = a.initializeClientSecretProvider(selectedCreds, log)
482+
if err != nil {
483+
return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to initialize service principal provider")
484+
}
485+
case v1beta1.IdentityTypeAzureWorkloadIdentityCredentials:
486+
log.Info("Using authentication method", "identityType", v1beta1.IdentityTypeAzureWorkloadIdentityCredentials)
487+
client, err = a.initializeWorkloadIdentityProvider(selectedCreds, log)
488+
if err != nil {
489+
return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to initialize workload identity provider")
490+
}
491+
}
492+
493+
// Setup the query request
494+
queryRequest := a.setupQueryRequest(in, allSubscriptionIDs, log)
495+
496+
// Create the query request, Run the query and get the results.
497+
results, err = client.Resources(ctx, queryRequest, nil)
498+
if err != nil {
499+
return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to finish the request")
500+
}
501+
return results, nil
502+
}
503+
504+
func (a *AzureQuery) initializeWorkloadIdentityProvider(azureCreds map[string]string, log logging.Logger) (*armresourcegraph.Client, error) {
505+
tokenFilePath := azureCreds[WorkloadIdentityCredentialPath]
506+
507+
options := &azidentity.WorkloadIdentityCredentialOptions{
508+
TokenFilePath: tokenFilePath,
509+
}
510+
511+
// Defaults to the value of the environment variable AZURE_TENANT_ID
512+
tenantID, found := azureCreds[TenantID]
513+
if found {
514+
options.TenantID = tenantID
515+
}
516+
517+
// Defaults to the value of the environment variable AZURE_CLIENT_ID
518+
clientID, found := azureCreds[ClientID]
519+
if found {
520+
options.ClientID = clientID
521+
}
522+
523+
// Create Azure credential
524+
log.Info("Initializing workload identity provider", "tokenFile", tokenFilePath)
525+
526+
cred, err := azidentity.NewWorkloadIdentityCredential(options)
527+
if err != nil {
528+
return nil, errors.Wrap(err, "failed to obtain workloadidentity credentials")
529+
}
530+
531+
// Create and authorize a ResourceGraph client
532+
client, err := armresourcegraph.NewClient(cred, nil)
533+
if err != nil {
534+
return nil, errors.Wrap(err, "failed to create client")
535+
}
460536

461-
// Log credential information using structured logging (without sensitive data)
462537
log.Debug("Selected service principal", "clientId", clientID)
463538

539+
return client, nil
540+
}
541+
542+
func (a *AzureQuery) initializeClientSecretProvider(azureCreds map[string]string, log logging.Logger) (*armresourcegraph.Client, error) {
543+
tenantID := azureCreds[TenantID]
544+
clientID := azureCreds[ClientID]
545+
clientSecret := azureCreds[ClientSecret]
546+
464547
// To configure DefaultAzureCredential to authenticate a user-assigned managed identity,
465548
// set the environment variable AZURE_CLIENT_ID to the identity's client ID.
466549
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
467550
if err != nil {
468-
return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to obtain credentials")
551+
return nil, errors.Wrap(err, "failed to obtain clientsecret credentials")
469552
}
470553

471554
// Create and authorize a ResourceGraph client
472555
client, err := armresourcegraph.NewClient(cred, nil)
473556
if err != nil {
474-
return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to create client")
557+
return nil, errors.Wrap(err, "failed to create client")
475558
}
476559

477-
// Setup the query request
478-
queryRequest := a.setupQueryRequest(in, allSubscriptionIDs, log)
560+
log.Debug("Selected service principal", "clientId", clientID)
479561

480-
// Create the query request, Run the query and get the results.
481-
results, err := client.Resources(ctx, queryRequest, nil)
482-
if err != nil {
483-
return armresourcegraph.ClientResourcesResponse{}, errors.Wrap(err, "failed to finish the request")
484-
}
485-
return results, nil
562+
return client, nil
486563
}
487564

488565
// ParseNestedKey enables the bracket and dot notation to key reference

0 commit comments

Comments
 (0)