sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/scope/workload_identity.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package scope 18 19 import ( 20 "context" 21 "os" 22 "strings" 23 "time" 24 25 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 26 "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 27 "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 28 "github.com/pkg/errors" 29 ) 30 31 /* 32 33 For workload identity to work we need the following. 34 35 |-----------------------------------------------------------------------------------| 36 |AZURE_AUTHORITY_HOST | The Azure Active Directory (AAD) endpoint. | 37 |AZURE_CLIENT_ID | The client ID of the Azure AD | 38 | | application or user-assigned managed identity. | 39 |AZURE_TENANT_ID | The tenant ID of the Azure subscription. | 40 |AZURE_FEDERATED_TOKEN_FILE | The path of the projected service account token file. | 41 |-----------------------------------------------------------------------------------| 42 43 With the current implementation, AZURE_CLIENT_ID and AZURE_TENANT_ID are read via AzureClusterIdentity 44 object and fallback to reading from env variables if not found on AzureClusterIdentity. 45 46 AZURE_FEDERATED_TOKEN_FILE is the path of the projected service account token which is by default 47 "/var/run/secrets/azure/tokens/azure-identity-token". 48 The path can be overridden by setting "AZURE_FEDERATED_TOKEN_FILE" env variable. 49 50 */ 51 52 const ( 53 // azureFederatedTokenFileEnvKey is the env key for AZURE_FEDERATED_TOKEN_FILE. 54 azureFederatedTokenFileEnvKey = "AZURE_FEDERATED_TOKEN_FILE" 55 // azureClientIDEnvKey is the env key for AZURE_CLIENT_ID. 56 azureClientIDEnvKey = "AZURE_CLIENT_ID" 57 // azureTenantIDEnvKey is the env key for AZURE_TENANT_ID. 58 azureTenantIDEnvKey = "AZURE_TENANT_ID" 59 // azureTokenFilePath is the path of the projected token. 60 azureTokenFilePath = "/var/run/secrets/azure/tokens/azure-identity-token" // #nosec G101 61 // azureFederatedTokenFileRefreshTime is the time interval after which it should be read again. 62 azureFederatedTokenFileRefreshTime = 5 * time.Minute 63 ) 64 65 type workloadIdentityCredential struct { 66 assertion string 67 file string 68 cred *azidentity.ClientAssertionCredential 69 lastRead time.Time 70 } 71 72 // WorkloadIdentityCredentialOptions contains the configurable options for azwi. 73 type WorkloadIdentityCredentialOptions struct { 74 azcore.ClientOptions 75 ClientID string 76 TenantID string 77 TokenFilePath string 78 } 79 80 // NewWorkloadIdentityCredentialOptions returns an empty instance of WorkloadIdentityCredentialOptions. 81 func NewWorkloadIdentityCredentialOptions() *WorkloadIdentityCredentialOptions { 82 return &WorkloadIdentityCredentialOptions{} 83 } 84 85 // WithClientID sets client ID to WorkloadIdentityCredentialOptions. 86 func (w *WorkloadIdentityCredentialOptions) WithClientID(clientID string) *WorkloadIdentityCredentialOptions { 87 w.ClientID = strings.TrimSpace(clientID) 88 return w 89 } 90 91 // WithTenantID sets tenant ID to WorkloadIdentityCredentialOptions. 92 func (w *WorkloadIdentityCredentialOptions) WithTenantID(tenantID string) *WorkloadIdentityCredentialOptions { 93 w.TenantID = strings.TrimSpace(tenantID) 94 return w 95 } 96 97 // getProjectedTokenPath return projected token file path from the env variable. 98 func getProjectedTokenPath() string { 99 tokenPath := strings.TrimSpace(os.Getenv(azureFederatedTokenFileEnvKey)) 100 if tokenPath == "" { 101 return azureTokenFilePath 102 } 103 return tokenPath 104 } 105 106 // WithDefaults sets token file path. It also sets the client tenant ID from injected env in 107 // case empty values are passed. 108 func (w *WorkloadIdentityCredentialOptions) WithDefaults() (*WorkloadIdentityCredentialOptions, error) { 109 w.TokenFilePath = getProjectedTokenPath() 110 111 // Fallback to using client ID from env variable if not set. 112 if w.ClientID == "" { 113 w.ClientID = strings.TrimSpace(os.Getenv(azureClientIDEnvKey)) 114 if w.ClientID == "" { 115 return nil, errors.New("empty client ID") 116 } 117 } 118 119 // Fallback to using tenant ID from env variable. 120 if w.TenantID == "" { 121 w.TenantID = strings.TrimSpace(os.Getenv(azureTenantIDEnvKey)) 122 if w.TenantID == "" { 123 return nil, errors.New("empty tenant ID") 124 } 125 } 126 return w, nil 127 } 128 129 // NewWorkloadIdentityCredential returns a workload identity credential. 130 func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) { 131 w := &workloadIdentityCredential{file: options.TokenFilePath} 132 cred, err := azidentity.NewClientAssertionCredential(options.TenantID, options.ClientID, w.getAssertion, &azidentity.ClientAssertionCredentialOptions{ClientOptions: options.ClientOptions}) 133 if err != nil { 134 return nil, err 135 } 136 w.cred = cred 137 return w, nil 138 } 139 140 // GetToken returns the token for azwi. 141 func (w *workloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { 142 return w.cred.GetToken(ctx, opts) 143 } 144 145 func (w *workloadIdentityCredential) getAssertion(context.Context) (string, error) { 146 if now := time.Now(); w.lastRead.Add(azureFederatedTokenFileRefreshTime).Before(now) { 147 content, err := os.ReadFile(w.file) 148 if err != nil { 149 return "", err 150 } 151 w.assertion = string(content) 152 w.lastRead = now 153 } 154 return w.assertion, nil 155 }