Skip to content

Commit

Permalink
fix(assume_role): Set the AWS STS endpoint region (#2587)
Browse files Browse the repository at this point in the history
  • Loading branch information
jfagoagas authored Jul 17, 2023
1 parent 6575121 commit 02519a4
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 22 deletions.
4 changes: 4 additions & 0 deletions docs/tutorials/aws/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ If your IAM entity enforces MFA you can use `--mfa` and Prowler will ask you to

- ARN of your MFA device
- TOTP (Time-Based One-Time Password)

## STS Endpoint Region

If you are using Prowler in AWS regions that are not enabled by default you need to use the argument `--sts-endpoint-region` to point the AWS STS API calls `assume-role` and `get-caller-identity` to the non-default region, e.g.: `prowler aws --sts-endpoint-region eu-south-2`.
6 changes: 5 additions & 1 deletion docs/tutorials/aws/role-assumption.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ prowler aws -R arn:aws:iam::<account_id>:role/<role_name>
prowler aws -T/--session-duration <seconds> -I/--external-id <external_id> -R arn:aws:iam::<account_id>:role/<role_name>
```

## STS Endpoint Region

If you are using Prowler in AWS regions that are not enabled by default you need to use the argument `--sts-endpoint-region` to point the AWS STS API calls `assume-role` and `get-caller-identity` to the non-default region, e.g.: `prowler aws --sts-endpoint-region eu-south-2`.

## Role MFA

If your IAM Role has MFA configured you can use `--mfa` along with `-R`/`--role <role_arn>` and Prowler will ask you to input the following values to get a new temporary session for the IAM Role provided:

- ARN of your MFA device
- TOTP (Time-Based One-Time Password)


## Create Role

To create a role to be assumed in one or multiple accounts you can use either as CloudFormation Stack or StackSet the following [template](https://github.com/prowler-cloud/prowler/blob/master/permissions/create_role_to_assume_cfn.yaml) and adapt it.
Expand Down
3 changes: 2 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 7 additions & 8 deletions prowler/lib/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,7 @@
default_output_directory,
)
from prowler.providers.aws.aws_provider import get_aws_available_regions
from prowler.providers.aws.lib.arn.arn import is_valid_arn


def arn_type(arn: str) -> bool:
"""arn_type returns a string ARN if it is valid and raises an argparse.ArgumentError if not."""
if not is_valid_arn(arn):
raise argparse.ArgumentError("Invalid ARN")
return arn
from prowler.providers.aws.lib.arn.arn import arn_type


class ProwlerArgumentParser:
Expand Down Expand Up @@ -289,6 +282,12 @@ def __init_aws_parser__(self):
help="ARN of the role to be assumed",
# Pending ARN validation
)
aws_auth_subparser.add_argument(
"--sts-endpoint-region",
nargs="?",
default=None,
help="Specify the AWS STS endpoint region to use. Read more at https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html",
)
aws_auth_subparser.add_argument(
"--mfa",
action="store_true",
Expand Down
15 changes: 12 additions & 3 deletions prowler/providers/aws/aws_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from prowler.lib.check.check import list_modules, recover_checks_from_service
from prowler.lib.logger import logger
from prowler.lib.utils.utils import open_file, parse_json_file
from prowler.providers.aws.config import AWS_STS_GLOBAL_ENDPOINT_REGION
from prowler.providers.aws.lib.audit_info.models import AWS_Assume_Role, AWS_Audit_Info


Expand Down Expand Up @@ -105,14 +106,19 @@ def refresh_credentials(self):
return refreshed_credentials


def assume_role(session: session.Session, assumed_role_info: AWS_Assume_Role) -> dict:
def assume_role(
session: session.Session,
assumed_role_info: AWS_Assume_Role,
sts_endpoint_region: str = None,
) -> dict:
try:
assume_role_arguments = {
"RoleArn": assumed_role_info.role_arn,
"RoleSessionName": "ProwlerAsessmentSession",
"DurationSeconds": assumed_role_info.session_duration,
}

# Set the info to assume the role from the partition, account and role name
if assumed_role_info.external_id:
assume_role_arguments["ExternalId"] = assumed_role_info.external_id

Expand All @@ -121,8 +127,11 @@ def assume_role(session: session.Session, assumed_role_info: AWS_Assume_Role) ->
assume_role_arguments["SerialNumber"] = mfa_ARN
assume_role_arguments["TokenCode"] = mfa_TOTP

# set the info to assume the role from the partition, account and role name
sts_client = session.client("sts")
# Set the STS Endpoint Region
if sts_endpoint_region is None:
sts_endpoint_region = AWS_STS_GLOBAL_ENDPOINT_REGION

sts_client = session.client("sts", sts_endpoint_region)
assumed_credentials = sts_client.assume_role(**assume_role_arguments)
except Exception as error:
logger.critical(
Expand Down
1 change: 1 addition & 0 deletions prowler/providers/aws/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AWS_STS_GLOBAL_ENDPOINT_REGION = "us-east-1"
8 changes: 8 additions & 0 deletions prowler/providers/aws/lib/arn/arn.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from argparse import ArgumentError

from prowler.providers.aws.lib.arn.error import (
RoleArnParsingEmptyResource,
Expand All @@ -11,6 +12,13 @@
from prowler.providers.aws.lib.arn.models import ARN


def arn_type(arn: str) -> bool:
"""arn_type returns a string ARN if it is valid and raises an argparse.ArgumentError if not."""
if not is_valid_arn(arn):
raise ArgumentError("Invalid ARN")
return arn


def parse_iam_credentials_arn(arn: str) -> ARN:
arn_parsed = ARN(arn)
# First check if region is empty (in IAM ARN's region is always empty)
Expand Down
15 changes: 11 additions & 4 deletions prowler/providers/aws/lib/credentials/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@
from colorama import Fore, Style

from prowler.lib.logger import logger
from prowler.providers.aws.config import AWS_STS_GLOBAL_ENDPOINT_REGION
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info

AWS_STS_GLOBAL_ENDPOINT_REGION = "us-east-1"


def validate_aws_credentials(session: session, input_regions: list) -> dict:
def validate_aws_credentials(
session: session, input_regions: list, sts_endpoint_region: str = None
) -> dict:
try:
# For a valid STS GetCallerIdentity we have to use the right AWS Region
if input_regions is None or len(input_regions) == 0:
# Check if the --sts-endpoint-region is set
if sts_endpoint_region is not None:
aws_region = sts_endpoint_region
# If there is no region passed with -f/--region/--filter-region
elif input_regions is None or len(input_regions) == 0:
# If you have a region configured in your AWS config or credentials file
if session.region_name is not None:
aws_region = session.region_name
else:
Expand All @@ -22,6 +28,7 @@ def validate_aws_credentials(session: session, input_regions: list) -> dict:
else:
# Get the first region passed to the -f/--region
aws_region = input_regions[0]

validate_credentials_client = session.client("sts", aws_region)
caller_identity = validate_credentials_client.get_caller_identity()
# Include the region where the caller_identity has validated the credentials
Expand Down
13 changes: 10 additions & 3 deletions prowler/providers/common/audit_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def set_aws_audit_info(self, arguments) -> AWS_Audit_Info:
input_session_duration = arguments.get("session_duration")
input_external_id = arguments.get("external_id")

# STS Endpoint Region
sts_endpoint_region = arguments.get("sts_endpoint_region")

# Since the range(i,j) goes from i to j-1 we have to j+1
if input_session_duration and input_session_duration not in range(900, 43201):
raise Exception("Value for -T option must be between 900 and 43200")
Expand Down Expand Up @@ -128,7 +131,7 @@ def set_aws_audit_info(self, arguments) -> AWS_Audit_Info:
logger.info("Validating credentials ...")
# Verificate if we have valid credentials
caller_identity = validate_aws_credentials(
current_audit_info.original_session, input_regions
current_audit_info.original_session, input_regions, sts_endpoint_region
)

logger.info("Credentials validated")
Expand Down Expand Up @@ -168,7 +171,9 @@ def set_aws_audit_info(self, arguments) -> AWS_Audit_Info:
f"Getting organizations metadata for account {organizations_role_arn}"
)
assumed_credentials = assume_role(
aws_provider.aws_session, aws_provider.role_info
aws_provider.aws_session,
aws_provider.role_info,
sts_endpoint_region,
)
current_audit_info.organizations_metadata = get_organizations_metadata(
current_audit_info.audited_account, assumed_credentials
Expand Down Expand Up @@ -201,7 +206,9 @@ def set_aws_audit_info(self, arguments) -> AWS_Audit_Info:
)
# Assume the role
assumed_role_response = assume_role(
aws_provider.aws_session, aws_provider.role_info
aws_provider.aws_session,
aws_provider.role_info,
sts_endpoint_region,
)
logger.info("Role assumed")
# Set the info needed to create a session with an assumed role
Expand Down
87 changes: 85 additions & 2 deletions tests/providers/aws/aws_provider_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_assume_role_without_mfa(self):
role_name = "test-role"
role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}"
session_duration_seconds = 900
audited_regions = "eu-west-1"
audited_regions = ["eu-west-1"]
sessionName = "ProwlerAsessmentSession"
# Boto 3 client to create our user
iam_client = boto3.client("iam", region_name="us-east-1")
Expand Down Expand Up @@ -105,7 +105,7 @@ def test_assume_role_with_mfa(self):
role_name = "test-role"
role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}"
session_duration_seconds = 900
audited_regions = "eu-west-1"
audited_regions = ["eu-west-1"]
sessionName = "ProwlerAsessmentSession"
# Boto 3 client to create our user
iam_client = boto3.client("iam", region_name="us-east-1")
Expand Down Expand Up @@ -184,6 +184,89 @@ def test_assume_role_with_mfa(self):
"AssumedRoleId"
].should.have.length_of(21 + 1 + len(sessionName))

@mock_iam
@mock_sts
def test_assume_role_with_sts_endpoint_region(self):
# Variables
role_name = "test-role"
role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}"
session_duration_seconds = 900
aws_region = "eu-west-1"
sts_endpoint_region = aws_region
audited_regions = [aws_region]
sessionName = "ProwlerAsessmentSession"
# Boto 3 client to create our user
iam_client = boto3.client("iam", region_name=aws_region)
# IAM user
iam_user = iam_client.create_user(UserName="test-user")["User"]
access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[
"AccessKey"
]
access_key_id = access_key["AccessKeyId"]
secret_access_key = access_key["SecretAccessKey"]
# New Boto3 session with the previously create user
session = boto3.session.Session(
aws_access_key_id=access_key_id,
aws_secret_access_key=secret_access_key,
region_name=aws_region,
)

# Fulfil the input session object for Prowler
audit_info = AWS_Audit_Info(
session_config=None,
original_session=session,
audit_session=None,
audited_account=None,
audited_account_arn=None,
audited_partition=None,
audited_identity_arn=None,
audited_user_id=None,
profile=None,
profile_region=None,
credentials=None,
assumed_role_info=AWS_Assume_Role(
role_arn=role_arn,
session_duration=session_duration_seconds,
external_id=None,
mfa_enabled=False,
),
audited_regions=audited_regions,
organizations_metadata=None,
audit_resources=None,
mfa_enabled=False,
)

# Call assume_role
aws_provider = AWS_Provider(audit_info)
assume_role_response = assume_role(
aws_provider.aws_session, aws_provider.role_info, sts_endpoint_region
)
# Recover credentials for the assume role operation
credentials = assume_role_response["Credentials"]
# Test the response
# SessionToken
credentials["SessionToken"].should.have.length_of(356)
credentials["SessionToken"].startswith("FQoGZXIvYXdzE")
# AccessKeyId
credentials["AccessKeyId"].should.have.length_of(20)
credentials["AccessKeyId"].startswith("ASIA")
# SecretAccessKey
credentials["SecretAccessKey"].should.have.length_of(40)
# Assumed Role
assume_role_response["AssumedRoleUser"]["Arn"].should.equal(
f"arn:aws:sts::{ACCOUNT_ID}:assumed-role/{role_name}/{sessionName}"
)
# AssumedRoleUser
assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].startswith(
"AROA"
)
assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].endswith(
":" + sessionName
)
assume_role_response["AssumedRoleUser"]["AssumedRoleId"].should.have.length_of(
21 + 1 + len(sessionName)
)

def test_generate_regional_clients(self):
# New Boto3 session with the previously create user
session = boto3.session.Session(
Expand Down
Loading

0 comments on commit 02519a4

Please sign in to comment.