1/* 2Package processcreds is a credential Provider to retrieve `credential_process` 3credentials. 4 5WARNING: The following describes a method of sourcing credentials from an external 6process. This can potentially be dangerous, so proceed with caution. Other 7credential providers should be preferred if at all possible. If using this 8option, you should make sure that the config file is as locked down as possible 9using security best practices for your operating system. 10 11You can use credentials from a `credential_process` in a variety of ways. 12 13One way is to setup your shared config file, located in the default 14location, with the `credential_process` key and the command you want to be 15called. You also need to set the AWS_SDK_LOAD_CONFIG environment variable 16(e.g., `export AWS_SDK_LOAD_CONFIG=1`) to use the shared config file. 17 18 [default] 19 credential_process = /command/to/call 20 21Creating a new session will use the credential process to retrieve credentials. 22NOTE: If there are credentials in the profile you are using, the credential 23process will not be used. 24 25 // Initialize a session to load credentials. 26 sess, _ := session.NewSession(&aws.Config{ 27 Region: aws.String("us-east-1")}, 28 ) 29 30 // Create S3 service client to use the credentials. 31 svc := s3.New(sess) 32 33Another way to use the `credential_process` method is by using 34`credentials.NewCredentials()` and providing a command to be executed to 35retrieve credentials: 36 37 // Create credentials using the ProcessProvider. 38 creds := processcreds.NewCredentials("/path/to/command") 39 40 // Create service client value configured for credentials. 41 svc := s3.New(sess, &aws.Config{Credentials: creds}) 42 43You can set a non-default timeout for the `credential_process` with another 44constructor, `credentials.NewCredentialsTimeout()`, providing the timeout. To 45set a one minute timeout: 46 47 // Create credentials using the ProcessProvider. 48 creds := processcreds.NewCredentialsTimeout( 49 "/path/to/command", 50 time.Duration(500) * time.Millisecond) 51 52If you need more control, you can set any configurable options in the 53credentials using one or more option functions. For example, you can set a two 54minute timeout, a credential duration of 60 minutes, and a maximum stdout 55buffer size of 2k. 56 57 creds := processcreds.NewCredentials( 58 "/path/to/command", 59 func(opt *ProcessProvider) { 60 opt.Timeout = time.Duration(2) * time.Minute 61 opt.Duration = time.Duration(60) * time.Minute 62 opt.MaxBufSize = 2048 63 }) 64 65You can also use your own `exec.Cmd`: 66 67 // Create an exec.Cmd 68 myCommand := exec.Command("/path/to/command") 69 70 // Create credentials using your exec.Cmd and custom timeout 71 creds := processcreds.NewCredentialsCommand( 72 myCommand, 73 func(opt *processcreds.ProcessProvider) { 74 opt.Timeout = time.Duration(1) * time.Second 75 }) 76*/ 77package processcreds 78 79import ( 80 "bytes" 81 "encoding/json" 82 "fmt" 83 "io" 84 "io/ioutil" 85 "os" 86 "os/exec" 87 "runtime" 88 "strings" 89 "time" 90 91 "github.com/aws/aws-sdk-go/aws/awserr" 92 "github.com/aws/aws-sdk-go/aws/credentials" 93 "github.com/aws/aws-sdk-go/internal/sdkio" 94) 95 96const ( 97 // ProviderName is the name this credentials provider will label any 98 // returned credentials Value with. 99 ProviderName = `ProcessProvider` 100 101 // ErrCodeProcessProviderParse error parsing process output 102 ErrCodeProcessProviderParse = "ProcessProviderParseError" 103 104 // ErrCodeProcessProviderVersion version error in output 105 ErrCodeProcessProviderVersion = "ProcessProviderVersionError" 106 107 // ErrCodeProcessProviderRequired required attribute missing in output 108 ErrCodeProcessProviderRequired = "ProcessProviderRequiredError" 109 110 // ErrCodeProcessProviderExecution execution of command failed 111 ErrCodeProcessProviderExecution = "ProcessProviderExecutionError" 112 113 // errMsgProcessProviderTimeout process took longer than allowed 114 errMsgProcessProviderTimeout = "credential process timed out" 115 116 // errMsgProcessProviderProcess process error 117 errMsgProcessProviderProcess = "error in credential_process" 118 119 // errMsgProcessProviderParse problem parsing output 120 errMsgProcessProviderParse = "parse failed of credential_process output" 121 122 // errMsgProcessProviderVersion version error in output 123 errMsgProcessProviderVersion = "wrong version in process output (not 1)" 124 125 // errMsgProcessProviderMissKey missing access key id in output 126 errMsgProcessProviderMissKey = "missing AccessKeyId in process output" 127 128 // errMsgProcessProviderMissSecret missing secret acess key in output 129 errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output" 130 131 // errMsgProcessProviderPrepareCmd prepare of command failed 132 errMsgProcessProviderPrepareCmd = "failed to prepare command" 133 134 // errMsgProcessProviderEmptyCmd command must not be empty 135 errMsgProcessProviderEmptyCmd = "command must not be empty" 136 137 // errMsgProcessProviderPipe failed to initialize pipe 138 errMsgProcessProviderPipe = "failed to initialize pipe" 139 140 // DefaultDuration is the default amount of time in minutes that the 141 // credentials will be valid for. 142 DefaultDuration = time.Duration(15) * time.Minute 143 144 // DefaultBufSize limits buffer size from growing to an enormous 145 // amount due to a faulty process. 146 DefaultBufSize = int(8 * sdkio.KibiByte) 147 148 // DefaultTimeout default limit on time a process can run. 149 DefaultTimeout = time.Duration(1) * time.Minute 150) 151 152// ProcessProvider satisfies the credentials.Provider interface, and is a 153// client to retrieve credentials from a process. 154type ProcessProvider struct { 155 staticCreds bool 156 credentials.Expiry 157 originalCommand []string 158 159 // Expiry duration of the credentials. Defaults to 15 minutes if not set. 160 Duration time.Duration 161 162 // ExpiryWindow will allow the credentials to trigger refreshing prior to 163 // the credentials actually expiring. This is beneficial so race conditions 164 // with expiring credentials do not cause request to fail unexpectedly 165 // due to ExpiredTokenException exceptions. 166 // 167 // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true 168 // 10 seconds before the credentials are actually expired. 169 // 170 // If ExpiryWindow is 0 or less it will be ignored. 171 ExpiryWindow time.Duration 172 173 // A string representing an os command that should return a JSON with 174 // credential information. 175 command *exec.Cmd 176 177 // MaxBufSize limits memory usage from growing to an enormous 178 // amount due to a faulty process. 179 MaxBufSize int 180 181 // Timeout limits the time a process can run. 182 Timeout time.Duration 183} 184 185// NewCredentials returns a pointer to a new Credentials object wrapping the 186// ProcessProvider. The credentials will expire every 15 minutes by default. 187func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials { 188 p := &ProcessProvider{ 189 command: exec.Command(command), 190 Duration: DefaultDuration, 191 Timeout: DefaultTimeout, 192 MaxBufSize: DefaultBufSize, 193 } 194 195 for _, option := range options { 196 option(p) 197 } 198 199 return credentials.NewCredentials(p) 200} 201 202// NewCredentialsTimeout returns a pointer to a new Credentials object with 203// the specified command and timeout, and default duration and max buffer size. 204func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials { 205 p := NewCredentials(command, func(opt *ProcessProvider) { 206 opt.Timeout = timeout 207 }) 208 209 return p 210} 211 212// NewCredentialsCommand returns a pointer to a new Credentials object with 213// the specified command, and default timeout, duration and max buffer size. 214func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials { 215 p := &ProcessProvider{ 216 command: command, 217 Duration: DefaultDuration, 218 Timeout: DefaultTimeout, 219 MaxBufSize: DefaultBufSize, 220 } 221 222 for _, option := range options { 223 option(p) 224 } 225 226 return credentials.NewCredentials(p) 227} 228 229type credentialProcessResponse struct { 230 Version int 231 AccessKeyID string `json:"AccessKeyId"` 232 SecretAccessKey string 233 SessionToken string 234 Expiration *time.Time 235} 236 237// Retrieve executes the 'credential_process' and returns the credentials. 238func (p *ProcessProvider) Retrieve() (credentials.Value, error) { 239 out, err := p.executeCredentialProcess() 240 if err != nil { 241 return credentials.Value{ProviderName: ProviderName}, err 242 } 243 244 // Serialize and validate response 245 resp := &credentialProcessResponse{} 246 if err = json.Unmarshal(out, resp); err != nil { 247 return credentials.Value{ProviderName: ProviderName}, awserr.New( 248 ErrCodeProcessProviderParse, 249 fmt.Sprintf("%s: %s", errMsgProcessProviderParse, string(out)), 250 err) 251 } 252 253 if resp.Version != 1 { 254 return credentials.Value{ProviderName: ProviderName}, awserr.New( 255 ErrCodeProcessProviderVersion, 256 errMsgProcessProviderVersion, 257 nil) 258 } 259 260 if len(resp.AccessKeyID) == 0 { 261 return credentials.Value{ProviderName: ProviderName}, awserr.New( 262 ErrCodeProcessProviderRequired, 263 errMsgProcessProviderMissKey, 264 nil) 265 } 266 267 if len(resp.SecretAccessKey) == 0 { 268 return credentials.Value{ProviderName: ProviderName}, awserr.New( 269 ErrCodeProcessProviderRequired, 270 errMsgProcessProviderMissSecret, 271 nil) 272 } 273 274 // Handle expiration 275 p.staticCreds = resp.Expiration == nil 276 if resp.Expiration != nil { 277 p.SetExpiration(*resp.Expiration, p.ExpiryWindow) 278 } 279 280 return credentials.Value{ 281 ProviderName: ProviderName, 282 AccessKeyID: resp.AccessKeyID, 283 SecretAccessKey: resp.SecretAccessKey, 284 SessionToken: resp.SessionToken, 285 }, nil 286} 287 288// IsExpired returns true if the credentials retrieved are expired, or not yet 289// retrieved. 290func (p *ProcessProvider) IsExpired() bool { 291 if p.staticCreds { 292 return false 293 } 294 return p.Expiry.IsExpired() 295} 296 297// prepareCommand prepares the command to be executed. 298func (p *ProcessProvider) prepareCommand() error { 299 300 var cmdArgs []string 301 if runtime.GOOS == "windows" { 302 cmdArgs = []string{"cmd.exe", "/C"} 303 } else { 304 cmdArgs = []string{"sh", "-c"} 305 } 306 307 if len(p.originalCommand) == 0 { 308 p.originalCommand = make([]string, len(p.command.Args)) 309 copy(p.originalCommand, p.command.Args) 310 311 // check for empty command because it succeeds 312 if len(strings.TrimSpace(p.originalCommand[0])) < 1 { 313 return awserr.New( 314 ErrCodeProcessProviderExecution, 315 fmt.Sprintf( 316 "%s: %s", 317 errMsgProcessProviderPrepareCmd, 318 errMsgProcessProviderEmptyCmd), 319 nil) 320 } 321 } 322 323 cmdArgs = append(cmdArgs, p.originalCommand...) 324 p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...) 325 p.command.Env = os.Environ() 326 327 return nil 328} 329 330// executeCredentialProcess starts the credential process on the OS and 331// returns the results or an error. 332func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) { 333 334 if err := p.prepareCommand(); err != nil { 335 return nil, err 336 } 337 338 // Setup the pipes 339 outReadPipe, outWritePipe, err := os.Pipe() 340 if err != nil { 341 return nil, awserr.New( 342 ErrCodeProcessProviderExecution, 343 errMsgProcessProviderPipe, 344 err) 345 } 346 347 p.command.Stderr = os.Stderr // display stderr on console for MFA 348 p.command.Stdout = outWritePipe // get creds json on process's stdout 349 p.command.Stdin = os.Stdin // enable stdin for MFA 350 351 output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize)) 352 353 stdoutCh := make(chan error, 1) 354 go readInput( 355 io.LimitReader(outReadPipe, int64(p.MaxBufSize)), 356 output, 357 stdoutCh) 358 359 execCh := make(chan error, 1) 360 go executeCommand(*p.command, execCh) 361 362 finished := false 363 var errors []error 364 for !finished { 365 select { 366 case readError := <-stdoutCh: 367 errors = appendError(errors, readError) 368 finished = true 369 case execError := <-execCh: 370 err := outWritePipe.Close() 371 errors = appendError(errors, err) 372 errors = appendError(errors, execError) 373 if errors != nil { 374 return output.Bytes(), awserr.NewBatchError( 375 ErrCodeProcessProviderExecution, 376 errMsgProcessProviderProcess, 377 errors) 378 } 379 case <-time.After(p.Timeout): 380 finished = true 381 return output.Bytes(), awserr.NewBatchError( 382 ErrCodeProcessProviderExecution, 383 errMsgProcessProviderTimeout, 384 errors) // errors can be nil 385 } 386 } 387 388 out := output.Bytes() 389 390 if runtime.GOOS == "windows" { 391 // windows adds slashes to quotes 392 out = []byte(strings.Replace(string(out), `\"`, `"`, -1)) 393 } 394 395 return out, nil 396} 397 398// appendError conveniently checks for nil before appending slice 399func appendError(errors []error, err error) []error { 400 if err != nil { 401 return append(errors, err) 402 } 403 return errors 404} 405 406func executeCommand(cmd exec.Cmd, exec chan error) { 407 // Start the command 408 err := cmd.Start() 409 if err == nil { 410 err = cmd.Wait() 411 } 412 413 exec <- err 414} 415 416func readInput(r io.Reader, w io.Writer, read chan error) { 417 tee := io.TeeReader(r, w) 418 419 _, err := ioutil.ReadAll(tee) 420 421 if err == io.EOF { 422 err = nil 423 } 424 425 read <- err // will only arrive here when write end of pipe is closed 426} 427