Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support workload identity federation #15615

Closed
chlowell opened this issue Sep 22, 2021 · 15 comments
Closed

Support workload identity federation #15615

chlowell opened this issue Sep 22, 2021 · 15 comments
Assignees
Labels
Azure.Identity Client This issue points to a problem in the data-plane of the library. feature-request This issue requires a new behavior in the product in order be resolved.
Milestone

Comments

@chlowell
Copy link
Member

chlowell commented Sep 22, 2021

Blocked on #17417

Workload Identity isn't yet supported out of the box but my comment below (#15615 (comment)) has an implementation you can copy in the meantime for use with azidentity v1.2.0-beta.2 or later.

@chlowell chlowell added Azure.Identity Client This issue points to a problem in the data-plane of the library. feature-request This issue requires a new behavior in the product in order be resolved. labels Sep 22, 2021
@chlowell chlowell added this to the Backlog milestone Sep 22, 2021
@chlowell chlowell self-assigned this Sep 22, 2021
@chlowell chlowell changed the title Support token exchange for managed identity Support workload identity federation Feb 9, 2022
@s-bauer
Copy link

s-bauer commented Feb 15, 2022

Hey any eta on when this will be added?

@chlowell
Copy link
Member Author

May at the earliest, in the current plan. #16902 has an example you could follow in the meantime if you want to implement it yourself.

@tkent
Copy link

tkent commented Jun 28, 2022

I hope this is added soon. This is a pretty important feature since azure-workload-identity is replacing the prior method of k8s identity assignment (aad-pod-identity, which does work with the existing SDK)

Getting azure-workload-identity setup is straightforward enough, but once you do you quickly realize it's unusable if your applications use identities resolved using the azure-go-sdk.

@tkent
Copy link

tkent commented Jun 30, 2022

For other folks stuck dealing with this, the code below is what we ended up using for our application. It's been working for the past few months in our AWS EKS clusters using Azure Workload Identity.

import (
	"context"
	"errors"
	"fmt"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
	"os"
	"sync"
	"time"
)

// !! Warning - A regrettable hack !!
//
// MSFT has put their golang sdk consumers in a tough spot. Azure Workload
// Identity is replacing AAD Pod Identity as the solution to provide
// dynamically assigned credentials to workloads in a k8s cluster.
//
// Unfortunately, the azure-go-sdk's identity module simply doesn't
// support this type of authentication and the request to do so has
// been open since 09/2021.
//
//	https://github.com/Azure/azure-sdk-for-go/issues/15615
//
// The contents of this file provides a functional azcore.TokenCredential
// implementation that will work with the assertion provided by Azure
// Workload Identity.
//
// It is very difficult to test this and no care around caching was taken.
// This has worked well in the short time we've used it and the hope is that
// MSFT will add the functionality into their golang sdk in the next months.
//
// A sample of using this alongside the `DefaultAzureCredential` to mimic
// the behavior in other SDKs:
//
//	func ResolveIdentity() (azcore.TokenCredential, error) {
//	   if os.Getenv("AZURE_FEDERATED_TOKEN_FILE") != "" {
//			return NewWorkloadIdentityClientHack()
//		  }
//	   return azidentity.NewDefaultAzureCredential(nil)
//	}
type WorkloadIdentityClientHack struct {
	tenantId      string
	clientId      string
	authorityUrl  string
	tokenFilePath string
	mu            sync.Mutex
	activeToken   *azcore.AccessToken
	minLifetime   time.Duration
}

func (c *WorkloadIdentityClientHack) readAssertionToken() (string, error) {
	tokenBytes, err := os.ReadFile(c.tokenFilePath)
	if err != nil {
		return "", err
	}
	return string(tokenBytes), nil
}

func (c *WorkloadIdentityClientHack) GetToken(
	ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	cutoff := time.Now().Add(c.minLifetime)

	if c.activeToken != nil && c.activeToken.ExpiresOn.After(cutoff) {
		return *c.activeToken, nil
	}
	c.activeToken = nil
	assertionToken, err := c.readAssertionToken()
	if err != nil {
		return azcore.AccessToken{}, err
	}

	cred, err := confidential.NewCredFromAssertion(assertionToken)

	client, err := confidential.New(
		c.clientId,
		cred,
		confidential.WithAuthority(c.authorityUrl),
	)

	result, err := client.AcquireTokenByCredential(ctx, opts.Scopes)
	if err != nil {
		return azcore.AccessToken{}, err
	}
	c.activeToken = &azcore.AccessToken{
		Token: result.AccessToken, ExpiresOn: result.ExpiresOn.UTC()}
	return *c.activeToken, nil
}

func NewWorkloadIdentityClientHack() (*WorkloadIdentityClientHack, error) {
	tenantId := os.Getenv("AZURE_TENANT_ID")
	clientId := os.Getenv("AZURE_CLIENT_ID")
	tokenFilePath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
	authorityHost := os.Getenv("AZURE_AUTHORITY_HOST")

	if tenantId == "" {
		return nil, errors.New("AZURE_TENANT_ID must be set")
	}
	if clientId == "" {
		return nil, errors.New("AZURE_CLIENT_ID must be set")
	}
	if tokenFilePath == "" {
		return nil, errors.New("AZURE_FEDERATED_TOKEN_FILE must be set")
	}
	if authorityHost == "" {
		return nil, errors.New("AZURE_AUTHORITY_HOST must be set")
	}

	return &WorkloadIdentityClientHack{
		tenantId:      tenantId,
		clientId:      clientId,
		authorityUrl:  fmt.Sprintf("%s%s/oauth2/token", authorityHost, tenantId),
		tokenFilePath: tokenFilePath,
		mu:            sync.Mutex{},
		minLifetime:   time.Minute * 5,
	}, nil
}

@chlowell
Copy link
Member Author

chlowell commented Aug 10, 2022

We published 1.2.0-beta.2 today with a new credential type, ClientAssertionCredential, that does most of what's required for Workload Identity. I expect to have full Workload Identity support in v1.2.0 but it didn't make the cut for this beta because we're still discussing where it fits in the public API. Edit: the API design has slipped to the next release cycle, so azidentity v1.2.0 won't have Workload Identity support in box. In the meantime, it's easy to leverage ClientAssertionCredential for Workload Identity. For example, here's my prototype:

import (
	"context"
	"os"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)

type workloadIdentityCredential struct {
	assertion, file string
	cred            *azidentity.ClientAssertionCredential
	lastRead        time.Time
}

type workloadIdentityCredentialOptions struct {
	azcore.ClientOptions
}

func newWorkloadIdentityCredential(tenantID, clientID, file string, options *workloadIdentityCredentialOptions) (*workloadIdentityCredential, error) {
	w := &workloadIdentityCredential{file: file}
	cred, err := azidentity.NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &azidentity.ClientAssertionCredentialOptions{ClientOptions: options.ClientOptions})
	if err != nil {
		return nil, err
	}
	w.cred = cred
	return w, nil
}

func (w *workloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
	return w.cred.GetToken(ctx, opts)
}

func (w *workloadIdentityCredential) getAssertion(context.Context) (string, error) {
	if now := time.Now(); w.lastRead.Add(5 * time.Minute).Before(now) {
		content, err := os.ReadFile(w.file)
		if err != nil {
			return "", err
		}
		w.assertion = string(content)
		w.lastRead = now
	}
	return w.assertion, nil
}

@berndverst
Copy link
Member

Dapr depends on this. We have a few users inquiring about this.

@mblaschke-daimlertruck
Copy link

Also interested in workload identity support for golang applications for our apps, there is a high demand here.

@jkroepke
Copy link

@mblaschke-daimlertruck in mean time, Azure provides a workload identity sidecar solution. If the applications supports managed identities, the sidecar approach may help here.

See https://learn.microsoft.com/en-us/azure/aks/workload-identity-migration-sidecar

@mblaschke-daimlertruck
Copy link

Is there any timeline for this feature?

@jkroepke
Copy link

It's already in public preview, you can use it

@chlowell chlowell modified the milestones: 2022-10, 2023-01 Oct 17, 2022
@mblaschke-daimlertruck
Copy link

@jkroepke
My question was related to the azure-sdk-for-go native support for workload identities.
I would like to avoid the sidecar solution if possible.

@chlowell
Copy link
Member Author

chlowell commented Nov 2, 2022

We plan to ship azidentity with built-in workload identity support in early January. In the meantime, my code above works with v1.2.0, currently available as a beta; stable v1.2.0 will ship next week.

@mblaschke-daimlertruck
Copy link

@chlowell
sorry for another question, will this be a stable release in early January and will there be a beta release before?

@chlowell
Copy link
Member Author

chlowell commented Nov 7, 2022

No problem, questions are welcome. It will be a beta release in January. I might be able to get a first beta out in December, depending on how vacations align.

@chlowell
Copy link
Member Author

We published azidentity v1.3.0-beta.1 today with a new WorkloadIdentityCredential. In this version DefaultAzureCredential also supports workload identity on Kubernetes when the environment contains configuration from the admission webhook (see its installation guide for details).

I'm closing this issue because the feature is now available, but please feel free to open another issue if you encounter any problems using the new azidentity.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Azure.Identity Client This issue points to a problem in the data-plane of the library. feature-request This issue requires a new behavior in the product in order be resolved.
Projects
None yet
Development

No branches or pull requests

7 participants