Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/platform-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions terraform/platform/ingress/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
9 changes: 5 additions & 4 deletions terraform/platform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
197 changes: 197 additions & 0 deletions terraform/platform/monitoring/detection-tuning.tf
Original file line number Diff line number Diff line change
@@ -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]
}
5 changes: 5 additions & 0 deletions terraform/platform/monitoring/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading