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) 94 95const ( 96 // ProviderName is the name this credentials provider will label any 97 // returned credentials Value with. 98 ProviderName = `ProcessProvider` 99 100 // ErrCodeProcessProviderParse error parsing process output 101 ErrCodeProcessProviderParse = "ProcessProviderParseError" 102 103 // ErrCodeProcessProviderVersion version error in output 104 ErrCodeProcessProviderVersion = "ProcessProviderVersionError" 105 106 // ErrCodeProcessProviderRequired required attribute missing in output 107 ErrCodeProcessProviderRequired = "ProcessProviderRequiredError" 108 109 // ErrCodeProcessProviderExecution execution of command failed 110 ErrCodeProcessProviderExecution = "ProcessProviderExecutionError" 111 112 // errMsgProcessProviderTimeout process took longer than allowed 113 errMsgProcessProviderTimeout = "credential process timed out" 114 115 // errMsgProcessProviderProcess process error 116 errMsgProcessProviderProcess = "error in credential_process" 117 118 // errMsgProcessProviderParse problem parsing output 119 errMsgProcessProviderParse = "parse failed of credential_process output" 120 121 // errMsgProcessProviderVersion version error in output 122 errMsgProcessProviderVersion = "wrong version in process output (not 1)" 123 124 // errMsgProcessProviderMissKey missing access key id in output 125 errMsgProcessProviderMissKey = "missing AccessKeyId in process output" 126 127 // errMsgProcessProviderMissSecret missing secret acess key in output 128 errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output" 129 130 // errMsgProcessProviderPrepareCmd prepare of command failed 131 errMsgProcessProviderPrepareCmd = "failed to prepare command" 132 133 // errMsgProcessProviderEmptyCmd command must not be empty 134 errMsgProcessProviderEmptyCmd = "command must not be empty" 135 136 // errMsgProcessProviderPipe failed to initialize pipe 137 errMsgProcessProviderPipe = "failed to initialize pipe" 138 139 // DefaultDuration is the default amount of time in minutes that the 140 // credentials will be valid for. 141 DefaultDuration = time.Duration(15) * time.Minute 142 143 // DefaultBufSize limits buffer size from growing to an enormous 144 // amount due to a faulty process. 145 DefaultBufSize = 1024 146 147 // DefaultTimeout default limit on time a process can run. 148 DefaultTimeout = time.Duration(1) * time.Minute 149) 150 151// ProcessProvider satisfies the credentials.Provider interface, and is a 152// client to retrieve credentials from a process. 153type ProcessProvider struct { 154 staticCreds bool 155 credentials.Expiry 156 originalCommand []string 157 158 // Expiry duration of the credentials. Defaults to 15 minutes if not set. 159 Duration time.Duration 160 161 // ExpiryWindow will allow the credentials to trigger refreshing prior to 162 // the credentials actually expiring. This is beneficial so race conditions 163 // with expiring credentials do not cause request to fail unexpectedly 164 // due to ExpiredTokenException exceptions. 165 // 166 // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true 167 // 10 seconds before the credentials are actually expired. 168 // 169 // If ExpiryWindow is 0 or less it will be ignored. 170 ExpiryWindow time.Duration 171 172 // A string representing an os command that should return a JSON with 173 // credential information. 174 command *exec.Cmd 175 176 // MaxBufSize limits memory usage from growing to an enormous 177 // amount due to a faulty process. 178 MaxBufSize int 179 180 // Timeout limits the time a process can run. 181 Timeout time.Duration 182} 183 184// NewCredentials returns a pointer to a new Credentials object wrapping the 185// ProcessProvider. The credentials will expire every 15 minutes by default. 186func NewCredentials(command string, options ...func(*ProcessProvider)) *credentials.Credentials { 187 p := &ProcessProvider{ 188 command: exec.Command(command), 189 Duration: DefaultDuration, 190 Timeout: DefaultTimeout, 191 MaxBufSize: DefaultBufSize, 192 } 193 194 for _, option := range options { 195 option(p) 196 } 197 198 return credentials.NewCredentials(p) 199} 200 201// NewCredentialsTimeout returns a pointer to a new Credentials object with 202// the specified command and timeout, and default duration and max buffer size. 203func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials { 204 p := NewCredentials(command, func(opt *ProcessProvider) { 205 opt.Timeout = timeout 206 }) 207 208 return p 209} 210 211// NewCredentialsCommand returns a pointer to a new Credentials object with 212// the specified command, and default timeout, duration and max buffer size. 213func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials { 214 p := &ProcessProvider{ 215 command: command, 216 Duration: DefaultDuration, 217 Timeout: DefaultTimeout, 218 MaxBufSize: DefaultBufSize, 219 } 220 221 for _, option := range options { 222 option(p) 223 } 224 225 return credentials.NewCredentials(p) 226} 227 228type credentialProcessResponse struct { 229 Version int 230 AccessKeyID string `json:"AccessKeyId"` 231 SecretAccessKey string 232 SessionToken string 233 Expiration *time.Time 234} 235 236// Retrieve executes the 'credential_process' and returns the credentials. 237func (p *ProcessProvider) Retrieve() (credentials.Value, error) { 238 out, err := p.executeCredentialProcess() 239 if err != nil { 240 return credentials.Value{ProviderName: ProviderName}, err 241 } 242 243 // Serialize and validate response 244 resp := &credentialProcessResponse{} 245 if err = json.Unmarshal(out, resp); err != nil { 246 return credentials.Value{ProviderName: ProviderName}, awserr.New( 247 ErrCodeProcessProviderParse, 248 fmt.Sprintf("%s: %s", errMsgProcessProviderParse, string(out)), 249 err) 250 } 251 252 if resp.Version != 1 { 253 return credentials.Value{ProviderName: ProviderName}, awserr.New( 254 ErrCodeProcessProviderVersion, 255 errMsgProcessProviderVersion, 256 nil) 257 } 258 259 if len(resp.AccessKeyID) == 0 { 260 return credentials.Value{ProviderName: ProviderName}, awserr.New( 261 ErrCodeProcessProviderRequired, 262 errMsgProcessProviderMissKey, 263 nil) 264 } 265 266 if len(resp.SecretAccessKey) == 0 { 267 return credentials.Value{ProviderName: ProviderName}, awserr.New( 268 ErrCodeProcessProviderRequired, 269 errMsgProcessProviderMissSecret, 270 nil) 271 } 272 273 // Handle expiration 274 p.staticCreds = resp.Expiration == nil 275 if resp.Expiration != nil { 276 p.SetExpiration(*resp.Expiration, p.ExpiryWindow) 277 } 278 279 return credentials.Value{ 280 ProviderName: ProviderName, 281 AccessKeyID: resp.AccessKeyID, 282 SecretAccessKey: resp.SecretAccessKey, 283 SessionToken: resp.SessionToken, 284 }, nil 285} 286 287// IsExpired returns true if the credentials retrieved are expired, or not yet 288// retrieved. 289func (p *ProcessProvider) IsExpired() bool { 290 if p.staticCreds { 291 return false 292 } 293 return p.Expiry.IsExpired() 294} 295 296// prepareCommand prepares the command to be executed. 297func (p *ProcessProvider) prepareCommand() error { 298 299 var cmdArgs []string 300 if runtime.GOOS == "windows" { 301 cmdArgs = []string{"cmd.exe", "/C"} 302 } else { 303 cmdArgs = []string{"sh", "-c"} 304 } 305 306 if len(p.originalCommand) == 0 { 307 p.originalCommand = make([]string, len(p.command.Args)) 308 copy(p.originalCommand, p.command.Args) 309 310 // check for empty command because it succeeds 311 if len(strings.TrimSpace(p.originalCommand[0])) < 1 { 312 return awserr.New( 313 ErrCodeProcessProviderExecution, 314 fmt.Sprintf( 315 "%s: %s", 316 errMsgProcessProviderPrepareCmd, 317 errMsgProcessProviderEmptyCmd), 318 nil) 319 } 320 } 321 322 cmdArgs = append(cmdArgs, p.originalCommand...) 323 p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...) 324 p.command.Env = os.Environ() 325 326 return nil 327} 328 329// executeCredentialProcess starts the credential process on the OS and 330// returns the results or an error. 331func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) { 332 333 if err := p.prepareCommand(); err != nil { 334 return nil, err 335 } 336 337 // Setup the pipes 338 outReadPipe, outWritePipe, err := os.Pipe() 339 if err != nil { 340 return nil, awserr.New( 341 ErrCodeProcessProviderExecution, 342 errMsgProcessProviderPipe, 343 err) 344 } 345 346 p.command.Stderr = os.Stderr // display stderr on console for MFA 347 p.command.Stdout = outWritePipe // get creds json on process's stdout 348 p.command.Stdin = os.Stdin // enable stdin for MFA 349 350 output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize)) 351 352 stdoutCh := make(chan error, 1) 353 go readInput( 354 io.LimitReader(outReadPipe, int64(p.MaxBufSize)), 355 output, 356 stdoutCh) 357 358 execCh := make(chan error, 1) 359 go executeCommand(*p.command, execCh) 360 361 finished := false 362 var errors []error 363 for !finished { 364 select { 365 case readError := <-stdoutCh: 366 errors = appendError(errors, readError) 367 finished = true 368 case execError := <-execCh: 369 err := outWritePipe.Close() 370 errors = appendError(errors, err) 371 errors = appendError(errors, execError) 372 if errors != nil { 373 return output.Bytes(), awserr.NewBatchError( 374 ErrCodeProcessProviderExecution, 375 errMsgProcessProviderProcess, 376 errors) 377 } 378 case <-time.After(p.Timeout): 379 finished = true 380 return output.Bytes(), awserr.NewBatchError( 381 ErrCodeProcessProviderExecution, 382 errMsgProcessProviderTimeout, 383 errors) // errors can be nil 384 } 385 } 386 387 out := output.Bytes() 388 389 if runtime.GOOS == "windows" { 390 // windows adds slashes to quotes 391 out = []byte(strings.Replace(string(out), `\"`, `"`, -1)) 392 } 393 394 return out, nil 395} 396 397// appendError conveniently checks for nil before appending slice 398func appendError(errors []error, err error) []error { 399 if err != nil { 400 return append(errors, err) 401 } 402 return errors 403} 404 405func executeCommand(cmd exec.Cmd, exec chan error) { 406 // Start the command 407 err := cmd.Start() 408 if err == nil { 409 err = cmd.Wait() 410 } 411 412 exec <- err 413} 414 415func readInput(r io.Reader, w io.Writer, read chan error) { 416 tee := io.TeeReader(r, w) 417 418 _, err := ioutil.ReadAll(tee) 419 420 if err == io.EOF { 421 err = nil 422 } 423 424 read <- err // will only arrive here when write end of pipe is closed 425} 426