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 }