vitess.io/vitess@v0.16.2/go/vt/dbconfigs/credentials.go (about)

     1  /*
     2  Copyright 2019 The Vitess 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 dbconfigs
    18  
    19  // This file contains logic for a pluggable credentials system.
    20  // The default implementation is file based.
    21  // The flags are global, but only programs that need to access the database
    22  // link with this library, so we should be safe.
    23  
    24  import (
    25  	"encoding/json"
    26  	"errors"
    27  	"os"
    28  	"os/signal"
    29  	"strings"
    30  	"sync"
    31  	"syscall"
    32  	"time"
    33  
    34  	vaultapi "github.com/aquarapid/vaultlib"
    35  	"github.com/spf13/pflag"
    36  
    37  	"vitess.io/vitess/go/mysql"
    38  	"vitess.io/vitess/go/vt/log"
    39  	"vitess.io/vitess/go/vt/servenv"
    40  )
    41  
    42  var (
    43  	dbCredentialsServer   = "file"
    44  	dbCredentialsFile     string
    45  	vaultAddr             string
    46  	vaultTimeout          = 10 * time.Second
    47  	vaultCACert           string
    48  	vaultPath             string
    49  	vaultCacheTTL         = 30 * time.Minute
    50  	vaultTokenFile        string
    51  	vaultRoleID           string
    52  	vaultRoleSecretIDFile string
    53  	vaultRoleMountPoint   = "approle"
    54  
    55  	// ErrUnknownUser is returned by credential server when the
    56  	// user doesn't exist
    57  	ErrUnknownUser = errors.New("unknown user")
    58  
    59  	cmdsWithDBCredentials = []string{
    60  		"mysqlctl",
    61  		"mysqlctld",
    62  		"vtbackup",
    63  		"vtcombo",
    64  		"vtgr",
    65  		"vttablet",
    66  	}
    67  )
    68  
    69  // CredentialsServer is the interface for a credential server
    70  type CredentialsServer interface {
    71  	// GetUserAndPassword returns the user / password to use for a given
    72  	// user. May return ErrUnknownUser. The user might be altered
    73  	// to support versioned users.
    74  	// Note this call needs to be thread safe, as we may call this from
    75  	// multiple go routines.
    76  	GetUserAndPassword(user string) (string, string, error)
    77  }
    78  
    79  // AllCredentialsServers contains all the known CredentialsServer
    80  // implementations.  Note we will only access this after flags have
    81  // been parsed.
    82  var AllCredentialsServers = make(map[string]CredentialsServer)
    83  
    84  func init() {
    85  	AllCredentialsServers["file"] = &FileCredentialsServer{}
    86  	AllCredentialsServers["vault"] = &VaultCredentialsServer{}
    87  
    88  	sigChan := make(chan os.Signal, 1)
    89  	signal.Notify(sigChan, syscall.SIGHUP)
    90  	go func() {
    91  		for range sigChan {
    92  			if fcs, ok := AllCredentialsServers["file"].(*FileCredentialsServer); ok {
    93  				fcs.mu.Lock()
    94  				fcs.dbCredentials = nil
    95  				fcs.mu.Unlock()
    96  			}
    97  			if vcs, ok := AllCredentialsServers["vault"].(*VaultCredentialsServer); ok {
    98  				vcs.mu.Lock()
    99  				vcs.dbCredsCache = nil
   100  				vcs.mu.Unlock()
   101  			}
   102  		}
   103  	}()
   104  
   105  	for _, cmd := range cmdsWithDBCredentials {
   106  		servenv.OnParseFor(cmd, func(fs *pflag.FlagSet) {
   107  			// generic flags
   108  			fs.StringVar(&dbCredentialsServer, "db-credentials-server", dbCredentialsServer, "db credentials server type ('file' - file implementation; 'vault' - HashiCorp Vault implementation)")
   109  
   110  			// 'file' implementation flags
   111  			fs.StringVar(&dbCredentialsFile, "db-credentials-file", dbCredentialsFile, "db credentials file; send SIGHUP to reload this file")
   112  
   113  			// 'vault' implementation flags
   114  			fs.StringVar(&vaultAddr, "db-credentials-vault-addr", vaultAddr, "URL to Vault server")
   115  			fs.DurationVar(&vaultTimeout, "db-credentials-vault-timeout", vaultTimeout, "Timeout for vault API operations")
   116  			fs.StringVar(&vaultCACert, "db-credentials-vault-tls-ca", vaultCACert, "Path to CA PEM for validating Vault server certificate")
   117  			fs.StringVar(&vaultPath, "db-credentials-vault-path", vaultPath, "Vault path to credentials JSON blob, e.g.: secret/data/prod/dbcreds")
   118  			fs.DurationVar(&vaultCacheTTL, "db-credentials-vault-ttl", vaultCacheTTL, "How long to cache DB credentials from the Vault server")
   119  			fs.StringVar(&vaultTokenFile, "db-credentials-vault-tokenfile", vaultTokenFile, "Path to file containing Vault auth token; token can also be passed using VAULT_TOKEN environment variable")
   120  			fs.StringVar(&vaultRoleID, "db-credentials-vault-roleid", vaultRoleID, "Vault AppRole id; can also be passed using VAULT_ROLEID environment variable")
   121  			fs.StringVar(&vaultRoleSecretIDFile, "db-credentials-vault-role-secretidfile", vaultRoleSecretIDFile, "Path to file containing Vault AppRole secret_id; can also be passed using VAULT_SECRETID environment variable")
   122  			fs.StringVar(&vaultRoleMountPoint, "db-credentials-vault-role-mountpoint", vaultRoleMountPoint, "Vault AppRole mountpoint; can also be passed using VAULT_MOUNTPOINT environment variable")
   123  		})
   124  	}
   125  }
   126  
   127  // GetCredentialsServer returns the current CredentialsServer. Only valid
   128  // after flag.Init was called.
   129  func GetCredentialsServer() CredentialsServer {
   130  	cs, ok := AllCredentialsServers[dbCredentialsServer]
   131  	if !ok {
   132  		log.Exitf("Invalid credential server: %v", dbCredentialsServer)
   133  	}
   134  	return cs
   135  }
   136  
   137  // FileCredentialsServer is a simple implementation of CredentialsServer using
   138  // a json file. Protected by mu.
   139  type FileCredentialsServer struct {
   140  	mu            sync.Mutex
   141  	dbCredentials map[string][]string
   142  }
   143  
   144  // VaultCredentialsServer implements CredentialsServer using
   145  // a Vault backend from HashiCorp.
   146  type VaultCredentialsServer struct {
   147  	mu                     sync.Mutex
   148  	dbCredsCache           map[string][]string
   149  	vaultCacheExpireTicker *time.Ticker
   150  	vaultClient            *vaultapi.Client
   151  	// We use a separate valid flag to allow invalidating the cache
   152  	// without destroying it, in case Vault is temp down.
   153  	cacheValid bool
   154  }
   155  
   156  // GetUserAndPassword is part of the CredentialsServer interface
   157  func (fcs *FileCredentialsServer) GetUserAndPassword(user string) (string, string, error) {
   158  	fcs.mu.Lock()
   159  	defer fcs.mu.Unlock()
   160  
   161  	if dbCredentialsFile == "" {
   162  		return "", "", ErrUnknownUser
   163  	}
   164  
   165  	// read the json file only once
   166  	if fcs.dbCredentials == nil {
   167  		fcs.dbCredentials = make(map[string][]string)
   168  
   169  		data, err := os.ReadFile(dbCredentialsFile)
   170  		if err != nil {
   171  			log.Warningf("Failed to read dbCredentials file: %v", dbCredentialsFile)
   172  			return "", "", err
   173  		}
   174  
   175  		if err = json.Unmarshal(data, &fcs.dbCredentials); err != nil {
   176  			log.Warningf("Failed to parse dbCredentials file: %v", dbCredentialsFile)
   177  			return "", "", err
   178  		}
   179  	}
   180  
   181  	passwd, ok := fcs.dbCredentials[user]
   182  	if !ok {
   183  		return "", "", ErrUnknownUser
   184  	}
   185  	return user, passwd[0], nil
   186  }
   187  
   188  // GetUserAndPassword for Vault implementation
   189  func (vcs *VaultCredentialsServer) GetUserAndPassword(user string) (string, string, error) {
   190  	vcs.mu.Lock()
   191  	defer vcs.mu.Unlock()
   192  
   193  	if vcs.vaultCacheExpireTicker == nil {
   194  		vcs.vaultCacheExpireTicker = time.NewTicker(vaultCacheTTL)
   195  		go func() {
   196  			for range vcs.vaultCacheExpireTicker.C {
   197  				if vcs, ok := AllCredentialsServers["vault"].(*VaultCredentialsServer); ok {
   198  					vcs.cacheValid = false
   199  				}
   200  			}
   201  		}()
   202  	}
   203  
   204  	if vcs.cacheValid && vcs.dbCredsCache != nil {
   205  		if vcs.dbCredsCache[user] == nil {
   206  			log.Errorf("Vault cache is valid, but user %s unknown in cache, will retry", user)
   207  			return "", "", ErrUnknownUser
   208  		}
   209  		return user, vcs.dbCredsCache[user][0], nil
   210  	}
   211  
   212  	if vaultAddr == "" {
   213  		return "", "", errors.New("No Vault server specified")
   214  	}
   215  
   216  	token, err := readFromFile(vaultTokenFile)
   217  	if err != nil {
   218  		return "", "", errors.New("No Vault token in provided filename")
   219  	}
   220  	secretID, err := readFromFile(vaultRoleSecretIDFile)
   221  	if err != nil {
   222  		return "", "", errors.New("No Vault secret_id in provided filename")
   223  	}
   224  
   225  	// From here on, errors might be transient, so we use ErrUnknownUser
   226  	// for everything, so we get retries
   227  	if vcs.vaultClient == nil {
   228  		config := vaultapi.NewConfig()
   229  
   230  		// All these can be overriden by environment
   231  		//   so we need to check if they have been set by NewConfig
   232  		if config.Address == "" {
   233  			config.Address = vaultAddr
   234  		}
   235  		if config.Timeout == (0 * time.Second) {
   236  			config.Timeout = vaultTimeout
   237  		}
   238  		if config.CACert == "" {
   239  			config.CACert = vaultCACert
   240  		}
   241  		if config.Token == "" {
   242  			config.Token = token
   243  		}
   244  		if config.AppRoleCredentials.RoleID == "" {
   245  			config.AppRoleCredentials.RoleID = vaultRoleID
   246  		}
   247  		if config.AppRoleCredentials.SecretID == "" {
   248  			config.AppRoleCredentials.SecretID = secretID
   249  		}
   250  		if config.AppRoleCredentials.MountPoint == "" {
   251  			config.AppRoleCredentials.MountPoint = vaultRoleMountPoint
   252  		}
   253  
   254  		if config.CACert != "" {
   255  			// If we provide a CA, ensure we actually use it
   256  			config.InsecureSSL = false
   257  		}
   258  
   259  		var err error
   260  		vcs.vaultClient, err = vaultapi.NewClient(config)
   261  		if err != nil || vcs.vaultClient == nil {
   262  			log.Errorf("Error in vault client initialization, will retry: %v", err)
   263  			vcs.vaultClient = nil
   264  			return "", "", ErrUnknownUser
   265  		}
   266  	}
   267  
   268  	secret, err := vcs.vaultClient.GetSecret(vaultPath)
   269  	if err != nil {
   270  		log.Errorf("Error in Vault server params: %v", err)
   271  		return "", "", ErrUnknownUser
   272  	}
   273  
   274  	if secret.JSONSecret == nil {
   275  		log.Errorf("Empty DB credentials retrieved from Vault server")
   276  		return "", "", ErrUnknownUser
   277  	}
   278  
   279  	dbCreds := make(map[string][]string)
   280  	if err = json.Unmarshal(secret.JSONSecret, &dbCreds); err != nil {
   281  		log.Errorf("Error unmarshaling DB credentials from Vault server")
   282  		return "", "", ErrUnknownUser
   283  	}
   284  	if dbCreds[user] == nil {
   285  		log.Warningf("Vault lookup for user not found: %v\n", user)
   286  		return "", "", ErrUnknownUser
   287  	}
   288  	log.Infof("Vault client status: %s", vcs.vaultClient.GetStatus())
   289  
   290  	vcs.dbCredsCache = dbCreds
   291  	vcs.cacheValid = true
   292  	return user, dbCreds[user][0], nil
   293  }
   294  
   295  func readFromFile(filePath string) (string, error) {
   296  	if filePath == "" {
   297  		return "", nil
   298  	}
   299  	fileBytes, err := os.ReadFile(filePath)
   300  	if err != nil {
   301  		return "", err
   302  	}
   303  	return strings.TrimSpace(string(fileBytes)), nil
   304  }
   305  
   306  // WithCredentials returns a copy of the provided ConnParams that we can use
   307  // to connect, after going through the CredentialsServer.
   308  func withCredentials(cp *mysql.ConnParams) (*mysql.ConnParams, error) {
   309  	result := *cp
   310  	user, passwd, err := GetCredentialsServer().GetUserAndPassword(cp.Uname)
   311  	switch err {
   312  	case nil:
   313  		result.Uname = user
   314  		result.Pass = passwd
   315  	case ErrUnknownUser:
   316  		// we just use what we have, and will fail later anyway
   317  		// except if the actual password is empty, in which case
   318  		// things will just "work"
   319  		err = nil
   320  	}
   321  	return &result, err
   322  }