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  }