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
« 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.
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
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
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
29This ensures all traffic goes through the authenticated path and prevents
30direct access to the Global Accelerator or regional ALBs.
31"""
33import json
34from typing import Any
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
50from gco.stacks.constants import LAMBDA_PYTHON_RUNTIME
53class GCOApiGatewayGlobalStack(Stack):
54 """
55 Global API Gateway with IAM authentication.
57 This stack creates the single authenticated entry point for all GCO
58 API requests. All requests must be signed with AWS credentials.
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 """
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)
77 self.ga_dns = global_accelerator_dns
78 self.regional_endpoints = regional_endpoints or {}
80 # Create secret token for ALB validation
81 self.secret = self._create_secret()
83 # Create proxy Lambda
84 self.proxy_lambda = self._create_proxy_lambda()
86 # Create cross-region aggregator Lambda
87 self.aggregator_lambda = self._create_aggregator_lambda()
89 # Create API Gateway
90 self.api = self._create_api_gateway()
92 # Create WAF WebACL and associate with API Gateway
93 self._create_waf()
95 # Export API endpoint
96 self._create_outputs()
98 # Apply cdk-nag suppressions
99 self._apply_nag_suppressions()
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
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)
109 def _create_secret(self) -> secretsmanager.Secret:
110 """Create secret token for validating requests from API Gateway.
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 )
131 # Create rotation Lambda and store as instance attribute for monitoring
132 self.rotation_lambda = self._create_rotation_lambda(secret)
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 )
141 return secret
143 def _create_rotation_lambda(self, secret: secretsmanager.Secret) -> lambda_.Function:
144 """Create Lambda function for secret rotation.
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 )
164 # Grant permissions to manage the secret
165 secret.grant_read(rotation_role)
166 secret.grant_write(rotation_role)
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 )
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 )
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 )
204 # Grant Secrets Manager permission to invoke the rotation Lambda
205 rotation_lambda.grant_invoke(iam.ServicePrincipal("secretsmanager.amazonaws.com"))
207 # cdk-nag suppression: CDK's grant methods generate Resource: * for
208 # the rotation function's execution role.
209 from cdk_nag import NagSuppressions
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 )
229 return rotation_lambda
231 def _create_proxy_lambda(self) -> lambda_.Function:
232 """Create Lambda function that proxies requests to Global Accelerator."""
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 )
246 # Grant read access to secret
247 self.secret.grant_read(lambda_role)
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 )
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 )
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
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 )
296 return proxy_lambda
298 def _create_aggregator_lambda(self) -> lambda_.Function:
299 """Create Lambda function for cross-region aggregation.
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 )
316 # Grant read access to secret
317 self.secret.grant_read(aggregator_role)
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 )
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 )
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 )
356 # cdk-nag suppression: the aggregator Lambda reads SSM parameters
357 # and invokes regional endpoints.
358 from cdk_nag import NagSuppressions
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 )
377 return aggregator_lambda
379 def _create_api_gateway(self) -> apigateway.RestApi:
380 """Create API Gateway with IAM authentication."""
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 )
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 )
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 )
435 # Create Lambda integration
436 lambda_integration = apigateway.LambdaIntegration(
437 self.proxy_lambda, proxy=True, timeout=Duration.seconds(29)
438 )
440 # Create /api resource
441 api_resource = api.root.add_resource("api")
442 v1_resource = api_resource.add_resource("v1")
444 # Add proxy resource to catch all paths
445 proxy_resource = v1_resource.add_resource("{proxy+}")
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 )
461 # Create global aggregation routes
462 self._create_global_routes(api, v1_resource)
464 # Create inference proxy route
465 # /inference/{proxy+} → proxy Lambda → GA → ALB → K8s Ingress
466 self._create_inference_routes(api, lambda_integration)
468 return api
470 def _create_global_routes(
471 self, api: apigateway.RestApi, v1_resource: apigateway.Resource
472 ) -> None:
473 """Create routes for cross-region aggregation endpoints.
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 )
486 # Create /global resource
487 global_resource = v1_resource.add_resource("global")
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 )
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 )
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 )
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.
534 Routes:
535 ANY /inference/{proxy+} → proxy Lambda → GA → ALB → K8s Ingress
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+}")
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 )
558 def _create_outputs(self) -> None:
559 """Export API Gateway endpoint."""
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 )
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 )
577 def _create_waf(self) -> None:
578 """Create WAF WebACL with AWS Managed Rules for API Gateway protection.
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)
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 )
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))
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 )
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 )
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 )
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 )