istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/credentialfetcher/plugin/gce.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // This is Google plugin of credentialfetcher.
    16  
    17  package plugin
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"cloud.google.com/go/compute/metadata"
    28  
    29  	"istio.io/istio/pkg/log"
    30  	"istio.io/istio/security/pkg/util"
    31  )
    32  
    33  var gcecredLog = log.RegisterScope("gcecred", "GCE credential fetcher for istio agent")
    34  
    35  // Token refresh frequency is default to 5 minutes.
    36  var rotationInterval = 5 * time.Minute
    37  
    38  // GCE VM credential needs refresh if remaining life time is below 25 minutes.
    39  var gracePeriod = 25 * time.Minute
    40  
    41  // rotateToken determines whether to start periodic token rotation job.
    42  // This is enabled by default.
    43  var rotateToken = true
    44  
    45  // SetTokenRotation enable/disable periodic token rotation job.
    46  // This is only for testing purpose, not thread safe.
    47  func SetTokenRotation(enable bool) {
    48  	rotateToken = enable
    49  }
    50  
    51  // GCEPlugin is the plugin object.
    52  type GCEPlugin struct {
    53  	// aud is the unique URI agreed upon by both the instance and the system verifying the instance's identity.
    54  	// For more info: https://cloud.google.com/compute/docs/instances/verifying-instance-identity
    55  	aud string
    56  
    57  	// The location to save the identity token
    58  	jwtPath string
    59  
    60  	// identity provider
    61  	identityProvider string
    62  
    63  	// token refresh
    64  	rotationTicker *time.Ticker
    65  	closing        chan bool
    66  	tokenCache     string
    67  	// mutex lock is required to avoid race condition when updating token file and token cache.
    68  	tokenMutex sync.RWMutex
    69  }
    70  
    71  // CreateGCEPlugin creates a Google credential fetcher plugin. Return the pointer to the created plugin.
    72  func CreateGCEPlugin(audience, jwtPath, identityProvider string) *GCEPlugin {
    73  	p := &GCEPlugin{
    74  		aud:              audience,
    75  		jwtPath:          jwtPath,
    76  		identityProvider: identityProvider,
    77  		closing:          make(chan bool),
    78  	}
    79  	if rotateToken {
    80  		go p.startTokenRotationJob()
    81  	}
    82  	return p
    83  }
    84  
    85  func (p *GCEPlugin) Stop() {
    86  	close(p.closing)
    87  }
    88  
    89  func (p *GCEPlugin) startTokenRotationJob() {
    90  	// Wake up once in a while and refresh GCE VM credential.
    91  	p.rotationTicker = time.NewTicker(rotationInterval)
    92  	for {
    93  		select {
    94  		case <-p.rotationTicker.C:
    95  			p.rotate()
    96  		case <-p.closing:
    97  			if p.rotationTicker != nil {
    98  				p.rotationTicker.Stop()
    99  			}
   100  			return
   101  		}
   102  	}
   103  }
   104  
   105  func (p *GCEPlugin) rotate() {
   106  	if p.shouldRotate(time.Now()) {
   107  		if _, err := p.GetPlatformCredential(); err != nil {
   108  			gcecredLog.Errorf("credential refresh failed: %+v", err)
   109  		}
   110  	}
   111  }
   112  
   113  func (p *GCEPlugin) shouldRotate(now time.Time) bool {
   114  	p.tokenMutex.RLock()
   115  	defer p.tokenMutex.RUnlock()
   116  
   117  	if p.tokenCache == "" {
   118  		return true
   119  	}
   120  	exp, err := util.GetExp(p.tokenCache)
   121  	// When fails to get expiration time from token, always refresh the token.
   122  	if err != nil || exp.IsZero() {
   123  		return true
   124  	}
   125  	rotate := now.After(exp.Add(-gracePeriod))
   126  	gcecredLog.Debugf("credential expiration: %s, grace period: %s, should rotate: %t",
   127  		exp.String(), gracePeriod.String(), rotate)
   128  	return rotate
   129  }
   130  
   131  // GetPlatformCredential fetches the GCE VM identity jwt token from its metadata server,
   132  // and write it to jwtPath. The local copy of the token in jwtPath is used by both
   133  // Envoy STS client and istio agent to fetch certificate and access token.
   134  // Note: this function only works in a GCE VM environment.
   135  func (p *GCEPlugin) GetPlatformCredential() (string, error) {
   136  	p.tokenMutex.Lock()
   137  	defer p.tokenMutex.Unlock()
   138  
   139  	if p.jwtPath == "" {
   140  		return "", fmt.Errorf("jwtPath is unset")
   141  	}
   142  	uri := fmt.Sprintf("instance/service-accounts/default/identity?audience=%s&format=full", p.aud)
   143  	token, err := metadata.GetWithContext(context.TODO(), uri)
   144  	if err != nil {
   145  		gcecredLog.Errorf("Failed to get vm identity token from metadata server: %v", err)
   146  		return "", err
   147  	}
   148  	// Update token cache.
   149  	p.tokenCache = token
   150  	gcecredLog.Debugf("Got GCE identity token: %d", len(token))
   151  	tokenbytes := []byte(token)
   152  	err = os.WriteFile(p.jwtPath, tokenbytes, 0o640)
   153  	if err != nil {
   154  		gcecredLog.Errorf("Encountered error when writing vm identity token: %v", err)
   155  		return "", err
   156  	}
   157  	return strings.TrimSpace(token), nil
   158  }
   159  
   160  // GetIdentityProvider returns the name of the identity provider that can authenticate the workload credential.
   161  // GCE identity provider is named "GoogleComputeEngine".
   162  func (p *GCEPlugin) GetIdentityProvider() string {
   163  	return p.identityProvider
   164  }