Coverage for gco / stacks / api_gateway_global_stack.py: 100%

98 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 21:47 +0000

1""" 

2Global API Gateway stack - Single authenticated entry point for all regions. 

3 

4This stack creates the centralized API Gateway that serves as the authenticated 

5entry point for all GCO API requests. It provides: 

6- Edge-optimized endpoint with CloudFront for global edge caching and DDoS protection 

7- IAM authentication (AWS SigV4) for all requests 

8- Lambda proxy that adds secret header for backend validation 

9- Secrets Manager secret with automatic rotation for request authentication 

10- Multi-region replication for the auth secret 

11- CloudWatch logging for audit and debugging 

12 

13Security Flow: 

14 1. Client signs request with AWS credentials (SigV4) 

15 2. CloudFront edge location receives request (managed by AWS) 

16 3. API Gateway validates IAM permissions 

17 4. Lambda proxy retrieves secret from Secrets Manager 

18 5. Lambda adds X-GCO-Auth-Token header 

19 6. Request forwarded to Global Accelerator 

20 7. Backend services validate the secret header 

21 

22Secret Rotation: 

23 The auth token is automatically rotated daily. During rotation: 

24 - A new token is generated and stored as AWSPENDING 

25 - Backend services accept both AWSCURRENT and AWSPENDING tokens 

26 - After validation, AWSPENDING becomes AWSCURRENT 

27 - Multi-region replication ensures all regions receive the new token 

28 

29This ensures all traffic goes through the authenticated path and prevents 

30direct access to the Global Accelerator or regional ALBs. 

31""" 

32 

33import json 

34from typing import Any 

35 

36from aws_cdk import ( 

37 CfnOutput, 

38 Duration, 

39 RemovalPolicy, 

40 Stack, 

41) 

42from aws_cdk import aws_apigateway as apigateway 

43from aws_cdk import aws_iam as iam 

44from aws_cdk import aws_lambda as lambda_ 

45from aws_cdk import aws_logs as logs 

46from aws_cdk import aws_secretsmanager as secretsmanager 

47from aws_cdk import aws_wafv2 as wafv2 

48from constructs import Construct 

49 

50from gco.stacks.constants import LAMBDA_PYTHON_RUNTIME 

51 

52 

53class GCOApiGatewayGlobalStack(Stack): 

54 """ 

55 Global API Gateway with IAM authentication. 

56 

57 This stack creates the single authenticated entry point for all GCO 

58 API requests. All requests must be signed with AWS credentials. 

59 

60 Attributes: 

61 secret: Secrets Manager secret for backend validation 

62 proxy_lambda: Lambda function that proxies requests to Global Accelerator 

63 aggregator_lambda: Lambda function for cross-region aggregation 

64 api: REST API with IAM authentication 

65 """ 

66 

67 def __init__( 

68 self, 

69 scope: Construct, 

70 construct_id: str, 

71 global_accelerator_dns: str, 

72 regional_endpoints: dict[str, str] | None = None, 

73 **kwargs: Any, 

74 ) -> None: 

75 super().__init__(scope, construct_id, **kwargs) 

76 

77 self.ga_dns = global_accelerator_dns 

78 self.regional_endpoints = regional_endpoints or {} 

79 

80 # Create secret token for ALB validation 

81 self.secret = self._create_secret() 

82 

83 # Create proxy Lambda 

84 self.proxy_lambda = self._create_proxy_lambda() 

85 

86 # Create cross-region aggregator Lambda 

87 self.aggregator_lambda = self._create_aggregator_lambda() 

88 

89 # Create API Gateway 

90 self.api = self._create_api_gateway() 

91 

92 # Create WAF WebACL and associate with API Gateway 

93 self._create_waf() 

94 

95 # Export API endpoint 

96 self._create_outputs() 

97 

98 # Apply cdk-nag suppressions 

99 self._apply_nag_suppressions() 

100 

101 def _apply_nag_suppressions(self) -> None: 

102 """Apply cdk-nag suppressions for this stack.""" 

103 from gco.stacks.nag_suppressions import apply_all_suppressions 

104 

105 # API Gateway stack needs global_region for SSM parameter access suppressions 

106 # The aggregator Lambda reads ALB hostnames from SSM in the global region 

107 apply_all_suppressions(self, stack_type="api_gateway", global_region=self.region) 

108 

109 def _create_secret(self) -> secretsmanager.Secret: 

110 """Create secret token for validating requests from API Gateway. 

111 

112 The secret is configured with: 

113 - Automatic rotation every 30 days 

114 - A rotation Lambda that generates new secure random tokens 

115 - Multi-region replication can be enabled via add_replica_region() 

116 """ 

117 secret = secretsmanager.Secret( 

118 self, 

119 "GCOAuthSecret", 

120 secret_name="gco/api-gateway-auth-token", # nosec B106 — this is the secret path, not a password 

121 description="Secret token for validating requests from API Gateway to ALB (auto-rotated)", 

122 generate_secret_string=secretsmanager.SecretStringGenerator( 

123 secret_string_template=json.dumps({"description": "GCO API Gateway auth token"}), 

124 generate_string_key="token", 

125 exclude_punctuation=True, 

126 password_length=64, 

127 ), 

128 removal_policy=RemovalPolicy.DESTROY, 

129 ) 

130 

131 # Create rotation Lambda and store as instance attribute for monitoring 

132 self.rotation_lambda = self._create_rotation_lambda(secret) 

133 

134 # Enable automatic rotation (daily for enhanced security) 

135 secret.add_rotation_schedule( 

136 "RotationSchedule", 

137 automatically_after=Duration.days(1), 

138 rotation_lambda=self.rotation_lambda, 

139 ) 

140 

141 return secret 

142 

143 def _create_rotation_lambda(self, secret: secretsmanager.Secret) -> lambda_.Function: 

144 """Create Lambda function for secret rotation. 

145 

146 This Lambda implements the 4-step Secrets Manager rotation protocol: 

147 1. createSecret - Generate new random token 

148 2. setSecret - No-op (no external system) 

149 3. testSecret - Validate token structure 

150 4. finishSecret - Move AWSPENDING to AWSCURRENT 

151 """ 

152 # Create IAM role for rotation Lambda 

153 rotation_role = iam.Role( 

154 self, 

155 "RotationLambdaRole", 

156 assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 

157 managed_policies=[ 

158 iam.ManagedPolicy.from_aws_managed_policy_name( 

159 "service-role/AWSLambdaBasicExecutionRole" 

160 ) 

161 ], 

162 ) 

163 

164 # Grant permissions to manage the secret 

165 secret.grant_read(rotation_role) 

166 secret.grant_write(rotation_role) 

167 

168 # Additional permissions for rotation 

169 rotation_role.add_to_policy( 

170 iam.PolicyStatement( 

171 actions=[ 

172 "secretsmanager:DescribeSecret", 

173 "secretsmanager:GetSecretValue", 

174 "secretsmanager:PutSecretValue", 

175 "secretsmanager:UpdateSecretVersionStage", 

176 ], 

177 resources=[secret.secret_arn], 

178 ) 

179 ) 

180 

181 # Create log group for rotation Lambda 

182 rotation_log_group = logs.LogGroup( 

183 self, 

184 "RotationLambdaLogGroup", 

185 retention=logs.RetentionDays.ONE_MONTH, 

186 removal_policy=RemovalPolicy.DESTROY, 

187 ) 

188 

189 # Create rotation Lambda 

190 rotation_lambda = lambda_.Function( 

191 self, 

192 "SecretRotationFunction", 

193 runtime=getattr(lambda_.Runtime, LAMBDA_PYTHON_RUNTIME), 

194 handler="handler.lambda_handler", 

195 code=lambda_.Code.from_asset("lambda/secret-rotation"), 

196 timeout=Duration.seconds(30), 

197 memory_size=128, 

198 role=rotation_role, 

199 log_group=rotation_log_group, 

200 description="Rotates the GCO API Gateway auth token", 

201 tracing=lambda_.Tracing.ACTIVE, 

202 ) 

203 

204 # Grant Secrets Manager permission to invoke the rotation Lambda 

205 rotation_lambda.grant_invoke(iam.ServicePrincipal("secretsmanager.amazonaws.com")) 

206 

207 # cdk-nag suppression: CDK's grant methods generate Resource: * for 

208 # the rotation function's execution role. 

209 from cdk_nag import NagSuppressions 

210 

211 NagSuppressions.add_resource_suppressions( 

212 rotation_role, 

213 [ 

214 { 

215 "id": "AwsSolutions-IAM5", 

216 "reason": ( 

217 "The secret rotation Lambda needs secretsmanager:GetSecretValue " 

218 "and PutSecretValue on the rotation secret. CDK's grant methods " 

219 "generate Resource: * for the rotation function's execution role " 

220 "because the secret ARN includes a random suffix not known at " 

221 "synth time." 

222 ), 

223 "appliesTo": ["Resource::*"], 

224 }, 

225 ], 

226 apply_to_children=True, 

227 ) 

228 

229 return rotation_lambda 

230 

231 def _create_proxy_lambda(self) -> lambda_.Function: 

232 """Create Lambda function that proxies requests to Global Accelerator.""" 

233 

234 # Create IAM role 

235 lambda_role = iam.Role( 

236 self, 

237 "ProxyLambdaRole", 

238 assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 

239 managed_policies=[ 

240 iam.ManagedPolicy.from_aws_managed_policy_name( 

241 "service-role/AWSLambdaBasicExecutionRole" 

242 ) 

243 ], 

244 ) 

245 

246 # Grant read access to secret 

247 self.secret.grant_read(lambda_role) 

248 

249 # Create log group for Lambda 

250 proxy_lambda_log_group = logs.LogGroup( 

251 self, 

252 "ProxyLambdaLogGroup", 

253 retention=logs.RetentionDays.ONE_WEEK, 

254 removal_policy=RemovalPolicy.DESTROY, 

255 ) 

256 

257 # Create Lambda function 

258 proxy_lambda = lambda_.Function( 

259 self, 

260 "ApiGatewayProxyFunction", 

261 runtime=getattr(lambda_.Runtime, LAMBDA_PYTHON_RUNTIME), 

262 handler="handler.lambda_handler", 

263 code=lambda_.Code.from_asset("lambda/api-gateway-proxy"), 

264 timeout=Duration.seconds(29), 

265 memory_size=256, 

266 role=lambda_role, 

267 environment={ 

268 "GLOBAL_ACCELERATOR_ENDPOINT": self.ga_dns, 

269 "SECRET_ARN": self.secret.secret_arn, 

270 }, 

271 log_group=proxy_lambda_log_group, 

272 tracing=lambda_.Tracing.ACTIVE, 

273 ) 

274 

275 # cdk-nag suppression: the proxy Lambda's execution role needs 

276 # broad network access for VPC Lambda execution. 

277 from cdk_nag import NagSuppressions 

278 

279 NagSuppressions.add_resource_suppressions( 

280 lambda_role, 

281 [ 

282 { 

283 "id": "AwsSolutions-IAM5", 

284 "reason": ( 

285 "The API Gateway proxy Lambda forwards requests to regional ALBs. " 

286 "Its execution role needs broad network access " 

287 "(ec2:CreateNetworkInterface, etc.) for VPC Lambda execution. " 

288 "These APIs do not support resource-level scoping." 

289 ), 

290 "appliesTo": ["Resource::*"], 

291 }, 

292 ], 

293 apply_to_children=True, 

294 ) 

295 

296 return proxy_lambda 

297 

298 def _create_aggregator_lambda(self) -> lambda_.Function: 

299 """Create Lambda function for cross-region aggregation. 

300 

301 This Lambda queries all regional ALBs in parallel and aggregates 

302 the results for global views of jobs, health, and metrics. 

303 """ 

304 # Create IAM role 

305 aggregator_role = iam.Role( 

306 self, 

307 "AggregatorLambdaRole", 

308 assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 

309 managed_policies=[ 

310 iam.ManagedPolicy.from_aws_managed_policy_name( 

311 "service-role/AWSLambdaBasicExecutionRole" 

312 ) 

313 ], 

314 ) 

315 

316 # Grant read access to secret 

317 self.secret.grant_read(aggregator_role) 

318 

319 # Grant SSM read access for discovering regional endpoints 

320 aggregator_role.add_to_policy( 

321 iam.PolicyStatement( 

322 effect=iam.Effect.ALLOW, 

323 actions=["ssm:GetParametersByPath", "ssm:GetParameter"], 

324 resources=[f"arn:aws:ssm:{self.region}:{self.account}:parameter/gco/*"], 

325 ) 

326 ) 

327 

328 # Create log group for Lambda 

329 aggregator_log_group = logs.LogGroup( 

330 self, 

331 "AggregatorLambdaLogGroup", 

332 retention=logs.RetentionDays.ONE_WEEK, 

333 removal_policy=RemovalPolicy.DESTROY, 

334 ) 

335 

336 # Create Lambda function 

337 aggregator_lambda = lambda_.Function( 

338 self, 

339 "CrossRegionAggregatorFunction", 

340 runtime=getattr(lambda_.Runtime, LAMBDA_PYTHON_RUNTIME), 

341 handler="handler.lambda_handler", 

342 code=lambda_.Code.from_asset("lambda/cross-region-aggregator"), 

343 timeout=Duration.seconds(29), 

344 memory_size=512, 

345 role=aggregator_role, 

346 environment={ 

347 "SECRET_ARN": self.secret.secret_arn, 

348 "PROJECT_NAME": "gco", 

349 "GLOBAL_REGION": self.region, 

350 }, 

351 log_group=aggregator_log_group, 

352 description="Aggregates data from all regional GCO clusters", 

353 tracing=lambda_.Tracing.ACTIVE, 

354 ) 

355 

356 # cdk-nag suppression: the aggregator Lambda reads SSM parameters 

357 # and invokes regional endpoints. 

358 from cdk_nag import NagSuppressions 

359 

360 NagSuppressions.add_resource_suppressions( 

361 aggregator_role, 

362 [ 

363 { 

364 "id": "AwsSolutions-IAM5", 

365 "reason": ( 

366 "The cross-region aggregator Lambda reads SSM parameters and " 

367 "invokes regional endpoints. Its execution role needs " 

368 "ssm:GetParameter on the project's parameter namespace and " 

369 "secretsmanager:GetSecretValue for the auth token." 

370 ), 

371 "appliesTo": ["Resource::*"], 

372 }, 

373 ], 

374 apply_to_children=True, 

375 ) 

376 

377 return aggregator_lambda 

378 

379 def _create_api_gateway(self) -> apigateway.RestApi: 

380 """Create API Gateway with IAM authentication.""" 

381 

382 # Create CloudWatch log group 

383 api_log_group = logs.LogGroup( 

384 self, 

385 "ApiGatewayLogs", 

386 log_group_name="/aws/apigateway/gco-global", 

387 retention=logs.RetentionDays.ONE_MONTH, 

388 removal_policy=RemovalPolicy.DESTROY, 

389 ) 

390 

391 # Create REST API with edge-optimized endpoint 

392 # Edge-optimized uses CloudFront for global edge caching and DDoS protection 

393 api = apigateway.RestApi( 

394 self, 

395 "GCOGlobalApi", 

396 rest_api_name="gco-global-api", 

397 description="Global authenticated API for GCO (Global Capacity Orchestrator on AWS) (edge-optimized)", 

398 endpoint_types=[apigateway.EndpointType.EDGE], 

399 deploy=True, 

400 deploy_options=apigateway.StageOptions( 

401 stage_name="prod", 

402 throttling_rate_limit=1000, 

403 throttling_burst_limit=2000, 

404 logging_level=apigateway.MethodLoggingLevel.INFO, 

405 data_trace_enabled=True, 

406 metrics_enabled=True, 

407 tracing_enabled=True, # Enable X-Ray tracing for request analysis 

408 access_log_destination=apigateway.LogGroupLogDestination(api_log_group), 

409 access_log_format=apigateway.AccessLogFormat.json_with_standard_fields( 

410 caller=True, 

411 http_method=True, 

412 ip=True, 

413 protocol=True, 

414 request_time=True, 

415 resource_path=True, 

416 response_length=True, 

417 status=True, 

418 user=True, 

419 ), 

420 ), 

421 cloud_watch_role=True, 

422 ) 

423 

424 # Add resource policy to restrict to account 

425 api.add_to_resource_policy( 

426 iam.PolicyStatement( 

427 effect=iam.Effect.ALLOW, 

428 principals=[iam.AnyPrincipal()], 

429 actions=["execute-api:Invoke"], 

430 resources=["execute-api:/*"], 

431 conditions={"StringEquals": {"aws:PrincipalAccount": self.account}}, 

432 ) 

433 ) 

434 

435 # Create Lambda integration 

436 lambda_integration = apigateway.LambdaIntegration( 

437 self.proxy_lambda, proxy=True, timeout=Duration.seconds(29) 

438 ) 

439 

440 # Create /api resource 

441 api_resource = api.root.add_resource("api") 

442 v1_resource = api_resource.add_resource("v1") 

443 

444 # Add proxy resource to catch all paths 

445 proxy_resource = v1_resource.add_resource("{proxy+}") 

446 

447 # Add methods with IAM authentication 

448 for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]: 

449 proxy_resource.add_method( 

450 method, 

451 lambda_integration, 

452 authorization_type=apigateway.AuthorizationType.IAM, 

453 method_responses=[ 

454 apigateway.MethodResponse(status_code="200"), 

455 apigateway.MethodResponse(status_code="400"), 

456 apigateway.MethodResponse(status_code="403"), 

457 apigateway.MethodResponse(status_code="500"), 

458 ], 

459 ) 

460 

461 # Create global aggregation routes 

462 self._create_global_routes(api, v1_resource) 

463 

464 # Create inference proxy route 

465 # /inference/{proxy+} → proxy Lambda → GA → ALB → K8s Ingress 

466 self._create_inference_routes(api, lambda_integration) 

467 

468 return api 

469 

470 def _create_global_routes( 

471 self, api: apigateway.RestApi, v1_resource: apigateway.Resource 

472 ) -> None: 

473 """Create routes for cross-region aggregation endpoints. 

474 

475 Routes: 

476 GET /api/v1/global/jobs - List jobs across all regions 

477 DELETE /api/v1/global/jobs - Bulk delete across all regions 

478 GET /api/v1/global/health - Health status across all regions 

479 GET /api/v1/global/status - Cluster status across all regions 

480 """ 

481 # Create Lambda integration for aggregator 

482 aggregator_integration = apigateway.LambdaIntegration( 

483 self.aggregator_lambda, proxy=True, timeout=Duration.seconds(29) 

484 ) 

485 

486 # Create /global resource 

487 global_resource = v1_resource.add_resource("global") 

488 

489 # /global/jobs 

490 global_jobs = global_resource.add_resource("jobs") 

491 for method in ["GET", "DELETE"]: 

492 global_jobs.add_method( 

493 method, 

494 aggregator_integration, 

495 authorization_type=apigateway.AuthorizationType.IAM, 

496 method_responses=[ 

497 apigateway.MethodResponse(status_code="200"), 

498 apigateway.MethodResponse(status_code="400"), 

499 apigateway.MethodResponse(status_code="500"), 

500 ], 

501 ) 

502 

503 # /global/health 

504 global_health = global_resource.add_resource("health") 

505 global_health.add_method( 

506 "GET", 

507 aggregator_integration, 

508 authorization_type=apigateway.AuthorizationType.IAM, 

509 method_responses=[ 

510 apigateway.MethodResponse(status_code="200"), 

511 apigateway.MethodResponse(status_code="500"), 

512 ], 

513 ) 

514 

515 # /global/status 

516 global_status = global_resource.add_resource("status") 

517 global_status.add_method( 

518 "GET", 

519 aggregator_integration, 

520 authorization_type=apigateway.AuthorizationType.IAM, 

521 method_responses=[ 

522 apigateway.MethodResponse(status_code="200"), 

523 apigateway.MethodResponse(status_code="500"), 

524 ], 

525 ) 

526 

527 def _create_inference_routes( 

528 self, 

529 api: apigateway.RestApi, 

530 lambda_integration: apigateway.LambdaIntegration, 

531 ) -> None: 

532 """Create proxy route for inference endpoints. 

533 

534 Routes: 

535 ANY /inference/{proxy+} → proxy Lambda → GA → ALB → K8s Ingress 

536 

537 This allows authenticated inference requests to flow through the 

538 API Gateway with IAM auth, then get proxied to the regional ALB 

539 where K8s Ingress routes them to the correct inference Service. 

540 """ 

541 inference_resource = api.root.add_resource("inference") 

542 inference_proxy = inference_resource.add_resource("{proxy+}") 

543 

544 for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]: 

545 inference_proxy.add_method( 

546 method, 

547 lambda_integration, 

548 authorization_type=apigateway.AuthorizationType.IAM, 

549 method_responses=[ 

550 apigateway.MethodResponse(status_code="200"), 

551 apigateway.MethodResponse(status_code="400"), 

552 apigateway.MethodResponse(status_code="404"), 

553 apigateway.MethodResponse(status_code="500"), 

554 apigateway.MethodResponse(status_code="502"), 

555 ], 

556 ) 

557 

558 def _create_outputs(self) -> None: 

559 """Export API Gateway endpoint.""" 

560 

561 CfnOutput( 

562 self, 

563 "ApiEndpoint", 

564 value=self.api.url, 

565 description="Global API Gateway endpoint (IAM authenticated)", 

566 export_name="gco-global-api-endpoint", 

567 ) 

568 

569 CfnOutput( 

570 self, 

571 "SecretArn", 

572 value=self.secret.secret_arn, 

573 description="Secret ARN for ALB validation", 

574 export_name="gco-auth-secret-arn", 

575 ) 

576 

577 def _create_waf(self) -> None: 

578 """Create WAF WebACL with AWS Managed Rules for API Gateway protection. 

579 

580 This implements a comprehensive WAF setup using AWS Managed Rule Groups 

581 for protection against: 

582 - Common web exploits (OWASP Top 10) 

583 - Known bad inputs 

584 - SQL injection 

585 - Linux-specific attacks 

586 - IP reputation threats 

587 - Anonymous IP addresses (Tor, VPNs, proxies) 

588 

589 The WebACL is associated with the API Gateway stage for edge protection. 

590 Logging is enabled to CloudWatch Logs for compliance (HIPAA, NIST, PCI-DSS). 

591 """ 

592 # Create CloudWatch Log Group for WAF logs 

593 # WAF requires log group name to start with "aws-waf-logs-" 

594 waf_log_group = logs.LogGroup( 

595 self, 

596 "WafLogGroup", 

597 log_group_name="aws-waf-logs-gco-api-gateway", 

598 retention=logs.RetentionDays.ONE_MONTH, 

599 removal_policy=RemovalPolicy.DESTROY, 

600 ) 

601 

602 # Create WAF WebACL with AWS Managed Rules 

603 # Note: For API Gateway (even edge-optimized), use REGIONAL scope 

604 # The WAF is associated with the API Gateway stage, not CloudFront directly 

605 # 

606 # Rule priority ordering: 

607 # 0 -> PerIPRateLimit (evaluated FIRST so abusive IPs are blocked 

608 # before expensive managed rule groups run) 

609 # 1-6 -> AWS Managed Rule Groups 

610 waf_config = self.node.try_get_context("waf") or {} 

611 per_ip_rate_limit = int(waf_config.get("per_ip_rate_limit", 100)) 

612 

613 self.web_acl = wafv2.CfnWebACL( 

614 self, 

615 "GCOWebAcl", 

616 name="gco-api-gateway-waf", 

617 description="WAF WebACL for GCO API Gateway with AWS Managed Rules", 

618 scope="REGIONAL", # REGIONAL for API Gateway association 

619 default_action=wafv2.CfnWebACL.DefaultActionProperty(allow={}), 

620 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

621 cloud_watch_metrics_enabled=True, 

622 metric_name="GCOApiGatewayWaf", 

623 sampled_requests_enabled=True, 

624 ), 

625 rules=[ 

626 # Rule 0: Per-source-IP rate limiting (HIGHEST PRIORITY). 

627 # Evaluated before any AWS Managed Rule Group so that abusive 

628 # IPs are blocked immediately without consuming WCUs on the 

629 # heavier managed rule groups. Aggregates requests per source 

630 # IP over a rolling 5-minute window (AWS WAF fixed behavior 

631 # for rate-based statements). 

632 # 

633 # The limit is configurable via `cdk.json` context 

634 # `waf.per_ip_rate_limit` (default: 100 requests / 5 min). 

635 wafv2.CfnWebACL.RuleProperty( 

636 name="PerIPRateLimit", 

637 priority=0, 

638 action=wafv2.CfnWebACL.RuleActionProperty(block={}), 

639 statement=wafv2.CfnWebACL.StatementProperty( 

640 rate_based_statement=wafv2.CfnWebACL.RateBasedStatementProperty( 

641 limit=per_ip_rate_limit, 

642 aggregate_key_type="IP", 

643 ) 

644 ), 

645 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

646 cloud_watch_metrics_enabled=True, 

647 metric_name="PerIPRateLimit", 

648 sampled_requests_enabled=True, 

649 ), 

650 ), 

651 # Rule 1: AWS Managed Rules - Common Rule Set (OWASP Top 10) 

652 wafv2.CfnWebACL.RuleProperty( 

653 name="AWSManagedRulesCommonRuleSet", 

654 priority=1, 

655 override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}), 

656 statement=wafv2.CfnWebACL.StatementProperty( 

657 managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty( 

658 vendor_name="AWS", 

659 name="AWSManagedRulesCommonRuleSet", 

660 ) 

661 ), 

662 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

663 cloud_watch_metrics_enabled=True, 

664 metric_name="AWSManagedRulesCommonRuleSet", 

665 sampled_requests_enabled=True, 

666 ), 

667 ), 

668 # Rule 2: AWS Managed Rules - Known Bad Inputs 

669 wafv2.CfnWebACL.RuleProperty( 

670 name="AWSManagedRulesKnownBadInputsRuleSet", 

671 priority=2, 

672 override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}), 

673 statement=wafv2.CfnWebACL.StatementProperty( 

674 managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty( 

675 vendor_name="AWS", 

676 name="AWSManagedRulesKnownBadInputsRuleSet", 

677 ) 

678 ), 

679 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

680 cloud_watch_metrics_enabled=True, 

681 metric_name="AWSManagedRulesKnownBadInputsRuleSet", 

682 sampled_requests_enabled=True, 

683 ), 

684 ), 

685 # Rule 3: AWS Managed Rules - SQL Injection 

686 wafv2.CfnWebACL.RuleProperty( 

687 name="AWSManagedRulesSQLiRuleSet", 

688 priority=3, 

689 override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}), 

690 statement=wafv2.CfnWebACL.StatementProperty( 

691 managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty( 

692 vendor_name="AWS", 

693 name="AWSManagedRulesSQLiRuleSet", 

694 ) 

695 ), 

696 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

697 cloud_watch_metrics_enabled=True, 

698 metric_name="AWSManagedRulesSQLiRuleSet", 

699 sampled_requests_enabled=True, 

700 ), 

701 ), 

702 # Rule 4: AWS Managed Rules - Linux OS (protects against Linux-specific attacks) 

703 wafv2.CfnWebACL.RuleProperty( 

704 name="AWSManagedRulesLinuxRuleSet", 

705 priority=4, 

706 override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}), 

707 statement=wafv2.CfnWebACL.StatementProperty( 

708 managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty( 

709 vendor_name="AWS", 

710 name="AWSManagedRulesLinuxRuleSet", 

711 ) 

712 ), 

713 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

714 cloud_watch_metrics_enabled=True, 

715 metric_name="AWSManagedRulesLinuxRuleSet", 

716 sampled_requests_enabled=True, 

717 ), 

718 ), 

719 # Rule 5: AWS Managed Rules - Amazon IP Reputation List 

720 wafv2.CfnWebACL.RuleProperty( 

721 name="AWSManagedRulesAmazonIpReputationList", 

722 priority=5, 

723 override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}), 

724 statement=wafv2.CfnWebACL.StatementProperty( 

725 managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty( 

726 vendor_name="AWS", 

727 name="AWSManagedRulesAmazonIpReputationList", 

728 ) 

729 ), 

730 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

731 cloud_watch_metrics_enabled=True, 

732 metric_name="AWSManagedRulesAmazonIpReputationList", 

733 sampled_requests_enabled=True, 

734 ), 

735 ), 

736 # Rule 6: AWS Managed Rules - Anonymous IP List (blocks Tor, VPNs, proxies) 

737 wafv2.CfnWebACL.RuleProperty( 

738 name="AWSManagedRulesAnonymousIpList", 

739 priority=6, 

740 override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}), 

741 statement=wafv2.CfnWebACL.StatementProperty( 

742 managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty( 

743 vendor_name="AWS", 

744 name="AWSManagedRulesAnonymousIpList", 

745 ) 

746 ), 

747 visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( 

748 cloud_watch_metrics_enabled=True, 

749 metric_name="AWSManagedRulesAnonymousIpList", 

750 sampled_requests_enabled=True, 

751 ), 

752 ), 

753 ], 

754 ) 

755 

756 # Enable WAF logging to CloudWatch Logs 

757 # This is required for HIPAA, NIST 800-53, and PCI-DSS compliance 

758 wafv2.CfnLoggingConfiguration( 

759 self, 

760 "WafLoggingConfig", 

761 resource_arn=self.web_acl.attr_arn, 

762 log_destination_configs=[waf_log_group.log_group_arn], 

763 ) 

764 

765 # Associate WAF WebACL with API Gateway stage 

766 # For API Gateway, use the stage ARN format 

767 wafv2.CfnWebACLAssociation( 

768 self, 

769 "GCOWebAclAssociation", 

770 resource_arn=self.api.deployment_stage.stage_arn, 

771 web_acl_arn=self.web_acl.attr_arn, 

772 ) 

773 

774 # Output WAF WebACL ARN 

775 CfnOutput( 

776 self, 

777 "WebAclArn", 

778 value=self.web_acl.attr_arn, 

779 description="WAF WebACL ARN for API Gateway protection", 

780 export_name="gco-waf-webacl-arn", 

781 )