Skip to content

Commit

Permalink
aws: Enable account ID check for assumed roles + EC2 instances
Browse files Browse the repository at this point in the history
  • Loading branch information
ColinHebert authored and radeksimko committed Apr 27, 2016
1 parent fb91833 commit f1f602c
Show file tree
Hide file tree
Showing 5 changed files with 939 additions and 457 deletions.
134 changes: 134 additions & 0 deletions builtin/providers/aws/auth_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package aws

import (
"fmt"
"log"
"os"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
awsCredentials "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/go-cleanhttp"
)

func GetAccountId(iamconn *iam.IAM, authProviderName string) (string, error) {
// If we have creds from instance profile, we can use metadata API
if authProviderName == ec2rolecreds.ProviderName {
log.Println("[DEBUG] Trying to get account ID via AWS Metadata API")

cfg := &aws.Config{}
setOptionalEndpoint(cfg)
metadataClient := ec2metadata.New(session.New(cfg))
info, err := metadataClient.IAMInfo()
if err != nil {
// This can be triggered when no IAM Role is assigned
// or AWS just happens to return invalid response
return "", fmt.Errorf("Failed getting EC2 IAM info: %s", err)
}

return parseAccountIdFromArn(info.InstanceProfileArn)
}

// Then try IAM GetUser
log.Println("[DEBUG] Trying to get account ID via iam:GetUser")
outUser, err := iamconn.GetUser(nil)
if err == nil {
return parseAccountIdFromArn(*outUser.User.Arn)
}

// Then try IAM ListRoles
awsErr, ok := err.(awserr.Error)
// AccessDenied and ValidationError can be raised
// if credentials belong to federated profile, so we ignore these
if !ok || (awsErr.Code() != "AccessDenied" && awsErr.Code() != "ValidationError") {
return "", fmt.Errorf("Failed getting account ID via 'iam:GetUser': %s", err)
}

log.Printf("[DEBUG] Getting account ID via iam:GetUser failed: %s", err)
log.Println("[DEBUG] Trying to get account ID via iam:ListRoles instead")
outRoles, err := iamconn.ListRoles(&iam.ListRolesInput{
MaxItems: aws.Int64(int64(1)),
})
if err != nil {
return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': %s", err)
}

if len(outRoles.Roles) < 1 {
return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': No roles available")
}

return parseAccountIdFromArn(*outRoles.Roles[0].Arn)
}

func parseAccountIdFromArn(arn string) (string, error) {
parts := strings.Split(arn, ":")
if len(parts) < 5 {
return "", fmt.Errorf("Unable to parse ID from invalid ARN: %q", arn)
}
return parts[4], nil
}

// This function is responsible for reading credentials from the
// environment in the case that they're not explicitly specified
// in the Terraform configuration.
func GetCredentials(key, secret, token, profile, credsfile string) *awsCredentials.Credentials {
// build a chain provider, lazy-evaulated by aws-sdk
providers := []awsCredentials.Provider{
&awsCredentials.StaticProvider{Value: awsCredentials.Value{
AccessKeyID: key,
SecretAccessKey: secret,
SessionToken: token,
}},
&awsCredentials.EnvProvider{},
&awsCredentials.SharedCredentialsProvider{
Filename: credsfile,
Profile: profile,
},
}

// Build isolated HTTP client to avoid issues with globally-shared settings
client := cleanhttp.DefaultClient()

// Keep the timeout low as we don't want to wait in non-EC2 environments
client.Timeout = 100 * time.Millisecond
cfg := &aws.Config{
HTTPClient: client,
}
usedEndpoint := setOptionalEndpoint(cfg)

// Real AWS should reply to a simple metadata request.
// We check it actually does to ensure something else didn't just
// happen to be listening on the same IP:Port
metadataClient := ec2metadata.New(session.New(cfg))
if metadataClient.Available() {
providers = append(providers, &ec2rolecreds.EC2RoleProvider{
Client: metadataClient,
})
log.Printf("[INFO] AWS EC2 instance detected via default metadata" +
" API endpoint, EC2RoleProvider added to the auth chain")
} else {
if usedEndpoint == "" {
usedEndpoint = "default location"
}
log.Printf("[WARN] Ignoring AWS metadata API endpoint at %s "+
"as it doesn't return any instance-id", usedEndpoint)
}

return awsCredentials.NewChainCredentials(providers)
}

func setOptionalEndpoint(cfg *aws.Config) string {
endpoint := os.Getenv("AWS_METADATA_URL")
if endpoint != "" {
log.Printf("[INFO] Setting custom metadata endpoint: %q", endpoint)
cfg.Endpoint = aws.String(endpoint)
return endpoint
}
return ""
}
Loading

0 comments on commit f1f602c

Please sign in to comment.