You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
tailscale/cmd/k8s-operator/tsclient.go

124 lines
3.1 KiB

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package main
import (
"context"
"fmt"
"net/url"
"os"
"sync"
"time"
"go.uber.org/zap"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale/v2"
"tailscale.com/ipn"
)
const (
oidcJWTPath = "/var/run/secrets/tailscale/serviceaccount/token"
)
func newTSClient(logger *zap.SugaredLogger, clientID, clientIDPath, clientSecretPath, loginServer string) (*tailscale.Client, error) {
baseURL := ipn.DefaultControlURL
if loginServer != "" {
baseURL = loginServer
}
base, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
client := &tailscale.Client{
UserAgent: "tailscale-k8s-operator",
BaseURL: base,
}
if clientID == "" {
// Use static client credentials mounted to disk.
clientIDBytes, err := os.ReadFile(clientIDPath)
if err != nil {
return nil, fmt.Errorf("error reading client ID %q: %w", clientIDPath, err)
}
clientSecretBytes, err := os.ReadFile(clientSecretPath)
if err != nil {
return nil, fmt.Errorf("reading client secret %q: %w", clientSecretPath, err)
}
client.Auth = &tailscale.OAuth{
ClientID: string(clientIDBytes),
ClientSecret: string(clientSecretBytes),
}
} else {
// Use workload identity federation.
tokenSrc := &jwtTokenSource{
logger: logger,
jwtPath: oidcJWTPath,
baseCfg: clientcredentials.Config{
ClientID: clientID,
TokenURL: fmt.Sprintf("%s%s", baseURL, "/api/v2/oauth/token-exchange"),
},
}
client.Auth = &tailscale.IdentityFederation{
ClientID: clientID,
IDTokenFunc: func() (string, error) {
token, err := tokenSrc.Token()
if err != nil {
return "", err
}
return token.AccessToken, nil
},
}
}
return client, nil
}
// jwtTokenSource implements the [oauth2.TokenSource] interface, but with the
// ability to regenerate a fresh underlying token source each time a new value
// of the JWT parameter is needed due to expiration.
type jwtTokenSource struct {
logger *zap.SugaredLogger
jwtPath string // Path to the file containing an automatically refreshed JWT.
baseCfg clientcredentials.Config // Holds config that doesn't change for the lifetime of the process.
mu sync.Mutex // Guards underlying.
underlying oauth2.TokenSource // The oauth2 client implementation. Does its own separate caching of the access token.
}
func (s *jwtTokenSource) Token() (*oauth2.Token, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.underlying != nil {
t, err := s.underlying.Token()
if err == nil && t != nil && t.Valid() {
return t, nil
}
}
s.logger.Debugf("Refreshing JWT from %s", s.jwtPath)
tk, err := os.ReadFile(s.jwtPath)
if err != nil {
return nil, fmt.Errorf("error reading JWT from %q: %w", s.jwtPath, err)
}
// Shallow copy of the base config.
credentials := s.baseCfg
credentials.EndpointParams = map[string][]string{
"jwt": {string(tk)},
}
src := credentials.TokenSource(context.Background())
s.underlying = oauth2.ReuseTokenSourceWithExpiry(nil, src, time.Minute)
return s.underlying.Token()
}