Skip to content

feat(monitoring): enable account security guardrails - #163

Merged
Alexanderamiri merged 2 commits into
mainfrom
feat/sec-guardrails
Oct 26, 2026
Merged

Alexanderamiri merged 2 commits into
mainfrom
feat/sec-guardrails

Conversation

@Alexanderamiri

Copy link
Copy Markdown
Member

Why

The security audit found these account-level guardrails off:

Guardrail Live state Security Hub control
EBS snapshot block-public-access unblocked EC2.182 (HIGH)
EBS encryption by default off EC2.7
SSM document public sharing Enable SSM.7 (CRITICAL, suppressed by hand)
IAM Access Analyzer none n/a
Inspector fully disabled Inspector.1-4 (HIGH)
Config recorder uses a custom role instead of the service-linked role, and does not record global IAM types Config.1 (CRITICAL)
Config bucket no TLS-only policy, no lifecycle S3.5, S3.13

What

New monitoring/guardrails.tf:

  • aws_ebs_snapshot_block_public_access set to block-all-sharing. No public snapshots exist.
  • aws_ec2_image_block_public_access set to block-new-sharing. This codifies the current live value, so nothing changes.
  • aws_ebs_encryption_by_default with aws/ebs. It applies to new volumes only.
  • aws_ssm_service_setting sets SSM document public sharing to Disable. The account shares no documents.
  • aws_accessanalyzer_analyzer named javabin-external-access, type ACCOUNT. This analyzer type is free.
  • aws_inspector2_enabler for ECR and LAMBDA, about $5-10/month. EC2 scanning and Lambda code scanning stay off.
  • EventBridge rule javabin-security-tampering, targeting the javabin-security SNS topic. It is a cheap replacement for the CIS CloudWatch metric-filter alarms, and fires on:
    • CloudTrail: StopLogging, DeleteTrail, UpdateTrail, PutEventSelectors.
    • Config: recorder and delivery-channel deletes.
    • KMS: DisableKey, ScheduleKeyDeletion.
    • Disables of GuardDuty, Security Hub, Access Analyzer and Inspector.
    • Disables of EBS/AMI block-public-access and EBS default encryption.
    • SSM UpdateServiceSetting.

monitoring/main.tf (Config block only):

  • New aws_iam_service_linked_role for config.amazonaws.com. It does not exist today.
  • The recorder now uses that role and sets include_global_resource_types = true. It is the only recorder in the account.
  • Removes aws_iam_role.config_role and its AWS_ConfigRole attachment. The service-linked role replaces them. The state records the dependency, so Terraform updates the recorder before it destroys the old role.
  • Config bucket policy changes:
    • Adds AWSConfigBucketExistenceCheck.
    • Scopes PutObject to AWSLogs/<account>/Config/*, the only prefix in use (checked).
    • Adds aws:SourceAccount conditions.
    • Adds DenyInsecureTransport.
  • Config bucket lifecycle: objects expire after 730 days.

Docs: platform-modules.md (monitoring) and the security-services line in CLAUDE.md.

How verified

  • terraform fmt -recursive -check and terraform validate pass with aws 5.100.0. All resource types used exist in that version.
  • Pytest passes (107).
  • Live state checked before writing:
    • Snapshot BPA is unblocked and AMI BPA is block-new-sharing.
    • EBS default encryption is false and SSM sharing is Enable.
    • There are no analyzers, and every Inspector resource type is DISABLED.
    • AWSServiceRoleForConfig does not exist, and the recorder uses javabin-config-role.
  • The org boundary on javabin-ci-infra does not deny any action used here. It denies Stop/Delete on the Config recorder, not PutConfigurationRecorder. iam:CreateServiceLinkedRole is not iam:CreateRole, so the CreateRole deny does not apply.
  • Expected plan:
    • About 10 creates.
    • Updates to the recorder and the bucket policy.
    • 2 intentional destroys: aws_iam_role.config_role and its policy attachment.

Cost

  • Inspector: about $5-10/month. ECR is billed per image pushed or rescanned, and Lambda is billed about $0.30 per function per month.
  • Global resource types add a few IAM configuration items, which costs little.
  • Everything else is free.

Follow-ups

  • After apply, remove the manual SSM.7 suppression in Security Hub. The control should pass by then.
  • Account S3 block-public-access has to wait until the legacy website buckets are dealt with (see the audit report).

🤖 Generated with Claude Code

@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 24, 2026 13:38
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 10 to add, 2 to change, 2 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # module.monitoring.aws_accessanalyzer_analyzer.external_access will be created
  + resource "aws_accessanalyzer_analyzer" "external_access" {
      + analyzer_name = "javabin-external-access"
      + arn           = (known after apply)
      + id            = (known after apply)
      + tags_all      = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + type          = "ACCOUNT"
    }

  # module.monitoring.aws_cloudwatch_event_rule.security_tampering will be created
  + resource "aws_cloudwatch_event_rule" "security_tampering" {
      + arn            = (known after apply)
      + description    = "Detect changes that disable logging, detection or account guardrails"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + eventName   = [
                      + "StopLogging",
                      + "DeleteTrail",
                      + "UpdateTrail",
                      + "PutEventSelectors",
                      + "StopConfigurationRecorder",
                      + "DeleteConfigurationRecorder",
                      + "DeleteDeliveryChannel",
                      + "DisableKey",
                      + "ScheduleKeyDeletion",
                      + "DeleteDetector",
                      + "UpdateDetector",
                      + "DisassociateFromMasterAccount",
                      + "DisableSecurityHub",
                      + "BatchDisableStandards",
                      + "DeleteAnalyzer",
                      + "DisableSnapshotBlockPublicAccess",
                      + "DisableEbsEncryptionByDefault",
                      + "DisableImageBlockPublicAccess",
                      + "UpdateServiceSetting",
                      + "Disable",
                    ]
                  + 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",
                    ]
                }
              + detail-type = [
                  + "AWS API Call via CloudTrail",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-security-tampering"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_cloudwatch_event_target.security_tampering_sns will be created
  + resource "aws_cloudwatch_event_target" "security_tampering_sns" {
      + arn            = "arn:aws:sns:eu-central-1:553637109631:javabin-security"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-security-tampering"
      + target_id      = "send-to-security-sns"
    }

  # module.monitoring.aws_config_configuration_recorder.main will be updated in-place
  ~ resource "aws_config_configuration_recorder" "main" {
        id       = "javabin-recorder"
        name     = "javabin-recorder"
      ~ role_arn = "arn:aws:iam::553637109631:role/javabin-config-role" -> (known after apply)

      ~ recording_group {
          ~ include_global_resource_types = false -> true
            # (2 unchanged attributes hidden)

            # (2 unchanged blocks hidden)
        }

        # (1 unchanged block hidden)
    }

  # module.monitoring.aws_ebs_encryption_by_default.main will be created
  + resource "aws_ebs_encryption_by_default" "main" {
      + enabled = true
      + id      = (known after apply)
    }

  # module.monitoring.aws_ebs_snapshot_block_public_access.main will be created
  + resource "aws_ebs_snapshot_block_public_access" "main" {
      + id    = (known after apply)
      + state = "block-all-sharing"
    }

  # module.monitoring.aws_ec2_image_block_public_access.main will be created
  + resource "aws_ec2_image_block_public_access" "main" {
      + id    = (known after apply)
      + state = "block-new-sharing"
    }

  # module.monitoring.aws_iam_role.config_role will be destroyed
  # (because aws_iam_role.config_role is not in configuration)
  - resource "aws_iam_role" "config_role" {
      - arn                   = "arn:aws:iam::553637109631:role/javabin-config-role" -> null
      - assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRole"
                      - Effect    = "Allow"
                      - Principal = {
                          - Service = "config.amazonaws.com"
                        }
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - create_date           = "2026-03-07T16:28:55Z" -> null
      - force_detach_policies = false -> null
      - id                    = "javabin-config-role" -> null
      - managed_policy_arns   = [
          - "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole",
        ] -> null
      - max_session_duration  = 3600 -> null
      - name                  = "javabin-config-role" -> null
      - path                  = "/" -> null
      - tags                  = {
          - "Name" = "javabin-config-role"
        } -> null
      - tags_all              = {
          - "Name"        = "javabin-config-role"
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
      - unique_id             = "AROAYBZ2X357VVJESYD6G" -> null
    }

  # module.monitoring.aws_iam_role_policy_attachment.config_role will be destroyed
  # (because aws_iam_role_policy_attachment.config_role is not in configuration)
  - resource "aws_iam_role_policy_attachment" "config_role" {
      - id         = "javabin-config-role-20260307162900971300000009" -> null
      - policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole" -> null
      - role       = "javabin-config-role" -> null
    }

  # module.monitoring.aws_iam_service_linked_role.config will be created
  + resource "aws_iam_service_linked_role" "config" {
      + arn              = (known after apply)
      + aws_service_name = "config.amazonaws.com"
      + create_date      = (known after apply)
      + id               = (known after apply)
      + name             = (known after apply)
      + path             = (known after apply)
      + tags_all         = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id        = (known after apply)
    }

  # module.monitoring.aws_inspector2_enabler.main will be created
  + resource "aws_inspector2_enabler" "main" {
      + account_ids    = [
          + "553637109631",
        ]
      + id             = (known after apply)
      + resource_types = [
          + "ECR",
          + "LAMBDA",
        ]
    }

  # module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "config_logs" {
      + bucket                                 = "javabin-config-553637109631"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-after-2-years"
          + status = "Enabled"

          + expiration {
              + days                         = 730
              + expired_object_delete_marker = false
            }

          + filter {
            }
        }
    }

  # module.monitoring.aws_s3_bucket_policy.config_logs will be updated in-place
  ~ resource "aws_s3_bucket_policy" "config_logs" {
        id     = "javabin-config-553637109631"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      + Condition = {
                          + StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                            }
                        }
                        # (5 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = "s3:PutObject" -> "s3:ListBucket"
                      ~ Condition = {
                          ~ StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                              - "s3:x-amz-acl"      = "bucket-owner-full-control"
                            }
                        }
                      ~ Resource  = "arn:aws:s3:::javabin-config-553637109631/*" -> "arn:aws:s3:::javabin-config-553637109631"
                      ~ Sid       = "AWSConfigBucketDelivery" -> "AWSConfigBucketExistenceCheck"
                        # (2 unchanged attributes hidden)
                    },
                  + {
                      + Action    = "s3:PutObject"
                      + Condition = {
                          + StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                              + "s3:x-amz-acl"      = "bucket-owner-full-control"
                            }
                        }
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "config.amazonaws.com"
                        }
                      + Resource  = "arn:aws:s3:::javabin-config-553637109631/AWSLogs/553637109631/Config/*"
                      + Sid       = "AWSConfigBucketDelivery"
                    },
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-config-553637109631",
                          + "arn:aws:s3:::javabin-config-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.monitoring.aws_ssm_service_setting.document_public_sharing will be created
  + resource "aws_ssm_service_setting" "document_public_sharing" {
      + arn           = (known after apply)
      + id            = (known after apply)
      + setting_id    = "arn:aws:ssm:eu-central-1:553637109631:servicesetting/ssm/documents/console/public-sharing-permission"
      + setting_value = "Disable"
      + status        = (known after apply)
    }

Plan: 10 to add, 2 to change, 2 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan adds account-level security guardrails (Access Analyzer, EBS/AMI/snapshot public-access blocks, Inspector, SSM public sharing disabled, a tampering-detection EventBridge rule) while replacing the AWS Config IAM role with a service-linked role and changing the Config S3 bucket policy; the Config role and its attachment are destroyed and the Config recorder's role_arn is changing, so AWS Config delivery must be verified before apply.

  • 💥 [destruction] aws_iam_role.config_role and its policy attachment are destroyed because they are removed from configuration, while aws_iam_service_linked_role.config is created as the replacement. The recorder's role_arn changes from javabin-config-role to (known after apply). Confirm the recorder will point at a working role after apply, otherwise Config recording can silently stop. The recorder itself is updated in place (include_global_resource_types false -> true), which records IAM global resources and adds cost.
  • 🔒 [security] The S3 bucket policy for the Config bucket changes: the AWSConfigBucketDelivery statement is split. The ListBucket existence check is now on the bucket ARN, and a new PutObject statement is scoped to the AWSLogs//Config/ prefix with aws:SourceAccount and s3:x-amz-acl conditions. A new DenyInsecureTransport statement is added, which is a tightening. The changes look intentional but should be checked against the AWS Config delivery requirements.
  • 🔒 [security] Positive hardening: EBS encryption by default enabled, EBS snapshot block public access set to block-all-sharing, AMI block public access set to block-new-sharing, SSM document public sharing disabled, and Access Analyzer (ACCOUNT scope) created. The EventBridge rule alerts on CloudTrail, Config, KMS, GuardDuty, SecurityHub, and similar tampering events, sent to the javabin-security SNS topic.
  • 💰 [cost] Inspector2 enabler for ECR and LAMBDA, AWS Config recording of global resource types, and Access Analyzer add ongoing usage-based charges. None of these are large fixed costs, but they are billable.
  • ✅ [routine] The S3 lifecycle rule expires objects in the Config bucket after 730 days. This is a retention decision that should be confirmed, since it deletes Config history after two years. The plan is otherwise additive, with 10 creates, 2 in-place updates and 2 destroys. It does not touch databases or security groups.

@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 10 to add, 2 to change, 2 to destroy.

Plan output
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # module.monitoring.aws_accessanalyzer_analyzer.external_access will be created
  + resource "aws_accessanalyzer_analyzer" "external_access" {
      + analyzer_name = "javabin-external-access"
      + arn           = (known after apply)
      + id            = (known after apply)
      + tags_all      = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + type          = "ACCOUNT"
    }

  # module.monitoring.aws_cloudwatch_event_rule.security_tampering will be created
  + resource "aws_cloudwatch_event_rule" "security_tampering" {
      + arn            = (known after apply)
      + description    = "Detect changes that disable logging, detection or account guardrails"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + eventName   = [
                      + "StopLogging",
                      + "DeleteTrail",
                      + "UpdateTrail",
                      + "PutEventSelectors",
                      + "StopConfigurationRecorder",
                      + "DeleteConfigurationRecorder",
                      + "DeleteDeliveryChannel",
                      + "DisableKey",
                      + "ScheduleKeyDeletion",
                      + "DeleteDetector",
                      + "UpdateDetector",
                      + "DisassociateFromMasterAccount",
                      + "DisableSecurityHub",
                      + "BatchDisableStandards",
                      + "DeleteAnalyzer",
                      + "DisableSnapshotBlockPublicAccess",
                      + "DisableEbsEncryptionByDefault",
                      + "DisableImageBlockPublicAccess",
                      + "UpdateServiceSetting",
                      + "Disable",
                      + "CreateFilter",
                      + "UpdateFilter",
                      + "CreateAutomationRules",
                      + "BatchUpdateAutomationRules",
                      + "UpdateStandardsControl",
                      + "BatchUpdateStandardsControlAssociations",
                      + "PutConfigurationRecorder",
                      + "EnableSnapshotBlockPublicAccess",
                    ]
                  + 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",
                    ]
                }
              + detail-type = [
                  + "AWS API Call via CloudTrail",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-security-tampering"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_cloudwatch_event_target.security_tampering_sns will be created
  + resource "aws_cloudwatch_event_target" "security_tampering_sns" {
      + arn            = "arn:aws:sns:eu-central-1:553637109631:javabin-security"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-security-tampering"
      + target_id      = "send-to-security-sns"
    }

  # module.monitoring.aws_config_configuration_recorder.main will be updated in-place
  ~ resource "aws_config_configuration_recorder" "main" {
        id       = "javabin-recorder"
        name     = "javabin-recorder"
      ~ role_arn = "arn:aws:iam::553637109631:role/javabin-config-role" -> (known after apply)

      ~ recording_group {
          ~ include_global_resource_types = false -> true
            # (2 unchanged attributes hidden)

            # (2 unchanged blocks hidden)
        }

        # (1 unchanged block hidden)
    }

  # module.monitoring.aws_ebs_encryption_by_default.main will be created
  + resource "aws_ebs_encryption_by_default" "main" {
      + enabled = true
      + id      = (known after apply)
    }

  # module.monitoring.aws_ebs_snapshot_block_public_access.main will be created
  + resource "aws_ebs_snapshot_block_public_access" "main" {
      + id    = (known after apply)
      + state = "block-all-sharing"
    }

  # module.monitoring.aws_ec2_image_block_public_access.main will be created
  + resource "aws_ec2_image_block_public_access" "main" {
      + id    = (known after apply)
      + state = "block-new-sharing"
    }

  # module.monitoring.aws_iam_role.config_role will be destroyed
  # (because aws_iam_role.config_role is not in configuration)
  - resource "aws_iam_role" "config_role" {
      - arn                   = "arn:aws:iam::553637109631:role/javabin-config-role" -> null
      - assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRole"
                      - Effect    = "Allow"
                      - Principal = {
                          - Service = "config.amazonaws.com"
                        }
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - create_date           = "2026-03-07T16:28:55Z" -> null
      - force_detach_policies = false -> null
      - id                    = "javabin-config-role" -> null
      - managed_policy_arns   = [
          - "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole",
        ] -> null
      - max_session_duration  = 3600 -> null
      - name                  = "javabin-config-role" -> null
      - path                  = "/" -> null
      - tags                  = {
          - "Name" = "javabin-config-role"
        } -> null
      - tags_all              = {
          - "Name"        = "javabin-config-role"
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
      - unique_id             = "AROAYBZ2X357VVJESYD6G" -> null
    }

  # module.monitoring.aws_iam_role_policy_attachment.config_role will be destroyed
  # (because aws_iam_role_policy_attachment.config_role is not in configuration)
  - resource "aws_iam_role_policy_attachment" "config_role" {
      - id         = "javabin-config-role-20260307162900971300000009" -> null
      - policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole" -> null
      - role       = "javabin-config-role" -> null
    }

  # module.monitoring.aws_iam_service_linked_role.config will be created
  + resource "aws_iam_service_linked_role" "config" {
      + arn              = (known after apply)
      + aws_service_name = "config.amazonaws.com"
      + create_date      = (known after apply)
      + id               = (known after apply)
      + name             = (known after apply)
      + path             = (known after apply)
      + tags_all         = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id        = (known after apply)
    }

  # module.monitoring.aws_inspector2_enabler.main will be created
  + resource "aws_inspector2_enabler" "main" {
      + account_ids    = [
          + "553637109631",
        ]
      + id             = (known after apply)
      + resource_types = [
          + "ECR",
          + "LAMBDA",
        ]
    }

  # module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "config_logs" {
      + bucket                                 = "javabin-config-553637109631"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-after-2-years"
          + status = "Enabled"

          + expiration {
              + days                         = 730
              + expired_object_delete_marker = false
            }

          + filter {
            }
        }
    }

  # module.monitoring.aws_s3_bucket_policy.config_logs will be updated in-place
  ~ resource "aws_s3_bucket_policy" "config_logs" {
        id     = "javabin-config-553637109631"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      + Condition = {
                          + StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                            }
                        }
                        # (5 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = "s3:PutObject" -> "s3:ListBucket"
                      ~ Condition = {
                          ~ StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                              - "s3:x-amz-acl"      = "bucket-owner-full-control"
                            }
                        }
                      ~ Resource  = "arn:aws:s3:::javabin-config-553637109631/*" -> "arn:aws:s3:::javabin-config-553637109631"
                      ~ Sid       = "AWSConfigBucketDelivery" -> "AWSConfigBucketExistenceCheck"
                        # (2 unchanged attributes hidden)
                    },
                  + {
                      + Action    = "s3:PutObject"
                      + Condition = {
                          + StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                              + "s3:x-amz-acl"      = "bucket-owner-full-control"
                            }
                        }
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "config.amazonaws.com"
                        }
                      + Resource  = "arn:aws:s3:::javabin-config-553637109631/AWSLogs/553637109631/Config/*"
                      + Sid       = "AWSConfigBucketDelivery"
                    },
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-config-553637109631",
                          + "arn:aws:s3:::javabin-config-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.monitoring.aws_ssm_service_setting.document_public_sharing will be created
  + resource "aws_ssm_service_setting" "document_public_sharing" {
      + arn           = (known after apply)
      + id            = (known after apply)
      + setting_id    = "arn:aws:ssm:eu-central-1:553637109631:servicesetting/ssm/documents/console/public-sharing-permission"
      + setting_value = "Disable"
      + status        = (known after apply)
    }

Plan: 10 to add, 2 to change, 2 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan adds security-monitoring and guardrail resources (Access Analyzer, EventBridge tampering alerts, EBS/AMI/snapshot public-access blocks, Inspector, SSM public sharing disabled) while modifying AWS Config recorder and S3 delivery policy, and destroying and replacing the Config IAM role and its policy attachment.
[{"category":"destruction","detail":"aws_iam_role.config_role and its policy attachment are destroyed because they are no longer in configuration, while aws_iam_service_linked_role.config is created in their place. The Config recorder's role_arn changes to (known after apply). Confirm the recorder will point at the service-linked role before apply, otherwise configuration recording may break or fail to deliver."},{"category":"security","detail":"The S3 config_logs bucket policy changes: the AWSConfigBucketDelivery statement's action goes from s3:PutObject to s3:ListBucket, and a new PutObject statement is added with a narrower resource path and the x-amz-acl condition. Config delivery permissions are being rearranged, so verify delivery still works. A DenyInsecureTransport statement is added, which is a security improvement."},{"category":"security","detail":"include_global_resource_types changes from false to true on the Config recorder. This is a valid setting, but it records IAM global resources and increases recorded configuration volume, so it is a deliberate scope change to confirm."},{"category":"security","detail":"New guardrails are positive: EBS encryption by default, EBS snapshot block-all-sharing, AMI block-new-sharing, SSM document public sharing disabled, and an EventBridge rule for logging/guardrail tampering routed to SNS. The EventBridge pattern includes broad eventNames such as 'Disable' and 'Create/UpdateFilter', so expect some alert noise."},{"category":"cost","detail":"Inspector2 enabler (ECR and Lambda scanning), Config lifecycle expiry at 730 days, and the Access Analyzer are billable or usage-based. Inspector and Config recording costs scale with resource and object counts, but there are no NAT gateways, RDS, or load balancers in this plan."},{"category":"routine","detail":"Plan totals: 10 to add, 2 to change, 2 to destroy. No database, security group, or public-access ingress changes are present. Risk is MEDIUM because of the IAM role destruction and Config delivery changes, not because of a dangerous or irreversible change."}]

@Alexanderamiri

Copy link
Copy Markdown
Member Author

Review fixes: the security-tampering rule now also matches GuardDuty CreateFilter/UpdateFilter, Security Hub CreateAutomationRules/BatchUpdateAutomationRules/UpdateStandardsControl/BatchUpdateStandardsControlAssociations, Config PutConfigurationRecorder and EnableSnapshotBlockPublicAccess. Docs note that UpdateDetector, UpdateServiceSetting and these events also fire on benign changes (including platform applies), and that enabling Inspector for ECR switches registry scanning from BASIC to ENHANCED, so scan_on_push no longer has an effect; expect a first burst of Inspector findings from the legacy Lambdas.

Alexanderamiri and others added 2 commits October 26, 2026 14:41
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) <noreply@anthropic.com>
…der 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) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 10 to add, 2 to change, 2 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # module.monitoring.aws_accessanalyzer_analyzer.external_access will be created
  + resource "aws_accessanalyzer_analyzer" "external_access" {
      + analyzer_name = "javabin-external-access"
      + arn           = (known after apply)
      + id            = (known after apply)
      + tags_all      = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + type          = "ACCOUNT"
    }

  # module.monitoring.aws_cloudwatch_event_rule.security_tampering will be created
  + resource "aws_cloudwatch_event_rule" "security_tampering" {
      + arn            = (known after apply)
      + description    = "Detect changes that disable logging, detection or account guardrails"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + eventName   = [
                      + "StopLogging",
                      + "DeleteTrail",
                      + "UpdateTrail",
                      + "PutEventSelectors",
                      + "StopConfigurationRecorder",
                      + "DeleteConfigurationRecorder",
                      + "DeleteDeliveryChannel",
                      + "DisableKey",
                      + "ScheduleKeyDeletion",
                      + "DeleteDetector",
                      + "UpdateDetector",
                      + "DisassociateFromMasterAccount",
                      + "DisableSecurityHub",
                      + "BatchDisableStandards",
                      + "DeleteAnalyzer",
                      + "DisableSnapshotBlockPublicAccess",
                      + "DisableEbsEncryptionByDefault",
                      + "DisableImageBlockPublicAccess",
                      + "UpdateServiceSetting",
                      + "Disable",
                      + "CreateFilter",
                      + "UpdateFilter",
                      + "CreateAutomationRules",
                      + "BatchUpdateAutomationRules",
                      + "UpdateStandardsControl",
                      + "BatchUpdateStandardsControlAssociations",
                      + "PutConfigurationRecorder",
                      + "EnableSnapshotBlockPublicAccess",
                    ]
                  + 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",
                    ]
                }
              + detail-type = [
                  + "AWS API Call via CloudTrail",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-security-tampering"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_cloudwatch_event_target.security_tampering_sns will be created
  + resource "aws_cloudwatch_event_target" "security_tampering_sns" {
      + arn            = "arn:aws:sns:eu-central-1:553637109631:javabin-security"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-security-tampering"
      + target_id      = "send-to-security-sns"
    }

  # module.monitoring.aws_config_configuration_recorder.main will be updated in-place
  ~ resource "aws_config_configuration_recorder" "main" {
        id       = "javabin-recorder"
        name     = "javabin-recorder"
      ~ role_arn = "arn:aws:iam::553637109631:role/javabin-config-role" -> (known after apply)

      ~ recording_group {
          ~ include_global_resource_types = false -> true
            # (2 unchanged attributes hidden)

            # (2 unchanged blocks hidden)
        }

        # (1 unchanged block hidden)
    }

  # module.monitoring.aws_ebs_encryption_by_default.main will be created
  + resource "aws_ebs_encryption_by_default" "main" {
      + enabled = true
      + id      = (known after apply)
    }

  # module.monitoring.aws_ebs_snapshot_block_public_access.main will be created
  + resource "aws_ebs_snapshot_block_public_access" "main" {
      + id    = (known after apply)
      + state = "block-all-sharing"
    }

  # module.monitoring.aws_ec2_image_block_public_access.main will be created
  + resource "aws_ec2_image_block_public_access" "main" {
      + id    = (known after apply)
      + state = "block-new-sharing"
    }

  # module.monitoring.aws_iam_role.config_role will be destroyed
  # (because aws_iam_role.config_role is not in configuration)
  - resource "aws_iam_role" "config_role" {
      - arn                   = "arn:aws:iam::553637109631:role/javabin-config-role" -> null
      - assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRole"
                      - Effect    = "Allow"
                      - Principal = {
                          - Service = "config.amazonaws.com"
                        }
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> null
      - create_date           = "2026-03-07T16:28:55Z" -> null
      - force_detach_policies = false -> null
      - id                    = "javabin-config-role" -> null
      - managed_policy_arns   = [
          - "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole",
        ] -> null
      - max_session_duration  = 3600 -> null
      - name                  = "javabin-config-role" -> null
      - path                  = "/" -> null
      - tags                  = {
          - "Name" = "javabin-config-role"
        } -> null
      - tags_all              = {
          - "Name"        = "javabin-config-role"
          - "environment" = "production"
          - "managed-by"  = "terraform"
          - "repo"        = "javaBin/platform"
          - "service"     = "platform"
          - "team"        = "platform"
        } -> null
      - unique_id             = "AROAYBZ2X357VVJESYD6G" -> null
    }

  # module.monitoring.aws_iam_role_policy_attachment.config_role will be destroyed
  # (because aws_iam_role_policy_attachment.config_role is not in configuration)
  - resource "aws_iam_role_policy_attachment" "config_role" {
      - id         = "javabin-config-role-20260307162900971300000009" -> null
      - policy_arn = "arn:aws:iam::aws:policy/service-role/AWS_ConfigRole" -> null
      - role       = "javabin-config-role" -> null
    }

  # module.monitoring.aws_iam_service_linked_role.config will be created
  + resource "aws_iam_service_linked_role" "config" {
      + arn              = (known after apply)
      + aws_service_name = "config.amazonaws.com"
      + create_date      = (known after apply)
      + id               = (known after apply)
      + name             = (known after apply)
      + path             = (known after apply)
      + tags_all         = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + unique_id        = (known after apply)
    }

  # module.monitoring.aws_inspector2_enabler.main will be created
  + resource "aws_inspector2_enabler" "main" {
      + account_ids    = [
          + "553637109631",
        ]
      + id             = (known after apply)
      + resource_types = [
          + "ECR",
          + "LAMBDA",
        ]
    }

  # module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs will be created
  + resource "aws_s3_bucket_lifecycle_configuration" "config_logs" {
      + bucket                                 = "javabin-config-553637109631"
      + expected_bucket_owner                  = (known after apply)
      + id                                     = (known after apply)
      + transition_default_minimum_object_size = "all_storage_classes_128K"

      + rule {
          + id     = "expire-after-2-years"
          + status = "Enabled"

          + expiration {
              + days                         = 730
              + expired_object_delete_marker = false
            }

          + filter {
            }
        }
    }

  # module.monitoring.aws_s3_bucket_policy.config_logs will be updated in-place
  ~ resource "aws_s3_bucket_policy" "config_logs" {
        id     = "javabin-config-553637109631"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      + Condition = {
                          + StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                            }
                        }
                        # (5 unchanged attributes hidden)
                    },
                  ~ {
                      ~ Action    = "s3:PutObject" -> "s3:ListBucket"
                      ~ Condition = {
                          ~ StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                              - "s3:x-amz-acl"      = "bucket-owner-full-control"
                            }
                        }
                      ~ Resource  = "arn:aws:s3:::javabin-config-553637109631/*" -> "arn:aws:s3:::javabin-config-553637109631"
                      ~ Sid       = "AWSConfigBucketDelivery" -> "AWSConfigBucketExistenceCheck"
                        # (2 unchanged attributes hidden)
                    },
                  + {
                      + Action    = "s3:PutObject"
                      + Condition = {
                          + StringEquals = {
                              + "aws:SourceAccount" = "553637109631"
                              + "s3:x-amz-acl"      = "bucket-owner-full-control"
                            }
                        }
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "config.amazonaws.com"
                        }
                      + Resource  = "arn:aws:s3:::javabin-config-553637109631/AWSLogs/553637109631/Config/*"
                      + Sid       = "AWSConfigBucketDelivery"
                    },
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-config-553637109631",
                          + "arn:aws:s3:::javabin-config-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.monitoring.aws_ssm_service_setting.document_public_sharing will be created
  + resource "aws_ssm_service_setting" "document_public_sharing" {
      + arn           = (known after apply)
      + id            = (known after apply)
      + setting_id    = "arn:aws:ssm:eu-central-1:553637109631:servicesetting/ssm/documents/console/public-sharing-permission"
      + setting_value = "Disable"
      + status        = (known after apply)
    }

Plan: 10 to add, 2 to change, 2 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟡 MEDIUM

Plan adds security-monitoring and hardening resources (EventBridge tampering alerts, Access Analyzer, Inspector, EBS/AMI/SSM public-sharing blocks) while replacing the AWS Config IAM role with a service-linked role and changing the Config S3 bucket policy; it contains no database or security-group destruction, but the Config role and bucket policy changes need verification before apply.

  • 💥 [destruction] Destroys the custom IAM role javabin-config-role and its AWS_ConfigRole policy attachment, while the configuration recorder's role_arn changes to a (known after apply) value and a new aws_iam_service_linked_role for config.amazonaws.com is created. Confirm the recorder ends up using the service-linked role; if it still references the destroyed role, AWS Config delivery could break. Destroying the role also removes any other dependents not visible in this plan.
  • 🔒 [security] The Config S3 bucket policy is rewritten: the AWSConfigBucketDelivery statement now allows s3:PutObject only on AWSLogs/553637109631/Config/* with the x-amz-acl condition, and a new AWSConfigBucketExistenceCheck (s3:ListBucket) statement is added. A new DenyInsecureTransport statement (Effect Deny, aws:SecureTransport=false) is added and is a positive control. Verify that the Config delivery path still matches the prefix used by the recorder/delivery channel, or log delivery may silently fail.
  • 🔒 [security] Positive hardening: EBS encryption by default enabled, EBS snapshot public access set to block-all-sharing, AMI sharing set to block-new-sharing, SSM document public sharing disabled, and Access Analyzer (ACCOUNT scope) created. The CloudWatch rule alerts on StopLogging, DeleteTrail, DisableKey, and similar events and routes to the security SNS topic.
  • 🔒 [security] Config recorder include_global_resource_types changes from false to true, so IAM and other global resources will be recorded. This increases recording coverage and cost but is not a security risk. The EventBridge rule includes broad eventName entries such as Disable and UpdateServiceSetting, which may produce noisy alerts.
  • 💰 [cost] Inspector2 enabler for ECR and LAMBDA, AWS Config recording of all global resource types, and Access Analyzer add ongoing billable usage. EBS encryption by default has no direct cost. The S3 lifecycle rule expires Config objects after 730 days, which bounds storage growth.
  • ✅ [routine] Plan summary is 10 to add, 2 to change, 2 to destroy. The S3 lifecycle configuration and SSM/EBS/AMI settings are account-level or additive. Nothing here opens security groups or exposes resources publicly, and no database or NAT/load balancer is created or destroyed. Overall risk is MEDIUM because of the IAM role destruction and the Config bucket policy rewrite, not because of any dangerous change.

@Alexanderamiri
Alexanderamiri merged commit ae85af5 into main Oct 26, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the feat/sec-guardrails branch October 26, 2026 13:42
Alexanderamiri added a commit that referenced this pull request Oct 26, 2026
… patterns (#164)

## Why

Security Hub and GuardDuty keep sending findings to `javabin-security`
that the audit verified as benign or as accepted risk. They drown out
the real signal:

- **GuardDuty `Impact:IAMUser/AnomalousBehavior` (sev 8):**
`javabin-team-provisioner` calls `cognito-idp:UpdateGroup` during its
scheduled group sync. CloudTrail confirms the caller is the Lambda.
- **GuardDuty `CredentialAccess:IAMUser/AnomalousBehavior` (sev 5) on
team SSO `ssm:GetParameter` is deliberately NOT filtered.** The team
permission set is `Action "*"` scoped by `StringEqualsIfExists
aws:ResourceTag/team`, so it does not limit reads to the team's path,
and this is the finding a stolen SSO session produces. One of the two
live findings (jzwatch) came from Hungary. A comment in
`detection-tuning.tf` and the docs records why.
- **CIS 1.2.0 was enabled by accident:** `aws_securityhub_account`
defaults `enable_default_standards` to true. Its 14 CloudWatch.*
metric-filter controls need CloudTrail to send logs to CloudWatch Logs,
which the platform does not do (cost).
- **FSBP controls that do not apply:** EKS (none), Macie, interface
endpoints, VPC block-public-access, and Inspector EC2/Lambda code
scanning.
- **Accepted findings:** `dbmoresleep` is public on purpose (RDS.2,
RDS.46). The platform ALB's port-80 listener only redirects to HTTPS
(ELB.18).

## What

New `monitoring/detection-tuning.tf`. Every entry states its reason in
code.

**GuardDuty ARCHIVE filters**
- `team-provisioner-cognito-sync`: matches that finding type, that
principal and `cognito-idp` only.
- Other anomaly types for this principal still alert.

**Security Hub controls disabled, each with `updated_reason`**
- CIS 1.2.0: CloudWatch.1-14 and CloudTrail.5.
- FSBP: CloudTrail.5, GuardDuty.7, Macie.1, EC2.10/55/56/57/58/60,
EC2.172, Inspector.1 and Inspector.3.

**Security Hub automation rules (set to SUPPRESSED with a note)**
- RDS.2 and RDS.46 on `dbmoresleep`. EC2.18 and EC2.19 on its security
group stay open on purpose, because narrowing that group is still an
open audit item.
- ELB.18 on the platform ALB's port-80 listener. The new ingress output
`http_listener_arn` is passed into monitoring for this.

**Docs:** a new "Detection tuning" section in `docs/platform-modules.md`
lists every suppression and its reason.

The RDS `MaliciousIPCaller` findings on dbmoresleep are not suppressed.
They are real signal.

## How verified

- `terraform fmt -recursive -check` and `terraform validate` pass.
Pytest passes (107).
- Every control ID exists in its standard: checked with
`list-security-control-definitions` for both enabled standards.
- There are no existing automation rules or GuardDuty filters to
conflict with.
- The live GuardDuty findings match the filter fields exactly:
- `resource.accessKeyDetails.userName` = `javabin-team-provisioner`,
service `cognito-idp.amazonaws.com`, api `UpdateGroup`.
- Expected plan:
- Creates: 1 GuardDuty filter, 27 control associations and 2 automation
rules.
- No changes to existing resources beyond the new output and variable
wiring.

## Review fixes

- Dropped the `team-sso-own-ssm-params` filter: its justification about
the permission set was false, and it would archive the
stolen-SSO-session signal regardless of source country.

## Follow-ups

- Merge after #163 and rebase: both edit the monitoring section of
`docs/platform-modules.md`.

- GuardDuty filters only apply to new findings. After apply, archive the
existing provisioner finding once in the console. Confirm the two
team-SSO GetParameter findings with their users (jzwatch: Hungary,
2026-10-24 10:19 UTC) before archiving them.
- Re-enable the matching controls if CloudTrail later goes to CloudWatch
Logs, or if VPC endpoints or Macie are adopted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Alexanderamiri added a commit that referenced this pull request Oct 26, 2026
… control

Budgets, Cost Anomaly Detection, Config recording ENI/VPC/Subnet daily
(not excluded, so Security Hub EC2.180 and Config.1 keep working), and
bounded retention for every platform Lambda log group (existing groups
adopted with import blocks).

Rebased onto #163 (which already expires Config history after two years,
so this PR's duplicate lifecycle rule is dropped) and #158 (adds a log
group for the new javabin-auth-claims-external trigger).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant