Skip to content

Commit

Permalink
fix: Support AWS_CONTAINER_CREDENTIALS_FULL_URI metadata endpoint
Browse files Browse the repository at this point in the history
Support loading credentials from the AWS_CONTAINER_CREDENTIALS_FULL_URI
metadata endpoint which is helpful for AWS SnapStart lambdas

Fixes open-policy-agent#6893
Signed-off-by: Matthew Bamber <[email protected]>
  • Loading branch information
mbamber committed Jul 29, 2024
1 parent 4d559c3 commit 2aaf6e0
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 6 deletions.
4 changes: 3 additions & 1 deletion docs/content/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,9 @@ To use the EC2 metadata service, the IAM role to use and the AWS region for the
be specified as `iam_role` and `aws_region` respectively.

To use the ECS metadata service, specify only the AWS region for the resource as `aws_region`. ECS
containers have at most one associated IAM role.
containers have at most one associated IAM role. As per the [AWS documentation](https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html), credentials are
sourced from the `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` metadata environment variable or the
`AWS_CONTAINER_CREDENTIALS_FULL_URI` metadata environment variable in order.

> Providing a value for `iam_role` will cause OPA to use the EC2 metadata service even
> if running inside an ECS container. This may result in unexpected problems if, for example,
Expand Down
28 changes: 23 additions & 5 deletions plugins/rest/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ const (
ec2DefaultTokenPath = "http://169.254.169.254/latest/api/token"

// ref. https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html
ecsDefaultCredServicePath = "http://169.254.170.2"
ecsRelativePathEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
ecsDefaultCredServicePath = "http://169.254.170.2"
ecsRelativePathEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
ecsFullPathEnvVar = "AWS_CONTAINER_CREDENTIALS_FULL_URI"
ecsAuthorizationTokenEnvVar = "AWS_CONTAINER_AUTHORIZATION_TOKEN"

// ref. https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
stsDefaultDomain = "amazonaws.com"
Expand Down Expand Up @@ -211,7 +213,12 @@ func (cs *awsMetadataCredentialService) urlForMetadataService() (string, error)
// otherwise, check environment to see if it looks like we're in an ECS
// container (with implied role association)
if isECS() {
return ecsDefaultCredServicePath + os.Getenv(ecsRelativePathEnvVar), nil
// first check if the relative env var exists; if so we use that otherwise we
// use the "full" var
if _, relativeExists := os.LookupEnv(ecsRelativePathEnvVar); relativeExists {
return ecsDefaultCredServicePath + os.Getenv(ecsRelativePathEnvVar), nil
}
return os.Getenv(ecsFullPathEnvVar), nil
}
// if there's no role name and we don't appear to have a path to the
// ECS container service, then the configuration is invalid
Expand Down Expand Up @@ -267,6 +274,16 @@ func (cs *awsMetadataCredentialService) refreshFromService(ctx context.Context)
return errors.New("unable to construct metadata HTTP request: " + err.Error())
}

// if using the AWS_CONTAINER_CREDENTIALS_FULL_URI variable, we need to associate the token
// to the request
if _, useFullPath := os.LookupEnv(ecsFullPathEnvVar); useFullPath {
token, tokenExists := os.LookupEnv(ecsAuthorizationTokenEnvVar)
if !tokenExists {
return errors.New("unable to get ECS metadata authorization token")
}
req.Header.Set("Authorization", token)
}

// if in the EC2 environment, we will use IMDSv2, which requires a session cookie from a
// PUT request on the token endpoint before it will give the credentials, this provides
// protection from SSRF attacks
Expand Down Expand Up @@ -604,8 +621,9 @@ func (cs *awsWebIdentityCredentialService) credentials(ctx context.Context) (aws

func isECS() bool {
// the special relative path URI is set by the container agent in the ECS environment only
_, isECS := os.LookupEnv(ecsRelativePathEnvVar)
return isECS
_, isECSRelative := os.LookupEnv(ecsRelativePathEnvVar)
_, isECSFull := os.LookupEnv(ecsFullPathEnvVar)
return isECSRelative || isECSFull
}

// ecrAuthPlugin authorizes requests to AWS ECR.
Expand Down
45 changes: 45 additions & 0 deletions plugins/rest/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,13 @@ func TestMetadataCredentialService(t *testing.T) {
_, err = cs.credentials(context.Background())
assertErr("metadata endpoint cannot be determined from settings and environment", err, t)

// wrong path: missing token
t.Setenv(ecsFullPathEnvVar, "fullPath")
os.Unsetenv(ecsAuthorizationTokenEnvVar)
_, err = cs.credentials(context.Background())
assertErr("unable to get ECS metadata authorization token", err, t)
os.Unsetenv(ecsFullPathEnvVar)

// wrong path: creds not found
cs = awsMetadataCredentialService{
RoleName: "not_my_iam_role", // not present
Expand Down Expand Up @@ -490,6 +497,34 @@ func TestMetadataCredentialService(t *testing.T) {
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
assertEq(creds.RegionName, cs.RegionName, t)
assertEq(creds.SessionToken, ts.payload.Token, t)

// happy path: credentials fetched from full path var
cs = awsMetadataCredentialService{
RegionName: "us-east-1",
credServicePath: "", // not set as we want to test env var resolution
logger: logging.Get(),
}
ts.payload = metadataPayload{
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
Code: "Success",
Token: "MYAWSSECURITYTOKENGOESHERE",
Expiration: time.Now().UTC().Add(time.Minute * 2)} // short time
t.Setenv(ecsFullPathEnvVar, ts.server.URL+"/fullPath")
t.Setenv(ecsAuthorizationTokenEnvVar, "THIS_IS_A_GOOD_TOKEN")

creds, err = cs.credentials(context.Background())
if err != nil {
// Cannot proceed with test if unable to fetch credentials.
t.Fatal(err)
}

assertEq(creds.AccessKey, ts.payload.AccessKeyID, t)
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
assertEq(creds.RegionName, cs.RegionName, t)
assertEq(creds.SessionToken, ts.payload.Token, t)
os.Unsetenv(ecsFullPathEnvVar)
os.Unsetenv(ecsAuthorizationTokenEnvVar)
}

func TestMetadataServiceErrorHandled(t *testing.T) {
Expand Down Expand Up @@ -957,6 +992,7 @@ type ec2CredTestServer struct {
func (t *ec2CredTestServer) handle(w http.ResponseWriter, r *http.Request) {
goodPath := "/latest/meta-data/iam/security-credentials/my_iam_role"
badPath := "/latest/meta-data/iam/security-credentials/my_bad_iam_role"
goodPathFull := "/fullPath"

goodTokenPath := "/latest/api/token"
badTokenPath := "/latest/api/bad_token"
Expand Down Expand Up @@ -987,6 +1023,15 @@ func (t *ec2CredTestServer) handle(w http.ResponseWriter, r *http.Request) {
// a metadata response that's not well-formed
w.WriteHeader(200)
_, _ = w.Write([]byte("This isn't a JSON payload"))
case goodPathFull:
// validate token...
if r.Header.Get("Authorization") == tokenValue {
w.WriteHeader(200)
_, _ = w.Write(jsonBytes)
} else {
// AWS returns a 404 if the token is wrong
w.WriteHeader(404)
}
default:
// something else that we won't be able to find
w.WriteHeader(404)
Expand Down

0 comments on commit 2aaf6e0

Please sign in to comment.