-
Notifications
You must be signed in to change notification settings - Fork 917
WorkloadIdentityCredential enhancement for AKS FIC limit #24442
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
base: main
Are you sure you want to change the base?
Changes from all commits
54ae573
2dc755d
06c701e
ec9cc34
1bbabbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
### Bugs Fixed | ||
|
||
### Other Changes | ||
* enhanced support for AKS workloads | ||
|
||
## 1.9.0 (2025-04-08) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,18 +7,31 @@ | |
package azidentity | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"strings" | ||
"sync" | ||
"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/azcore/runtime" | ||
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo" | ||
) | ||
|
||
const credNameWorkloadIdentity = "WorkloadIdentityCredential" | ||
const ( | ||
aksCAData = "AZURE_KUBERNETES_CA_DATA" | ||
aksCAFile = "AZURE_KUBERNETES_CA_FILE" | ||
aksSNIName = "AZURE_KUBERNETES_SNI_NAME" | ||
aksTokenEndpoint = "AZURE_KUBERNETES_TOKEN_ENDPOINT" | ||
credNameWorkloadIdentity = "WorkloadIdentityCredential" | ||
) | ||
|
||
// WorkloadIdentityCredential supports Azure workload identity on Kubernetes. | ||
// See [Azure Kubernetes Service documentation] for more information. | ||
|
@@ -94,6 +107,13 @@ func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) ( | |
ClientOptions: options.ClientOptions, | ||
DisableInstanceDiscovery: options.DisableInstanceDiscovery, | ||
} | ||
if p, err := newAKSTokenRequestPolicy(); err != nil { | ||
return nil, err | ||
} else if p != nil { | ||
// add the policy to the end of the pipeline. It will run | ||
// after all other policies, including any added by the caller | ||
caco.ClientOptions.PerRetryPolicies = append(caco.ClientOptions.PerRetryPolicies, p) | ||
} | ||
cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &caco) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -139,3 +159,115 @@ func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, erro | |
} | ||
return w.assertion, nil | ||
} | ||
|
||
// aksTokenRequestPolicy redirects token requests to the AKS token endpoint, sending them via its | ||
// own HTTP client. It sends all other requests unchanged, via the pipeline's configured transport. | ||
type aksTokenRequestPolicy struct { | ||
// c is configured for the AKS token endpoint | ||
c *http.Client | ||
Comment on lines
+166
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will the request seen by this client still contain the correct headers such as user agent, etc? |
||
// ca trusted by c | ||
ca []byte | ||
caFile, host, serverName string | ||
} | ||
|
||
func newAKSTokenRequestPolicy() (*aksTokenRequestPolicy, error) { | ||
host := os.Getenv(aksTokenEndpoint) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a host, it is a full URL that can contain a path. |
||
serverName := os.Getenv(aksSNIName) | ||
if host == "" || serverName == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Server name should not be required. Only host should be needed to use this code path. |
||
// the AKS feature isn't enabled for this process | ||
return nil, nil | ||
} | ||
b := []byte(os.Getenv(aksCAData)) | ||
f := os.Getenv(aksCAFile) | ||
switch { | ||
case len(b) == 0 && len(f) == 0: | ||
return nil, fmt.Errorf("no value found for %s or %s", aksCAData, aksCAFile) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are optional, system roots should be used if these are not given. |
||
case len(b) > 0 && len(f) > 0: | ||
return nil, fmt.Errorf("found values for both %s and %s", aksCAData, aksCAFile) | ||
} | ||
p := &aksTokenRequestPolicy{caFile: f, ca: b, host: host, serverName: serverName} | ||
if _, err := p.client(); err != nil { | ||
return nil, err | ||
} | ||
return p, nil | ||
} | ||
|
||
func (a *aksTokenRequestPolicy) Do(req *policy.Request) (*http.Response, error) { | ||
if r := req.Raw(); strings.HasSuffix(r.URL.Path, "/token") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not believe this assumption is correct. |
||
c, err := a.client() | ||
if err != nil { | ||
return nil, errorinfo.NonRetriableError(err) | ||
} | ||
r.URL.Host = a.host | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not correct. |
||
r.Host = "" | ||
res, err := c.Do(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if res == nil { | ||
// this policy is effectively a transport, so it must handle | ||
// this rare case. Returning an error makes the retry policy | ||
// try the request again | ||
err = errors.New("received nil response") | ||
} | ||
return res, err | ||
} | ||
return req.Next() | ||
} | ||
|
||
func (a *aksTokenRequestPolicy) client() (*http.Client, error) { | ||
// this function doesn't need synchronization because | ||
// it's called under confidentialClient's lock | ||
|
||
if a.caFile == "" { | ||
// host provided CA bytes in AZURE_KUBERNETES_CA_DATA and can't change | ||
// them now, so we need to create a client only if we haven't done so yet | ||
if a.c == nil { | ||
if len(a.ca) == 0 { | ||
return nil, errors.New("no value found for " + aksCAData) | ||
} | ||
cp := x509.NewCertPool() | ||
if !cp.AppendCertsFromPEM(a.ca) { | ||
return nil, errors.New("couldn't parse " + aksCAData) | ||
} | ||
a.c = &http.Client{ | ||
Transport: &http.Transport{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there other transport level defaults that the SDK normally uses? |
||
TLSClientConfig: &tls.Config{ | ||
RootCAs: cp, | ||
ServerName: a.serverName, | ||
}, | ||
}, | ||
} | ||
// this copy of the CA bytes is redundant because we've | ||
// configured the client and won't execute this block again | ||
a.ca = nil | ||
} | ||
return a.c, nil | ||
} | ||
|
||
// host provided the CA bytes in a file whose contents it can change, | ||
// so we must read that file and maybe create a new client | ||
b, err := os.ReadFile(a.caFile) | ||
if err != nil { | ||
return nil, fmt.Errorf("couldn't parse %s: %s", aksCAFile, err) | ||
} | ||
if len(b) == 0 { | ||
return nil, errors.New(aksCAFile + " specifies an empty file") | ||
} | ||
if !bytes.Equal(b, a.ca) { | ||
a.ca = b | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should only be set after |
||
cp := x509.NewCertPool() | ||
if !cp.AppendCertsFromPEM(a.ca) { | ||
return nil, errors.New("couldn't parse " + aksCAFile) | ||
} | ||
a.c = &http.Client{ | ||
Transport: &http.Transport{ | ||
TLSClientConfig: &tls.Config{ | ||
RootCAs: cp, | ||
ServerName: a.serverName, | ||
}, | ||
}, | ||
} | ||
} | ||
return a.c, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the SDK changes are generic and are meant to be usable on any kube running on Azure, i.e. AKS, CAPZ, etc.