2 Package processcreds is a credential Provider to retrieve `credential_process`
5 WARNING: The following describes a method of sourcing credentials from an external
6 process. This can potentially be dangerous, so proceed with caution. Other
7 credential providers should be preferred if at all possible. If using this
8 option, you should make sure that the config file is as locked down as possible
9 using security best practices for your operating system.
11 You can use credentials from a `credential_process` in a variety of ways.
13 One way is to setup your shared config file, located in the default
14 location, with the `credential_process` key and the command you want to be
15 called. 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.
19 credential_process = /command/to/call
21 Creating a new session will use the credential process to retrieve credentials.
22 NOTE: If there are credentials in the profile you are using, the credential
23 process will not be used.
25 // Initialize a session to load credentials.
26 sess, _ := session.NewSession(&aws.Config{
27 Region: aws.String("us-east-1")},
30 // Create S3 service client to use the credentials.
33 Another way to use the `credential_process` method is by using
34 `credentials.NewCredentials()` and providing a command to be executed to
37 // Create credentials using the ProcessProvider.
38 creds := processcreds.NewCredentials("/path/to/command")
40 // Create service client value configured for credentials.
41 svc := s3.New(sess, &aws.Config{Credentials: creds})
43 You can set a non-default timeout for the `credential_process` with another
44 constructor, `credentials.NewCredentialsTimeout()`, providing the timeout. To
45 set a one minute timeout:
47 // Create credentials using the ProcessProvider.
48 creds := processcreds.NewCredentialsTimeout(
50 time.Duration(500) * time.Millisecond)
52 If you need more control, you can set any configurable options in the
53 credentials using one or more option functions. For example, you can set a two
54 minute timeout, a credential duration of 60 minutes, and a maximum stdout
57 creds := processcreds.NewCredentials(
59 func(opt *ProcessProvider) {
60 opt.Timeout = time.Duration(2) * time.Minute
61 opt.Duration = time.Duration(60) * time.Minute
65 You can also use your own `exec.Cmd`:
68 myCommand := exec.Command("/path/to/command")
70 // Create credentials using your exec.Cmd and custom timeout
71 creds := processcreds.NewCredentialsCommand(
73 func(opt *processcreds.ProcessProvider) {
74 opt.Timeout = time.Duration(1) * time.Second
91 "github.com/aws/aws-sdk-go/aws/awserr"
92 "github.com/aws/aws-sdk-go/aws/credentials"
96 // ProviderName is the name this credentials provider will label any
97 // returned credentials Value with.
98 ProviderName = `ProcessProvider`
100 // ErrCodeProcessProviderParse error parsing process output
101 ErrCodeProcessProviderParse = "ProcessProviderParseError"
103 // ErrCodeProcessProviderVersion version error in output
104 ErrCodeProcessProviderVersion = "ProcessProviderVersionError"
106 // ErrCodeProcessProviderRequired required attribute missing in output
107 ErrCodeProcessProviderRequired = "ProcessProviderRequiredError"
109 // ErrCodeProcessProviderExecution execution of command failed
110 ErrCodeProcessProviderExecution = "ProcessProviderExecutionError"
112 // errMsgProcessProviderTimeout process took longer than allowed
113 errMsgProcessProviderTimeout = "credential process timed out"
115 // errMsgProcessProviderProcess process error
116 errMsgProcessProviderProcess = "error in credential_process"
118 // errMsgProcessProviderParse problem parsing output
119 errMsgProcessProviderParse = "parse failed of credential_process output"
121 // errMsgProcessProviderVersion version error in output
122 errMsgProcessProviderVersion = "wrong version in process output (not 1)"
124 // errMsgProcessProviderMissKey missing access key id in output
125 errMsgProcessProviderMissKey = "missing AccessKeyId in process output"
127 // errMsgProcessProviderMissSecret missing secret acess key in output
128 errMsgProcessProviderMissSecret = "missing SecretAccessKey in process output"
130 // errMsgProcessProviderPrepareCmd prepare of command failed
131 errMsgProcessProviderPrepareCmd = "failed to prepare command"
133 // errMsgProcessProviderEmptyCmd command must not be empty
134 errMsgProcessProviderEmptyCmd = "command must not be empty"
136 // errMsgProcessProviderPipe failed to initialize pipe
137 errMsgProcessProviderPipe = "failed to initialize pipe"
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
143 // DefaultBufSize limits buffer size from growing to an enormous
144 // amount due to a faulty process.
145 DefaultBufSize = 1024
147 // DefaultTimeout default limit on time a process can run.
148 DefaultTimeout = time.Duration(1) * time.Minute
151 // ProcessProvider satisfies the credentials.Provider interface, and is a
152 // client to retrieve credentials from a process.
153 type ProcessProvider struct {
156 originalCommand []string
158 // Expiry duration of the credentials. Defaults to 15 minutes if not set.
159 Duration time.Duration
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.
166 // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true
167 // 10 seconds before the credentials are actually expired.
169 // If ExpiryWindow is 0 or less it will be ignored.
170 ExpiryWindow time.Duration
172 // A string representing an os command that should return a JSON with
173 // credential information.
176 // MaxBufSize limits memory usage from growing to an enormous
177 // amount due to a faulty process.
180 // Timeout limits the time a process can run.
181 Timeout time.Duration
184 // NewCredentials returns a pointer to a new Credentials object wrapping the
185 // ProcessProvider. The credentials will expire every 15 minutes by default.
186 func 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,
194 for _, option := range options {
198 return credentials.NewCredentials(p)
201 // NewCredentialsTimeout returns a pointer to a new Credentials object with
202 // the specified command and timeout, and default duration and max buffer size.
203 func NewCredentialsTimeout(command string, timeout time.Duration) *credentials.Credentials {
204 p := NewCredentials(command, func(opt *ProcessProvider) {
205 opt.Timeout = timeout
211 // NewCredentialsCommand returns a pointer to a new Credentials object with
212 // the specified command, and default timeout, duration and max buffer size.
213 func NewCredentialsCommand(command *exec.Cmd, options ...func(*ProcessProvider)) *credentials.Credentials {
214 p := &ProcessProvider{
216 Duration: DefaultDuration,
217 Timeout: DefaultTimeout,
218 MaxBufSize: DefaultBufSize,
221 for _, option := range options {
225 return credentials.NewCredentials(p)
228 type credentialProcessResponse struct {
230 AccessKeyID string `json:"AccessKeyId"`
231 SecretAccessKey string
233 Expiration *time.Time
236 // Retrieve executes the 'credential_process' and returns the credentials.
237 func (p *ProcessProvider) Retrieve() (credentials.Value, error) {
238 out, err := p.executeCredentialProcess()
240 return credentials.Value{ProviderName: ProviderName}, err
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)),
252 if resp.Version != 1 {
253 return credentials.Value{ProviderName: ProviderName}, awserr.New(
254 ErrCodeProcessProviderVersion,
255 errMsgProcessProviderVersion,
259 if len(resp.AccessKeyID) == 0 {
260 return credentials.Value{ProviderName: ProviderName}, awserr.New(
261 ErrCodeProcessProviderRequired,
262 errMsgProcessProviderMissKey,
266 if len(resp.SecretAccessKey) == 0 {
267 return credentials.Value{ProviderName: ProviderName}, awserr.New(
268 ErrCodeProcessProviderRequired,
269 errMsgProcessProviderMissSecret,
274 p.staticCreds = resp.Expiration == nil
275 if resp.Expiration != nil {
276 p.SetExpiration(*resp.Expiration, p.ExpiryWindow)
279 return credentials.Value{
280 ProviderName: ProviderName,
281 AccessKeyID: resp.AccessKeyID,
282 SecretAccessKey: resp.SecretAccessKey,
283 SessionToken: resp.SessionToken,
287 // IsExpired returns true if the credentials retrieved are expired, or not yet
289 func (p *ProcessProvider) IsExpired() bool {
293 return p.Expiry.IsExpired()
296 // prepareCommand prepares the command to be executed.
297 func (p *ProcessProvider) prepareCommand() error {
300 if runtime.GOOS == "windows" {
301 cmdArgs = []string{"cmd.exe", "/C"}
303 cmdArgs = []string{"sh", "-c"}
306 if len(p.originalCommand) == 0 {
307 p.originalCommand = make([]string, len(p.command.Args))
308 copy(p.originalCommand, p.command.Args)
310 // check for empty command because it succeeds
311 if len(strings.TrimSpace(p.originalCommand[0])) < 1 {
313 ErrCodeProcessProviderExecution,
316 errMsgProcessProviderPrepareCmd,
317 errMsgProcessProviderEmptyCmd),
322 cmdArgs = append(cmdArgs, p.originalCommand...)
323 p.command = exec.Command(cmdArgs[0], cmdArgs[1:]...)
324 p.command.Env = os.Environ()
329 // executeCredentialProcess starts the credential process on the OS and
330 // returns the results or an error.
331 func (p *ProcessProvider) executeCredentialProcess() ([]byte, error) {
333 if err := p.prepareCommand(); err != nil {
338 outReadPipe, outWritePipe, err := os.Pipe()
340 return nil, awserr.New(
341 ErrCodeProcessProviderExecution,
342 errMsgProcessProviderPipe,
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
350 output := bytes.NewBuffer(make([]byte, 0, p.MaxBufSize))
352 stdoutCh := make(chan error, 1)
354 io.LimitReader(outReadPipe, int64(p.MaxBufSize)),
358 execCh := make(chan error, 1)
359 go executeCommand(*p.command, execCh)
365 case readError := <-stdoutCh:
366 errors = appendError(errors, readError)
368 case execError := <-execCh:
369 err := outWritePipe.Close()
370 errors = appendError(errors, err)
371 errors = appendError(errors, execError)
373 return output.Bytes(), awserr.NewBatchError(
374 ErrCodeProcessProviderExecution,
375 errMsgProcessProviderProcess,
378 case <-time.After(p.Timeout):
380 return output.Bytes(), awserr.NewBatchError(
381 ErrCodeProcessProviderExecution,
382 errMsgProcessProviderTimeout,
383 errors) // errors can be nil
387 out := output.Bytes()
389 if runtime.GOOS == "windows" {
390 // windows adds slashes to quotes
391 out = []byte(strings.Replace(string(out), `\"`, `"`, -1))
397 // appendError conveniently checks for nil before appending slice
398 func appendError(errors []error, err error) []error {
400 return append(errors, err)
405 func executeCommand(cmd exec.Cmd, exec chan error) {
415 func readInput(r io.Reader, w io.Writer, read chan error) {
416 tee := io.TeeReader(r, w)
418 _, err := ioutil.ReadAll(tee)
424 read <- err // will only arrive here when write end of pipe is closed