diff --git a/docs/platform-modules.md b/docs/platform-modules.md index e8a2da0..5405329 100644 --- a/docs/platform-modules.md +++ b/docs/platform-modules.md @@ -85,6 +85,26 @@ SNS topics, EventBridge rules, Config, GuardDuty, Security Hub, account guardrai | `javabin-alert-dedup` DynamoDB | Deduplication table used by slack-alert Lambda | | Cost allocation tags | `aws_ce_cost_allocation_tag` resources activating 7 tags: team, service, repo, environment, managed-by, created-by, commit | +### Detection tuning + +`monitoring/detection-tuning.tf` holds every suppression, each with its reason in code. Review this list when a reason stops being true. + +| Kind | What | Why | +|------|------|-----| +| GuardDuty filter (archive) | `Impact:IAMUser/AnomalousBehavior` from `javabin-team-provisioner` on `cognito-idp` | Scheduled Cognito group sync by the team-provisioner Lambda | +| Security Hub control disabled | CIS 1.2.0 CloudWatch.1-14, CloudTrail.5; FSBP CloudTrail.5 | No CloudTrail to CloudWatch Logs (cost). EventBridge rules and GuardDuty give the equivalent alerts | +| Security Hub control disabled | FSBP GuardDuty.7 | No EKS | +| Security Hub control disabled | FSBP Macie.1 | Not adopted (cost); revisit if PII lands in S3 | +| Security Hub control disabled | FSBP EC2.10, EC2.55-58, EC2.60 | No VPC interface endpoints (~$15/month each); traffic goes through NAT over TLS | +| Security Hub control disabled | FSBP EC2.172 | VPC block-public-access is incompatible with the public ALB and NAT | +| Security Hub control disabled | FSBP Inspector.1, Inspector.3 | Inspector EC2 and Lambda code scanning are off (legacy EB only; cost) | +| Security Hub automation rule (suppress) | RDS.2, RDS.46 on `dbmoresleep` | Intentionally public database; EC2.18/EC2.19 on its security group stay open | +| Security Hub automation rule (suppress) | ELB.18 on the platform ALB port-80 listener | Redirect-only listener | + +GuardDuty filters only affect new findings; archive existing matches once in the console. + +Team SSO roles' `CredentialAccess:IAMUser/AnomalousBehavior` on `ssm:GetParameter` is deliberately not filtered. The `javabin-team-{team}` permission set is `Action "*"` scoped by `StringEqualsIfExists aws:ResourceTag/team`, so untagged parameters are readable too, and this is the finding a stolen SSO session produces. Check the source country and confirm with the user before archiving one. + ## lambdas 11 Lambda functions — see [lambda-functions.md](lambda-functions.md) for details. diff --git a/terraform/platform/ingress/outputs.tf b/terraform/platform/ingress/outputs.tf index f4bf480..dbd213e 100644 --- a/terraform/platform/ingress/outputs.tf +++ b/terraform/platform/ingress/outputs.tf @@ -22,3 +22,8 @@ output "route53_zone_id" { description = "Zone ID of the Route53 hosted zone" value = data.aws_route53_zone.main.zone_id } + +output "http_listener_arn" { + description = "ARN of the port-80 HTTP-to-HTTPS redirect listener" + value = aws_lb_listener.http_redirect.arn +} diff --git a/terraform/platform/main.tf b/terraform/platform/main.tf index ab0e7bd..c757fa4 100644 --- a/terraform/platform/main.tf +++ b/terraform/platform/main.tf @@ -55,10 +55,11 @@ module "compute" { } module "monitoring" { - source = "./monitoring" - project = var.project - region = var.region - aws_account_id = var.aws_account_id + source = "./monitoring" + project = var.project + region = var.region + aws_account_id = var.aws_account_id + alb_http_listener_arn = module.ingress.http_listener_arn } module "cost_analytics" { diff --git a/terraform/platform/monitoring/detection-tuning.tf b/terraform/platform/monitoring/detection-tuning.tf new file mode 100644 index 0000000..6a2e084 --- /dev/null +++ b/terraform/platform/monitoring/detection-tuning.tf @@ -0,0 +1,197 @@ +################################################################################ +# Detection tuning — GuardDuty filters and Security Hub control/finding +# suppression for patterns verified as benign or as accepted risk. +# +# Every entry states why. docs/platform-modules.md ("Detection tuning") lists +# them all; review the list when the reason stops being true. +################################################################################ + +# ------------------------------------------------------------------ +# GuardDuty archive filters (apply to new findings only) +# ------------------------------------------------------------------ + +# The team-provisioner Lambda syncs Cognito group descriptions on a schedule +# (cognito-idp:UpdateGroup); GuardDuty flags it as Impact:IAMUser/ +# AnomalousBehavior. Only that type, that principal and that service. +resource "aws_guardduty_filter" "team_provisioner_cognito_sync" { + name = "team-provisioner-cognito-sync" + description = "Benign: team-provisioner Lambda syncs Cognito groups" + detector_id = aws_guardduty_detector.main.id + action = "ARCHIVE" + rank = 1 + + finding_criteria { + criterion { + field = "type" + equals = ["Impact:IAMUser/AnomalousBehavior"] + } + criterion { + field = "resource.accessKeyDetails.userName" + equals = ["${var.project}-team-provisioner"] + } + criterion { + field = "service.action.awsApiCallAction.serviceName" + equals = ["cognito-idp.amazonaws.com"] + } + } +} + +# Deliberately no filter for team SSO roles' CredentialAccess:IAMUser/ +# AnomalousBehavior on ssm:GetParameter. The team permission set is +# Action "*" scoped by StringEqualsIfExists aws:ResourceTag/team, so it does +# not limit reads to the team's own path, and this finding type is exactly +# what a stolen SSO session reading secrets produces. Triage it by hand. + +# ------------------------------------------------------------------ +# Security Hub controls disabled per standard +# ------------------------------------------------------------------ + +locals { + cis_1_2_arn = "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0" + fsbp_1_0_arn = "arn:aws:securityhub:${var.region}::standards/aws-foundational-security-best-practices/v/1.0.0" + + reason_no_cw_logs = "CloudTrail is not sent to CloudWatch Logs (ingestion cost). Equivalent alerts come from EventBridge rules (iam-changes, console-login, resource-creation/modification, security-tampering) to javabin-security, and GuardDuty covers unauthorized API calls and root use." + + disabled_controls = merge( + # CIS 1.2.0 metric-filter alarms all need CloudTrail -> CloudWatch Logs. + { for n in range(1, 15) : "cis/CloudWatch.${n}" => { + standard = local.cis_1_2_arn + control = "CloudWatch.${n}" + reason = local.reason_no_cw_logs + } }, + { + "cis/CloudTrail.5" = { + standard = local.cis_1_2_arn + control = "CloudTrail.5" + reason = local.reason_no_cw_logs + } + "fsbp/CloudTrail.5" = { + standard = local.fsbp_1_0_arn + control = "CloudTrail.5" + reason = local.reason_no_cw_logs + } + "fsbp/GuardDuty.7" = { + standard = local.fsbp_1_0_arn + control = "GuardDuty.7" + reason = "No EKS clusters in the account." + } + "fsbp/Macie.1" = { + standard = local.fsbp_1_0_arn + control = "Macie.1" + reason = "Macie not adopted: cost not justified for current S3 data. Revisit if PII is stored in S3." + } + "fsbp/EC2.172" = { + standard = local.fsbp_1_0_arn + control = "EC2.172" + reason = "VPC block-public-access cannot be on: the public ALB and NAT gateway need the internet gateway." + } + "fsbp/Inspector.1" = { + standard = local.fsbp_1_0_arn + control = "Inspector.1" + reason = "Inspector EC2 scanning off: only legacy Elastic Beanstalk instances (not SSM-managed), being migrated to ECS." + } + "fsbp/Inspector.3" = { + standard = local.fsbp_1_0_arn + control = "Inspector.3" + reason = "Inspector Lambda code scanning off (cost). Lambda package scanning (Inspector.4) is on." + } + }, + # Interface endpoints cost ~$15/month each across two AZs; AWS API traffic + # leaves through NAT over TLS, which is acceptable here. + { for c in ["EC2.10", "EC2.55", "EC2.56", "EC2.57", "EC2.58", "EC2.60"] : "fsbp/${c}" => { + standard = local.fsbp_1_0_arn + control = c + reason = "No VPC interface endpoints (~$15/month each). AWS API traffic uses the NAT gateway over TLS." + } }, + ) +} + +resource "aws_securityhub_standards_control_association" "disabled" { + for_each = local.disabled_controls + + standards_arn = each.value.standard + security_control_id = each.value.control + association_status = "DISABLED" + updated_reason = each.value.reason + + depends_on = [aws_securityhub_account.main] +} + +# ------------------------------------------------------------------ +# Security Hub automation rules — suppress specific accepted findings +# ------------------------------------------------------------------ + +# moresleep's database is intentionally publicly reachable (public API with +# existing external clients; no architecture change during migration). +# Narrowing its security group is tracked separately, so EC2.18/EC2.19 on that +# group stay open on purpose. +resource "aws_securityhub_automation_rule" "moresleep_public_rds" { + rule_name = "${var.project}-accepted-moresleep-public-rds" + description = "dbmoresleep is intentionally publicly accessible (accepted risk)." + rule_order = 1 + rule_status = "ENABLED" + + criteria { + compliance_security_control_id { + comparison = "EQUALS" + value = "RDS.2" + } + compliance_security_control_id { + comparison = "EQUALS" + value = "RDS.46" + } + resource_id { + comparison = "EQUALS" + value = "arn:aws:rds:${var.region}:${var.aws_account_id}:db:dbmoresleep" + } + } + + actions { + type = "FINDING_FIELDS_UPDATE" + finding_fields_update { + workflow { + status = "SUPPRESSED" + } + note { + text = "Accepted risk: dbmoresleep is a public endpoint by design. Security group narrowing is tracked in the security audit (H3)." + updated_by = "${var.project}-platform-terraform" + } + } + } + + depends_on = [aws_securityhub_account.main] +} + +# The platform ALB's port-80 listener only redirects to HTTPS. +resource "aws_securityhub_automation_rule" "alb_http_redirect" { + rule_name = "${var.project}-accepted-alb-http-redirect" + description = "The platform ALB port-80 listener only redirects to HTTPS." + rule_order = 2 + rule_status = "ENABLED" + + criteria { + compliance_security_control_id { + comparison = "EQUALS" + value = "ELB.18" + } + resource_id { + comparison = "EQUALS" + value = var.alb_http_listener_arn + } + } + + actions { + type = "FINDING_FIELDS_UPDATE" + finding_fields_update { + workflow { + status = "SUPPRESSED" + } + note { + text = "False positive: redirect-only listener (HTTP 301 to HTTPS)." + updated_by = "${var.project}-platform-terraform" + } + } + } + + depends_on = [aws_securityhub_account.main] +} diff --git a/terraform/platform/monitoring/variables.tf b/terraform/platform/monitoring/variables.tf index f7430eb..6a9595f 100644 --- a/terraform/platform/monitoring/variables.tf +++ b/terraform/platform/monitoring/variables.tf @@ -12,3 +12,8 @@ variable "aws_account_id" { description = "AWS account ID" type = string } + +variable "alb_http_listener_arn" { + description = "ARN of the platform ALB's port-80 redirect listener (Security Hub ELB.18 suppression)" + type = string +}