From 5f16f4a238f5f1f23e82f586ad4741ce618b4943 Mon Sep 17 00:00:00 2001 From: Ashutosh Narkar Date: Fri, 15 Mar 2024 15:18:54 -0700 Subject: [PATCH] plugins/rest: Add support to get temp creds via AssumeRole Adds support for signing AWS requests using temporary credentials obtained from AWS STS via AssumeRole operation. One use-case of this mechanism is for allowing existing IAM users to access AWS resources that they don't already have access to. It is also useful as a means to temporarily gain privileged access. Signed-off-by: Ashutosh Narkar --- docs/content/configuration.md | 46 +++++ docs/content/management-bundles.md | 33 ++- plugins/rest/auth.go | 13 ++ plugins/rest/auth_test.go | 56 +++++ plugins/rest/aws.go | 201 ++++++++++++++++-- plugins/rest/aws_test.go | 319 +++++++++++++++++++++++++++-- plugins/rest/rest_test.go | 50 +++++ 7 files changed, 684 insertions(+), 34 deletions(-) diff --git a/docs/content/configuration.md b/docs/content/configuration.md index c0e47c0ffb..2148e6b5df 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -572,6 +572,52 @@ containers have at most one associated IAM role. | `services[_].credentials.s3_signing.metadata_credentials.aws_region` | `string` | No | The AWS region to use for the AWS signing service credential method. If unset, the `AWS_REGION` environment variable must be set | | `services[_].credentials.s3_signing.metadata_credentials.iam_role` | `string` | No | The IAM role to use for the AWS signing service credential method | + +##### Using AWS Security Token Service (AWS STS) via AssumeRole +If specifying `assume_role_credentials`, OPA will use [AWS STS](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) +to obtain temporary security credentials for accessing AWS resources. In order to retrieve temporary security credentials from STS +via [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) valid AWS security credentials are required. + +{{< info >}} +For using `services[_].credentials.s3_signing.assume_role_credentials`, a method for setting the AWS credentials has to be specified in the `services[_].credentials.s3_signing.assume_role_credentials.aws_signing`. +The value of `services[_].credentials.s3_signing.assume_role_credentials.aws_signing.service` is set to `STS`. Several methods of obtaining the necessary credentials are available; exactly one must be specified, +see description for `services[_].credentials.s3_signing`. Currently supported methods are `services[_].credentials.s3_signing.environment_credentials`, `services[_].credentials.s3_signing.profile_credentials` and +`services[_].credentials.s3_signing.metadata_credentials`. OPA will follow this *internally defined* order of precedence when multiple credential providers are specified. +{{< /info >}} + + +| Field | Type | Required | Description | +|-------------------------------------------------------------------| --- | -- | --- | +| `services[_].credentials.s3_signing.assume_role_credentials.aws_region` | `string` | Yes | The AWS region to use for the sts regional endpoint. Uses the global endpoint by default | +| `services[_].credentials.s3_signing.assume_role_credentials.iam_role_arn` | `string` | Yes | The IAM Role ARN to be assumed. Can also be set via the `AWS_ROLE_ARN` environment variable (config takes precedence) | +| `services[_].credentials.s3_signing.assume_role_credentials.aws_signing` | `{}` | Yes | AWS credentials for signing requests. | +| `services[_].credentials.s3_signing.assume_role_credentials.session_name` | `string` | No | The session name used to identify the assumed role session. Default: `open-policy-agent` | +| `services[_].credentials.s3_signing.assume_role_credentials.aws_domain` | `string` | No | The AWS domain name to use. Default: `amazonaws.com`. Can also be set via the `AWS_DOMAIN` environment variable (config takes precedence) | + +##### Example + +Using Assume Role Credentials type with EC2 Metadata Credentials signing plugin. + +```yaml +services: + remote: + url: ${BUNDLE_SERVICE_URL} + credentials: + assume_role_credentials: + aws_region: us-east-1 + iam_role_arn: arn:aws::iam::123456789012:role/demo + session_name: demo + aws_signing: # similar to s3_signing + metadata_credentials: + aws_region: us-east-1 + iam_role: s3access + +bundles: + authz: + service: remote + resource: bundles/http/example/authz.tar.gz +``` + ##### Using EKS IAM Roles for Service Account (Web Identity) Credentials If specifying `web_identity_credentials`, OPA will expect to find environment variables for `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE`, in accordance with the convention used by the [AWS EKS IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). diff --git a/docs/content/management-bundles.md b/docs/content/management-bundles.md index 4b4f9c0cd4..1da6d94b1b 100644 --- a/docs/content/management-bundles.md +++ b/docs/content/management-bundles.md @@ -827,6 +827,32 @@ bundles: **NOTE:** the S3 `url` is the bucket's regional endpoint. + +##### Assume Role Credentials + +```yaml +services: + s3: + url: https://my-example-opa-bucket.s3.us-east-1.amazonaws.com + credentials: + s3_signing: + assume_role_credentials: + aws_region: us-east-1 + iam_role_arn: arn:aws::iam::123456789012:role/demo + session_name: my-open-policy-agent # Optional. Default: open-policy-agent + aws_signing: # similar to s3_signing + metadata_credentials: + aws_region: us-east-1 + iam_role: s3access + +bundles: + authz: + service: s3 + resource: bundle.tar.gz +``` + +**NOTE:** the S3 `url` is the bucket's regional endpoint. + ##### Web Identity Credentials ```yaml @@ -852,9 +878,10 @@ bundles: Multiple AWS credential providers can be configured. OPA will follow an *internally defined* order to try each of the credential provider given in the configuration till success. Following order of precedence is followed when multiple credential provider is given in the configuration 1. Environment Credential -2. Web Identity Credential -3. Profile Credential -4. Metadata Credential +1. Assume Role Credential +1. Web Identity Credential +1. Profile Credential +1. Metadata Credential ```yaml services: diff --git a/plugins/rest/auth.go b/plugins/rest/auth.go index f69496f08b..52e9e81335 100644 --- a/plugins/rest/auth.go +++ b/plugins/rest/auth.go @@ -700,6 +700,7 @@ func (ap *clientTLSAuthPlugin) Prepare(req *http.Request) error { type awsSigningAuthPlugin struct { AWSEnvironmentCredentials *awsEnvironmentCredentialService `json:"environment_credentials,omitempty"` AWSMetadataCredentials *awsMetadataCredentialService `json:"metadata_credentials,omitempty"` + AWSAssumeRoleCredentials *awsAssumeRoleCredentialService `json:"assume_role_credentials,omitempty"` AWSWebIdentityCredentials *awsWebIdentityCredentialService `json:"web_identity_credentials,omitempty"` AWSProfileCredentials *awsProfileCredentialService `json:"profile_credentials,omitempty"` @@ -796,6 +797,11 @@ func (ap *awsSigningAuthPlugin) awsCredentialService() awsCredentialService { chain.addService(ap.AWSEnvironmentCredentials) } + if ap.AWSAssumeRoleCredentials != nil { + ap.AWSAssumeRoleCredentials.logger = ap.logger + chain.addService(ap.AWSAssumeRoleCredentials) + } + if ap.AWSWebIdentityCredentials != nil { ap.AWSWebIdentityCredentials.logger = ap.logger chain.addService(ap.AWSWebIdentityCredentials) @@ -851,6 +857,7 @@ func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error cfgs := map[bool]int{} cfgs[ap.AWSEnvironmentCredentials != nil]++ cfgs[ap.AWSMetadataCredentials != nil]++ + cfgs[ap.AWSAssumeRoleCredentials != nil]++ cfgs[ap.AWSWebIdentityCredentials != nil]++ cfgs[ap.AWSProfileCredentials != nil]++ @@ -864,6 +871,12 @@ func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error } } + if ap.AWSAssumeRoleCredentials != nil { + if err := ap.AWSAssumeRoleCredentials.populateFromEnv(); err != nil { + return err + } + } + if ap.AWSWebIdentityCredentials != nil { if err := ap.AWSWebIdentityCredentials.populateFromEnv(); err != nil { return err diff --git a/plugins/rest/auth_test.go b/plugins/rest/auth_test.go index f67bbcf64f..10e375f180 100644 --- a/plugins/rest/auth_test.go +++ b/plugins/rest/auth_test.go @@ -135,3 +135,59 @@ func TestOauth2WithAWSKMS(t *testing.T) { t.Errorf("OAuth2.AWSSigningPlugin.kmsSignPlugin isn't setup") } } + +func TestAssumeRoleWithNoSigningProvider(t *testing.T) { + conf := `{ + "name": "foo", + "url": "https://my-example-opa-bucket.s3.eu-north-1.amazonaws.com", + "credentials": { + "s3_signing": { + "service": "s3", + "assume_role_credentials": {} + } + } + }` + + client, err := New([]byte(conf), map[string]*keys.Config{}) + if err != nil { + t.Fatal(err) + } + + _, err = client.config.Credentials.S3Signing.NewClient(client.config) + if err == nil { + t.Fatal("expected error but got nil") + } + + expErrMsg := "a AWS signing plugin must be specified when AssumeRole credential provider is enabled" + if err.Error() != expErrMsg { + t.Fatalf("expected error: %v but got: %v", expErrMsg, err) + } +} + +func TestAssumeRoleWithUnsupportedSigningProvider(t *testing.T) { + conf := `{ + "name": "foo", + "url": "https://my-example-opa-bucket.s3.eu-north-1.amazonaws.com", + "credentials": { + "s3_signing": { + "service": "s3", + "assume_role_credentials": {"aws_signing": {"web_identity_credentials": {}}} + } + } + }` + + client, err := New([]byte(conf), map[string]*keys.Config{}) + if err != nil { + t.Fatal(err) + } + + _, err = client.config.Credentials.S3Signing.NewClient(client.config) + if err == nil { + t.Fatal("expected error but got nil") + } + + expErrMsg := "unsupported AWS signing plugin with AssumeRole credential provider" + if err.Error() != expErrMsg { + t.Fatalf("expected error: %v but got: %v", expErrMsg, err) + } +} diff --git a/plugins/rest/aws.go b/plugins/rest/aws.go index 3e78b1fad2..ae1baa2647 100644 --- a/plugins/rest/aws.go +++ b/plugins/rest/aws.go @@ -318,6 +318,169 @@ func (cs *awsMetadataCredentialService) credentials(ctx context.Context) (aws.Cr return cs.creds, nil } +// awsAssumeRoleCredentialService represents a STS credential service that uses active IAM credentials +// to obtain temporary security credentials generated by AWS STS via AssumeRole API operation +type awsAssumeRoleCredentialService struct { + RegionName string `json:"aws_region"` + RoleArn string `json:"iam_role_arn"` + SessionName string `json:"session_name"` + Domain string `json:"aws_domain"` + AWSSigningPlugin *awsSigningAuthPlugin `json:"aws_signing,omitempty"` + stsURL string + creds aws.Credentials + expiration time.Time + logger logging.Logger +} + +func (cs *awsAssumeRoleCredentialService) populateFromEnv() error { + if cs.AWSSigningPlugin == nil { + return errors.New("a AWS signing plugin must be specified when AssumeRole credential provider is enabled") + } + + switch { + case cs.AWSSigningPlugin.AWSEnvironmentCredentials != nil: + case cs.AWSSigningPlugin.AWSProfileCredentials != nil: + case cs.AWSSigningPlugin.AWSMetadataCredentials != nil: + default: + return errors.New("unsupported AWS signing plugin with AssumeRole credential provider") + } + + if cs.AWSSigningPlugin.AWSMetadataCredentials != nil { + if cs.AWSSigningPlugin.AWSMetadataCredentials.RegionName == "" { + if cs.AWSSigningPlugin.AWSMetadataCredentials.RegionName = os.Getenv(awsRegionEnvVar); cs.AWSSigningPlugin.AWSMetadataCredentials.RegionName == "" { + return errors.New("no " + awsRegionEnvVar + " set in environment or configuration") + } + } + } + + if cs.AWSSigningPlugin.AWSSignatureVersion == "" { + cs.AWSSigningPlugin.AWSSignatureVersion = "4" + } + + if cs.Domain == "" { + cs.Domain = os.Getenv(awsDomainEnvVar) + } + + if cs.RegionName == "" { + if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" { + return errors.New("no " + awsRegionEnvVar + " set in environment or configuration") + } + } + + if cs.RoleArn == "" { + if cs.RoleArn = os.Getenv(awsRoleArnEnvVar); cs.RoleArn == "" { + return errors.New("no " + awsRoleArnEnvVar + " set in environment or configuration") + } + } + + return nil +} + +func (cs *awsAssumeRoleCredentialService) signingCredentials(ctx context.Context) (aws.Credentials, error) { + if cs.AWSSigningPlugin.AWSEnvironmentCredentials != nil { + cs.AWSSigningPlugin.AWSEnvironmentCredentials.logger = cs.logger + return cs.AWSSigningPlugin.AWSEnvironmentCredentials.credentials(ctx) + } + + if cs.AWSSigningPlugin.AWSProfileCredentials != nil { + cs.AWSSigningPlugin.AWSProfileCredentials.logger = cs.logger + return cs.AWSSigningPlugin.AWSProfileCredentials.credentials(ctx) + } + + cs.AWSSigningPlugin.AWSMetadataCredentials.logger = cs.logger + return cs.AWSSigningPlugin.AWSMetadataCredentials.credentials(ctx) +} + +func (cs *awsAssumeRoleCredentialService) stsPath() string { + return getSTSPath(cs.Domain, cs.stsURL, cs.RegionName) +} + +func (cs *awsAssumeRoleCredentialService) refreshFromService(ctx context.Context) error { + // define the expected JSON payload from the EC2 credential service + // ref. https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html + type responsePayload struct { + Result struct { + Credentials struct { + SessionToken string + SecretAccessKey string + Expiration time.Time + AccessKeyID string `xml:"AccessKeyId"` + } + } `xml:"AssumeRoleResult"` + } + + // short circuit if a reasonable amount of time until credential expiration remains + if time.Now().Add(time.Minute * 5).Before(cs.expiration) { + cs.logger.Debug("Credentials previously obtained from sts service still valid.") + return nil + } + + cs.logger.Debug("Obtaining credentials from sts for role %s.", cs.RoleArn) + + var sessionName string + if cs.SessionName == "" { + sessionName = "open-policy-agent" + } else { + sessionName = cs.SessionName + } + + queryVals := url.Values{ + "Action": []string{"AssumeRole"}, + "RoleSessionName": []string{sessionName}, + "RoleArn": []string{cs.RoleArn}, + "Version": []string{"2011-06-15"}, + } + stsRequestURL, _ := url.Parse(cs.stsPath()) + + // construct an HTTP client with a reasonably short timeout + client := &http.Client{Timeout: time.Second * 10} + req, err := http.NewRequestWithContext(ctx, http.MethodPost, stsRequestURL.String(), strings.NewReader(queryVals.Encode())) + if err != nil { + return errors.New("unable to construct STS HTTP request: " + err.Error()) + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + // Note: Calls to AWS STS AssumeRole must be signed using the access key ID + // and secret access key + signingCreds, err := cs.signingCredentials(ctx) + if err != nil { + return err + } + + err = aws.SignRequest(req, "STS", signingCreds, time.Now(), cs.AWSSigningPlugin.AWSSignatureVersion) + if err != nil { + return err + } + + body, err := aws.DoRequestWithClient(req, client, "STS", cs.logger) + if err != nil { + return err + } + + var payload responsePayload + err = xml.Unmarshal(body, &payload) + if err != nil { + return errors.New("failed to parse credential response from STS service: " + err.Error()) + } + + cs.expiration = payload.Result.Credentials.Expiration + cs.creds.AccessKey = payload.Result.Credentials.AccessKeyID + cs.creds.SecretKey = payload.Result.Credentials.SecretAccessKey + cs.creds.SessionToken = payload.Result.Credentials.SessionToken + cs.creds.RegionName = cs.RegionName + + return nil +} + +func (cs *awsAssumeRoleCredentialService) credentials(ctx context.Context) (aws.Credentials, error) { + err := cs.refreshFromService(ctx) + if err != nil { + return cs.creds, err + } + return cs.creds, nil +} + // awsWebIdentityCredentialService represents an STS WebIdentity credential services type awsWebIdentityCredentialService struct { RoleArn string @@ -354,23 +517,7 @@ func (cs *awsWebIdentityCredentialService) populateFromEnv() error { } func (cs *awsWebIdentityCredentialService) stsPath() string { - var domain string - if cs.Domain != "" { - domain = strings.ToLower(cs.Domain) - } else { - domain = stsDefaultDomain - } - - var stsPath string - switch { - case cs.stsURL != "": - stsPath = cs.stsURL - case cs.RegionName != "": - stsPath = fmt.Sprintf(stsRegionPath, strings.ToLower(cs.RegionName), domain) - default: - stsPath = fmt.Sprintf(stsDefaultPath, domain) - } - return stsPath + return getSTSPath(cs.Domain, cs.stsURL, cs.RegionName) } func (cs *awsWebIdentityCredentialService) refreshFromService(ctx context.Context) error { @@ -555,3 +702,23 @@ func (ap *awsKMSSignPlugin) SignDigest(ctx context.Context, digest []byte, keyID return signature, nil } + +func getSTSPath(stsDomain, stsURL, regionName string) string { + var domain string + if stsDomain != "" { + domain = strings.ToLower(stsDomain) + } else { + domain = stsDefaultDomain + } + + var stsPath string + switch { + case stsURL != "": + stsPath = stsURL + case regionName != "": + stsPath = fmt.Sprintf(stsRegionPath, strings.ToLower(regionName), domain) + default: + stsPath = fmt.Sprintf(stsDefaultPath, domain) + } + return stsPath +} diff --git a/plugins/rest/aws_test.go b/plugins/rest/aws_test.go index 04e7b7461f..ebd743f89b 100644 --- a/plugins/rest/aws_test.go +++ b/plugins/rest/aws_test.go @@ -1006,8 +1006,9 @@ func TestWebIdentityCredentialService(t *testing.T) { testAccessKey := "ASgeIAIOSFODNN7EXAMPLE" ts := stsTestServer{ - t: t, - accessKey: testAccessKey, + t: t, + accessKey: testAccessKey, + assumeRoleWithWebIdentity: true, } ts.start() defer ts.stop() @@ -1092,6 +1093,255 @@ func TestWebIdentityCredentialService(t *testing.T) { }) } +func TestAssumeRoleCredentialServiceUsingWrongSigningProvider(t *testing.T) { + t.Setenv("AWS_REGION", "us-west-1") + + testAccessKey := "ASgeIAIOSFODNN7EXAMPLE" + ts := stsTestServer{ + t: t, + accessKey: testAccessKey, + assumeRoleWithWebIdentity: false, + } + ts.start() + defer ts.stop() + cs := awsAssumeRoleCredentialService{ + stsURL: ts.server.URL, + logger: logging.Get(), + } + + // wrong path: no AWS signing plugin + err := cs.populateFromEnv() + assertErr("a AWS signing plugin must be specified when AssumeRole credential provider is enabled", err, t) + + // wrong path: unsupported AWS signing plugin + cs.AWSSigningPlugin = &awsSigningAuthPlugin{AWSWebIdentityCredentials: &awsWebIdentityCredentialService{}} + err = cs.populateFromEnv() + assertErr("unsupported AWS signing plugin with AssumeRole credential provider", err, t) +} + +func TestAssumeRoleCredentialServiceUsingEnvCredentialsProvider(t *testing.T) { + t.Setenv("AWS_REGION", "us-west-1") + + testAccessKey := "ASgeIAIOSFODNN7EXAMPLE" + ts := stsTestServer{ + t: t, + accessKey: testAccessKey, + assumeRoleWithWebIdentity: false, + } + ts.start() + defer ts.stop() + cs := awsAssumeRoleCredentialService{ + stsURL: ts.server.URL, + logger: logging.Get(), + AWSSigningPlugin: &awsSigningAuthPlugin{AWSEnvironmentCredentials: &awsEnvironmentCredentialService{}}, + } + + // wrong path: no AWS IAM Role ARN set in environment or config + err := cs.populateFromEnv() + assertErr("no AWS_ROLE_ARN set in environment or configuration", err, t) + t.Setenv("AWS_ROLE_ARN", "role:arn") + + // happy path: set AWS IAM Role ARN as env var + err = cs.populateFromEnv() + if err != nil { + t.Fatalf("Error while getting env vars: %s", err) + } + + // happy path: set AWS IAM Role ARN in config + os.Unsetenv("AWS_ROLE_ARN") + cs.RoleArn = "role:arn" + + err = cs.populateFromEnv() + if err != nil { + t.Fatalf("Error while getting env vars: %s", err) + } + + // wrong path: refresh and get credentials but signing credentials not set via env variables + _, err = cs.credentials(context.Background()) + assertErr("no AWS_ACCESS_KEY_ID set in environment", err, t) + + t.Setenv("AWS_ACCESS_KEY_ID", "MYAWSACCESSKEYGOESHERE") + + _, err = cs.credentials(context.Background()) + assertErr("no AWS_SECRET_ACCESS_KEY set in environment", err, t) + + t.Setenv("AWS_SECRET_ACCESS_KEY", "MYAWSSECRETACCESSKEYGOESHERE") + + // happy path: refresh and get credentials + creds, _ := cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: refresh with session and get credentials + cs.expiration = time.Now() + cs.SessionName = "TEST_SESSION" + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: don't refresh as credentials not expired so STS not called + // verify existing credentials haven't changed + ts.accessKey = "OTHERKEY" + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: refresh expired credentials + // verify new credentials are set + cs.expiration = time.Now() + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, ts.accessKey, t) +} + +func TestAssumeRoleCredentialServiceUsingProfileCredentialsProvider(t *testing.T) { + t.Setenv("AWS_REGION", "us-west-1") + + testAccessKey := "ASgeIAIOSFODNN7EXAMPLE" + ts := stsTestServer{ + t: t, + accessKey: testAccessKey, + assumeRoleWithWebIdentity: false, + } + ts.start() + defer ts.stop() + + defaultKey := "MYAWSACCESSKEYGOESHERE" + defaultSecret := "MYAWSSECRETACCESSKEYGOESHERE" + defaultSessionToken := "AQoEXAMPLEH4aoAH0gNCAPy" + + config := fmt.Sprintf(` +[foo] +aws_access_key_id=%v +aws_secret_access_key=%v +aws_session_token=%v +`, defaultKey, defaultSecret, defaultSessionToken) + + files := map[string]string{ + "example.ini": config, + } + + test.WithTempFS(files, func(path string) { + cfgPath := filepath.Join(path, "example.ini") + + cs := awsAssumeRoleCredentialService{ + stsURL: ts.server.URL, + logger: logging.Get(), + AWSSigningPlugin: &awsSigningAuthPlugin{AWSProfileCredentials: &awsProfileCredentialService{Path: cfgPath, Profile: "foo"}}, + } + + // wrong path: no AWS IAM Role ARN set in environment or config + err := cs.populateFromEnv() + assertErr("no AWS_ROLE_ARN set in environment or configuration", err, t) + t.Setenv("AWS_ROLE_ARN", "role:arn") + + // happy path: set AWS IAM Role ARN as env var + err = cs.populateFromEnv() + if err != nil { + t.Fatalf("Error while getting env vars: %s", err) + } + + // happy path: set AWS IAM Role ARN in config + os.Unsetenv("AWS_ROLE_ARN") + cs.RoleArn = "role:arn" + + err = cs.populateFromEnv() + if err != nil { + t.Fatalf("Error while getting env vars: %s", err) + } + + // happy path: refresh and get credentials + creds, _ := cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: refresh with session and get credentials + cs.expiration = time.Now() + cs.SessionName = "TEST_SESSION" + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: don't refresh as credentials not expired so STS not called + // verify existing credentials haven't changed + ts.accessKey = "OTHERKEY" + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: refresh expired credentials + // verify new credentials are set + cs.expiration = time.Now() + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, ts.accessKey, t) + }) +} + +func TestAssumeRoleCredentialServiceUsingMetadataCredentialsProvider(t *testing.T) { + t.Setenv("AWS_REGION", "us-west-1") + + testAccessKey := "ASgeIAIOSFODNN7EXAMPLE" + ts := stsTestServer{ + t: t, + accessKey: testAccessKey, + assumeRoleWithWebIdentity: false, + } + ts.start() + defer ts.stop() + + tsMetadata := ec2CredTestServer{} + tsMetadata.payload = metadataPayload{ + AccessKeyID: "MYAWSACCESSKEYGOESHERE", + SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE", + Code: "Success", + Token: "MYAWSSECURITYTOKENGOESHERE", + Expiration: time.Now().UTC().Add(time.Minute * 300)} + tsMetadata.start() + defer tsMetadata.stop() + + cs := awsAssumeRoleCredentialService{ + stsURL: ts.server.URL, + logger: logging.Get(), + AWSSigningPlugin: &awsSigningAuthPlugin{AWSMetadataCredentials: &awsMetadataCredentialService{RoleName: "my_iam_role", credServicePath: tsMetadata.server.URL + "/latest/meta-data/iam/security-credentials/", + tokenPath: tsMetadata.server.URL + "/latest/api/token"}}, + } + + // wrong path: no AWS IAM Role ARN set in environment or config + err := cs.populateFromEnv() + assertErr("no AWS_ROLE_ARN set in environment or configuration", err, t) + t.Setenv("AWS_ROLE_ARN", "role:arn") + + // happy path: set AWS IAM Role ARN as env var + err = cs.populateFromEnv() + if err != nil { + t.Fatalf("Error while getting env vars: %s", err) + } + + // happy path: set AWS IAM Role ARN in config + os.Unsetenv("AWS_ROLE_ARN") + cs.RoleArn = "role:arn" + + err = cs.populateFromEnv() + if err != nil { + t.Fatalf("Error while getting env vars: %s", err) + } + + // happy path: refresh and get credentials + creds, _ := cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: refresh with session and get credentials + cs.expiration = time.Now() + cs.SessionName = "TEST_SESSION" + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: don't refresh as credentials not expired so STS not called + // verify existing credentials haven't changed + ts.accessKey = "OTHERKEY" + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, testAccessKey, t) + + // happy path: refresh expired credentials + // verify new credentials are set + cs.expiration = time.Now() + creds, _ = cs.credentials(context.Background()) + assertEq(creds.AccessKey, ts.accessKey, t) +} + func TestStsPath(t *testing.T) { cs := awsWebIdentityCredentialService{} @@ -1186,11 +1436,12 @@ func TestStsPathFromEnv(t *testing.T) { } } -// simulate EC2 metadata service +// simulate AWS Security Token Service (AWS STS) type stsTestServer struct { - t *testing.T - server *httptest.Server - accessKey string + t *testing.T + server *httptest.Server + accessKey string + assumeRoleWithWebIdentity bool } func (t *stsTestServer) handle(w http.ResponseWriter, r *http.Request) { @@ -1199,7 +1450,12 @@ func (t *stsTestServer) handle(w http.ResponseWriter, r *http.Request) { return } - if err := r.ParseForm(); err != nil || r.PostForm.Get("Action") != "AssumeRoleWithWebIdentity" { + if err := r.ParseForm(); err != nil { + w.WriteHeader(400) + return + } + + if r.PostForm.Get("Action") != "AssumeRoleWithWebIdentity" && r.PostForm.Get("Action") != "AssumeRole" { w.WriteHeader(400) return } @@ -1210,17 +1466,22 @@ func (t *stsTestServer) handle(w http.ResponseWriter, r *http.Request) { return } - token := r.PostForm.Get("WebIdentityToken") - if token != "good-token" { - w.WriteHeader(401) - return + if t.assumeRoleWithWebIdentity { + token := r.PostForm.Get("WebIdentityToken") + if token != "good-token" { + w.WriteHeader(401) + return + } + w.WriteHeader(200) } - w.WriteHeader(200) sessionName := r.PostForm.Get("RoleSessionName") - // Taken from STS docs: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html - xmlResponse := ` + var xmlResponse string + + if t.assumeRoleWithWebIdentity { + // Taken from STS docs: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html + xmlResponse = ` amzn1.account.AF6RHO7KZU5XRVQJGXK6HB56KR2A client.5498841531868486423.1548@apps.example.com @@ -1240,6 +1501,36 @@ func (t *stsTestServer) handle(w http.ResponseWriter, r *http.Request) { ad4156e9-bce1-11e2-82e6-6b6efEXAMPLE ` + } else { + // Taken from STS docs: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html + xmlResponse = ` + +DevUser123 + + arn:aws:sts::123456789012:assumed-role/demo/John + ARO123EXAMPLE123:%[1]s + + + + AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW + LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd + QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU + 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz + +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA== + + + wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY + + %s + %s + +8 + + +c6104cbe-af31-11e0-8154-cbc7ccf896c7 + +` + } _, _ = w.Write([]byte(fmt.Sprintf(xmlResponse, sessionName, time.Now().Add(time.Hour).Format(time.RFC3339), t.accessKey))) } diff --git a/plugins/rest/rest_test.go b/plugins/rest/rest_test.go index 27ba37e6cf..fb0b632a0e 100644 --- a/plugins/rest/rest_test.go +++ b/plugins/rest/rest_test.go @@ -623,6 +623,56 @@ func TestNew(t *testing.T) { awsRegionEnvVar: "us-west-1", }, }, + { + name: "S3AssumeRoleMissingEnvVars", + input: `{ + "name": "foo", + "url": "http://localhost", + "credentials": { + "s3_signing": { + "assume_role_credentials": {} + }, + } + }`, + wantErr: true, + }, + { + name: "S3AssumeRoleCredsMissingSigningPlugin", + input: `{ + "name": "foo", + "url": "http://localhost", + "credentials": { + "s3_signing": { + "assume_role_credentials": {} + }, + } + }`, + env: map[string]string{ + awsRoleArnEnvVar: "TEST", + accessKeyEnvVar: "TEST", + secretKeyEnvVar: "TEST", + awsRegionEnvVar: "us-west-1", + }, + wantErr: true, + }, + { + name: "S3AssumeRoleCreds", + input: `{ + "name": "foo", + "url": "http://localhost", + "credentials": { + "s3_signing": { + "assume_role_credentials": {"aws_signing": {"environment_credentials": {}}} + }, + } + }`, + env: map[string]string{ + awsRoleArnEnvVar: "TEST", + accessKeyEnvVar: "TEST", + secretKeyEnvVar: "TEST", + awsRegionEnvVar: "us-west-1", + }, + }, { name: "ValidGCPMetadataIDTokenOptions", input: `{