Skip to content

Commit

Permalink
Credentials: Support assuming role via WebIdentityTokenFile
Browse files Browse the repository at this point in the history
This supports the new AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_ARN environment
variables, that allow exchanging OIDC tokens given to pods in EKS for access
tokens.

Fixes minio#1156
  • Loading branch information
saracen committed Nov 12, 2019
1 parent 437215b commit 9df10ec
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 16 deletions.
54 changes: 40 additions & 14 deletions pkg/credentials/iam_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -53,20 +54,10 @@ type IAM struct {
const (
defaultIAMRoleEndpoint = "http://169.254.169.254"
defaultECSRoleEndpoint = "http://169.254.170.2"
defaultSTSRoleEndpoint = "https://sts.amazonaws.com"
defaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials/"
)

// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
func getEndpoint(endpoint string) (string, bool) {
if endpoint != "" {
return endpoint, os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != ""
}
if ecsURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"); ecsURI != "" {
return fmt.Sprintf("%s%s", defaultECSRoleEndpoint, ecsURI), true
}
return defaultIAMRoleEndpoint, false
}

// NewIAM returns a pointer to a new Credentials object wrapping the IAM.
func NewIAM(endpoint string) *Credentials {
p := &IAM{
Expand All @@ -82,14 +73,49 @@ func NewIAM(endpoint string) *Credentials {
// Error will be returned if the request fails, or unable to extract
// the desired
func (m *IAM) Retrieve() (Value, error) {
endpoint, isEcsTask := getEndpoint(m.endpoint)
var roleCreds ec2RoleCredRespBody
var err error
if isEcsTask {

endpoint := m.endpoint
switch {
case len(os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE")) > 0:
if len(endpoint) == 0 {
if len(os.Getenv("AWS_REGION")) > 0 {
endpoint = "sts." + os.Getenv("AWS_REGION") + ".amazonaws.com"
} else {
endpoint = defaultSTSRoleEndpoint
}
}

creds := &STSWebIdentity{
Client: m.Client,
stsEndpoint: endpoint,
roleARN: os.Getenv("AWS_ROLE_ARN"),
roleSessionName: os.Getenv("AWS_ROLE_SESSION_NAME"),
getWebIDTokenExpiry: func() (*WebIdentityToken, error) {
token, err := ioutil.ReadFile(os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE"))
if err != nil {
return nil, err
}

return &WebIdentityToken{Token: string(token)}, nil
},
}

return creds.Retrieve()

case len(os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) > 0:
if len(endpoint) == 0 {
endpoint = fmt.Sprintf("%s%s", defaultECSRoleEndpoint,
os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))
}

roleCreds, err = getEcsTaskCredentials(m.Client, endpoint)
} else {

default:
roleCreds, err = getCredentials(m.Client, endpoint)
}

if err != nil {
return Value{}, err
}
Expand Down
79 changes: 79 additions & 0 deletions pkg/credentials/iam_aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package credentials

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
Expand Down Expand Up @@ -51,6 +52,27 @@ const credsRespEcsTaskTmpl = `{
"Expiration" : "%s"
}`

const credsRespStsImpl = `<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithWebIdentityResult>
<SubjectFromWebIdentityToken>amzn1.account.AF6RHO7KZU5XRVQJGXK6HB56KR2A</SubjectFromWebIdentityToken>
<Audience>[email protected]</Audience>
<AssumedRoleUser>
<Arn>arn:aws:sts::123456789012:assumed-role/FederatedWebIdentityRole/app1</Arn>
<AssumedRoleId>AROACLKWSDQRAOEXAMPLE:app1</AssumedRoleId>
</AssumedRoleUser>
<Credentials>
<SessionToken>token</SessionToken>
<SecretAccessKey>secret</SecretAccessKey>
<Expiration>%s</Expiration>
<AccessKeyId>accessKey</AccessKeyId>
</Credentials>
<Provider>www.amazon.com</Provider>
</AssumeRoleWithWebIdentityResult>
<ResponseMetadata>
<RequestId>ad4156e9-bce1-11e2-82e6-6b6efEXAMPLE</RequestId>
</ResponseMetadata>
</AssumeRoleWithWebIdentityResponse>`

func initTestFailServer() *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not allowed", http.StatusBadRequest)
Expand Down Expand Up @@ -91,6 +113,22 @@ func initEcsTaskTestServer(expireOn string) *httptest.Server {
return server
}

func initStsTestServer(expireOn string) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
required := []string{"RoleArn", "RoleSessionName", "WebIdentityToken", "Version"}
for _, field := range required {
if _, ok := r.URL.Query()[field]; !ok {
http.Error(w, fmt.Sprintf("%s missing", field), http.StatusBadRequest)
return
}
}

fmt.Fprintf(w, credsRespStsImpl, expireOn)
}))

return server
}

func TestIAMMalformedEndpoint(t *testing.T) {
creds := NewIAM("%%%%")
_, err := creds.Get()
Expand Down Expand Up @@ -243,3 +281,44 @@ func TestEcsTask(t *testing.T) {
t.Error("Expected creds to be expired.")
}
}

func TestSts(t *testing.T) {
server := initStsTestServer("2014-12-16T01:51:37Z")
defer server.Close()
p := &IAM{
Client: http.DefaultClient,
endpoint: server.URL,
}

f, err := ioutil.TempFile("", "minio-go")
if err != nil {
t.Errorf("Unexpected failure %s", err)
}
defer os.Remove(f.Name())
f.Write([]byte("token"))
f.Close()

os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", f.Name())
os.Setenv("AWS_ROLE_ARN", "arn:aws:sts::123456789012:assumed-role/FederatedWebIdentityRole/app1")
creds, err := p.Retrieve()
os.Unsetenv("AWS_WEB_IDENTITY_TOKEN_FILE")
os.Unsetenv("AWS_ROLE_ARN")
if err != nil {
t.Errorf("Unexpected failure %s", err)
}
if "accessKey" != creds.AccessKeyID {
t.Errorf("Expected \"accessKey\", got %s", creds.AccessKeyID)
}

if "secret" != creds.SecretAccessKey {
t.Errorf("Expected \"secret\", got %s", creds.SecretAccessKey)
}

if "token" != creds.SessionToken {
t.Errorf("Expected \"token\", got %s", creds.SessionToken)
}

if !p.IsExpired() {
t.Error("Expected creds to be expired.")
}
}
20 changes: 18 additions & 2 deletions pkg/credentials/sts_web_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)

Expand Down Expand Up @@ -75,6 +76,13 @@ type STSWebIdentity struct {
// this token.
// This is a customer provided function and is mandatory.
getWebIDTokenExpiry func() (*WebIdentityToken, error)

// roleARN is the Amazon Resource Name (ARN) of the role that the caller is
// assuming.
roleARN string

// roleSessionName is the identifier for the assumed role session.
roleSessionName string
}

// NewSTSWebIdentity returns a pointer to a new
Expand All @@ -95,7 +103,7 @@ func NewSTSWebIdentity(stsEndpoint string, getWebIDTokenExpiry func() (*WebIdent
}), nil
}

func getWebIdentityCredentials(clnt *http.Client, endpoint string,
func getWebIdentityCredentials(clnt *http.Client, endpoint, roleARN, roleSessionName string,
getWebIDTokenExpiry func() (*WebIdentityToken, error)) (AssumeRoleWithWebIdentityResponse, error) {
idToken, err := getWebIDTokenExpiry()
if err != nil {
Expand All @@ -104,6 +112,14 @@ func getWebIdentityCredentials(clnt *http.Client, endpoint string,

v := url.Values{}
v.Set("Action", "AssumeRoleWithWebIdentity")
if len(roleARN) > 0 {
v.Set("RoleArn", roleARN)

if len(roleSessionName) == 0 {
roleSessionName = strconv.FormatInt(time.Now().UnixNano(), 10)
}
v.Set("RoleSessionName", roleSessionName)
}
v.Set("WebIdentityToken", idToken.Token)
v.Set("DurationSeconds", fmt.Sprintf("%d", idToken.Expiry))
v.Set("Version", "2011-06-15")
Expand Down Expand Up @@ -141,7 +157,7 @@ func getWebIdentityCredentials(clnt *http.Client, endpoint string,
// Retrieve retrieves credentials from the MinIO service.
// Error will be returned if the request fails.
func (m *STSWebIdentity) Retrieve() (Value, error) {
a, err := getWebIdentityCredentials(m.Client, m.stsEndpoint, m.getWebIDTokenExpiry)
a, err := getWebIdentityCredentials(m.Client, m.stsEndpoint, m.roleARN, m.roleSessionName, m.getWebIDTokenExpiry)
if err != nil {
return Value{}, err
}
Expand Down

0 comments on commit 9df10ec

Please sign in to comment.