Skip to content

Conversation

nishkersh
Copy link

Proposed change

This is the first major PR for the IaC PoC as discussed in Resolves #2215. It establishes the foundational, production-grade network infrastructure for the Nest application on AWS.

This PR introduces the first reusable Terraform module: modules/01-Network.

Key Architectural Features:

  • High-Availability: Creates a VPC with public and private subnets distributed across multiple Availability Zones.
  • Security:
    • Implements a single, resilient NAT Gateway for controlled and monitored egress from private subnets.
    • Provisions a public Application Load Balancer (ALB) with a security group that enforces HTTPS.
    • Includes a listener that automatically redirects all HTTP traffic to HTTPS.
  • Modularity: The module is self-contained with a clean interface (variables.tf and outputs.tf) and is ready to be consumed by other modules (like compute and database).

Next Steps:

Once this foundational network is approved and merged, I will proceed with the database and storage modules as planned.

Looking forward to your feedback!

Checklist

  • I've read and followed the contributing guidelines.
  • I've run make check-test locally; all checks and tests passed.

…ional structure and documentation for the new Terraform-based IaC
In this commit I introduced the initial, production-grade Terraform module for the core AWS network infrastructure.

Key features of this module include:
- Creates a new VPC with public and private subnets across multiple Availability Zones for high availability.
- Implements a single, cost-effective, and resilient NAT Gateway for private subnet egress.
- Provisions a public Application Load Balancer (ALB) with a secure default configuration and an HTTPS redirect.
- Establishes a secure-by-default posture with a locked-down ALB security group.
Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Summary by CodeRabbit

  • New Features
    • Added a Terraform network module with VPC, public/private subnets across AZs, NAT gateway, route tables, and an ALB with HTTP→HTTPS redirect and optional access logging. Exposes key outputs for use by other modules.
  • Documentation
    • Introduced a Terraform operational guide covering prerequisites, multi-environment setup, remote state/locking, secrets management, rollout plan, and deploy workflow.
  • Chores
    • Integrated Terraform pre-commit hooks for formatting, validation, linting, and security scanning.

Walkthrough

Adds Terraform pre-commit hooks, a new modular network Terraform module (VPC, subnets, NAT, route tables, ALB with HTTP→HTTPS redirect and optional access logging) with variables and outputs, and a comprehensive Terraform operational guide for multi-environment deployments and remote state.

Changes

Cohort / File(s) Summary
Pre-commit integration
.pre-commit-config.yaml
Adds Anton Babenko's pre-commit-terraform entry (rev v1.92.0) and four Terraform hooks: terraform_fmt, terraform_validate, terraform_tflint, terraform_trivy (file filter \.tf$).
Documentation
Terraform/Operational-Guide.md
New operational guide covering multi-environment Terraform setup (dev/staging/prod), remote state (S3/DynamoDB), secrets management (AWS Secrets Manager), prerequisites, one-time state setup, phased rollout, module overview, and common workflows (init/plan/apply).
Terraform network module (resources)
Terraform/modules/01-Network/main.tf
Adds VPC, Internet Gateway, per-AZ public and private subnets, single EIP and NAT gateway, public/private route tables and associations, ALB with HTTP→HTTPS redirect and HTTPS listener (fixed 404 default), ALB security group, and optional ALB access-logs S3 bucket plus policies and IAM document.
Terraform network module (variables)
Terraform/modules/01-Network/variables.tf
Adds variables: project_prefix, environment (`dev
Terraform network module (outputs)
Terraform/modules/01-Network/outputs.tf
Adds outputs: vpc_id, public_subnet_ids, private_subnet_ids, alb_security_group_id, alb_dns_name, alb_https_listener_arn.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The changes only deliver the foundational network module component of the IaC PoC but do not fulfill the broader acceptance criteria of provisioning all core services such as Lambda, ECS, S3, RDS, ElastiCache, and EC2 as specified in issue #2215. To comply with the linked issue, extend this pull request or create follow-up PRs that implement the remaining infrastructure components including Lambda backends, ECS frontend, storage buckets, RDS, ElastiCache, and batch EC2 tasks so that the proof of concept meets the full acceptance criteria of issue #2215.
Out of Scope Changes Check ⚠️ Warning In addition to the network module, this pull request introduces pre-commit Terraform hooks and an operational guide document which are not mentioned in the described objectives or linked issue and thus fall outside the stated scope of foundational network infrastructure. Consider extracting the pre-commit configuration changes and the operational guide into separate pull requests so that each PR remains focused on a single objective and aligns precisely with its linked issue scope.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly indicates the introduction of the foundational Terraform network module, which is the main change in this pull request, and avoids vague or irrelevant wording.
Description Check ✅ Passed The pull request description directly explains the implementation of the first reusable Terraform network module, outlines its key architectural features, and clearly links to the relevant issue, making it relevant to the changeset as required.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the docs Improvements or additions to documentation label Sep 24, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
.pre-commit-config.yaml (1)

90-100: Add terraform_docs and prefer types-based matching over regex.

  • Add the terraform_docs hook to keep variables/outputs documented and in sync.
  • Prefer types_or: [terraform] over files regex to catch all Terraform sources consistently (including nested paths).

Apply this diff:

   - repo: https://github.com/antonbabenko/pre-commit-terraform
-    rev: v1.92.0 
+    rev: v1.92.0
     hooks:
+      - id: terraform_docs
+        args:
+          - --hook-config=--create-file-if-not-exist=true
       - id: terraform_fmt
-        files: \.tf$
+        types_or:
+          - terraform
       - id: terraform_validate
-        files: \.tf$
+        types_or:
+          - terraform
       - id: terraform_tflint
-        files: \.tf$
+        types_or:
+          - terraform
       - id: terraform_trivy
-        files: \.tf$
+        types_or:
+          - terraform
Terraform/Operational-Guide.md (1)

18-21: Fix module path to match repository structure.

Doc lists modules/network but the added code resides under modules/01-Network. Align the path or rename for consistency.

Terraform/modules/01-Network/variables.tf (3)

19-23: Validate vpc_cidr format.

Add a simple CIDR validation to catch typos early.

 variable "vpc_cidr" {
   description = "The CIDR block for the VPC."
   type        = string
   default     = "10.0.0.0/16"
+  validation {
+    condition     = can(cidrhost(var.vpc_cidr, 0))
+    error_message = "vpc_cidr must be a valid IPv4 CIDR block."
+  }
 }

25-28: Ensure at least one AZ provided.

Add length check to avoid empty lists.

 variable "availability_zones" {
   description = "A list of Availability Zones to deploy resources into. Must match the number of subnets. e.g., [\"us-east-1a\", \"us-east-1b\"]"
   type        = list(string)
+  validation {
+    condition     = length(var.availability_zones) > 0
+    error_message = "At least one availability zone must be provided."
+  }
 }

48-52: Optionally add a toggle for per‑AZ NAT gateways.

Expose a boolean to support HA NAT as an opt-in later.

variable "enable_multi_az_nat_gateways" {
  description = "If true, create a NAT Gateway in each public subnet (higher availability, higher cost)."
  type        = bool
  default     = false
}
Terraform/modules/01-Network/main.tf (1)

65-73: Allocate the EIP in VPC scope explicitly.

Set domain = "vpc" to ensure correct allocation context for the NAT.

 resource "aws_eip" "nat" {
-  # Only one EIP is needed for the single NAT Gateway.
+  domain = "vpc" # Ensure EIP is allocated in the VPC scope
+  # Only one EIP is needed for the single NAT Gateway.
   tags = merge(
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f6ec86 and 60f9aab.

📒 Files selected for processing (5)
  • .pre-commit-config.yaml (1 hunks)
  • Terraform/Operational-Guide.md (1 hunks)
  • Terraform/modules/01-Network/main.tf (1 hunks)
  • Terraform/modules/01-Network/outputs.tf (1 hunks)
  • Terraform/modules/01-Network/variables.tf (1 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
Terraform/modules/01-Network/main.tf

[medium] 179-195: Ensure that ALB drops HTTP headers

(CKV_AWS_131)


[high] 212-227: Ensure that load balancer is using at least TLS 1.2

(CKV_AWS_103)

🔇 Additional comments (1)
Terraform/modules/01-Network/outputs.tf (1)

1-29: Outputs look good.

Clear and minimal set to enable composition by downstream modules.

Comment on lines 111 to 113
cd terraform/environments/dev
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify environment directory path/casing.

Doc uses terraform/environments/dev, while repo paths appear under Terraform/. Ensure the path exists and matches case-sensitive filesystems.


🏁 Script executed:

#!/bin/bash
# Discover environment and module paths (case-insensitive) in repo
fd -i -t d '^environments$' -d 4
fd -i -t d '01-Network' -d 6

Length of output: 53


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Listing Terraform/environments =="
ls -la Terraform/environments || true

echo
echo "== Searching for 'dev' directories under Terraform =="
fd -t d -i dev Terraform -d 4 || true

echo
echo "== Showing Operational-Guide.md around lines 100-120 =="
sed -n '100,120p' Terraform/Operational-Guide.md || true

Length of output: 991


Fix path casing and missing dev environment in docs

Terraform/Operational-Guide.md (lines 111–113) shows cd terraform/environments/dev, but the repo has Terraform/environments/ (only .gitkeep — no dev directory). Update the doc to use the correct path/casing (e.g., cd Terraform/environments/<env>) and either document creating the environment directory or add Terraform/environments/dev/ to the repo.

🤖 Prompt for AI Agents
In Terraform/Operational-Guide.md around lines 111 to 113, the documented path
uses the wrong casing and references a non-existent dev directory; update the
path to use the actual repo casing and a placeholder (e.g., cd
Terraform/environments/<env>) and either add a short step that instructs users
to create the environment directory (mkdir -p Terraform/environments/dev &&
touch Terraform/environments/dev/.gitkeep) or add Terraform/environments/dev/ to
the repository so the example works as written; ensure the text matches the
repository layout and casing consistently.

@nishkersh
Copy link
Author

I'm reviewing all the suggestions now and will push up a new commit with the fixes shortly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
Terraform/Operational-Guide.md (1)

134-139: Add a concrete remote backend example to avoid local state.

Without a backend block, init will default to local state. Provide a sample backend to match the S3/DynamoDB created above.

Add this as backend.tf in the environment directory:

terraform {
  backend "s3" {
    bucket         = env.TF_STATE_BUCKET
    key            = "owasp-nest/${env.ENVIRONMENT}/terraform.tfstate"
    region         = env.AWS_REGION
    dynamodb_table = env.TF_STATE_LOCK_TABLE
    encrypt        = true
  }
}

Alternatively, document using a backend config file (e.g., -backend-config=backend.hcl).

Terraform/modules/01-Network/variables.tf (1)

60-70: Expose a few config toggles to avoid hard-coding policy and behavior.

Make TLS policy, ALB deletion protection, NAT topology, and ALB log retention configurable.

 variable "alb_access_logs_bucket_name" {
   description = "The name of the S3 bucket to store ALB access logs. Must be globally unique. If left empty, a name will be generated."
   type        = string
   default     = ""
 }
+
+variable "alb_ssl_policy" {
+  description = "SSL policy for the ALB HTTPS listener."
+  type        = string
+  default     = "ELBSecurityPolicy-TLS13-1-2-2021-06"
+}
+
+variable "alb_deletion_protection_enabled" {
+  description = "Explicitly enable ALB deletion protection. If not set, production is enabled by default in main.tf."
+  type        = bool
+  default     = false
+}
+
+variable "nat_per_az" {
+  description = "If true, create one NAT Gateway per AZ for higher availability (higher cost). If false, use a single NAT GW."
+  type        = bool
+  default     = false
+}
+
+variable "alb_logs_retention_days" {
+  description = "Days to retain ALB access logs in S3."
+  type        = number
+  default     = 90
+}

Follow-up: wire these variables in main.tf where applicable.

Terraform/modules/01-Network/main.tf (2)

231-233: Make ALB deletion protection controllable by variable with sane default.

Allows overriding in non-prod and forces on in prod by default.

-  enable_deletion_protection = var.environment == "prod" ? true : false
+  enable_deletion_protection = var.alb_deletion_protection_enabled || (var.environment == "prod")

263-269: LGTM: TLS 1.2/1.3 policy enforced.

Updated ssl_policy to a modern policy. Consider sourcing from var.alb_ssl_policy for consistency.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60f9aab and 6b598bf.

📒 Files selected for processing (3)
  • Terraform/Operational-Guide.md (1 hunks)
  • Terraform/modules/01-Network/main.tf (1 hunks)
  • Terraform/modules/01-Network/variables.tf (1 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
Terraform/modules/01-Network/main.tf

[medium] 141-154: Ensure that an S3 bucket has a lifecycle configuration

(CKV2_AWS_61)

🔇 Additional comments (6)
Terraform/Operational-Guide.md (1)

49-70: Good fix: us-east-1 handling + hardened state bucket.

Conditional create-bucket for us-east-1, versioning, public access block, and SSE are all correct and align with best practices.

Terraform/modules/01-Network/variables.tf (2)

34-36: Good validation on public subnet CIDRs vs AZs.

Prevents index mismatches at apply time.


43-45: Good validation on private subnet CIDRs vs AZs.

Avoids out-of-range indexing and enforces module contract.

Terraform/modules/01-Network/main.tf (3)

75-89: Good: clear note on single-NAT SPOF trade-off.

Accurately documents availability/cost implications.


223-246: Ensure provider compatibility for “drop_invalid_header_fields”.

Some provider versions require setting this via load_balancer_attributes. If your version errors on the top-level arg, switch to the attribute block below.

 resource "aws_lb" "main" {
@@
-  drop_invalid_header_fields = true
+  load_balancer_attributes {
+    key   = "routing.http.drop_invalid_header_fields.enabled"
+    value = "true"
+  }

Please confirm your aws provider version supports the chosen form.


234-238: Avoid null bucket when logging is disabled.

Use a dynamic block so bucket is only referenced when enabled (prevents accidental eval of a zero-count reference).

-  access_logs {
-    bucket  = var.enable_alb_access_logs ? aws_s3_bucket.alb_access_logs[0].bucket : null
-    enabled = var.enable_alb_access_logs
-    prefix  = "alb"
-  }
+  dynamic "access_logs" {
+    for_each = var.enable_alb_access_logs ? [1] : []
+    content {
+      bucket  = aws_s3_bucket.alb_access_logs[0].bucket
+      enabled = true
+      prefix  = "alb"
+    }
+  }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
Terraform/modules/01-Network/main.tf (4)

62-64: Fix misleading resilience claim for single NAT/single RT.

Single NAT + single private route table is cost‑optimized, not more resilient. It increases AZ blast radius and adds cross‑AZ egress charges when other AZs traverse the NAT’s AZ.

Apply this diff to correct the comment:

-#  We create a SINGLE NAT Gateway and a SINGLE private route table. This is more
-#  resilient, cost-effective, and simpler to manage than a per-AZ NAT Gateway.
+#  We create a SINGLE NAT Gateway and a SINGLE private route table. This is more
+#  cost-effective and simpler to manage than per-AZ NAT Gateways, but less resilient
+#  (AZ egress SPOF) and may incur cross-AZ data transfer for other AZs.

76-89: Optional: add per‑AZ NAT support to avoid egress SPOF and cross‑AZ charges.

Expose a toggle to create one NAT per AZ and per‑AZ private route tables; keep current single‑NAT as default.

Example (outside this file; minimal sketch):

variable "enable_multi_az_nat" { type = bool, default = false }

resource "aws_nat_gateway" "az" {
  count         = var.enable_multi_az_nat ? length(aws_subnet.public) : 0
  allocation_id = aws_eip.nat_multi[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  depends_on    = [aws_internet_gateway.main]
  tags          = merge(var.tags, { Name = "${var.project_prefix}-${var.environment}-nat-gw-${count.index}" })
}

resource "aws_route_table" "private_per_az" {
  count = var.enable_multi_az_nat ? length(aws_subnet.private) : 0
  vpc_id = aws_vpc.main.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.az[count.index].id
  }
  tags = merge(var.tags, { Name = "${var.project_prefix}-${var.environment}-private-rt-${count.index}" })
}

31-44: Validate CIDR/AZ list lengths to prevent index errors.

If list lengths diverge, Terraform panics at index access.

Add validations in variables.tf (outside this file):

variable "availability_zones" {
  type = list(string)
  validation {
    condition     = length(var.availability_zones) == length(var.public_subnet_cidrs)
    error_message = "public_subnet_cidrs must match availability_zones length."
  }
  validation {
    condition     = length(var.availability_zones) == length(var.private_subnet_cidrs)
    error_message = "private_subnet_cidrs must match availability_zones length."
  }
}

Also applies to: 46-58


108-113: Use for_each to avoid index coupling on associations.

for_each is safer and clearer than count-index ties.

-resource "aws_route_table_association" "public" {
-  count          = length(aws_subnet.public)
-  subnet_id      = aws_subnet.public[count.index].id
-  route_table_id = aws_route_table.public.id
-}
+resource "aws_route_table_association" "public" {
+  for_each      = { for s in aws_subnet.public : s.id => s }
+  subnet_id      = each.value.id
+  route_table_id = aws_route_table.public.id
+}
@@
-resource "aws_route_table_association" "private" {
-  count          = length(aws_subnet.private)
-  subnet_id      = aws_subnet.private[count.index].id
-  route_table_id = aws_route_table.private.id
-}
+resource "aws_route_table_association" "private" {
+  for_each       = { for s in aws_subnet.private : s.id => s }
+  subnet_id      = each.value.id
+  route_table_id = aws_route_table.private.id
+}

Also applies to: 132-137

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b598bf and 2caacc7.

📒 Files selected for processing (1)
  • Terraform/modules/01-Network/main.tf (1 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
Terraform/modules/01-Network/main.tf

[medium] 148-161: Ensure that an S3 bucket has a lifecycle configuration

(CKV2_AWS_61)


[medium] 174-185: Ensure that an S3 bucket has a lifecycle configuration

(CKV2_AWS_61)

🔇 Additional comments (6)
Terraform/modules/01-Network/main.tf (6)

148-161: Harden ALB logs bucket: SSE, ownership controls, lifecycle.

Add SSE-S3, BucketOwnerPreferred, and lifecycle to meet checks and best practices.

 resource "aws_s3_bucket" "alb_access_logs" {
   # Only create this bucket if logging is enabled
   count = var.enable_alb_access_logs ? 1 : 0
@@
   )
 }
+
+resource "aws_s3_bucket_server_side_encryption_configuration" "alb_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.alb_access_logs[0].id
+  rule {
+    apply_server_side_encryption_by_default {
+      sse_algorithm = "AES256"
+    }
+  }
+}
+
+resource "aws_s3_bucket_ownership_controls" "alb_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.alb_access_logs[0].id
+  rule { object_ownership = "BucketOwnerPreferred" }
+}
+
+resource "aws_s3_bucket_lifecycle_configuration" "alb_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.alb_access_logs[0].id
+  rule {
+    id     = "expire-alb-logs"
+    status = "Enabled"
+    expiration { days = var.alb_logs_retention_days }
+  }
+}

Note: Checkov flags CKV2_AWS_61 for lifecycle. [Based on static analysis hints]


173-185: Also harden the S3 server access logs bucket.

Apply public access block, SSE, ownership, lifecycle to the target logs bucket too.

 resource "aws_s3_bucket" "s3_server_access_logs" {
   count = var.enable_alb_access_logs ? 1 : 0
@@
   )
 }
+
+resource "aws_s3_bucket_public_access_block" "s3_server_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.s3_server_access_logs[0].id
+  block_public_acls       = true
+  block_public_policy     = true
+  ignore_public_acls      = true
+  restrict_public_buckets = true
+}
+
+resource "aws_s3_bucket_server_side_encryption_configuration" "s3_server_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.s3_server_access_logs[0].id
+  rule {
+    apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
+  }
+}
+
+resource "aws_s3_bucket_ownership_controls" "s3_server_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.s3_server_access_logs[0].id
+  rule { object_ownership = "BucketOwnerPreferred" }
+}
+
+resource "aws_s3_bucket_lifecycle_configuration" "s3_server_access_logs" {
+  count  = var.enable_alb_access_logs ? 1 : 0
+  bucket = aws_s3_bucket.s3_server_access_logs[0].id
+  rule {
+    id     = "expire-s3-logs"
+    status = "Enabled"
+    expiration { days = var.alb_logs_retention_days }
+  }
+}

280-303: ALB config looks good (HTTPS, deletion protection gating, header hardening).

LGTM.


320-326: TLS policy upgraded to TLS1.2/1.3.

Good security posture.


65-73: Set EIP domain to “vpc” for NAT Gateway.

Without domain="vpc", allocation can misbehave in edge cases and fail NAT creation.

 resource "aws_eip" "nat" {
+  domain = "vpc"
   # Only one EIP is needed for the single NAT Gateway.
   tags = merge(

196-230: Fix ALB logs bucket policy (invalid Effect, wrong resource path, missing GetBucketAcl).

Effect must be “Allow”. Resource must point to AWSLogs//*, and ALB needs s3:GetBucketAcl. Add ACL condition.

 data "aws_iam_policy_document" "alb_access_logs" {
   count = var.enable_alb_access_logs ? 1 : 0
 
   # This statement allows the ALB service to write logs to the bucket.
   statement {
-    effect    = "AllowALBToWriteLogs"
+    sid       = "AWSLogDeliveryWrite"
+    effect    = "Allow"
     actions   = ["s3:PutObject"]
-    resources = ["${aws_s3_bucket.alb_access_logs[0].arn}/*"]
+    resources = ["${aws_s3_bucket.alb_access_logs[0].arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]
 
     principals {
       type        = "AWS"
-      identifiers = [data.aws_elb_service_account.current.arn]
+      identifiers = [data.aws_elb_service_account.current.arn]
     }
+    condition {
+      test     = "StringEquals"
+      variable = "s3:x-amz-acl"
+      values   = ["bucket-owner-full-control"]
+    }
   }
 
-  #  This statement denies any access to the bucket over insecure HTTP.
+  # Allow the ALB service to read bucket ACL for delivery checks.
+  statement {
+    sid       = "AWSLogDeliveryCheck"
+    effect    = "Allow"
+    actions   = ["s3:GetBucketAcl"]
+    resources = [aws_s3_bucket.alb_access_logs[0].arn]
+    principals {
+      type        = "AWS"
+      identifiers = [data.aws_elb_service_account.current.arn]
+    }
+  }
+
+  #  This statement denies any access to the bucket over insecure HTTP.
   statement {
     sid       = "DenyInsecureTransport"
     effect    = "Deny"
     actions   = ["s3:*"]
     resources = ["${aws_s3_bucket.alb_access_logs[0].arn}/*"]
 
     principals {
       type        = "*"
       identifiers = ["*"]
     }
 
     condition {
       test     = "Bool"
       variable = "aws:SecureTransport"
       values   = ["false"]
     }
   }
 }

This commit resolves all SonarQube security hotspots for the S3 buckets by:
- Adding Public Access Block configurations to both log buckets.
- Enforcing HTTPS-only access via bucket policies on both buckets.
- Enabling server-access logging for both buckets.
- Adding versioning, encryption, and lifecycle policies for a complete production-ready configuration.
Copy link

sonarqubecloud bot commented Oct 2, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
5 Security Hotspots

See analysis details on SonarQube Cloud

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Terraform/modules/01-Network/main.tf (2)

65-72: Explicitly scope the NAT EIP to the VPC domain.

Setting domain = "vpc" avoids legacy Classic allocations and is the documented requirement for NAT gateways. Please add the explicit domain for clarity and robustness.

 resource "aws_eip" "nat" {
+  domain = "vpc"
   tags = merge(
     var.tags,
     {
       Name = "${var.project_prefix}-${var.environment}-nat-eip"
     }
   )
 }

244-287: Add abort rules for incomplete multipart uploads.

Lifecycle policies should clean up failed multipart uploads to satisfy CKV_AWS_300 and prevent unbounded storage costs.

     noncurrent_version_expiration {
       noncurrent_days = 30
     }
+
+    abort_incomplete_multipart_upload {
+      days_after_initiation = 7
+    }
   }
 }
@@
     noncurrent_version_expiration {
       noncurrent_days = 30
     }
+
+    abort_incomplete_multipart_upload {
+      days_after_initiation = 7
+    }
   }
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2caacc7 and 2f44956.

📒 Files selected for processing (1)
  • Terraform/modules/01-Network/main.tf (1 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
Terraform/modules/01-Network/main.tf

[medium] 240-266: Ensure S3 lifecycle configuration sets period for aborting failed uploads

(CKV_AWS_300)


[medium] 268-289: Ensure S3 lifecycle configuration sets period for aborting failed uploads

(CKV_AWS_300)

Comment on lines +62 to +64
# We create a SINGLE NAT Gateway and a SINGLE private route table. This is more
# resilient, cost-effective, and simpler to manage than a per-AZ NAT Gateway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Document or mitigate the single NAT SPOF.

A single NAT gateway is not “more resilient”; it is an AZ-level SPOF for private subnets if that AZ or the NAT fails. Either expose an option for per-AZ NAT gateways or update the comment to call out the risk so operators can make an informed choice.

-#  We create a SINGLE NAT Gateway and a SINGLE private route table. This is more
-# resilient, cost-effective, and simpler to manage than a per-AZ NAT Gateway.
+#  We create a SINGLE NAT Gateway and a SINGLE private route table. This is a cost
+# optimization but introduces a single-AZ egress SPOF compared to per-AZ NAT gateways.
+# Scale to one NAT per AZ if higher availability is required.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# We create a SINGLE NAT Gateway and a SINGLE private route table. This is more
# resilient, cost-effective, and simpler to manage than a per-AZ NAT Gateway.
# We create a SINGLE NAT Gateway and a SINGLE private route table. This is a cost
# optimization but introduces a single-AZ egress SPOF compared to per-AZ NAT gateways.
# Scale to one NAT per AZ if higher availability is required.
🤖 Prompt for AI Agents
In Terraform/modules/01-Network/main.tf around lines 62 to 64, the comment
incorrectly claims a single NAT Gateway is "more resilient" and omits the
AZ-level single point of failure; either add a module variable to allow per-AZ
NAT gateways (e.g., enable_per_az_nat boolean and count/for_each to create a NAT
per AZ and associated route tables) and wire that option into resource counts
and route table associations, or update the comment to explicitly call out the
AZ-level SPOF and recommend operators enable per-AZ NATs for high availability;
implement one of these two changes so the module documents or mitigates the
single NAT SPOF.

Comment on lines +219 to +237
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = true
}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "s3_server_access_logs" {
count = var.enable_alb_access_logs ? 1 : 0
bucket = aws_s3_bucket.s3_server_access_logs[0].id

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = true
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix invalid bucket key usage with SSE-S3.

Setting bucket_key_enabled = true while sse_algorithm = "AES256" causes AWS to reject the request (BucketKeyEnabled only applies to KMS encryption). Apply the change below to prevent plan/apply failures.

 resource "aws_s3_bucket_server_side_encryption_configuration" "alb_access_logs" {
   count  = var.enable_alb_access_logs ? 1 : 0
   bucket = aws_s3_bucket.alb_access_logs[0].id

   rule {
     apply_server_side_encryption_by_default {
       sse_algorithm = "AES256"
     }
-    bucket_key_enabled = true
   }
 }

 resource "aws_s3_bucket_server_side_encryption_configuration" "s3_server_access_logs" {
   count  = var.enable_alb_access_logs ? 1 : 0
   bucket = aws_s3_bucket.s3_server_access_logs[0].id

   rule {
     apply_server_side_encryption_by_default {
       sse_algorithm = "AES256"
     }
-    bucket_key_enabled = true
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "s3_server_access_logs" {
count = var.enable_alb_access_logs ? 1 : 0
bucket = aws_s3_bucket.s3_server_access_logs[0].id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "alb_access_logs" {
count = var.enable_alb_access_logs ? 1 : 0
bucket = aws_s3_bucket.alb_access_logs[0].id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "s3_server_access_logs" {
count = var.enable_alb_access_logs ? 1 : 0
bucket = aws_s3_bucket.s3_server_access_logs[0].id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
🤖 Prompt for AI Agents
In Terraform/modules/01-Network/main.tf around lines 219-237, the code sets
bucket_key_enabled = true while using sse_algorithm = "AES256", which is invalid
because BucketKeyEnabled only applies to KMS encryption; to fix, either
remove/set bucket_key_enabled = false for these SSE-S3 blocks, or make
bucket_key_enabled conditional so it is true only when sse_algorithm ==
"aws:kms" (and if using KMS also ensure kms_master_key_id is provided); update
both aws_s3_bucket and aws_s3_bucket_server_side_encryption_configuration
resources accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Research and Implement IaC PoC for OWASP Nest Infrastructure
1 participant