From 2dd1c02edc74a298d2e6b5f763e37f08b4d750af Mon Sep 17 00:00:00 2001 From: Alexanderamiri Date: Sat, 24 Oct 2026 15:38:22 +0200 Subject: [PATCH 1/2] feat(monitoring): enable account security guardrails EBS snapshot block-public-access, EBS default encryption, SSM document sharing off, AMI BPA codified, Access Analyzer, Inspector (ECR+Lambda), security-tampering EventBridge alert. Config recorder moves to the service-linked role with global types; Config bucket gets TLS-only policy and 2-year lifecycle. Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 2 +- docs/platform-modules.md | 9 +- terraform/platform/monitoring/guardrails.tf | 106 ++++++++++++++++++++ terraform/platform/monitoring/main.tf | 79 ++++++++++----- 4 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 terraform/platform/monitoring/guardrails.tf diff --git a/CLAUDE.md b/CLAUDE.md index 9ec3274..a905614 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ sourced from this repo. Our CI roles attach to the same provider with different trust conditions. - **Auth0**: login.javazone.no — existing IdP, stays as-is - **Existing TF state**: `javazone-terraform-state` bucket + `terraform-lock` table — manages SQS only, unrelated to our work -- **Security services**: GuardDuty, Security Hub, and Config deployed by our platform. CloudTrail not managed by us. +- **Security services**: GuardDuty, Security Hub, Config, Inspector (ECR + Lambda), IAM Access Analyzer and account guardrails (EBS/AMI/SSM block-public-access, EBS default encryption) deployed by our platform (`monitoring/`). CloudTrail not managed by us. ### Coexistence Strategy **Full separation.** We create our own VPC, ALB, ECS cluster, OIDC provider, state bucket — everything. diff --git a/docs/platform-modules.md b/docs/platform-modules.md index 2af7bfc..966a281 100644 --- a/docs/platform-modules.md +++ b/docs/platform-modules.md @@ -69,14 +69,17 @@ ECS Fargate cluster + CI ECR repositories. ## monitoring -SNS topics, EventBridge rules, Config, GuardDuty, Security Hub. +SNS topics, EventBridge rules, Config, GuardDuty, Security Hub, account guardrails (`guardrails.tf`). | Resource | Purpose | |----------|---------| | `javabin-alerts` SNS | Cost anomalies, budgets | | `javabin-security` SNS | IAM changes, GuardDuty, Config, Security Hub | -| EventBridge rules | IAM key creation, root login, console sign-in, resource creation | -| AWS Config | Configuration recorder + S3 delivery | +| EventBridge rules | IAM key creation, root login, console sign-in, resource creation, security tampering (CloudTrail/Config/GuardDuty/Security Hub/KMS/guardrail disables) | +| AWS Config | Configuration recorder (service-linked role, includes global IAM types) + S3 delivery (TLS-only, 2-year expiry) | +| Account guardrails | EBS snapshot block-public-access (block all), AMI block-public-access (block new), EBS encryption by default, SSM document public sharing off | +| IAM Access Analyzer | `javabin-external-access`, external-access findings only (free) | +| Inspector | ECR + Lambda package scanning (EC2 and Lambda code scanning off) | | GuardDuty | Threat detection | | Security Hub | Findings aggregation | | `javabin-alert-dedup` DynamoDB | Deduplication table used by slack-alert Lambda | diff --git a/terraform/platform/monitoring/guardrails.tf b/terraform/platform/monitoring/guardrails.tf new file mode 100644 index 0000000..688a1af --- /dev/null +++ b/terraform/platform/monitoring/guardrails.tf @@ -0,0 +1,106 @@ +################################################################################ +# Account security guardrails +# +# Account-level settings that close Security Hub findings at no or low cost. +# None of them changes how running workloads behave: they block new public +# sharing, encrypt new volumes, and add detection. +################################################################################ + +# EC2.182: no EBS snapshot can be shared publicly. No public snapshots exist. +resource "aws_ebs_snapshot_block_public_access" "main" { + state = "block-all-sharing" +} + +# Codifies the live value (already block-new-sharing) so drift is caught. +resource "aws_ec2_image_block_public_access" "main" { + state = "block-new-sharing" +} + +# EC2.7: new EBS volumes are encrypted with the AWS-managed aws/ebs key. +# Existing volumes are untouched; only legacy EB auto-scaling groups launch +# volumes in this account, and their service-linked role can use aws/ebs. +resource "aws_ebs_encryption_by_default" "main" { + enabled = true +} + +# SSM.7: block public sharing of SSM documents. The account shares none. +resource "aws_ssm_service_setting" "document_public_sharing" { + setting_id = "arn:aws:ssm:${var.region}:${var.aws_account_id}:servicesetting/ssm/documents/console/public-sharing-permission" + setting_value = "Disable" +} + +# IAM Access Analyzer, external access only (free). Flags resource policies +# that grant access outside the account. Unused-access analysis is paid per +# role/user and is left off. +resource "aws_accessanalyzer_analyzer" "external_access" { + analyzer_name = "${var.project}-external-access" + type = "ACCOUNT" +} + +# Inspector: ECR image and Lambda package scanning (about $5-10/month at +# current volume). EC2 scanning is off (only legacy EB instances, not +# SSM-managed) and Lambda code scanning is off (cost); the matching Security +# Hub controls are disabled with reasons in the detection-tuning change. +resource "aws_inspector2_enabler" "main" { + account_ids = [var.aws_account_id] + resource_types = ["ECR", "LAMBDA"] +} + +################################################################################ +# Security tampering → javabin-security SNS +# +# Alerts when anyone turns off logging, detection or the guardrails above. +# This is the cheap replacement for the CIS CloudWatch metric-filter alarms +# (CloudTrail → CloudWatch Logs costs ingestion). Rule 1 (iam_changes) and +# the console-login rule already cover IAM and sign-in. +################################################################################ + +resource "aws_cloudwatch_event_rule" "security_tampering" { + name = "${var.project}-security-tampering" + description = "Detect changes that disable logging, detection or account guardrails" + + event_pattern = jsonencode({ + detail-type = ["AWS API Call via CloudTrail"] + detail = { + eventSource = [ + "cloudtrail.amazonaws.com", + "config.amazonaws.com", + "kms.amazonaws.com", + "guardduty.amazonaws.com", + "securityhub.amazonaws.com", + "access-analyzer.amazonaws.com", + "ec2.amazonaws.com", + "ssm.amazonaws.com", + "inspector2.amazonaws.com", + ] + eventName = [ + "StopLogging", + "DeleteTrail", + "UpdateTrail", + "PutEventSelectors", + "StopConfigurationRecorder", + "DeleteConfigurationRecorder", + "DeleteDeliveryChannel", + "DisableKey", + "ScheduleKeyDeletion", + "DeleteDetector", + "UpdateDetector", + "DisassociateFromMasterAccount", + "DisableSecurityHub", + "BatchDisableStandards", + "DeleteAnalyzer", + "DisableSnapshotBlockPublicAccess", + "DisableEbsEncryptionByDefault", + "DisableImageBlockPublicAccess", + "UpdateServiceSetting", + "Disable", + ] + } + }) +} + +resource "aws_cloudwatch_event_target" "security_tampering_sns" { + rule = aws_cloudwatch_event_rule.security_tampering.name + target_id = "send-to-security-sns" + arn = aws_sns_topic.security.arn +} diff --git a/terraform/platform/monitoring/main.tf b/terraform/platform/monitoring/main.tf index 4e736f2..b3db101 100644 --- a/terraform/platform/monitoring/main.tf +++ b/terraform/platform/monitoring/main.tf @@ -235,13 +235,20 @@ resource "aws_cloudwatch_event_target" "resource_modification_sns" { # AWS Config Recorder — needed for Config Rules to evaluate resources ################################################################################ +# Config.1: the recorder must use the AWS Config service-linked role, and +# record global (IAM) resource types so IAM controls are evaluated. This is +# the only recorder in the account (single region), so it owns global types. +resource "aws_iam_service_linked_role" "config" { + aws_service_name = "config.amazonaws.com" +} + resource "aws_config_configuration_recorder" "main" { name = "${var.project}-recorder" - role_arn = aws_iam_role.config_role.arn + role_arn = aws_iam_service_linked_role.config.arn recording_group { all_supported = true - include_global_resource_types = false + include_global_resource_types = true } } @@ -291,6 +298,8 @@ resource "aws_s3_bucket_public_access_block" "config_logs" { resource "aws_s3_bucket_policy" "config_logs" { bucket = aws_s3_bucket.config_logs.id + # Delivery statements follow the AWS-documented Config bucket policy, + # scoped to this account with aws:SourceAccount. policy = jsonencode({ Version = "2012-10-17" Statement = [ @@ -300,47 +309,67 @@ resource "aws_s3_bucket_policy" "config_logs" { Principal = { Service = "config.amazonaws.com" } Action = "s3:GetBucketAcl" Resource = aws_s3_bucket.config_logs.arn + Condition = { + StringEquals = { "aws:SourceAccount" = var.aws_account_id } + } + }, + { + Sid = "AWSConfigBucketExistenceCheck" + Effect = "Allow" + Principal = { Service = "config.amazonaws.com" } + Action = "s3:ListBucket" + Resource = aws_s3_bucket.config_logs.arn + Condition = { + StringEquals = { "aws:SourceAccount" = var.aws_account_id } + } }, { Sid = "AWSConfigBucketDelivery" Effect = "Allow" Principal = { Service = "config.amazonaws.com" } Action = "s3:PutObject" - Resource = "${aws_s3_bucket.config_logs.arn}/*" + Resource = "${aws_s3_bucket.config_logs.arn}/AWSLogs/${var.aws_account_id}/Config/*" Condition = { - StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" } + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + "aws:SourceAccount" = var.aws_account_id + } } - } + }, + { + # S3.5 + Sid = "DenyInsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.config_logs.arn, + "${aws_s3_bucket.config_logs.arn}/*", + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }, ] }) } -################################################################################ -# IAM Role for AWS Config -################################################################################ +# S3.13: keep two years of Config history (audits), then expire. +resource "aws_s3_bucket_lifecycle_configuration" "config_logs" { + bucket = aws_s3_bucket.config_logs.id -resource "aws_iam_role" "config_role" { - name = "${var.project}-config-role" + rule { + id = "expire-after-2-years" + status = "Enabled" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Principal = { Service = "config.amazonaws.com" } - Action = "sts:AssumeRole" - }] - }) + filter {} - tags = { - Name = "${var.project}-config-role" + expiration { + days = 730 + } } } -resource "aws_iam_role_policy_attachment" "config_role" { - role = aws_iam_role.config_role.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole" -} - ################################################################################ # Config Rule — Required Tags (service, team, managed-by) ################################################################################ From ae46e0f1dc6c7ea5f6b70336ce9ad9f09c9f91bc Mon Sep 17 00:00:00 2001 From: Alexanderamiri Date: Sat, 24 Oct 2026 16:01:55 +0200 Subject: [PATCH 2/2] fix(monitoring): alert on finding filters, automation rules and recorder changes Add GuardDuty filter, Security Hub automation rule and control, Config recorder and snapshot block-public-access changes to the tamper rule. Document the ECR BASIC to ENHANCED scanning switch from Inspector. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/platform-modules.md | 4 ++-- terraform/platform/monitoring/guardrails.tf | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/platform-modules.md b/docs/platform-modules.md index 966a281..e8a2da0 100644 --- a/docs/platform-modules.md +++ b/docs/platform-modules.md @@ -75,11 +75,11 @@ SNS topics, EventBridge rules, Config, GuardDuty, Security Hub, account guardrai |----------|---------| | `javabin-alerts` SNS | Cost anomalies, budgets | | `javabin-security` SNS | IAM changes, GuardDuty, Config, Security Hub | -| EventBridge rules | IAM key creation, root login, console sign-in, resource creation, security tampering (CloudTrail/Config/GuardDuty/Security Hub/KMS/guardrail disables) | +| EventBridge rules | IAM key creation, root login, console sign-in, resource creation, security tampering (CloudTrail/Config/GuardDuty/Security Hub/KMS/guardrail disables, plus GuardDuty filters, Security Hub automation rules and control changes, Config recorder changes). `UpdateDetector`, `UpdateServiceSetting` and the filter/rule events also fire on benign changes, including platform applies | | AWS Config | Configuration recorder (service-linked role, includes global IAM types) + S3 delivery (TLS-only, 2-year expiry) | | Account guardrails | EBS snapshot block-public-access (block all), AMI block-public-access (block new), EBS encryption by default, SSM document public sharing off | | IAM Access Analyzer | `javabin-external-access`, external-access findings only (free) | -| Inspector | ECR + Lambda package scanning (EC2 and Lambda code scanning off) | +| Inspector | ECR + Lambda package scanning (EC2 and Lambda code scanning off). Enabling it switches ECR registry scanning from BASIC to ENHANCED, so `scan_on_push` on repositories (`compute/`, `modules/ecr-repo`) no longer has an effect. Expect a first burst of findings into Security Hub, mostly from legacy Lambdas on deprecated runtimes | | GuardDuty | Threat detection | | Security Hub | Findings aggregation | | `javabin-alert-dedup` DynamoDB | Deduplication table used by slack-alert Lambda | diff --git a/terraform/platform/monitoring/guardrails.tf b/terraform/platform/monitoring/guardrails.tf index 688a1af..3983791 100644 --- a/terraform/platform/monitoring/guardrails.tf +++ b/terraform/platform/monitoring/guardrails.tf @@ -94,6 +94,17 @@ resource "aws_cloudwatch_event_rule" "security_tampering" { "DisableImageBlockPublicAccess", "UpdateServiceSetting", "Disable", + # Changes that hide findings or narrow recording rather than turn a + # service off. Platform applies that touch these also alert, which + # is the intended audit trail. + "CreateFilter", + "UpdateFilter", + "CreateAutomationRules", + "BatchUpdateAutomationRules", + "UpdateStandardsControl", + "BatchUpdateStandardsControlAssociations", + "PutConfigurationRecorder", + "EnableSnapshotBlockPublicAccess", ] } })