diff --git a/docs/content/configuration.md b/docs/content/configuration.md index feec3fb401..962da8620f 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -265,6 +265,8 @@ If specifying `environment_credentials`, OPA will expect to find environment var for `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`, in accordance with the convention used by the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). +Please note that if you are using temporary IAM credentials (e.g. assumed IAM role credentials) you have to provide additional `AWS_SESSION_TOKEN` or `AWS_SECURITY_TOKEN` environment variable. + | Field | Type | Required | Description | | --- | --- | --- | --- | | `services[_].credentials.s3_signing.environment_credentials` | `{}` | Yes | Enables AWS signing using environment variables to source the configuration and credentials | diff --git a/plugins/rest/aws.go b/plugins/rest/aws.go index fe6e28409f..48c1b8bbd2 100644 --- a/plugins/rest/aws.go +++ b/plugins/rest/aws.go @@ -29,17 +29,19 @@ const ( ecsRelativePathEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" // ref. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html - accessKeyEnvVar = "AWS_ACCESS_KEY_ID" - secretKeyEnvVar = "AWS_SECRET_ACCESS_KEY" - awsRegionEnvVar = "AWS_REGION" + accessKeyEnvVar = "AWS_ACCESS_KEY_ID" + secretKeyEnvVar = "AWS_SECRET_ACCESS_KEY" + securityTokenEnvVar = "AWS_SECURITY_TOKEN" + sessionTokenEnvVar = "AWS_SESSION_TOKEN" + awsRegionEnvVar = "AWS_REGION" ) // awsCredentials represents the credentials obtained from an AWS credential provider type awsCredentials struct { - AccessKey string - SecretKey string - RegionName string - SecurityToken string + AccessKey string + SecretKey string + RegionName string + SessionToken string } // awsCredentialService represents the interface for AWS credential providers @@ -64,7 +66,15 @@ func (cs *awsEnvironmentCredentialService) credentials() (awsCredentials, error) if creds.RegionName == "" { return creds, errors.New("no " + awsRegionEnvVar + " set in environment") } - creds.SecurityToken = "" // not applicable to this credential provider + // SessionToken is required if using temporaty ENV credentials from assumed IAM role + // Missing SessionToken results with 403 s3 error. + creds.SessionToken = os.Getenv(sessionTokenEnvVar) + if creds.SessionToken == "" { + // In case of missing SessionToken try to get SecurityToken + // AWS switched to use SessionToken, but SecurityToken was left for backward compatibility + creds.SessionToken = os.Getenv(securityTokenEnvVar) + } + return creds, nil } @@ -162,7 +172,7 @@ func (cs *awsMetadataCredentialService) refreshFromService() error { cs.expiration = payload.Expiration cs.creds.AccessKey = payload.AccessKeyID cs.creds.SecretKey = payload.SecretAccessKey - cs.creds.SecurityToken = payload.Token + cs.creds.SessionToken = payload.Token cs.creds.RegionName = cs.RegionName return nil @@ -227,8 +237,8 @@ func signV4(req *http.Request, credService awsCredentialService, theTime time.Ti // the security token header is necessary for ephemeral credentials, e.g. from // the EC2 metadata service - if creds.SecurityToken != "" { - awsHeaders["x-amz-security-token"] = creds.SecurityToken + if creds.SessionToken != "" { + awsHeaders["x-amz-security-token"] = creds.SessionToken } // ref. https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html diff --git a/plugins/rest/aws_test.go b/plugins/rest/aws_test.go index 15cb8e97e4..05b1d15183 100644 --- a/plugins/rest/aws_test.go +++ b/plugins/rest/aws_test.go @@ -42,6 +42,8 @@ func TestEnvironmentCredentialService(t *testing.T) { os.Setenv("AWS_ACCESS_KEY_ID", "") os.Setenv("AWS_SECRET_ACCESS_KEY", "") os.Setenv("AWS_REGION", "") + os.Setenv("AWS_SECURITY_TOKEN", "") + os.Setenv("AWS_SESSION_TOKEN", "") cs := &awsEnvironmentCredentialService{} @@ -59,20 +61,36 @@ func TestEnvironmentCredentialService(t *testing.T) { os.Setenv("AWS_REGION", "us-east-1") - // happy path: all required environment is present - envCreds, err = cs.credentials() - if err != nil { - t.Error("unexpected error: " + err.Error()) + expectedCreds := awsCredentials{ + AccessKey: "MYAWSACCESSKEYGOESHERE", + SecretKey: "MYAWSSECRETACCESSKEYGOESHERE", + RegionName: "us-east-1", + SessionToken: ""} + + testCases := []struct { + tokenEnv string + tokenValue string + }{ + // happy path: all required environment is present + {"", ""}, + // happy path: all required environment is present including security token + {"AWS_SECURITY_TOKEN", "MYSECURITYTOKENGOESHERE"}, + // happy path: all required environment is present including session token that is preferred over security token + {"AWS_SESSION_TOKEN", "MYSESSIONTOKENGOESHERE"}, } - expectedCreds := awsCredentials{ - AccessKey: "MYAWSACCESSKEYGOESHERE", - SecretKey: "MYAWSSECRETACCESSKEYGOESHERE", - RegionName: "us-east-1", - SecurityToken: ""} + for _, testCase := range testCases { + os.Setenv(testCase.tokenEnv, testCase.tokenValue) + expectedCreds.SessionToken = testCase.tokenValue - if envCreds != expectedCreds { - t.Error("expected: ", expectedCreds, " but got: ", envCreds) + envCreds, err = cs.credentials() + if err != nil { + t.Error("unexpected error: " + err.Error()) + } + + if envCreds != expectedCreds { + t.Error("expected: ", expectedCreds, " but got: ", envCreds) + } } } @@ -143,7 +161,7 @@ func TestMetadataCredentialService(t *testing.T) { assertEq(creds.AccessKey, ts.payload.AccessKeyID, t) assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t) assertEq(creds.RegionName, cs.RegionName, t) - assertEq(creds.SecurityToken, ts.payload.Token, t) + assertEq(creds.SessionToken, ts.payload.Token, t) // happy path: verify credentials are cached based on expiry ts.payload.AccessKeyID = "ICHANGEDTHISBUTWEWONTSEEIT" @@ -152,7 +170,7 @@ func TestMetadataCredentialService(t *testing.T) { assertEq(creds.AccessKey, "MYAWSACCESSKEYGOESHERE", t) // the original value assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t) assertEq(creds.RegionName, cs.RegionName, t) - assertEq(creds.SecurityToken, ts.payload.Token, t) + assertEq(creds.SessionToken, ts.payload.Token, t) // happy path: with refresh // first time through @@ -172,7 +190,7 @@ func TestMetadataCredentialService(t *testing.T) { assertEq(creds.AccessKey, ts.payload.AccessKeyID, t) assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t) assertEq(creds.RegionName, cs.RegionName, t) - assertEq(creds.SecurityToken, ts.payload.Token, t) + assertEq(creds.SessionToken, ts.payload.Token, t) // second time through, with changes ts.payload.AccessKeyID = "ICHANGEDTHISANDWEWILLSEEIT" @@ -181,16 +199,16 @@ func TestMetadataCredentialService(t *testing.T) { assertEq(creds.AccessKey, ts.payload.AccessKeyID, t) // the new value assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t) assertEq(creds.RegionName, cs.RegionName, t) - assertEq(creds.SecurityToken, ts.payload.Token, t) + assertEq(creds.SessionToken, ts.payload.Token, t) } type testCredentialService struct{} func (cs *testCredentialService) credentials() (awsCredentials, error) { return awsCredentials{AccessKey: "MYAWSACCESSKEYGOESHERE", - SecretKey: "MYAWSSECRETACCESSKEYGOESHERE", - RegionName: "us-east-1", - SecurityToken: "MYAWSSECURITYTOKENGOESHERE"}, nil + SecretKey: "MYAWSSECRETACCESSKEYGOESHERE", + RegionName: "us-east-1", + SessionToken: "MYAWSSECURITYTOKENGOESHERE"}, nil } func TestV4Signing(t *testing.T) {