aws-cis-controls-assessment 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_cis_assessment/__init__.py +11 -0
- aws_cis_assessment/cli/__init__.py +3 -0
- aws_cis_assessment/cli/examples.py +274 -0
- aws_cis_assessment/cli/main.py +1259 -0
- aws_cis_assessment/cli/utils.py +356 -0
- aws_cis_assessment/config/__init__.py +1 -0
- aws_cis_assessment/config/config_loader.py +328 -0
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
- aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
- aws_cis_assessment/controls/__init__.py +1 -0
- aws_cis_assessment/controls/base_control.py +400 -0
- aws_cis_assessment/controls/ig1/__init__.py +239 -0
- aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
- aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
- aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
- aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
- aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
- aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
- aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
- aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
- aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
- aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
- aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
- aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
- aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
- aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
- aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
- aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
- aws_cis_assessment/controls/ig2/__init__.py +172 -0
- aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
- aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
- aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
- aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
- aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
- aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
- aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
- aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
- aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
- aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
- aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
- aws_cis_assessment/controls/ig3/__init__.py +49 -0
- aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
- aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
- aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
- aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
- aws_cis_assessment/core/__init__.py +1 -0
- aws_cis_assessment/core/accuracy_validator.py +425 -0
- aws_cis_assessment/core/assessment_engine.py +1266 -0
- aws_cis_assessment/core/audit_trail.py +491 -0
- aws_cis_assessment/core/aws_client_factory.py +313 -0
- aws_cis_assessment/core/error_handler.py +607 -0
- aws_cis_assessment/core/models.py +166 -0
- aws_cis_assessment/core/scoring_engine.py +459 -0
- aws_cis_assessment/reporters/__init__.py +8 -0
- aws_cis_assessment/reporters/base_reporter.py +454 -0
- aws_cis_assessment/reporters/csv_reporter.py +835 -0
- aws_cis_assessment/reporters/html_reporter.py +2162 -0
- aws_cis_assessment/reporters/json_reporter.py +561 -0
- aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
- aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
- aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
- aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
- docs/README.md +94 -0
- docs/assessment-logic.md +766 -0
- docs/cli-reference.md +698 -0
- docs/config-rule-mappings.md +393 -0
- docs/developer-guide.md +858 -0
- docs/installation.md +299 -0
- docs/troubleshooting.md +634 -0
- docs/user-guide.md +487 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
"""Control 3.3: Configure Data Access Control Lists assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any, Optional
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
9
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
10
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IAMPasswordPolicyAssessment(BaseConfigRuleAssessment):
|
|
16
|
+
"""Assessment for iam-password-policy Config rule - ensures strong password policy."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, parameters: Optional[Dict[str, Any]] = None):
|
|
19
|
+
"""Initialize IAM password policy assessment."""
|
|
20
|
+
default_params = {
|
|
21
|
+
'MinimumPasswordLength': 14,
|
|
22
|
+
'RequireSymbols': True,
|
|
23
|
+
'RequireNumbers': True,
|
|
24
|
+
'RequireUppercaseCharacters': True,
|
|
25
|
+
'RequireLowercaseCharacters': True,
|
|
26
|
+
'MaxPasswordAge': 90,
|
|
27
|
+
'PasswordReusePrevention': 24,
|
|
28
|
+
'AllowUsersToChangePassword': True,
|
|
29
|
+
'HardExpiry': False
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if parameters:
|
|
33
|
+
# Validate parameter types and ranges
|
|
34
|
+
validated_params = self._validate_parameters(parameters)
|
|
35
|
+
default_params.update(validated_params)
|
|
36
|
+
|
|
37
|
+
super().__init__(
|
|
38
|
+
rule_name="iam-password-policy",
|
|
39
|
+
control_id="5.2", # Updated to reflect Control 5.2 for password management
|
|
40
|
+
resource_types=["AWS::::Account"],
|
|
41
|
+
parameters=default_params
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _validate_parameters(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
45
|
+
"""Validate and sanitize input parameters."""
|
|
46
|
+
validated = {}
|
|
47
|
+
|
|
48
|
+
# Validate MinimumPasswordLength
|
|
49
|
+
if 'MinimumPasswordLength' in parameters:
|
|
50
|
+
min_length = parameters['MinimumPasswordLength']
|
|
51
|
+
if isinstance(min_length, int) and 6 <= min_length <= 128:
|
|
52
|
+
validated['MinimumPasswordLength'] = min_length
|
|
53
|
+
else:
|
|
54
|
+
logger.warning(f"Invalid MinimumPasswordLength: {min_length}. Must be integer between 6-128. Using default: 14")
|
|
55
|
+
|
|
56
|
+
# Validate MaxPasswordAge
|
|
57
|
+
if 'MaxPasswordAge' in parameters:
|
|
58
|
+
max_age = parameters['MaxPasswordAge']
|
|
59
|
+
if isinstance(max_age, int) and 1 <= max_age <= 1095:
|
|
60
|
+
validated['MaxPasswordAge'] = max_age
|
|
61
|
+
else:
|
|
62
|
+
logger.warning(f"Invalid MaxPasswordAge: {max_age}. Must be integer between 1-1095 days. Using default: 90")
|
|
63
|
+
|
|
64
|
+
# Validate PasswordReusePrevention
|
|
65
|
+
if 'PasswordReusePrevention' in parameters:
|
|
66
|
+
reuse_prevention = parameters['PasswordReusePrevention']
|
|
67
|
+
if isinstance(reuse_prevention, int) and 1 <= reuse_prevention <= 24:
|
|
68
|
+
validated['PasswordReusePrevention'] = reuse_prevention
|
|
69
|
+
else:
|
|
70
|
+
logger.warning(f"Invalid PasswordReusePrevention: {reuse_prevention}. Must be integer between 1-24. Using default: 24")
|
|
71
|
+
|
|
72
|
+
# Validate boolean parameters
|
|
73
|
+
boolean_params = [
|
|
74
|
+
'RequireSymbols', 'RequireNumbers', 'RequireUppercaseCharacters',
|
|
75
|
+
'RequireLowercaseCharacters', 'AllowUsersToChangePassword', 'HardExpiry'
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
for param in boolean_params:
|
|
79
|
+
if param in parameters:
|
|
80
|
+
value = parameters[param]
|
|
81
|
+
if isinstance(value, bool):
|
|
82
|
+
validated[param] = value
|
|
83
|
+
elif isinstance(value, str) and value.lower() in ['true', '1', 'yes', 'on', 'false', '0', 'no', 'off']:
|
|
84
|
+
validated[param] = value.lower() in ['true', '1', 'yes', 'on']
|
|
85
|
+
else:
|
|
86
|
+
logger.warning(f"Invalid {param}: {value}. Must be boolean. Using default.")
|
|
87
|
+
# Don't add to validated, will use default
|
|
88
|
+
|
|
89
|
+
return validated
|
|
90
|
+
|
|
91
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
92
|
+
"""Get account-level resource for password policy check."""
|
|
93
|
+
if resource_type != "AWS::::Account":
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
# Account-level resource - return single item representing the account
|
|
97
|
+
try:
|
|
98
|
+
account_info = aws_factory.get_account_info()
|
|
99
|
+
return [{
|
|
100
|
+
'AccountId': account_info.get('account_id', 'unknown'),
|
|
101
|
+
'ResourceType': 'AWS::::Account'
|
|
102
|
+
}]
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Error getting account info: {e}")
|
|
105
|
+
return [{
|
|
106
|
+
'AccountId': 'unknown',
|
|
107
|
+
'ResourceType': 'AWS::::Account'
|
|
108
|
+
}]
|
|
109
|
+
|
|
110
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
111
|
+
"""Evaluate IAM password policy compliance."""
|
|
112
|
+
account_id = resource.get('AccountId', 'unknown')
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
116
|
+
|
|
117
|
+
# Get password policy
|
|
118
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
119
|
+
lambda: iam_client.get_account_password_policy()
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
policy = response.get('PasswordPolicy', {})
|
|
123
|
+
|
|
124
|
+
# Check each requirement
|
|
125
|
+
violations = []
|
|
126
|
+
|
|
127
|
+
# Minimum password length
|
|
128
|
+
min_length = policy.get('MinimumPasswordLength', 0)
|
|
129
|
+
required_min_length = self.parameters.get('MinimumPasswordLength', 14)
|
|
130
|
+
if min_length < required_min_length:
|
|
131
|
+
violations.append(f"Minimum password length is {min_length}, required: {required_min_length}")
|
|
132
|
+
|
|
133
|
+
# Character requirements
|
|
134
|
+
if self.parameters.get('RequireSymbols', True) and not policy.get('RequireSymbols', False):
|
|
135
|
+
violations.append("Password policy does not require symbols")
|
|
136
|
+
|
|
137
|
+
if self.parameters.get('RequireNumbers', True) and not policy.get('RequireNumbers', False):
|
|
138
|
+
violations.append("Password policy does not require numbers")
|
|
139
|
+
|
|
140
|
+
if self.parameters.get('RequireUppercaseCharacters', True) and not policy.get('RequireUppercaseCharacters', False):
|
|
141
|
+
violations.append("Password policy does not require uppercase characters")
|
|
142
|
+
|
|
143
|
+
if self.parameters.get('RequireLowercaseCharacters', True) and not policy.get('RequireLowercaseCharacters', False):
|
|
144
|
+
violations.append("Password policy does not require lowercase characters")
|
|
145
|
+
|
|
146
|
+
# Password age
|
|
147
|
+
max_age = policy.get('MaxPasswordAge')
|
|
148
|
+
required_max_age = self.parameters.get('MaxPasswordAge', 90)
|
|
149
|
+
if max_age is None or max_age > required_max_age:
|
|
150
|
+
violations.append(f"Maximum password age is {max_age or 'unlimited'}, required: {required_max_age} days")
|
|
151
|
+
|
|
152
|
+
# Password reuse prevention
|
|
153
|
+
reuse_prevention = policy.get('PasswordReusePrevention', 0)
|
|
154
|
+
required_reuse_prevention = self.parameters.get('PasswordReusePrevention', 24)
|
|
155
|
+
if reuse_prevention < required_reuse_prevention:
|
|
156
|
+
violations.append(f"Password reuse prevention is {reuse_prevention}, required: {required_reuse_prevention}")
|
|
157
|
+
|
|
158
|
+
# Allow users to change password
|
|
159
|
+
allow_change = policy.get('AllowUsersToChangePassword', False)
|
|
160
|
+
required_allow_change = self.parameters.get('AllowUsersToChangePassword', True)
|
|
161
|
+
if required_allow_change and not allow_change:
|
|
162
|
+
violations.append("Password policy does not allow users to change their own passwords")
|
|
163
|
+
elif not required_allow_change and allow_change:
|
|
164
|
+
violations.append("Password policy allows users to change passwords when it should not")
|
|
165
|
+
|
|
166
|
+
# Hard expiry
|
|
167
|
+
hard_expiry = policy.get('HardExpiry', False)
|
|
168
|
+
required_hard_expiry = self.parameters.get('HardExpiry', False)
|
|
169
|
+
if required_hard_expiry and not hard_expiry:
|
|
170
|
+
violations.append("Password policy does not enforce hard expiry")
|
|
171
|
+
elif not required_hard_expiry and hard_expiry:
|
|
172
|
+
violations.append("Password policy enforces hard expiry when it should not")
|
|
173
|
+
|
|
174
|
+
if violations:
|
|
175
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
176
|
+
evaluation_reason = f"IAM password policy violations: {'; '.join(violations)}"
|
|
177
|
+
else:
|
|
178
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
179
|
+
evaluation_reason = "IAM password policy meets all security requirements"
|
|
180
|
+
|
|
181
|
+
except ClientError as e:
|
|
182
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
183
|
+
if error_code == 'NoSuchEntity':
|
|
184
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
185
|
+
evaluation_reason = "No IAM password policy is configured"
|
|
186
|
+
elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
187
|
+
compliance_status = ComplianceStatus.ERROR
|
|
188
|
+
evaluation_reason = "Insufficient permissions to check IAM password policy"
|
|
189
|
+
else:
|
|
190
|
+
compliance_status = ComplianceStatus.ERROR
|
|
191
|
+
evaluation_reason = f"Error checking IAM password policy: {str(e)}"
|
|
192
|
+
except Exception as e:
|
|
193
|
+
compliance_status = ComplianceStatus.ERROR
|
|
194
|
+
evaluation_reason = f"Unexpected error checking IAM password policy: {str(e)}"
|
|
195
|
+
|
|
196
|
+
return ComplianceResult(
|
|
197
|
+
resource_id=account_id,
|
|
198
|
+
resource_type="AWS::::Account",
|
|
199
|
+
compliance_status=compliance_status,
|
|
200
|
+
evaluation_reason=evaluation_reason,
|
|
201
|
+
config_rule_name=self.rule_name,
|
|
202
|
+
region=region
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
206
|
+
"""Get specific remediation steps for IAM password policy."""
|
|
207
|
+
return [
|
|
208
|
+
"Update the IAM password policy to meet security requirements:",
|
|
209
|
+
f" - Set minimum password length to {self.parameters.get('MinimumPasswordLength', 14)} characters",
|
|
210
|
+
" - Require symbols, numbers, uppercase and lowercase characters",
|
|
211
|
+
f" - Set maximum password age to {self.parameters.get('MaxPasswordAge', 90)} days",
|
|
212
|
+
f" - Set password reuse prevention to {self.parameters.get('PasswordReusePrevention', 24)} passwords",
|
|
213
|
+
f" - {'Allow' if self.parameters.get('AllowUsersToChangePassword', True) else 'Disallow'} users to change their own passwords",
|
|
214
|
+
f" - {'Enable' if self.parameters.get('HardExpiry', False) else 'Disable'} hard expiry for passwords",
|
|
215
|
+
"Use AWS CLI command:",
|
|
216
|
+
f" aws iam update-account-password-policy \\",
|
|
217
|
+
f" --minimum-password-length {self.parameters.get('MinimumPasswordLength', 14)} \\",
|
|
218
|
+
f" {'--require-symbols' if self.parameters.get('RequireSymbols', True) else '--no-require-symbols'} \\",
|
|
219
|
+
f" {'--require-numbers' if self.parameters.get('RequireNumbers', True) else '--no-require-numbers'} \\",
|
|
220
|
+
f" {'--require-uppercase-characters' if self.parameters.get('RequireUppercaseCharacters', True) else '--no-require-uppercase-characters'} \\",
|
|
221
|
+
f" {'--require-lowercase-characters' if self.parameters.get('RequireLowercaseCharacters', True) else '--no-require-lowercase-characters'} \\",
|
|
222
|
+
f" --max-password-age {self.parameters.get('MaxPasswordAge', 90)} \\",
|
|
223
|
+
f" --password-reuse-prevention {self.parameters.get('PasswordReusePrevention', 24)} \\",
|
|
224
|
+
f" {'--allow-users-to-change-password' if self.parameters.get('AllowUsersToChangePassword', True) else '--no-allow-users-to-change-password'}" +
|
|
225
|
+
(f" \\\n {'--hard-expiry' if self.parameters.get('HardExpiry', False) else '--no-hard-expiry'}" if 'HardExpiry' in self.parameters else ""),
|
|
226
|
+
"Or use AWS Console: IAM > Account settings > Password policy",
|
|
227
|
+
"Additional recommendations:",
|
|
228
|
+
" - Communicate password policy changes to all users in advance",
|
|
229
|
+
" - Provide password manager recommendations to users",
|
|
230
|
+
" - Consider implementing password complexity training",
|
|
231
|
+
" - Monitor password policy compliance regularly"
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class IAMUserMFAEnabledAssessment(BaseConfigRuleAssessment):
|
|
236
|
+
"""Assessment for iam-user-mfa-enabled Config rule - ensures IAM users have MFA."""
|
|
237
|
+
|
|
238
|
+
def __init__(self):
|
|
239
|
+
"""Initialize IAM user MFA assessment."""
|
|
240
|
+
super().__init__(
|
|
241
|
+
rule_name="iam-user-mfa-enabled",
|
|
242
|
+
control_id="3.3",
|
|
243
|
+
resource_types=["AWS::IAM::User"]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
247
|
+
"""Get all IAM users."""
|
|
248
|
+
if resource_type != "AWS::IAM::User":
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
253
|
+
|
|
254
|
+
users = []
|
|
255
|
+
paginator = iam_client.get_paginator('list_users')
|
|
256
|
+
|
|
257
|
+
for page in paginator.paginate():
|
|
258
|
+
for user in page.get('Users', []):
|
|
259
|
+
users.append({
|
|
260
|
+
'UserName': user.get('UserName'),
|
|
261
|
+
'UserId': user.get('UserId'),
|
|
262
|
+
'Arn': user.get('Arn'),
|
|
263
|
+
'CreateDate': user.get('CreateDate'),
|
|
264
|
+
'PasswordLastUsed': user.get('PasswordLastUsed'),
|
|
265
|
+
'Tags': user.get('Tags', [])
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
logger.debug(f"Found {len(users)} IAM users")
|
|
269
|
+
return users
|
|
270
|
+
|
|
271
|
+
except ClientError as e:
|
|
272
|
+
logger.error(f"Error retrieving IAM users: {e}")
|
|
273
|
+
raise
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Unexpected error retrieving IAM users: {e}")
|
|
276
|
+
raise
|
|
277
|
+
|
|
278
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
279
|
+
"""Evaluate if IAM user has MFA enabled."""
|
|
280
|
+
user_name = resource.get('UserName', 'unknown')
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
284
|
+
|
|
285
|
+
# Check if user has console access (login profile)
|
|
286
|
+
has_console_access = False
|
|
287
|
+
try:
|
|
288
|
+
aws_factory.aws_api_call_with_retry(
|
|
289
|
+
lambda: iam_client.get_login_profile(UserName=user_name)
|
|
290
|
+
)
|
|
291
|
+
has_console_access = True
|
|
292
|
+
except ClientError as e:
|
|
293
|
+
if e.response.get('Error', {}).get('Code') != 'NoSuchEntity':
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
# If no console access, user doesn't need MFA
|
|
297
|
+
if not has_console_access:
|
|
298
|
+
return ComplianceResult(
|
|
299
|
+
resource_id=user_name,
|
|
300
|
+
resource_type="AWS::IAM::User",
|
|
301
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
302
|
+
evaluation_reason=f"User {user_name} does not have console access",
|
|
303
|
+
config_rule_name=self.rule_name,
|
|
304
|
+
region=region
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Check MFA devices
|
|
308
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
309
|
+
lambda: iam_client.list_mfa_devices(UserName=user_name)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
mfa_devices = response.get('MFADevices', [])
|
|
313
|
+
|
|
314
|
+
if mfa_devices:
|
|
315
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
316
|
+
device_count = len(mfa_devices)
|
|
317
|
+
evaluation_reason = f"User {user_name} has {device_count} MFA device(s) configured"
|
|
318
|
+
else:
|
|
319
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
320
|
+
evaluation_reason = f"User {user_name} has console access but no MFA devices configured"
|
|
321
|
+
|
|
322
|
+
except ClientError as e:
|
|
323
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
324
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
325
|
+
compliance_status = ComplianceStatus.ERROR
|
|
326
|
+
evaluation_reason = f"Insufficient permissions to check MFA for user {user_name}"
|
|
327
|
+
else:
|
|
328
|
+
compliance_status = ComplianceStatus.ERROR
|
|
329
|
+
evaluation_reason = f"Error checking MFA for user {user_name}: {str(e)}"
|
|
330
|
+
except Exception as e:
|
|
331
|
+
compliance_status = ComplianceStatus.ERROR
|
|
332
|
+
evaluation_reason = f"Unexpected error checking MFA for user {user_name}: {str(e)}"
|
|
333
|
+
|
|
334
|
+
return ComplianceResult(
|
|
335
|
+
resource_id=user_name,
|
|
336
|
+
resource_type="AWS::IAM::User",
|
|
337
|
+
compliance_status=compliance_status,
|
|
338
|
+
evaluation_reason=evaluation_reason,
|
|
339
|
+
config_rule_name=self.rule_name,
|
|
340
|
+
region=region
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
344
|
+
"""Get specific remediation steps for IAM user MFA."""
|
|
345
|
+
return [
|
|
346
|
+
"Enable MFA for all IAM users with console access:",
|
|
347
|
+
"1. Identify users without MFA who have console access",
|
|
348
|
+
"2. For each user, enable MFA using one of these methods:",
|
|
349
|
+
" - Virtual MFA device (Google Authenticator, Authy, etc.)",
|
|
350
|
+
" - Hardware MFA device (YubiKey, etc.)",
|
|
351
|
+
" - SMS MFA (not recommended for high security)",
|
|
352
|
+
"Use AWS CLI: aws iam enable-mfa-device --user-name <username> --serial-number <device-arn> --authentication-code1 <code1> --authentication-code2 <code2>",
|
|
353
|
+
"Or use AWS Console: IAM > Users > [User] > Security credentials > Multi-factor authentication",
|
|
354
|
+
"Consider enforcing MFA through IAM policies",
|
|
355
|
+
"Provide MFA setup instructions to users"
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class IAMRootAccessKeyAssessment(BaseConfigRuleAssessment):
|
|
360
|
+
"""Assessment for iam-root-access-key-check Config rule - ensures root has no access keys."""
|
|
361
|
+
|
|
362
|
+
def __init__(self):
|
|
363
|
+
"""Initialize IAM root access key assessment."""
|
|
364
|
+
super().__init__(
|
|
365
|
+
rule_name="iam-root-access-key-check",
|
|
366
|
+
control_id="3.3",
|
|
367
|
+
resource_types=["AWS::::Account"]
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
371
|
+
"""Get account-level resource for root access key check."""
|
|
372
|
+
if resource_type != "AWS::::Account":
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
account_info = aws_factory.get_account_info()
|
|
377
|
+
return [{
|
|
378
|
+
'AccountId': account_info.get('account_id', 'unknown'),
|
|
379
|
+
'ResourceType': 'AWS::::Account'
|
|
380
|
+
}]
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Error getting account info: {e}")
|
|
383
|
+
return [{
|
|
384
|
+
'AccountId': 'unknown',
|
|
385
|
+
'ResourceType': 'AWS::::Account'
|
|
386
|
+
}]
|
|
387
|
+
|
|
388
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
389
|
+
"""Evaluate if root user has access keys."""
|
|
390
|
+
account_id = resource.get('AccountId', 'unknown')
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
394
|
+
|
|
395
|
+
# Get account summary which includes root access key info
|
|
396
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
397
|
+
lambda: iam_client.get_account_summary()
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
summary = response.get('SummaryMap', {})
|
|
401
|
+
|
|
402
|
+
# Check for root access keys
|
|
403
|
+
root_access_keys_present = summary.get('AccountAccessKeysPresent', 0)
|
|
404
|
+
|
|
405
|
+
if root_access_keys_present > 0:
|
|
406
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
407
|
+
evaluation_reason = f"Root user has {root_access_keys_present} access key(s) - this is a security risk"
|
|
408
|
+
else:
|
|
409
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
410
|
+
evaluation_reason = "Root user has no access keys configured"
|
|
411
|
+
|
|
412
|
+
except ClientError as e:
|
|
413
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
414
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
415
|
+
compliance_status = ComplianceStatus.ERROR
|
|
416
|
+
evaluation_reason = "Insufficient permissions to check root access keys"
|
|
417
|
+
else:
|
|
418
|
+
compliance_status = ComplianceStatus.ERROR
|
|
419
|
+
evaluation_reason = f"Error checking root access keys: {str(e)}"
|
|
420
|
+
except Exception as e:
|
|
421
|
+
compliance_status = ComplianceStatus.ERROR
|
|
422
|
+
evaluation_reason = f"Unexpected error checking root access keys: {str(e)}"
|
|
423
|
+
|
|
424
|
+
return ComplianceResult(
|
|
425
|
+
resource_id=account_id,
|
|
426
|
+
resource_type="AWS::::Account",
|
|
427
|
+
compliance_status=compliance_status,
|
|
428
|
+
evaluation_reason=evaluation_reason,
|
|
429
|
+
config_rule_name=self.rule_name,
|
|
430
|
+
region=region
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
434
|
+
"""Get specific remediation steps for root access keys."""
|
|
435
|
+
return [
|
|
436
|
+
"Remove root user access keys immediately:",
|
|
437
|
+
"1. Log in to AWS Console as root user",
|
|
438
|
+
"2. Go to 'My Security Credentials' in the account menu",
|
|
439
|
+
"3. In the 'Access keys' section, delete any existing access keys",
|
|
440
|
+
"4. Create IAM users with appropriate permissions instead",
|
|
441
|
+
"5. Use IAM roles for applications and services",
|
|
442
|
+
"Alternative using AWS CLI (if you have the keys):",
|
|
443
|
+
" aws iam delete-access-key --access-key-id <access-key-id>",
|
|
444
|
+
"Best practices:",
|
|
445
|
+
" - Never use root credentials for daily operations",
|
|
446
|
+
" - Enable MFA on the root account",
|
|
447
|
+
" - Use IAM users and roles for all programmatic access"
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class S3BucketPublicReadProhibitedAssessment(BaseConfigRuleAssessment):
|
|
452
|
+
"""Assessment for s3-bucket-public-read-prohibited Config rule."""
|
|
453
|
+
|
|
454
|
+
def __init__(self):
|
|
455
|
+
"""Initialize S3 bucket public read assessment."""
|
|
456
|
+
super().__init__(
|
|
457
|
+
rule_name="s3-bucket-public-read-prohibited",
|
|
458
|
+
control_id="3.3",
|
|
459
|
+
resource_types=["AWS::S3::Bucket"]
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
463
|
+
"""Get all S3 buckets."""
|
|
464
|
+
if resource_type != "AWS::S3::Bucket":
|
|
465
|
+
return []
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
469
|
+
|
|
470
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
471
|
+
lambda: s3_client.list_buckets()
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
buckets = []
|
|
475
|
+
for bucket in response.get('Buckets', []):
|
|
476
|
+
buckets.append({
|
|
477
|
+
'Name': bucket.get('Name'),
|
|
478
|
+
'CreationDate': bucket.get('CreationDate')
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
logger.debug(f"Found {len(buckets)} S3 buckets")
|
|
482
|
+
return buckets
|
|
483
|
+
|
|
484
|
+
except ClientError as e:
|
|
485
|
+
logger.error(f"Error retrieving S3 buckets: {e}")
|
|
486
|
+
raise
|
|
487
|
+
except Exception as e:
|
|
488
|
+
logger.error(f"Unexpected error retrieving S3 buckets: {e}")
|
|
489
|
+
raise
|
|
490
|
+
|
|
491
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
492
|
+
"""Evaluate if S3 bucket allows public read access."""
|
|
493
|
+
bucket_name = resource.get('Name', 'unknown')
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
497
|
+
|
|
498
|
+
public_access_issues = []
|
|
499
|
+
|
|
500
|
+
# Check bucket ACL
|
|
501
|
+
try:
|
|
502
|
+
acl_response = aws_factory.aws_api_call_with_retry(
|
|
503
|
+
lambda: s3_client.get_bucket_acl(Bucket=bucket_name)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
for grant in acl_response.get('Grants', []):
|
|
507
|
+
grantee = grant.get('Grantee', {})
|
|
508
|
+
permission = grant.get('Permission', '')
|
|
509
|
+
|
|
510
|
+
# Check for public read permissions
|
|
511
|
+
if grantee.get('Type') == 'Group':
|
|
512
|
+
uri = grantee.get('URI', '')
|
|
513
|
+
if 'AllUsers' in uri and permission in ['READ', 'FULL_CONTROL']:
|
|
514
|
+
public_access_issues.append(f"Bucket ACL grants {permission} to AllUsers")
|
|
515
|
+
elif 'AuthenticatedUsers' in uri and permission in ['READ', 'FULL_CONTROL']:
|
|
516
|
+
public_access_issues.append(f"Bucket ACL grants {permission} to AuthenticatedUsers")
|
|
517
|
+
|
|
518
|
+
except ClientError as e:
|
|
519
|
+
if e.response.get('Error', {}).get('Code') not in ['AccessDenied', 'NoSuchBucket']:
|
|
520
|
+
raise
|
|
521
|
+
|
|
522
|
+
# Check bucket policy
|
|
523
|
+
try:
|
|
524
|
+
policy_response = aws_factory.aws_api_call_with_retry(
|
|
525
|
+
lambda: s3_client.get_bucket_policy(Bucket=bucket_name)
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
policy_doc = json.loads(policy_response.get('Policy', '{}'))
|
|
529
|
+
|
|
530
|
+
for statement in policy_doc.get('Statement', []):
|
|
531
|
+
effect = statement.get('Effect', '')
|
|
532
|
+
principal = statement.get('Principal', {})
|
|
533
|
+
action = statement.get('Action', [])
|
|
534
|
+
|
|
535
|
+
if effect == 'Allow':
|
|
536
|
+
# Check for public principals
|
|
537
|
+
if principal == '*' or (isinstance(principal, dict) and principal.get('AWS') == '*'):
|
|
538
|
+
# Check for read actions
|
|
539
|
+
actions = action if isinstance(action, list) else [action]
|
|
540
|
+
for act in actions:
|
|
541
|
+
if any(read_action in act for read_action in ['s3:GetObject', 's3:ListBucket', 's3:*']):
|
|
542
|
+
public_access_issues.append(f"Bucket policy allows public {act}")
|
|
543
|
+
|
|
544
|
+
except ClientError as e:
|
|
545
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
546
|
+
if error_code not in ['NoSuchBucketPolicy', 'AccessDenied', 'NoSuchBucket']:
|
|
547
|
+
raise
|
|
548
|
+
|
|
549
|
+
# Check public access block settings
|
|
550
|
+
try:
|
|
551
|
+
pab_response = aws_factory.aws_api_call_with_retry(
|
|
552
|
+
lambda: s3_client.get_public_access_block(Bucket=bucket_name)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
pab_config = pab_response.get('PublicAccessBlockConfiguration', {})
|
|
556
|
+
|
|
557
|
+
if not pab_config.get('BlockPublicAcls', False):
|
|
558
|
+
public_access_issues.append("Public Access Block does not block public ACLs")
|
|
559
|
+
if not pab_config.get('IgnorePublicAcls', False):
|
|
560
|
+
public_access_issues.append("Public Access Block does not ignore public ACLs")
|
|
561
|
+
if not pab_config.get('BlockPublicPolicy', False):
|
|
562
|
+
public_access_issues.append("Public Access Block does not block public policies")
|
|
563
|
+
if not pab_config.get('RestrictPublicBuckets', False):
|
|
564
|
+
public_access_issues.append("Public Access Block does not restrict public buckets")
|
|
565
|
+
|
|
566
|
+
except ClientError as e:
|
|
567
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
568
|
+
if error_code == 'NoSuchPublicAccessBlockConfiguration':
|
|
569
|
+
public_access_issues.append("No Public Access Block configuration found")
|
|
570
|
+
elif error_code not in ['AccessDenied', 'NoSuchBucket']:
|
|
571
|
+
raise
|
|
572
|
+
|
|
573
|
+
if public_access_issues:
|
|
574
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
575
|
+
evaluation_reason = f"Bucket {bucket_name} has public read access issues: {'; '.join(public_access_issues)}"
|
|
576
|
+
else:
|
|
577
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
578
|
+
evaluation_reason = f"Bucket {bucket_name} does not allow public read access"
|
|
579
|
+
|
|
580
|
+
except ClientError as e:
|
|
581
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
582
|
+
if error_code == 'NoSuchBucket':
|
|
583
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
584
|
+
evaluation_reason = f"Bucket {bucket_name} does not exist"
|
|
585
|
+
elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
586
|
+
compliance_status = ComplianceStatus.ERROR
|
|
587
|
+
evaluation_reason = f"Insufficient permissions to check bucket {bucket_name}"
|
|
588
|
+
else:
|
|
589
|
+
compliance_status = ComplianceStatus.ERROR
|
|
590
|
+
evaluation_reason = f"Error checking bucket {bucket_name}: {str(e)}"
|
|
591
|
+
except Exception as e:
|
|
592
|
+
compliance_status = ComplianceStatus.ERROR
|
|
593
|
+
evaluation_reason = f"Unexpected error checking bucket {bucket_name}: {str(e)}"
|
|
594
|
+
|
|
595
|
+
return ComplianceResult(
|
|
596
|
+
resource_id=bucket_name,
|
|
597
|
+
resource_type="AWS::S3::Bucket",
|
|
598
|
+
compliance_status=compliance_status,
|
|
599
|
+
evaluation_reason=evaluation_reason,
|
|
600
|
+
config_rule_name=self.rule_name,
|
|
601
|
+
region=region
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
605
|
+
"""Get specific remediation steps for S3 public read access."""
|
|
606
|
+
return [
|
|
607
|
+
"Remove public read access from S3 buckets:",
|
|
608
|
+
"1. Enable S3 Public Access Block at bucket level:",
|
|
609
|
+
" aws s3api put-public-access-block --bucket <bucket-name> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true",
|
|
610
|
+
"2. Review and remove public permissions from bucket ACLs:",
|
|
611
|
+
" aws s3api get-bucket-acl --bucket <bucket-name>",
|
|
612
|
+
" aws s3api put-bucket-acl --bucket <bucket-name> --acl private",
|
|
613
|
+
"3. Review and update bucket policies to remove public access:",
|
|
614
|
+
" aws s3api get-bucket-policy --bucket <bucket-name>",
|
|
615
|
+
" aws s3api delete-bucket-policy --bucket <bucket-name> # if policy grants public access",
|
|
616
|
+
"4. Use CloudFront or signed URLs for legitimate public content access",
|
|
617
|
+
"5. Regularly audit bucket permissions using AWS Config or Trusted Advisor"
|
|
618
|
+
]
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
class EC2InstanceNoPublicIPAssessment(BaseConfigRuleAssessment):
|
|
622
|
+
"""Assessment for ec2-instance-no-public-ip Config rule."""
|
|
623
|
+
|
|
624
|
+
def __init__(self):
|
|
625
|
+
"""Initialize EC2 instance no public IP assessment."""
|
|
626
|
+
super().__init__(
|
|
627
|
+
rule_name="ec2-instance-no-public-ip",
|
|
628
|
+
control_id="3.3",
|
|
629
|
+
resource_types=["AWS::EC2::Instance"]
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
633
|
+
"""Get all EC2 instances."""
|
|
634
|
+
if resource_type != "AWS::EC2::Instance":
|
|
635
|
+
return []
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
639
|
+
|
|
640
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
641
|
+
lambda: ec2_client.describe_instances()
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
instances = []
|
|
645
|
+
for reservation in response.get('Reservations', []):
|
|
646
|
+
for instance in reservation.get('Instances', []):
|
|
647
|
+
instances.append({
|
|
648
|
+
'InstanceId': instance.get('InstanceId'),
|
|
649
|
+
'State': instance.get('State', {}),
|
|
650
|
+
'PublicIpAddress': instance.get('PublicIpAddress'),
|
|
651
|
+
'PrivateIpAddress': instance.get('PrivateIpAddress'),
|
|
652
|
+
'SubnetId': instance.get('SubnetId'),
|
|
653
|
+
'VpcId': instance.get('VpcId'),
|
|
654
|
+
'Tags': instance.get('Tags', [])
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
logger.debug(f"Found {len(instances)} EC2 instances in region {region}")
|
|
658
|
+
return instances
|
|
659
|
+
|
|
660
|
+
except ClientError as e:
|
|
661
|
+
logger.error(f"Error retrieving EC2 instances in region {region}: {e}")
|
|
662
|
+
raise
|
|
663
|
+
except Exception as e:
|
|
664
|
+
logger.error(f"Unexpected error retrieving EC2 instances in region {region}: {e}")
|
|
665
|
+
raise
|
|
666
|
+
|
|
667
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
668
|
+
"""Evaluate if EC2 instance has a public IP address."""
|
|
669
|
+
instance_id = resource.get('InstanceId', 'unknown')
|
|
670
|
+
state = resource.get('State', {})
|
|
671
|
+
state_name = state.get('Name', 'unknown')
|
|
672
|
+
public_ip = resource.get('PublicIpAddress')
|
|
673
|
+
|
|
674
|
+
# Only evaluate running instances
|
|
675
|
+
if state_name not in ['running', 'stopped']:
|
|
676
|
+
return ComplianceResult(
|
|
677
|
+
resource_id=instance_id,
|
|
678
|
+
resource_type="AWS::EC2::Instance",
|
|
679
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
680
|
+
evaluation_reason=f"Instance {instance_id} is in state '{state_name}'",
|
|
681
|
+
config_rule_name=self.rule_name,
|
|
682
|
+
region=region
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
if public_ip:
|
|
686
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
687
|
+
evaluation_reason = f"Instance {instance_id} has public IP address {public_ip}"
|
|
688
|
+
else:
|
|
689
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
690
|
+
evaluation_reason = f"Instance {instance_id} does not have a public IP address"
|
|
691
|
+
|
|
692
|
+
return ComplianceResult(
|
|
693
|
+
resource_id=instance_id,
|
|
694
|
+
resource_type="AWS::EC2::Instance",
|
|
695
|
+
compliance_status=compliance_status,
|
|
696
|
+
evaluation_reason=evaluation_reason,
|
|
697
|
+
config_rule_name=self.rule_name,
|
|
698
|
+
region=region
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
702
|
+
"""Get specific remediation steps for EC2 instances with public IPs."""
|
|
703
|
+
return [
|
|
704
|
+
"Remove public IP addresses from EC2 instances:",
|
|
705
|
+
"1. For existing instances with public IPs:",
|
|
706
|
+
" - Stop the instance if it has a dynamic public IP",
|
|
707
|
+
" - Disassociate Elastic IP if attached: aws ec2 disassociate-address --association-id <assoc-id>",
|
|
708
|
+
" - Start the instance without public IP assignment",
|
|
709
|
+
"2. For new instances, launch in private subnets:",
|
|
710
|
+
" - Use subnets that don't auto-assign public IPs",
|
|
711
|
+
" - Set 'Auto-assign Public IP' to 'Disable'",
|
|
712
|
+
"3. Set up internet access through NAT Gateway or NAT Instance:",
|
|
713
|
+
" - Create NAT Gateway in public subnet",
|
|
714
|
+
" - Route private subnet traffic through NAT Gateway",
|
|
715
|
+
"4. Use Application Load Balancer or CloudFront for web applications",
|
|
716
|
+
"5. Use VPN or Direct Connect for administrative access",
|
|
717
|
+
"6. Consider using AWS Systems Manager Session Manager for secure shell access"
|
|
718
|
+
]
|