-
Notifications
You must be signed in to change notification settings - Fork 839
/
workload_identity.go
131 lines (120 loc) · 5.33 KB
/
workload_identity.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"os"
"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"
)
const credNameWorkloadIdentity = "WorkloadIdentityCredential"
// WorkloadIdentityCredential supports Azure workload identity on Kubernetes.
// See [Azure Kubernetes Service documentation] for more information.
//
// [Azure Kubernetes Service documentation]: https://learn.microsoft.com/azure/aks/workload-identity-overview
type WorkloadIdentityCredential struct {
assertion, file string
cred *ClientAssertionCredential
expires time.Time
mtx *sync.RWMutex
}
// WorkloadIdentityCredentialOptions contains optional parameters for WorkloadIdentityCredential.
type WorkloadIdentityCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// ClientID of the service principal. Defaults to the value of the environment variable AZURE_CLIENT_ID.
ClientID string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Microsoft Entra instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID of the service principal. Defaults to the value of the environment variable AZURE_TENANT_ID.
TenantID string
// TokenFilePath is the path of a file containing a Kubernetes service account token. Defaults to the value of the
// environment variable AZURE_FEDERATED_TOKEN_FILE.
TokenFilePath string
}
// NewWorkloadIdentityCredential constructs a WorkloadIdentityCredential. Service principal configuration is read
// from environment variables as set by the Azure workload identity webhook. Set options to override those values.
func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (*WorkloadIdentityCredential, error) {
if options == nil {
options = &WorkloadIdentityCredentialOptions{}
}
ok := false
clientID := options.ClientID
if clientID == "" {
if clientID, ok = os.LookupEnv(azureClientID); !ok {
return nil, errors.New("no client ID specified. Check pod configuration or set ClientID in the options")
}
}
file := options.TokenFilePath
if file == "" {
if file, ok = os.LookupEnv(azureFederatedTokenFile); !ok {
return nil, errors.New("no token file specified. Check pod configuration or set TokenFilePath in the options")
}
}
tenantID := options.TenantID
if tenantID == "" {
if tenantID, ok = os.LookupEnv(azureTenantID); !ok {
return nil, errors.New("no tenant ID specified. Check pod configuration or set TenantID in the options")
}
}
w := WorkloadIdentityCredential{file: file, mtx: &sync.RWMutex{}}
caco := ClientAssertionCredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &caco)
if err != nil {
return nil, err
}
// we want "WorkloadIdentityCredential" in log messages, not "ClientAssertionCredential"
cred.client.name = credNameWorkloadIdentity
w.cred = cred
return &w, nil
}
// GetToken requests an access token from Microsoft Entra ID. Azure SDK clients call this method automatically.
func (w *WorkloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var err error
ctx, endSpan := runtime.StartSpan(ctx, credNameWorkloadIdentity+"."+traceOpGetToken, w.cred.client.azClient.Tracer(), nil)
defer func() { endSpan(err) }()
tk, err := w.cred.GetToken(ctx, opts)
return tk, err
}
// getAssertion returns the specified file's content, which is expected to be a Kubernetes service account token.
// Kubernetes is responsible for updating the file as service account tokens expire.
func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, error) {
w.mtx.RLock()
if w.expires.Before(time.Now()) {
// ensure only one goroutine at a time updates the assertion
w.mtx.RUnlock()
w.mtx.Lock()
defer w.mtx.Unlock()
// double check because another goroutine may have acquired the write lock first and done the update
if now := time.Now(); w.expires.Before(now) {
content, err := os.ReadFile(w.file)
if err != nil {
return "", err
}
w.assertion = string(content)
// Kubernetes rotates service account tokens when they reach 80% of their total TTL. The shortest TTL
// is 1 hour. That implies the token we just read is valid for at least 12 minutes (20% of 1 hour),
// but we add some margin for safety.
w.expires = now.Add(10 * time.Minute)
}
} else {
defer w.mtx.RUnlock()
}
return w.assertion, nil
}