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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions docs/platform-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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). 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 |
Expand Down
117 changes: 117 additions & 0 deletions terraform/platform/monitoring/guardrails.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
################################################################################
# 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",
# 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",
]
}
})
}

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
}
79 changes: 54 additions & 25 deletions terraform/platform/monitoring/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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 = [
Expand All @@ -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)
################################################################################
Expand Down
Loading