@@ -11,9 +11,11 @@ import (
11
11
"crypto/tls"
12
12
"crypto/x509"
13
13
"errors"
14
+ "fmt"
14
15
"net/http"
15
16
"net/url"
16
17
"os"
18
+ "strings"
17
19
"sync"
18
20
"time"
19
21
@@ -34,10 +36,14 @@ type WorkloadIdentityCredential struct {
34
36
expires time.Time
35
37
mtx * sync.RWMutex
36
38
// identity binding mode fields
37
- identityBinding bool
38
- kubernetesCAFile string
39
- kubernetesSNIName string
40
- kubernetesTokenEndpoint string
39
+ identityBinding bool
40
+ kubernetesCAFile string
41
+ kubernetesSNIName string
42
+ kubernetesTokenEndpoint * url.URL
43
+ // CA certificate caching fields
44
+ caCert []byte
45
+ caCertPool * x509.CertPool
46
+ caExpires time.Time
41
47
}
42
48
43
49
// WorkloadIdentityCredentialOptions contains optional parameters for WorkloadIdentityCredential.
@@ -100,16 +106,23 @@ func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (
100
106
w := WorkloadIdentityCredential {file : file , mtx : & sync.RWMutex {}}
101
107
102
108
// Check for identity binding mode environment variables
103
- kubernetesTokenEndpoint := os .Getenv (azureKubernetesTokenEndpoint )
109
+ kubernetesTokenEndpointStr := os .Getenv (azureKubernetesTokenEndpoint )
104
110
kubernetesSNIName := os .Getenv (azureKubernetesSNIName )
105
111
kubernetesCAFile := os .Getenv (azureKubernetesCAFile )
106
112
107
113
// If any of the identity binding environment variables are present, enable identity binding mode
108
- if kubernetesTokenEndpoint != "" || kubernetesSNIName != "" || kubernetesCAFile != "" {
114
+ if kubernetesTokenEndpointStr != "" || kubernetesSNIName != "" || kubernetesCAFile != "" {
109
115
// All three variables must be present for identity binding mode
110
- if kubernetesTokenEndpoint == "" || kubernetesSNIName == "" || kubernetesCAFile == "" {
116
+ if kubernetesTokenEndpointStr == "" || kubernetesSNIName == "" || kubernetesCAFile == "" {
111
117
return nil , errors .New ("identity binding mode requires all three environment variables: AZURE_KUBERNETES_TOKEN_ENDPOINT, AZURE_KUBERNETES_SNI_NAME, and AZURE_KUBERNETES_CA_FILE" )
112
118
}
119
+
120
+ // Parse the Kubernetes token endpoint URL
121
+ kubernetesTokenEndpoint , err := url .Parse (kubernetesTokenEndpointStr )
122
+ if err != nil {
123
+ return nil , fmt .Errorf ("failed to parse Kubernetes token endpoint URL: %w" , err )
124
+ }
125
+
113
126
w .identityBinding = true
114
127
w .kubernetesTokenEndpoint = kubernetesTokenEndpoint
115
128
w .kubernetesSNIName = kubernetesSNIName
@@ -125,29 +138,10 @@ func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (
125
138
126
139
// If identity binding mode is enabled, configure a custom HTTP client
127
140
if w .identityBinding {
128
- // Load the CA certificate for the Kubernetes endpoint
129
- caCert , err := os .ReadFile (w .kubernetesCAFile )
130
- if err != nil {
131
- return nil , errors .New ("failed to read Kubernetes CA file: " + err .Error ())
132
- }
133
-
134
- caCertPool := x509 .NewCertPool ()
135
- if ! caCertPool .AppendCertsFromPEM (caCert ) {
136
- return nil , errors .New ("failed to parse Kubernetes CA certificate" )
137
- }
138
-
139
- // Create custom transport with the CA and SNI configuration
140
- transport := & http.Transport {
141
- TLSClientConfig : & tls.Config {
142
- RootCAs : caCertPool ,
143
- ServerName : w .kubernetesSNIName ,
144
- },
145
- }
146
-
147
141
// Override the client options to use our custom transport
148
142
caco .ClientOptions .Transport = & identityBindingTransport {
149
- base : transport ,
150
- kubernetesTokenEndpoint : w .kubernetesTokenEndpoint ,
143
+ credential : & w ,
144
+ kubernetesSNIName : w .kubernetesSNIName ,
151
145
}
152
146
}
153
147
@@ -197,42 +191,87 @@ func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, erro
197
191
return w .assertion , nil
198
192
}
199
193
194
+ // loadKubernetesCA loads and caches the Kubernetes CA certificate
195
+ func (w * WorkloadIdentityCredential ) loadKubernetesCA () (* x509.CertPool , error ) {
196
+ w .mtx .RLock ()
197
+ if w .caExpires .After (time .Now ()) && w .caCertPool != nil {
198
+ defer w .mtx .RUnlock ()
199
+ return w .caCertPool , nil
200
+ }
201
+
202
+ // ensure only one goroutine at a time updates the CA cert
203
+ w .mtx .RUnlock ()
204
+ w .mtx .Lock ()
205
+ defer w .mtx .Unlock ()
206
+
207
+ // double check because another goroutine may have acquired the write lock first and done the update
208
+ if now := time .Now (); w .caExpires .After (now ) && w .caCertPool != nil {
209
+ return w .caCertPool , nil
210
+ }
211
+
212
+ // Load the CA certificate for the Kubernetes endpoint
213
+ caCert , err := os .ReadFile (w .kubernetesCAFile )
214
+ if err != nil {
215
+ return nil , fmt .Errorf ("failed to read Kubernetes CA file: %w" , err )
216
+ }
217
+
218
+ caCertPool := x509 .NewCertPool ()
219
+ if ! caCertPool .AppendCertsFromPEM (caCert ) {
220
+ return nil , errors .New ("failed to parse Kubernetes CA certificate" )
221
+ }
222
+
223
+ // Cache the CA certificate for 10 minutes (same as token assertion)
224
+ w .caCert = caCert
225
+ w .caCertPool = caCertPool
226
+ w .caExpires = time .Now ().Add (10 * time .Minute )
227
+
228
+ return caCertPool , nil
229
+ }
230
+
200
231
// identityBindingTransport is a custom HTTP transport that redirects token requests
201
232
// to the Kubernetes token endpoint when in identity binding mode
202
233
type identityBindingTransport struct {
203
- base http. RoundTripper
204
- kubernetesTokenEndpoint string
234
+ credential * WorkloadIdentityCredential
235
+ kubernetesSNIName string
205
236
}
206
237
207
238
func (t * identityBindingTransport ) Do (req * http.Request ) (* http.Response , error ) {
208
239
// Check if this is a token request to the Azure authority host
209
- if req .URL .Path != "" && (req .URL .Host == "login.microsoftonline.com" ||
240
+ if strings . HasSuffix ( req .URL .Path , "/oauth2/v2.0/token" ) && (req .URL .Host == "login.microsoftonline.com" ||
210
241
req .URL .Host == "login.microsoftonline.us" ||
211
242
req .URL .Host == "login.partner.microsoftonline.cn" ||
212
243
req .URL .Host == "login.microsoftonline.de" ) {
213
244
// This is a token request, redirect to Kubernetes endpoint
214
245
215
- // Clone the request to avoid modifying the original
216
- newReq := req .Clone (req .Context ())
217
-
218
- // Parse the Kubernetes token endpoint
219
- kubernetesURL , err := url .Parse (t .kubernetesTokenEndpoint )
246
+ // Load the CA certificate (this will use cached version if still valid)
247
+ caCertPool , err := t .credential .loadKubernetesCA ()
220
248
if err != nil {
221
249
return nil , err
222
250
}
223
251
252
+ // Create custom transport with the CA and SNI configuration
253
+ transport := & http.Transport {
254
+ TLSClientConfig : & tls.Config {
255
+ RootCAs : caCertPool ,
256
+ ServerName : t .kubernetesSNIName ,
257
+ },
258
+ }
259
+
260
+ // Clone the request to avoid modifying the original
261
+ newReq := req .Clone (req .Context ())
262
+
224
263
// Update the URL to point to the Kubernetes endpoint
225
- newReq .URL .Scheme = kubernetesURL .Scheme
226
- newReq .URL .Host = kubernetesURL .Host
227
- newReq .Host = kubernetesURL .Host
264
+ newReq .URL .Scheme = t . credential . kubernetesTokenEndpoint .Scheme
265
+ newReq .URL .Host = t . credential . kubernetesTokenEndpoint .Host
266
+ newReq .Host = t . credential . kubernetesTokenEndpoint .Host
228
267
229
268
// Preserve the original path (contains tenant ID and token endpoint path)
230
269
// The path should be something like "/tenant-id/oauth2/v2.0/token"
231
270
// Keep the original path to maintain the token request structure
232
271
233
- return t . base .RoundTrip (newReq )
272
+ return transport .RoundTrip (newReq )
234
273
}
235
274
236
- // For non-token requests, use the base transport as-is
237
- return t . base .RoundTrip (req )
275
+ // For non-token requests, use the default transport
276
+ return http . DefaultTransport .RoundTrip (req )
238
277
}
0 commit comments