Skip to content

Commit 865eebe

Browse files
authored
fix(aws): configurable boto3 timeouts, 10s connect default (#12774)
1 parent 8270979 commit 865eebe

13 files changed

Lines changed: 258 additions & 17 deletions

File tree

docs/getting-started/basic-usage/prowler-cli.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
title: 'Basic Usage'
33
---
44

5+
import { VersionBadge } from "/snippets/version-badge.mdx"
6+
57
## Running Prowler
68

79
Running Prowler requires specifying the provider (e.g. `aws`, `gcp`, `azure`, `kubernetes`, `m365`, `github`, `iac` or `mongodbatlas`):
@@ -91,6 +93,18 @@ By default, `prowler` will scan all AWS regions.
9193
</Note>
9294
See more details about AWS Authentication in the [Authentication Section](/user-guide/providers/aws/authentication) section.
9395

96+
- **AWS Retrier and Timeout Configuration**
97+
98+
<VersionBadge version="5.42.0" />
99+
100+
Tune the Boto3 standard retrier and the endpoint timeouts when AWS throttles the scan or when some endpoints are unreachable from the network Prowler runs in:
101+
102+
```console
103+
prowler aws --aws-retries-max-attempts 5 --aws-connect-timeout 5 --aws-read-timeout 30
104+
```
105+
106+
See the [Boto3 configuration](/user-guide/providers/aws/boto3-configuration) page for defaults and environment variables.
107+
94108
## Azure
95109

96110
Azure requires specifying the auth method:

docs/user-guide/providers/aws/boto3-configuration.mdx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
11
---
2-
title: "Boto3 Retrier Configuration in Prowler"
2+
title: "Boto3 Retrier and Timeout Configuration in Prowler"
33
---
44

5+
import { VersionBadge } from "/snippets/version-badge.mdx"
6+
57
Prowler's AWS Provider leverages Boto3's [Standard](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html) retry mode to automatically retry client calls to AWS services when encountering errors or exceptions.
68

9+
## Timeout Configuration
10+
11+
<VersionBadge version="5.42.0" />
12+
13+
Every AWS API call is bounded by two timeouts:
14+
15+
- Connect timeout: seconds to wait to establish a connection (TCP, proxy tunnel and TLS handshake) to the AWS endpoint. Prowler's default is 10 seconds, configurable via `--aws-connect-timeout 5`.
16+
- Read timeout: seconds to wait for a response once connected. Prowler's default is 60 seconds, configurable via `--aws-read-timeout 30`.
17+
18+
Both timeouts can also be set through environment variables, which is the way to tune them in Prowler Cloud and other deployments without a CLI:
19+
20+
```console
21+
export PROWLER_AWS_BOTO3_CONNECT_TIMEOUT=5
22+
export PROWLER_AWS_BOTO3_READ_TIMEOUT=30
23+
```
24+
25+
CLI flags take precedence over the environment variables. Prowler sets both timeouts explicitly, so `AWS_DEFAULTS_MODE` and a `connect_timeout` in `~/.aws/config` are ignored; use the flag or the environment variable instead.
26+
27+
<Note>
28+
Boto3 defaults both timeouts to 60 seconds. In networks with restricted egress (for example VPC endpoints for a subset of services, GovCloud or private deployments), every AWS service without a reachable endpoint used to cost up to 4 attempts × 60 seconds (the first call plus the 3 retries) for each region. Prowler lowers the connect timeout to 10 seconds so unreachable endpoints fail fast; lower it further together with `--aws-retries-max-attempts 0`, which disables retries and leaves a single attempt per call, if a scan still spends most of its time waiting on unreachable services.
29+
30+
</Note>
31+
732
## Retry Behavior Overview
833

934
Boto3's Standard retry mode includes the following mechanisms:
1035

11-
- Maximum Retry Attempts: Default value set to 3, configurable via the `--aws-retries-max-attempts 5` argument.
36+
- Maximum Retry Attempts: Default value set to 3, configurable via the `--aws-retries-max-attempts 5` argument. `0` disables retries.
1237

1338
- Expanded Error Handling: Retries occur for a comprehensive set of errors.
1439

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AWS provider default Boto3 connect timeout lowered from 60 to 10 seconds, so scans in restricted-egress networks (VPC endpoints for a subset of services, GovCloud, private deployments) no longer spend 4 minutes per region on every service whose endpoint is unreachable
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`--aws-connect-timeout` and `--aws-read-timeout` CLI flags, plus `PROWLER_AWS_BOTO3_CONNECT_TIMEOUT` and `PROWLER_AWS_BOTO3_READ_TIMEOUT` environment variables, to bound how long each AWS API call waits for an endpoint
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`--aws-retries-max-attempts 0` now disables Boto3 retries instead of being silently ignored in favour of the default of 3

prowler/providers/aws/aws_provider.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def __init__(
126126
aws_access_key_id: str = None,
127127
aws_secret_access_key: str = None,
128128
aws_session_token: Optional[str] = None,
129+
connect_timeout: Optional[int] = None,
130+
read_timeout: Optional[int] = None,
129131
):
130132
"""
131133
Initializes the AWS provider.
@@ -155,6 +157,8 @@ def __init__(
155157
- aws_access_key_id: The AWS access key ID.
156158
- aws_secret_access_key: The AWS secret access key.
157159
- aws_session_token: The AWS session token, optional.
160+
- connect_timeout: Seconds to wait to establish a connection to an AWS endpoint.
161+
- read_timeout: Seconds to wait for a response from an AWS endpoint.
158162
159163
Raises:
160164
- ArgumentTypeError: If the input MFA ARN is invalid.
@@ -229,7 +233,9 @@ def __init__(
229233

230234
# TODO: Use AwsSetUpSession ?????
231235
# Configure the initial AWS Session using the local credentials: profile or environment variables
232-
session_config = self.set_session_config(retries_max_attempts)
236+
session_config = self.set_session_config(
237+
retries_max_attempts, connect_timeout, read_timeout
238+
)
233239
aws_session = self.setup_session(
234240
mfa=mfa,
235241
profile=profile,
@@ -1165,26 +1171,35 @@ def input_role_mfa_token_and_code() -> AWSMFAInfo:
11651171
return AWSMFAInfo(arn=mfa_ARN, totp=mfa_TOTP)
11661172

11671173
@staticmethod
1168-
def set_session_config(retries_max_attempts: int) -> Config:
1174+
def set_session_config(
1175+
retries_max_attempts: int,
1176+
connect_timeout: Optional[int] = None,
1177+
read_timeout: Optional[int] = None,
1178+
) -> Config:
11691179
"""
1170-
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier configuration if nothing is passed as argument
1180+
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier and timeout configuration if nothing is passed as argument
11711181
11721182
Args:
11731183
- retries_max_attempts: The maximum number of retries for the standard retrier config
1184+
- connect_timeout: Seconds to wait to establish a connection to an AWS endpoint
1185+
- read_timeout: Seconds to wait for a response from an AWS endpoint
11741186
11751187
Returns:
11761188
- Config: The botocore Config object
11771189
"""
11781190
default_session_config = get_default_session_config()
1179-
if retries_max_attempts:
1180-
default_session_config = default_session_config.merge(
1181-
Config(
1182-
retries={
1183-
"max_attempts": retries_max_attempts,
1184-
"mode": "standard",
1185-
},
1186-
)
1187-
)
1191+
overrides = {}
1192+
if retries_max_attempts is not None:
1193+
overrides["retries"] = {
1194+
"max_attempts": retries_max_attempts,
1195+
"mode": "standard",
1196+
}
1197+
if connect_timeout:
1198+
overrides["connect_timeout"] = connect_timeout
1199+
if read_timeout:
1200+
overrides["read_timeout"] = read_timeout
1201+
if overrides:
1202+
default_session_config = default_session_config.merge(Config(**overrides))
11881203

11891204
return default_session_config
11901205

prowler/providers/aws/config.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,39 @@
22

33
from botocore.config import Config
44

5+
from prowler.providers.aws.exceptions.exceptions import AWSInvalidBoto3TimeoutError
6+
57
AWS_STS_GLOBAL_ENDPOINT_REGION = "us-east-1"
68
AWS_REGION_US_EAST_1 = "us-east-1"
79
BOTO3_USER_AGENT_EXTRA = os.getenv("PROWLER_AWS_BOTO3_USER_AGENT_EXTRA", "APN_1826889")
10+
BOTO3_RETRIES_MAX_ATTEMPTS = 3
11+
# botocore defaults both to 60s
12+
BOTO3_CONNECT_TIMEOUT = 10
13+
BOTO3_READ_TIMEOUT = 60
814
ROLE_SESSION_NAME = "ProwlerAssessmentSession"
915

1016

17+
def get_boto3_timeout_from_env(name: str, default: int) -> int:
18+
"""Positive integer seconds read from the environment, or default when unset."""
19+
raw = os.getenv(name, "").strip()
20+
if not raw:
21+
return default
22+
if not raw.isdecimal() or int(raw) == 0:
23+
raise AWSInvalidBoto3TimeoutError(
24+
file=os.path.basename(__file__),
25+
message=f"{name} must be a positive integer number of seconds, got {raw!r}",
26+
)
27+
return int(raw)
28+
29+
1130
def get_default_session_config() -> Config:
1231
return Config(
1332
user_agent_extra=BOTO3_USER_AGENT_EXTRA,
14-
retries={"max_attempts": 3, "mode": "standard"},
33+
retries={"max_attempts": BOTO3_RETRIES_MAX_ATTEMPTS, "mode": "standard"},
34+
connect_timeout=get_boto3_timeout_from_env(
35+
"PROWLER_AWS_BOTO3_CONNECT_TIMEOUT", BOTO3_CONNECT_TIMEOUT
36+
),
37+
read_timeout=get_boto3_timeout_from_env(
38+
"PROWLER_AWS_BOTO3_READ_TIMEOUT", BOTO3_READ_TIMEOUT
39+
),
1540
)

prowler/providers/aws/exceptions/exceptions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ class AWSBaseException(ProwlerException):
7878
"message": "The provided AWS partition is invalid",
7979
"remediation": "Check the provided AWS partition and ensure it is valid.",
8080
},
81+
(1918, "AWSInvalidBoto3TimeoutError"): {
82+
"message": "The Boto3 timeout configured through the environment is invalid",
83+
"remediation": "Set PROWLER_AWS_BOTO3_CONNECT_TIMEOUT and PROWLER_AWS_BOTO3_READ_TIMEOUT to a positive integer number of seconds.",
84+
},
8185
}
8286

8387
def __init__(self, code, file=None, original_exception=None, message=None):
@@ -231,3 +235,12 @@ def __init__(self, file=None, original_exception=None, message=None):
231235
super().__init__(
232236
1917, file=file, original_exception=original_exception, message=message
233237
)
238+
239+
240+
class AWSInvalidBoto3TimeoutError(AWSBaseException):
241+
"""Boto3 timeout configured through the environment is not a positive integer."""
242+
243+
def __init__(self, file=None, original_exception=None, message=None):
244+
super().__init__(
245+
1918, file=file, original_exception=original_exception, message=message
246+
)

prowler/providers/aws/lib/arguments/arguments.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,21 @@ def init_parser(self):
156156
nargs="?",
157157
default=None,
158158
type=int,
159-
help="Set the maximum attemps for the Boto3 standard retrier config (Default: 3)",
159+
help="Set the maximum retries for the Boto3 standard retrier config, 0 disables retries (Default: 3)",
160+
)
161+
boto3_config_subparser.add_argument(
162+
"--aws-connect-timeout",
163+
nargs="?",
164+
default=None,
165+
type=validate_timeout,
166+
help="Seconds to wait to establish a connection (TCP, proxy tunnel and TLS) to an AWS endpoint before retrying (Default: 10)",
167+
)
168+
boto3_config_subparser.add_argument(
169+
"--aws-read-timeout",
170+
nargs="?",
171+
default=None,
172+
type=validate_timeout,
173+
help="Seconds to wait for a response from an AWS endpoint before retrying (Default: 60)",
160174
)
161175

162176
# Scan Unused Services
@@ -190,6 +204,13 @@ def validate_session_duration(session_duration: int) -> int:
190204
return duration
191205

192206

207+
def validate_timeout(value: str) -> int:
208+
"""validate_timeout validates that the input is a whole number of seconds greater than zero"""
209+
if not value.isdecimal() or int(value) == 0:
210+
raise ArgumentTypeError(f"{value} is not a positive integer")
211+
return int(value)
212+
213+
193214
def validate_role_session_name(session_name) -> str:
194215
"""
195216
Validates that the role session name is valid.

prowler/providers/aws/lib/session/aws_set_up_session.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def __init__(
4242
aws_session_token: Optional[str] = None,
4343
retries_max_attempts: int = 3,
4444
regions: set = set(),
45+
connect_timeout: Optional[int] = None,
46+
read_timeout: Optional[int] = None,
4547
) -> None:
4648
"""
4749
The constructor for the AwsSetUpSession class.
@@ -58,6 +60,8 @@ def __init__(
5860
- aws_session_token: The AWS session token, optional.
5961
- retries_max_attempts: The maximum number of retries for the AWS client.
6062
- regions: A set of regions to audit.
63+
- connect_timeout: Seconds to wait to establish a connection to an AWS endpoint.
64+
- read_timeout: Seconds to wait for a response from an AWS endpoint.
6165
6266
Returns:
6367
@@ -73,7 +77,9 @@ def __init__(
7377
aws_access_key_id=aws_access_key_id,
7478
aws_secret_access_key=aws_secret_access_key,
7579
)
76-
session_config = AwsProvider.set_session_config(retries_max_attempts)
80+
session_config = AwsProvider.set_session_config(
81+
retries_max_attempts, connect_timeout, read_timeout
82+
)
7783
aws_session = AwsProvider.setup_session(
7884
mfa=mfa,
7985
profile=profile,

0 commit comments

Comments
 (0)