From 782e3e0cce083ad85fb02b28add4cfc021550f15 Mon Sep 17 00:00:00 2001 From: 3olly Date: Sun, 6 Jul 2025 12:28:11 +0900 Subject: [PATCH 01/19] feat: cloudtrail>s3&eventbridge>lambda>opensearch --- management-team-account/.gitignore | 28 ++++ management-team-account/main.tf | 52 +++++++ management-team-account/outputs.tf | 4 + management-team-account/variables.tf | 31 +++++ operation-team-account/.gitignore | 28 ++++ operation-team-account/main.tf | 74 ++++++++++ .../iam/lambda_execution_policy.json.tpl | 37 +++++ .../modules/detection/lambda/index.py | 77 +++++++++++ .../modules/detection/lambda/requirements.txt | 3 + .../modules/detection/main.tf | 107 +++++++++++++++ .../modules/detection/outputs.tf | 14 ++ .../modules/detection/variables.tf | 45 ++++++ .../modules/network/main.tf | 39 ++++++ .../modules/network/outputs.tf | 16 +++ .../modules/network/variables.tf | 4 + .../modules/opensearch/main.tf | 49 +++++++ .../modules/opensearch/outputs.tf | 14 ++ .../modules/opensearch/policy.tf | 33 +++++ .../modules/opensearch/variables.tf | 8 ++ operation-team-account/modules/s3/main.tf | 129 ++++++++++++++++++ .../modules/s3/variables.tf | 4 + operation-team-account/outputs.tf | 24 ++++ operation-team-account/variables.tf | 75 ++++++++++ 23 files changed, 895 insertions(+) create mode 100644 management-team-account/.gitignore create mode 100644 management-team-account/main.tf create mode 100644 management-team-account/outputs.tf create mode 100644 management-team-account/variables.tf create mode 100644 operation-team-account/.gitignore create mode 100644 operation-team-account/main.tf create mode 100644 operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl create mode 100644 operation-team-account/modules/detection/lambda/index.py create mode 100644 operation-team-account/modules/detection/lambda/requirements.txt create mode 100644 operation-team-account/modules/detection/main.tf create mode 100644 operation-team-account/modules/detection/outputs.tf create mode 100644 operation-team-account/modules/detection/variables.tf create mode 100644 operation-team-account/modules/network/main.tf create mode 100644 operation-team-account/modules/network/outputs.tf create mode 100644 operation-team-account/modules/network/variables.tf create mode 100644 operation-team-account/modules/opensearch/main.tf create mode 100644 operation-team-account/modules/opensearch/outputs.tf create mode 100644 operation-team-account/modules/opensearch/policy.tf create mode 100644 operation-team-account/modules/opensearch/variables.tf create mode 100644 operation-team-account/modules/s3/main.tf create mode 100644 operation-team-account/modules/s3/variables.tf create mode 100644 operation-team-account/outputs.tf create mode 100644 operation-team-account/variables.tf diff --git a/management-team-account/.gitignore b/management-team-account/.gitignore new file mode 100644 index 00000000..6f6a3603 --- /dev/null +++ b/management-team-account/.gitignore @@ -0,0 +1,28 @@ +# Terraform 관련 파일 무시 +.terraform/ # terraform init 시 생성되는 폴더 +*.tfstate # 상태 파일 (리소스 실제 정보 포함) +*.tfstate.* # 상태 파일 백업 +terraform.tfstate +terraform.tfstate.* +*.tfvars +*.tfstate.backup +.terraform.lock.hcl +terraform.tfvars # 민감 정보 입력용 파일 +*.auto.tfvars +crash.log # Terraform 충돌 로그 +.terraform +override.tf +override.tf.json +# AWS CLI 자격 증명 +.aws/ # ~/.aws/credentials, config 등 +~/.aws/ +.aws +# 시스템 자동 생성 파일 (Windows/macOS) +.DS_Store # macOS +Thumbs.db # Windows +ehthumbs.db # Windows +*.log # 일반 로그 파일 +*.tmp # 임시 파일 + +# VSCode 설정 (선택사항) +.vscode/ diff --git a/management-team-account/main.tf b/management-team-account/main.tf new file mode 100644 index 00000000..063c1b4a --- /dev/null +++ b/management-team-account/main.tf @@ -0,0 +1,52 @@ +data "terraform_remote_state" "operation" { + backend = "s3" + config = { + bucket = "cloudfence-operation-s3" + key = "monitoring/terraform.tfstate" + region = "ap-northeast-2" + profile = "whs-sso-operation" + } +} + +terraform { + required_version = ">= 1.1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + backend "s3" { + bucket = "cloudfence-management-s3" + key = "cloudtrail/terraform.tfstate" + region = "ap-northeast-2" + encrypt = true + dynamodb_table = "tfstate-management-lock" + profile = "whs-sso-management" + } +} + +provider "aws" { + region = var.aws_region + profile = "whs-sso-management" +} + +data "aws_caller_identity" "current" {} + +resource "aws_cloudtrail" "org" { + name = var.org_trail_name + is_organization_trail = true + is_multi_region_trail = true + include_global_service_events = true + enable_log_file_validation = true + enable_logging = true + + s3_bucket_name = var.destination_s3_bucket_name + kms_key_id = var.s3_kms_key_arn + + tags = { + Name = var.org_trail_name + Environment = "prod" + Owner = "security-team" + } +} \ No newline at end of file diff --git a/management-team-account/outputs.tf b/management-team-account/outputs.tf new file mode 100644 index 00000000..d8f71786 --- /dev/null +++ b/management-team-account/outputs.tf @@ -0,0 +1,4 @@ +output "management_account_id" { + description = "Account ID of the management account" + value = data.aws_caller_identity.current.account_id +} \ No newline at end of file diff --git a/management-team-account/variables.tf b/management-team-account/variables.tf new file mode 100644 index 00000000..00465e80 --- /dev/null +++ b/management-team-account/variables.tf @@ -0,0 +1,31 @@ +variable "aws_region" { + description = "AWS Region" + type = string + default = "ap-northeast-2" +} + +variable "org_trail_name" { + description = "Name of the organization trail" + type = string + default = "org-cloudtrail" +} + +variable "destination_s3_bucket_name" { + description = "S3 bucket name in operation account" + type = string +} + +variable "destination_s3_bucket_arn" { + description = "ARN of the S3 bucket in operation account" + type = string +} + +variable "s3_kms_key_arn" { + description = "ARN of the KMS key used to encrypt the logs (in operation account)" + type = string +} + +variable "sso_role_name" { + description = "The name of the AWS SSO role to attach policy to" + type = string +} \ No newline at end of file diff --git a/operation-team-account/.gitignore b/operation-team-account/.gitignore new file mode 100644 index 00000000..6f6a3603 --- /dev/null +++ b/operation-team-account/.gitignore @@ -0,0 +1,28 @@ +# Terraform 관련 파일 무시 +.terraform/ # terraform init 시 생성되는 폴더 +*.tfstate # 상태 파일 (리소스 실제 정보 포함) +*.tfstate.* # 상태 파일 백업 +terraform.tfstate +terraform.tfstate.* +*.tfvars +*.tfstate.backup +.terraform.lock.hcl +terraform.tfvars # 민감 정보 입력용 파일 +*.auto.tfvars +crash.log # Terraform 충돌 로그 +.terraform +override.tf +override.tf.json +# AWS CLI 자격 증명 +.aws/ # ~/.aws/credentials, config 등 +~/.aws/ +.aws +# 시스템 자동 생성 파일 (Windows/macOS) +.DS_Store # macOS +Thumbs.db # Windows +ehthumbs.db # Windows +*.log # 일반 로그 파일 +*.tmp # 임시 파일 + +# VSCode 설정 (선택사항) +.vscode/ diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf new file mode 100644 index 00000000..85e010ca --- /dev/null +++ b/operation-team-account/main.tf @@ -0,0 +1,74 @@ +data "aws_caller_identity" "current" {} + +terraform { + required_version = ">= 1.1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + bucket = "cloudfence-operation-s3" + key = "monitoring/terraform.tfstate" + region = "ap-northeast-2" + encrypt = true + dynamodb_table = "tfstate-operation-lock" + profile = "whs-sso-operation" + } +} + +provider "aws" { + region = var.aws_region + profile = "whs-sso-operation" +} + +provider "aws" { + alias = "management" + region = var.aws_region + profile = "whs-sso-management" +} + +module "s3" { + source = "./modules/s3" + bucket_name = var.cloudtrail_bucket_name + cloudtrail_name = var.org_trail_name + aws_region = var.aws_region +} + +module "detection" { + source = "./modules/detection" + sns_topic_name = var.alerts_sns_topic + lambda_function_name = "eventbridge-processor" + opensearch_domain_endpoint = module.opensearch.endpoint + slack_webhook_url = var.slack_webhook_url + lambda_zip_path = "./lambda/lambda_package.zip" + kms_key_arn = module.s3.kms_key_arn + opensearch_domain_arn = module.opensearch.domain_arn + aws_region = var.aws_region +} + +output "opensearch_vpc_endpoint_id" { + value = aws_vpc_endpoint.opensearch.id + description = "The ID of the VPC endpoint for OpenSearch" +} + + +module "opensearch" { + source = "./modules/opensearch" + domain_name = var.opensearch_domain_name + engine_version = var.opensearch_engine_version + cluster_instance_type = var.opensearch_instance_type + cluster_instance_count = var.opensearch_instance_count + ebs_volume_size = var.opensearch_ebs_size + kms_key_arn = module.s3.kms_key_arn + lambda_role_arn = module.detection.lambda_function_role_arn + vpc_endpoint_id = module.network.opensearch_vpc_endpoint_id +} + +module "network" { + source = "./modules/network" + aws_region = var.aws_region +} + diff --git a/operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl b/operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl new file mode 100644 index 00000000..c93480e5 --- /dev/null +++ b/operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl @@ -0,0 +1,37 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "es:ESHttpPost", + "es:ESHttpPut", + "es:ESHttpGet" + ], + "Resource": "${opensearch_arn}/*" + }, + { + "Effect": "Allow", + "Action": [ + "kms:Decrypt" + ], + "Resource": "${kms_key_arn}" + }, + { + "Effect": "Allow", + "Action": [ + "secretsmanager:GetSecretValue" + ], + "Resource": "${slack_secret_arn}" + } + ] +} diff --git a/operation-team-account/modules/detection/lambda/index.py b/operation-team-account/modules/detection/lambda/index.py new file mode 100644 index 00000000..72fcda79 --- /dev/null +++ b/operation-team-account/modules/detection/lambda/index.py @@ -0,0 +1,77 @@ +import json +import os +import requests +from datetime import datetime +from opensearchpy import OpenSearch, RequestsHttpConnection +from requests_aws4auth import AWS4Auth +import boto3 + +region = os.environ['AWS_REGION'] +host = os.environ['OPENSEARCH_ENDPOINT'] +slack_webhook_url = os.environ['SLACK_WEBHOOK_URL'] + +credentials = boto3.Session().get_credentials() +awsauth = AWS4Auth( + credentials.access_key, + credentials.secret_key, + region, + 'es', + session_token=credentials.token +) + +client = OpenSearch( + hosts=[{'host': host, 'port': 443}], + http_auth=awsauth, + use_ssl=True, + verify_certs=True, + connection_class=RequestsHttpConnection +) + +def lambda_handler(event, context): + print("Received event:", json.dumps(event, indent=2)) + + detail = event.get("detail", {}) + detail_type = event.get("detail-type", "Unknown Event") + event_name = detail.get("eventName", "N/A") + user_identity = detail.get("userIdentity", {}).get("arn", "Unknown User") + source_ip = detail.get("sourceIPAddress", "Unknown IP") + time = event.get("time", datetime.utcnow().isoformat()) + + # Slack message formatting + slack_message = { + "text": ( + f"🚨 *Security Alert Detected* 🚨\n" + f"*Type:* {detail_type}\n" + f"*Event:* `{event_name}`\n" + f"*User:* `{user_identity}`\n" + f"*IP:* `{source_ip}`\n" + f"*Time:* `{time}`" + ) + } + + try: + requests.post( + slack_webhook_url, + data=json.dumps(slack_message), + headers={'Content-Type': 'application/json'} + ) + if response.status_code != 200: + raise Exception(f"Slack returned status code {response.status_code}, body: {response.text}") + except Exception as e: + print(f"Error posting to Slack: {e}") + + # Index to OpenSearch + try: + index_name = "security-events-" + datetime.utcnow().strftime("%Y-%m-%d") + response = client.index( + index=index_name, + body=event # Store full event for auditing + ) + print(f"Indexed to OpenSearch: {response}") + except Exception as e: + print(f"Error indexing to OpenSearch: {e}") + + return { + 'statusCode': 200, + 'body': 'Processed event successfully' + } \ No newline at end of file diff --git a/operation-team-account/modules/detection/lambda/requirements.txt b/operation-team-account/modules/detection/lambda/requirements.txt new file mode 100644 index 00000000..61ea07f9 --- /dev/null +++ b/operation-team-account/modules/detection/lambda/requirements.txt @@ -0,0 +1,3 @@ +requests +opensearch-py +requests-aws4auth \ No newline at end of file diff --git a/operation-team-account/modules/detection/main.tf b/operation-team-account/modules/detection/main.tf new file mode 100644 index 00000000..55014672 --- /dev/null +++ b/operation-team-account/modules/detection/main.tf @@ -0,0 +1,107 @@ +resource "aws_kms_key" "sns" { + description = "KMS key for SNS topic encryption" + deletion_window_in_days = 30 +} + +resource "aws_sns_topic" "alerts" { + name = var.sns_topic_name + kms_master_key_id = aws_kms_key.sns.arn + tags = { + Environment = "prod" + Team = "monitoring" + } +} + +resource "aws_sns_topic_policy" "alerts_policy" { + arn = aws_sns_topic.alerts.arn + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Sid = "AllowEventBridgePublish", + Effect = "Allow", + Principal = { Service = "events.amazonaws.com" }, + Action = "sns:Publish", + Resource = aws_sns_topic.alerts.arn, + Condition = { + ArnLike = { + "aws:SourceArn" = "arn:aws:events:*:*:rule/*" + } + } + } + ] + }) +} + +resource "aws_iam_role" "lambda_exec" { + name = "eventbridge_lambda_exec_role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + Service = "lambda.amazonaws.com" + }, + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role_policy" "lambda_policy" { + name = "eventbridge_lambda_policy" + role = aws_iam_role.lambda_exec.id + policy = templatefile("${path.module}/iam/lambda_execution_policy.json.tpl", { + opensearch_arn = var.opensearch_domain_arn + kms_key_arn = var.kms_key_arn + slack_secret_arn = "arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.current.account_id}:secret:slack-webhook-*" + }) +} + +resource "aws_lambda_function" "eventbridge_processor" { + function_name = var.lambda_function_name + role = aws_iam_role.lambda_exec.arn + handler = "index.lambda_handler" + runtime = "python3.11" + filename = var.lambda_zip_path + + environment { + variables = { + OPENSEARCH_ENDPOINT = var.opensearch_domain_endpoint + SLACK_WEBHOOK_URL = var.slack_webhook_url + } + } +} + +resource "aws_cloudwatch_event_rule" "mfa_deactivation" { + name = "detect-mfa-deactivation" + description = "Detect deactivation of MFA for IAM users" + event_pattern = jsonencode({ + source = ["aws.iam"], + "detail-type" = ["AWS API Call via CloudTrail"], + detail = { + eventName = ["DeactivateMFADevice", "DeleteVirtualMFADevice"] + } + }) +} + +resource "aws_cloudwatch_event_target" "mfa_to_lambda" { + rule = aws_cloudwatch_event_rule.mfa_deactivation.name + target_id = "MFAAlertLambda" + arn = aws_lambda_function.eventbridge_processor.arn +} + +resource "aws_lambda_permission" "allow_eventbridge" { + statement_id = "AllowExecutionFromEventBridge" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.eventbridge_processor.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.mfa_deactivation.arn +} + +variable "aws_region" { + type = string + description = "AWS region in use" +} \ No newline at end of file diff --git a/operation-team-account/modules/detection/outputs.tf b/operation-team-account/modules/detection/outputs.tf new file mode 100644 index 00000000..daa59273 --- /dev/null +++ b/operation-team-account/modules/detection/outputs.tf @@ -0,0 +1,14 @@ +output "lambda_function_arn" { + description = "ARN of the Lambda function" + value = aws_lambda_function.eventbridge_processor.arn +} + +output "lambda_function_role_arn" { + description = "IAM Role ARN of the Lambda function" + value = aws_iam_role.lambda_exec.arn +} + +output "sns_topic_arn" { + description = "ARN of the SNS topic" + value = aws_sns_topic.alerts.arn +} \ No newline at end of file diff --git a/operation-team-account/modules/detection/variables.tf b/operation-team-account/modules/detection/variables.tf new file mode 100644 index 00000000..80c3aa0d --- /dev/null +++ b/operation-team-account/modules/detection/variables.tf @@ -0,0 +1,45 @@ +variable "lambda_function_name" { + description = "Name of the Lambda function triggered by EventBridge" + type = string +} + +variable "sns_topic_name" { + description = "Name of the SNS topic for alerting" + type = string +} + +variable "opensearch_domain_endpoint" { + description = "OpenSearch domain endpoint" + type = string +} + +variable "slack_webhook_url" { + description = "Slack webhook URL" + type = string + sensitive = true +} + +variable "lambda_zip_path" { + description = "Path to Lambda deployment package zip" + type = string +} + +variable "opensearch_domain_arn" { + type = string + description = "ARN of the OpenSearch domain" +} + +variable "kms_key_arn" { + type = string + description = "KMS key ARN" +} + +variable "kms_key_arn" { + description = "KMS Key ARN used by Lambda" + type = string +} + +variable "opensearch_domain_arn" { + description = "OpenSearch domain ARN" + type = string +} \ No newline at end of file diff --git a/operation-team-account/modules/network/main.tf b/operation-team-account/modules/network/main.tf new file mode 100644 index 00000000..21caca2e --- /dev/null +++ b/operation-team-account/modules/network/main.tf @@ -0,0 +1,39 @@ +resource "aws_vpc_endpoint" "opensearch" { + vpc_id = aws_vpc.main.id + service_name = "com.amazonaws.${var.aws_region}.es" + vpc_endpoint_type = "Interface" + subnet_ids = [aws_subnet.a.id] + security_group_ids = [aws_security_group.lambda_to_opensearch.id] + private_dns_enabled = true + + tags = { + Name = "opensearch-vpc-endpoint" + } +} + +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + tags = { Name = "main-vpc" } +} + +resource "aws_subnet" "a" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + availability_zone = "${var.aws_region}a" + tags = { Name = "subnet-a" } +} + +resource "aws_security_group" "lambda_to_opensearch" { + name = "lambda-to-opensearch" + description = "Allow Lambda to access OpenSearch" + vpc_id = aws_vpc.main.id + + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { Name = "lambda-egress" } +} \ No newline at end of file diff --git a/operation-team-account/modules/network/outputs.tf b/operation-team-account/modules/network/outputs.tf new file mode 100644 index 00000000..c09bf802 --- /dev/null +++ b/operation-team-account/modules/network/outputs.tf @@ -0,0 +1,16 @@ +output "opensearch_vpc_endpoint_id" { + value = aws_vpc_endpoint.opensearch.id + description = "The ID of the VPC endpoint for OpenSearch" +} + +output "vpc_id" { + value = aws_vpc.main.id +} + +output "subnet_ids" { + value = [aws_subnet.a.id] +} + +output "security_group_ids" { + value = [aws_security_group.lambda_to_opensearch.id] +} \ No newline at end of file diff --git a/operation-team-account/modules/network/variables.tf b/operation-team-account/modules/network/variables.tf new file mode 100644 index 00000000..5ec9b61e --- /dev/null +++ b/operation-team-account/modules/network/variables.tf @@ -0,0 +1,4 @@ +variable "aws_region" { + type = string + description = "AWS Region" +} \ No newline at end of file diff --git a/operation-team-account/modules/opensearch/main.tf b/operation-team-account/modules/opensearch/main.tf new file mode 100644 index 00000000..1ab62f1a --- /dev/null +++ b/operation-team-account/modules/opensearch/main.tf @@ -0,0 +1,49 @@ +variable "domain_name" { type = string } +variable "engine_version" { type = string } +variable "cluster_instance_type" { type = string } +variable "cluster_instance_count"{ type = number } +variable "ebs_volume_size" { type = number } +variable "kms_key_arn" { type = string } + +resource "aws_opensearch_domain" "siem" { + domain_name = "siem-${var.domain_name}" + engine_version = var.engine_version + + cluster_config { + instance_type = var.cluster_instance_type + instance_count = var.cluster_instance_count + } + + ebs_options { + ebs_enabled = true + volume_size = var.ebs_volume_size + } + + encrypt_at_rest { + enabled = true + kms_key_id = var.kms_key_arn + } + + node_to_node_encryption { + enabled = true + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } + + tags = { + Name = "siem-opensearch" + Environment = "dev" + Owner = "monitoring-team" + } +} + +output "endpoint" { + value = aws_opensearch_domain.siem.endpoint +} + +output "domain_arn" { + value = aws_opensearch_domain.siem.arn +} \ No newline at end of file diff --git a/operation-team-account/modules/opensearch/outputs.tf b/operation-team-account/modules/opensearch/outputs.tf new file mode 100644 index 00000000..964430be --- /dev/null +++ b/operation-team-account/modules/opensearch/outputs.tf @@ -0,0 +1,14 @@ +output "domain_name" { + description = "Name of the OpenSearch domain" + value = aws_opensearch_domain.siem.domain_name +} + +output "domain_endpoint" { + description = "Endpoint of the OpenSearch domain" + value = aws_opensearch_domain.siem.endpoint +} + +output "domain_arn" { + description = "ARN of the OpenSearch domain" + value = aws_opensearch_domain.siem.arn +} \ No newline at end of file diff --git a/operation-team-account/modules/opensearch/policy.tf b/operation-team-account/modules/opensearch/policy.tf new file mode 100644 index 00000000..389800a2 --- /dev/null +++ b/operation-team-account/modules/opensearch/policy.tf @@ -0,0 +1,33 @@ +variable "lambda_role_arn" { + description = "ARN of the Lambda execution role" + type = string +} + +resource "aws_opensearch_domain_policy" "siem_policy" { + domain_name = aws_opensearch_domain.siem.domain_name + + access_policies = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = var.lambda_role_arn + }, + Action = [ + "es:ESHttpPut", + "es:ESHttpPost", + "es:ESHttpGet" + ], + Resource = [ + "${aws_opensearch_domain.siem.arn}/security-events-*" + ], + Condition = { + StringEquals = { + "aws:SourceVpce" = var.vpc_endpoint_id + } + } + } + ] +}) +} \ No newline at end of file diff --git a/operation-team-account/modules/opensearch/variables.tf b/operation-team-account/modules/opensearch/variables.tf new file mode 100644 index 00000000..a3d95905 --- /dev/null +++ b/operation-team-account/modules/opensearch/variables.tf @@ -0,0 +1,8 @@ +variable "lambda_role_arn" { + description = "ARN of the Lambda execution role" + type = string +} +variable "vpc_endpoint_id" { + description = "VPC Endpoint ID for OpenSearch" + type = string +} diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf new file mode 100644 index 00000000..d052e42b --- /dev/null +++ b/operation-team-account/modules/s3/main.tf @@ -0,0 +1,129 @@ +data "aws_caller_identity" "current" {} + +variable "bucket_name" { type = string } + +resource "aws_kms_key" "cloudtrail" { + description = "KMS key for encrypting CloudTrail logs" + enable_key_rotation = true + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Sid = "AllowRootAccountFullAccess", + Effect = "Allow", + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + }, + Action = "kms:*", + Resource = "*" + }, + { + Sid = "AllowCloudTrailUseOfTheKey", + Effect = "Allow", + Principal = { + Service = "cloudtrail.amazonaws.com" + }, + Action = [ + "kms:GenerateDataKey*", + "kms:Decrypt" + ], + Resource = "*" + } + ] + }) +} + +resource "aws_s3_bucket" "logs" { + bucket = var.bucket_name +} + +resource "aws_s3_bucket_versioning" "logs" { + bucket = aws_s3_bucket.logs.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { + bucket = aws_s3_bucket.logs.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + kms_master_key_id = aws_kms_key.cloudtrail.arn + } + } +} + +resource "aws_s3_bucket_public_access_block" "block" { + bucket = aws_s3_bucket.logs.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_policy" "cloudtrail" { + bucket = aws_s3_bucket.logs.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Sid = "AllowCloudTrailAclCheck", + Effect = "Allow", + Principal = { Service = "cloudtrail.amazonaws.com" }, + Action = "s3:GetBucketAcl", + Resource = aws_s3_bucket.logs.arn + }, + { + Sid = "AllowCloudTrailWrite", + Effect = "Allow", + Principal = { Service = "cloudtrail.amazonaws.com" }, + Action = "s3:PutObject", + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*", + Condition = { + StringEquals = { + "aws:SourceArn" = "arn:aws:cloudtrail:${var.aws_region}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}" + "s3:x-amz-server-side-encryption" = "aws:kms" + "s3:x-amz-acl" = "bucket-owner-full-control" + } +} + +} + ] + }) +} + +resource "aws_s3_bucket_lifecycle_configuration" "logs" { + bucket = aws_s3_bucket.logs.id + + rule { + id = "expire-logs-after-30-days" + status = "Enabled" + + filter { prefix = "" } + + expiration { + days = 30 + } + } +} + +output "bucket_name" { + value = aws_s3_bucket.logs.bucket +} + +output "bucket_arn" { + value = aws_s3_bucket.logs.arn +} + +output "kms_key_arn" { + value = aws_kms_key.cloudtrail.arn +} + +variable "aws_region" { + type = string + description = "AWS region in use" +} diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf new file mode 100644 index 00000000..adee9af8 --- /dev/null +++ b/operation-team-account/modules/s3/variables.tf @@ -0,0 +1,4 @@ +variable "cloudtrail_name" { + type = string + description = "Name of the CloudTrail trail in management account" +} \ No newline at end of file diff --git a/operation-team-account/outputs.tf b/operation-team-account/outputs.tf new file mode 100644 index 00000000..11e3a67a --- /dev/null +++ b/operation-team-account/outputs.tf @@ -0,0 +1,24 @@ +output "opensearch_endpoint" { + description = "Endpoint URL of the OpenSearch domain" + value = module.opensearch.endpoint +} + +output "bucket_name" { + description = "S3 bucket name for CloudTrail logs" + value = module.s3.bucket_name +} + +output "bucket_arn" { + description = "S3 bucket ARN for CloudTrail logs" + value = module.s3.bucket_arn +} + +output "kms_key_arn" { + description = "KMS key ARN used to encrypt CloudTrail logs" + value = module.s3.kms_key_arn +} + +output "operation_account_id" { + description = "Account ID of the operation account" + value = data.aws_caller_identity.current.account_id +} diff --git a/operation-team-account/variables.tf b/operation-team-account/variables.tf new file mode 100644 index 00000000..dabbada5 --- /dev/null +++ b/operation-team-account/variables.tf @@ -0,0 +1,75 @@ +variable "aws_region" { + description = "AWS Region" + type = string + default = "ap-northeast-2" +} + +variable "cloudtrail_bucket_name" { + description = "S3 bucket name used by organization CloudTrail (from management account)" + type = string + default = "whs-cloudtrail-logs" +} + +variable "opensearch_domain_name" { + description = "OpenSearch domain name" + type = string + default = "whs-domain" +} + +variable "opensearch_engine_version" { + description = "OpenSearch engine version" + type = string + default = "OpenSearch_2.9" +} + +variable "opensearch_instance_type" { + description = "OpenSearch instance type" + type = string + default = "t3.small.search" +} + +variable "opensearch_instance_count" { + description = "Number of OpenSearch instances" + type = number + default = 1 +} + +variable "opensearch_ebs_size" { + description = "EBS volume size for OpenSearch" + type = number + default = 10 +} + +variable "alerts_sns_topic" { + description = "SNS topic name for security alerts" + type = string + default = "whs-security-alerts" +} + +variable "management_account_id" { + description = "Management AWS Account ID" + type = string +} + +variable "slack_webhook_url" { + description = "Slack Webhook URL for notifications" + type = string + sensitive = true +} + +variable "org_trail_name" { + description = "Name of the organization CloudTrail trail" + type = string +} + +variable "vpc_id" { + type = string +} + +variable "subnet_ids" { + type = list(string) +} + +variable "security_group_ids" { + type = list(string) +} \ No newline at end of file From 703cfe5bbb01314374e0123f3c9150e23cbb2b7e Mon Sep 17 00:00:00 2001 From: 3olly Date: Sun, 6 Jul 2025 14:12:21 +0900 Subject: [PATCH 02/19] =?UTF-8?q?fix:=20eventbridge,=20lambda=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/main.tf | 64 +++-- .../iam/lambda_execution_policy.json.tpl | 37 --- .../modules/detection/lambda/index.py | 77 ----- .../modules/detection/lambda/requirements.txt | 3 - .../modules/detection/main.tf | 107 ------- .../modules/detection/outputs.tf | 14 - .../modules/detection/variables.tf | 45 --- .../modules/eventbridge/main.tf | 263 ++++++++++++++++++ .../modules/eventbridge/outputs.tf | 4 + .../modules/eventbridge/variables.tf | 14 + operation-team-account/modules/lambda/main.tf | 79 ++++++ .../modules/lambda/outputs.tf | 14 + .../modules/lambda/variables.tf | 45 +++ .../modules/network/main.tf | 39 --- .../modules/network/outputs.tf | 16 -- .../modules/network/variables.tf | 4 - .../modules/opensearch/main.tf | 11 - .../modules/opensearch/policy.tf | 7 +- .../modules/opensearch/variables.tf | 37 ++- operation-team-account/modules/s3/main.tf | 65 ++--- .../modules/s3/variables.tf | 17 +- operation-team-account/outputs.tf | 2 +- operation-team-account/variables.tf | 25 +- 23 files changed, 548 insertions(+), 441 deletions(-) delete mode 100644 operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl delete mode 100644 operation-team-account/modules/detection/lambda/index.py delete mode 100644 operation-team-account/modules/detection/lambda/requirements.txt delete mode 100644 operation-team-account/modules/detection/main.tf delete mode 100644 operation-team-account/modules/detection/outputs.tf delete mode 100644 operation-team-account/modules/detection/variables.tf create mode 100644 operation-team-account/modules/eventbridge/main.tf create mode 100644 operation-team-account/modules/eventbridge/outputs.tf create mode 100644 operation-team-account/modules/eventbridge/variables.tf create mode 100644 operation-team-account/modules/lambda/main.tf create mode 100644 operation-team-account/modules/lambda/outputs.tf create mode 100644 operation-team-account/modules/lambda/variables.tf delete mode 100644 operation-team-account/modules/network/main.tf delete mode 100644 operation-team-account/modules/network/outputs.tf delete mode 100644 operation-team-account/modules/network/variables.tf diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index 85e010ca..4f40ec8c 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -1,5 +1,3 @@ -data "aws_caller_identity" "current" {} - terraform { required_version = ">= 1.1.0" required_providers { @@ -8,7 +6,6 @@ terraform { version = "~> 5.0" } } - backend "s3" { bucket = "cloudfence-operation-s3" key = "monitoring/terraform.tfstate" @@ -30,31 +27,28 @@ provider "aws" { profile = "whs-sso-management" } +data "aws_caller_identity" "current" {} + +# 1) OpenSearch 전용 VPC Endpoint 생성 +resource "aws_vpc_endpoint" "opensearch" { + vpc_id = var.vpc_id + service_name = "com.amazonaws.${var.aws_region}.es" + vpc_endpoint_type = "Interface" + subnet_ids = var.subnet_ids + security_group_ids = var.security_group_ids + private_dns_enabled = true +} + +# 2) S3 모듈: CloudTrail 로그 버킷 + KMS module "s3" { source = "./modules/s3" bucket_name = var.cloudtrail_bucket_name cloudtrail_name = var.org_trail_name - aws_region = var.aws_region + aws_region = var.aws_region + management_account_id = var.management_account_id } -module "detection" { - source = "./modules/detection" - sns_topic_name = var.alerts_sns_topic - lambda_function_name = "eventbridge-processor" - opensearch_domain_endpoint = module.opensearch.endpoint - slack_webhook_url = var.slack_webhook_url - lambda_zip_path = "./lambda/lambda_package.zip" - kms_key_arn = module.s3.kms_key_arn - opensearch_domain_arn = module.opensearch.domain_arn - aws_region = var.aws_region -} - -output "opensearch_vpc_endpoint_id" { - value = aws_vpc_endpoint.opensearch.id - description = "The ID of the VPC endpoint for OpenSearch" -} - - +# 3) OpenSearch 모듈: 도메인 생성 + 접근 정책 module "opensearch" { source = "./modules/opensearch" domain_name = var.opensearch_domain_name @@ -63,12 +57,28 @@ module "opensearch" { cluster_instance_count = var.opensearch_instance_count ebs_volume_size = var.opensearch_ebs_size kms_key_arn = module.s3.kms_key_arn - lambda_role_arn = module.detection.lambda_function_role_arn - vpc_endpoint_id = module.network.opensearch_vpc_endpoint_id + lambda_role_arn = module.lambda.lambda_function_role_arn + vpc_endpoint_id = aws_vpc_endpoint.opensearch.id } -module "network" { - source = "./modules/network" - aws_region = var.aws_region +# 4) Lambda 모듈: 로그 파싱 → OpenSearch + Slack 전송 +module "lambda" { + source = "./modules/lambda" + lambda_function_name = "cloudtrail-log-processor" + lambda_zip_path = "./lambda/lambda_package.zip" + opensearch_domain_arn = module.opensearch.domain_arn + opensearch_endpoint = module.opensearch.endpoint + slack_webhook_url = var.slack_webhook_url + kms_key_arn = module.s3.kms_key_arn + bucket_arn = module.s3.bucket_arn + lambda_subnet_ids = var.subnet_ids + lambda_security_group_ids = var.security_group_ids } +# 5) EventBridge 모듈: S3 PutObject → Lambda 트리거 +module "eventbridge" { + source = "./modules/eventbridge" + bucket_name = module.s3.bucket_name + lambda_function_name = module.lambda.lambda_function_name + lambda_function_arn = module.lambda.lambda_function_arn +} \ No newline at end of file diff --git a/operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl b/operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl deleted file mode 100644 index c93480e5..00000000 --- a/operation-team-account/modules/detection/iam/lambda_execution_policy.json.tpl +++ /dev/null @@ -1,37 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "es:ESHttpPost", - "es:ESHttpPut", - "es:ESHttpGet" - ], - "Resource": "${opensearch_arn}/*" - }, - { - "Effect": "Allow", - "Action": [ - "kms:Decrypt" - ], - "Resource": "${kms_key_arn}" - }, - { - "Effect": "Allow", - "Action": [ - "secretsmanager:GetSecretValue" - ], - "Resource": "${slack_secret_arn}" - } - ] -} diff --git a/operation-team-account/modules/detection/lambda/index.py b/operation-team-account/modules/detection/lambda/index.py deleted file mode 100644 index 72fcda79..00000000 --- a/operation-team-account/modules/detection/lambda/index.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import os -import requests -from datetime import datetime -from opensearchpy import OpenSearch, RequestsHttpConnection -from requests_aws4auth import AWS4Auth -import boto3 - -region = os.environ['AWS_REGION'] -host = os.environ['OPENSEARCH_ENDPOINT'] -slack_webhook_url = os.environ['SLACK_WEBHOOK_URL'] - -credentials = boto3.Session().get_credentials() -awsauth = AWS4Auth( - credentials.access_key, - credentials.secret_key, - region, - 'es', - session_token=credentials.token -) - -client = OpenSearch( - hosts=[{'host': host, 'port': 443}], - http_auth=awsauth, - use_ssl=True, - verify_certs=True, - connection_class=RequestsHttpConnection -) - -def lambda_handler(event, context): - print("Received event:", json.dumps(event, indent=2)) - - detail = event.get("detail", {}) - detail_type = event.get("detail-type", "Unknown Event") - event_name = detail.get("eventName", "N/A") - user_identity = detail.get("userIdentity", {}).get("arn", "Unknown User") - source_ip = detail.get("sourceIPAddress", "Unknown IP") - time = event.get("time", datetime.utcnow().isoformat()) - - # Slack message formatting - slack_message = { - "text": ( - f"🚨 *Security Alert Detected* 🚨\n" - f"*Type:* {detail_type}\n" - f"*Event:* `{event_name}`\n" - f"*User:* `{user_identity}`\n" - f"*IP:* `{source_ip}`\n" - f"*Time:* `{time}`" - ) - } - - try: - requests.post( - slack_webhook_url, - data=json.dumps(slack_message), - headers={'Content-Type': 'application/json'} - ) - if response.status_code != 200: - raise Exception(f"Slack returned status code {response.status_code}, body: {response.text}") - except Exception as e: - print(f"Error posting to Slack: {e}") - - # Index to OpenSearch - try: - index_name = "security-events-" + datetime.utcnow().strftime("%Y-%m-%d") - response = client.index( - index=index_name, - body=event # Store full event for auditing - ) - print(f"Indexed to OpenSearch: {response}") - except Exception as e: - print(f"Error indexing to OpenSearch: {e}") - - return { - 'statusCode': 200, - 'body': 'Processed event successfully' - } \ No newline at end of file diff --git a/operation-team-account/modules/detection/lambda/requirements.txt b/operation-team-account/modules/detection/lambda/requirements.txt deleted file mode 100644 index 61ea07f9..00000000 --- a/operation-team-account/modules/detection/lambda/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -opensearch-py -requests-aws4auth \ No newline at end of file diff --git a/operation-team-account/modules/detection/main.tf b/operation-team-account/modules/detection/main.tf deleted file mode 100644 index 55014672..00000000 --- a/operation-team-account/modules/detection/main.tf +++ /dev/null @@ -1,107 +0,0 @@ -resource "aws_kms_key" "sns" { - description = "KMS key for SNS topic encryption" - deletion_window_in_days = 30 -} - -resource "aws_sns_topic" "alerts" { - name = var.sns_topic_name - kms_master_key_id = aws_kms_key.sns.arn - tags = { - Environment = "prod" - Team = "monitoring" - } -} - -resource "aws_sns_topic_policy" "alerts_policy" { - arn = aws_sns_topic.alerts.arn - policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Sid = "AllowEventBridgePublish", - Effect = "Allow", - Principal = { Service = "events.amazonaws.com" }, - Action = "sns:Publish", - Resource = aws_sns_topic.alerts.arn, - Condition = { - ArnLike = { - "aws:SourceArn" = "arn:aws:events:*:*:rule/*" - } - } - } - ] - }) -} - -resource "aws_iam_role" "lambda_exec" { - name = "eventbridge_lambda_exec_role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Effect = "Allow", - Principal = { - Service = "lambda.amazonaws.com" - }, - Action = "sts:AssumeRole" - } - ] - }) -} - -resource "aws_iam_role_policy" "lambda_policy" { - name = "eventbridge_lambda_policy" - role = aws_iam_role.lambda_exec.id - policy = templatefile("${path.module}/iam/lambda_execution_policy.json.tpl", { - opensearch_arn = var.opensearch_domain_arn - kms_key_arn = var.kms_key_arn - slack_secret_arn = "arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.current.account_id}:secret:slack-webhook-*" - }) -} - -resource "aws_lambda_function" "eventbridge_processor" { - function_name = var.lambda_function_name - role = aws_iam_role.lambda_exec.arn - handler = "index.lambda_handler" - runtime = "python3.11" - filename = var.lambda_zip_path - - environment { - variables = { - OPENSEARCH_ENDPOINT = var.opensearch_domain_endpoint - SLACK_WEBHOOK_URL = var.slack_webhook_url - } - } -} - -resource "aws_cloudwatch_event_rule" "mfa_deactivation" { - name = "detect-mfa-deactivation" - description = "Detect deactivation of MFA for IAM users" - event_pattern = jsonencode({ - source = ["aws.iam"], - "detail-type" = ["AWS API Call via CloudTrail"], - detail = { - eventName = ["DeactivateMFADevice", "DeleteVirtualMFADevice"] - } - }) -} - -resource "aws_cloudwatch_event_target" "mfa_to_lambda" { - rule = aws_cloudwatch_event_rule.mfa_deactivation.name - target_id = "MFAAlertLambda" - arn = aws_lambda_function.eventbridge_processor.arn -} - -resource "aws_lambda_permission" "allow_eventbridge" { - statement_id = "AllowExecutionFromEventBridge" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.eventbridge_processor.function_name - principal = "events.amazonaws.com" - source_arn = aws_cloudwatch_event_rule.mfa_deactivation.arn -} - -variable "aws_region" { - type = string - description = "AWS region in use" -} \ No newline at end of file diff --git a/operation-team-account/modules/detection/outputs.tf b/operation-team-account/modules/detection/outputs.tf deleted file mode 100644 index daa59273..00000000 --- a/operation-team-account/modules/detection/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -output "lambda_function_arn" { - description = "ARN of the Lambda function" - value = aws_lambda_function.eventbridge_processor.arn -} - -output "lambda_function_role_arn" { - description = "IAM Role ARN of the Lambda function" - value = aws_iam_role.lambda_exec.arn -} - -output "sns_topic_arn" { - description = "ARN of the SNS topic" - value = aws_sns_topic.alerts.arn -} \ No newline at end of file diff --git a/operation-team-account/modules/detection/variables.tf b/operation-team-account/modules/detection/variables.tf deleted file mode 100644 index 80c3aa0d..00000000 --- a/operation-team-account/modules/detection/variables.tf +++ /dev/null @@ -1,45 +0,0 @@ -variable "lambda_function_name" { - description = "Name of the Lambda function triggered by EventBridge" - type = string -} - -variable "sns_topic_name" { - description = "Name of the SNS topic for alerting" - type = string -} - -variable "opensearch_domain_endpoint" { - description = "OpenSearch domain endpoint" - type = string -} - -variable "slack_webhook_url" { - description = "Slack webhook URL" - type = string - sensitive = true -} - -variable "lambda_zip_path" { - description = "Path to Lambda deployment package zip" - type = string -} - -variable "opensearch_domain_arn" { - type = string - description = "ARN of the OpenSearch domain" -} - -variable "kms_key_arn" { - type = string - description = "KMS key ARN" -} - -variable "kms_key_arn" { - description = "KMS Key ARN used by Lambda" - type = string -} - -variable "opensearch_domain_arn" { - description = "OpenSearch domain ARN" - type = string -} \ No newline at end of file diff --git a/operation-team-account/modules/eventbridge/main.tf b/operation-team-account/modules/eventbridge/main.tf new file mode 100644 index 00000000..c7f797a3 --- /dev/null +++ b/operation-team-account/modules/eventbridge/main.tf @@ -0,0 +1,263 @@ +resource "aws_eventbridge_rule" "s3_object_created" { + name = "cloudtrail-s3-event-rule" + description = "Trigger Lambda on CloudTrail S3 delivery objects" + + event_pattern = jsonencode({ + source = ["aws.s3"] + detail = { + eventName = ["PutObject"] + requestParameters = { + bucketName = [var.bucket_name] + key = [{ prefix = "AWSLogs/" }] + } + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "lambda" { + rule = aws_eventbridge_rule.s3_object_created.name + target_id = "lambda-target" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_eventbridge" { + statement_id = "AllowExecutionFromEventBridge" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.s3_object_created.arn +} + +# 루트 계정 로그인 실패 탐지 +resource "aws_eventbridge_rule" "detect_root_fail" { + name = "detect-root-login-failure" + description = "Detect failed root console login" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = ["ConsoleLogin"] + errorCode = ["FailedAuthentication"] + userIdentity = { type = ["Root"] } + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_root_fail" { + rule = aws_eventbridge_rule.detect_root_fail.name + target_id = "root-fail" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_root_fail" { + statement_id = "AllowDetectRootFail" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_root_fail.arn +} + +# 권한 변경 액션 탐지 +resource "aws_eventbridge_rule" "detect_permission_change" { + name = "detect-iam-permission-change" + description = "Detect changes to IAM policies" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = [ + "AttachUserPolicy","DetachUserPolicy", + "PutUserPolicy","DeleteUserPolicy", + "CreatePolicy","DeletePolicy" + ] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_permission_change" { + rule = aws_eventbridge_rule.detect_permission_change.name + target_id = "permission-change" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_permission_change" { + statement_id = "AllowDetectPermissionChange" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_permission_change.arn +} + +# IAM 사용자/역할 삭제 탐지 +resource "aws_eventbridge_rule" "detect_iam_delete" { + name = "detect-iam-deletion" + description = "Detect deletion of IAM users, roles, or login profiles" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = ["DeleteUser","DeleteRole","DeleteLoginProfile"] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_iam_delete" { + rule = aws_eventbridge_rule.detect_iam_delete.name + target_id = "iam-deletion" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_iam_delete" { + statement_id = "AllowDetectIamDelete" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_iam_delete.arn +} + +# CloudTrail 로그 중지/삭제 탐지 +resource "aws_eventbridge_rule" "detect_cloudtrail_disable" { + name = "detect-cloudtrail-disable" + description = "Detect when CloudTrail is stopped or deleted" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = ["StopLogging","DeleteTrail"] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_cloudtrail_disable" { + rule = aws_eventbridge_rule.detect_cloudtrail_disable.name + target_id = "cloudtrail-disable" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_cloudtrail_disable" { + statement_id = "AllowDetectCloudtrailDisable" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_cloudtrail_disable.arn +} + +# MFA 디바이스 비활성화 탐지 +resource "aws_eventbridge_rule" "detect_mfa_deactivate" { + name = "detect-mfa-deactivation" + description = "Detect MFA deactivation or deletion" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = ["DeactivateMFADevice","DeleteVirtualMFADevice"] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_mfa_deactivate" { + rule = aws_eventbridge_rule.detect_mfa_deactivate.name + target_id = "mfa-deactivate" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_mfa_deactivate" { + statement_id = "AllowDetectMfaDeactivate" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_mfa_deactivate.arn +} + +# 보안 그룹 규칙 변경 탐지 +resource "aws_eventbridge_rule" "detect_sg_change" { + name = "detect-security-group-change" + description = "Detect changes to Security Group ingress/egress rules" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = [ + "AuthorizeSecurityGroupIngress","RevokeSecurityGroupIngress", + "AuthorizeSecurityGroupEgress","RevokeSecurityGroupEgress" + ] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_sg_change" { + rule = aws_eventbridge_rule.detect_sg_change.name + target_id = "sg-change" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_sg_change" { + statement_id = "AllowDetectSgChange" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_sg_change.arn +} + +# S3 퍼블릭 접근 허용 탐지 +resource "aws_eventbridge_rule" "detect_s3_public" { + name = "detect-s3-public-access" + description = "Detect when S3 bucket ACL or policy makes it public" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = ["PutBucketAcl","PutBucketPolicy","PutPublicAccessBlock"] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_s3_public" { + rule = aws_eventbridge_rule.detect_s3_public.name + target_id = "s3-public" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_s3_public" { + statement_id = "AllowDetectS3Public" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_s3_public.arn +} + +# EC2 인스턴스 생성 탐지 +resource "aws_eventbridge_rule" "detect_ec2_launch" { + name = "detect-ec2-instance-launch" + description = "Detect when EC2 instances are launched" + event_pattern = jsonencode({ + source = ["aws.cloudtrail"] + "detail-type" = ["AWS API Call via CloudTrail"] + detail = { + eventName = ["RunInstances"] + } + }) + event_bus_name = "default" +} + +resource "aws_eventbridge_target" "detect_ec2_launch" { + rule = aws_eventbridge_rule.detect_ec2_launch.name + target_id = "ec2-launch" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_detect_ec2_launch" { + statement_id = "AllowDetectEc2Launch" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_eventbridge_rule.detect_ec2_launch.arn +} \ No newline at end of file diff --git a/operation-team-account/modules/eventbridge/outputs.tf b/operation-team-account/modules/eventbridge/outputs.tf new file mode 100644 index 00000000..82d05a51 --- /dev/null +++ b/operation-team-account/modules/eventbridge/outputs.tf @@ -0,0 +1,4 @@ +output "event_rule_arn" { + description = "ARN of the CloudWatch Event Rule" + value = aws_cloudwatch_event_rule.s3_object_created.arn +} \ No newline at end of file diff --git a/operation-team-account/modules/eventbridge/variables.tf b/operation-team-account/modules/eventbridge/variables.tf new file mode 100644 index 00000000..f7d96b7c --- /dev/null +++ b/operation-team-account/modules/eventbridge/variables.tf @@ -0,0 +1,14 @@ +variable "bucket_name" { + description = "Name of the S3 bucket where CloudTrail logs are stored" + type = string +} + +variable "lambda_function_name" { + description = "Lambda function name to be triggered" + type = string +} + +variable "lambda_function_arn" { + description = "ARN of the Lambda function to trigger" + type = string +} \ No newline at end of file diff --git a/operation-team-account/modules/lambda/main.tf b/operation-team-account/modules/lambda/main.tf new file mode 100644 index 00000000..c73a60f6 --- /dev/null +++ b/operation-team-account/modules/lambda/main.tf @@ -0,0 +1,79 @@ +resource "aws_iam_role" "lambda_exec" { + name = "lambda-log-processor-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + Service = "lambda.amazonaws.com" + }, + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role_policy" "lambda_policy" { + name = "lambda-log-policy" + role = aws_iam_role.lambda_exec.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Sid = "AllowCloudWatchLogs", + Effect = "Allow", + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Resource = "*" + }, + { + Sid = "AllowOpenSearchAccess", + Effect = "Allow", + Action = [ + "es:ESHttpPost", + "es:ESHttpPut", + "es:ESHttpGet" + ], + Resource = "${var.opensearch_domain_arn}/security-events-*" + }, + { + Sid = "AllowKMSDecrypt", + Effect = "Allow", + Action = [ + "kms:Decrypt" + ], + Resource = var.kms_key_arn + }, + { + Sid = "AllowS3Read" + Effect = "Allow" + Action = ["s3:GetObject"] + Resource = "${var.bucket_arn}/*" +} + ] + }) +} + +resource "aws_lambda_function" "log_processor" { + function_name = var.lambda_function_name + handler = "lambda_function.lambda_handler" + runtime = "python3.11" + role = aws_iam_role.lambda_exec.arn + timeout = 30 + memory_size = 256 + filename = var.lambda_zip_path + source_code_hash = filebase64sha256(var.lambda_zip_path) + + environment { + variables = { + SLACK_WEBHOOK_URL = var.slack_webhook_url + OPENSEARCH_URL = var.opensearch_endpoint + } + } +} \ No newline at end of file diff --git a/operation-team-account/modules/lambda/outputs.tf b/operation-team-account/modules/lambda/outputs.tf new file mode 100644 index 00000000..3a9ee18f --- /dev/null +++ b/operation-team-account/modules/lambda/outputs.tf @@ -0,0 +1,14 @@ +output "lambda_function_name" { + value = aws_lambda_function.log_processor.function_name + description = "Name of the deployed Lambda function" +} + +output "lambda_function_role_arn" { + value = aws_iam_role.lambda_exec.arn + description = "IAM Role ARN for Lambda execution" +} + +output "lambda_function_arn" { + value = aws_lambda_function.log_processor.arn + description = "ARN of the Lambda function" +} \ No newline at end of file diff --git a/operation-team-account/modules/lambda/variables.tf b/operation-team-account/modules/lambda/variables.tf new file mode 100644 index 00000000..cdb999ed --- /dev/null +++ b/operation-team-account/modules/lambda/variables.tf @@ -0,0 +1,45 @@ +variable "lambda_function_name" { + type = string + description = "Name of the Lambda function" +} + +variable "lambda_zip_path" { + type = string + description = "Path to the zipped Lambda package" +} + +variable "opensearch_domain_arn" { + type = string + description = "ARN of the OpenSearch domain" +} + +variable "opensearch_endpoint" { + type = string + description = "OpenSearch endpoint URL" +} + +variable "slack_webhook_url" { + type = string + description = "Slack Webhook URL" + sensitive = true +} + +variable "kms_key_arn" { + type = string + description = "KMS key for decrypting Slack secret (if encrypted)" +} + +variable "bucket_arn" { + description = "ARN of the S3 bucket for CloudTrail logs" + type = string +} + +variable "lambda_subnet_ids" { + description = "List of subnet IDs for the Lambda function to attach to the VPC" + type = list(string) +} + +variable "lambda_security_group_ids" { + description = "Security group IDs for the Lambda function in the VPC" + type = list(string) +} \ No newline at end of file diff --git a/operation-team-account/modules/network/main.tf b/operation-team-account/modules/network/main.tf deleted file mode 100644 index 21caca2e..00000000 --- a/operation-team-account/modules/network/main.tf +++ /dev/null @@ -1,39 +0,0 @@ -resource "aws_vpc_endpoint" "opensearch" { - vpc_id = aws_vpc.main.id - service_name = "com.amazonaws.${var.aws_region}.es" - vpc_endpoint_type = "Interface" - subnet_ids = [aws_subnet.a.id] - security_group_ids = [aws_security_group.lambda_to_opensearch.id] - private_dns_enabled = true - - tags = { - Name = "opensearch-vpc-endpoint" - } -} - -resource "aws_vpc" "main" { - cidr_block = "10.0.0.0/16" - tags = { Name = "main-vpc" } -} - -resource "aws_subnet" "a" { - vpc_id = aws_vpc.main.id - cidr_block = "10.0.1.0/24" - availability_zone = "${var.aws_region}a" - tags = { Name = "subnet-a" } -} - -resource "aws_security_group" "lambda_to_opensearch" { - name = "lambda-to-opensearch" - description = "Allow Lambda to access OpenSearch" - vpc_id = aws_vpc.main.id - - egress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { Name = "lambda-egress" } -} \ No newline at end of file diff --git a/operation-team-account/modules/network/outputs.tf b/operation-team-account/modules/network/outputs.tf deleted file mode 100644 index c09bf802..00000000 --- a/operation-team-account/modules/network/outputs.tf +++ /dev/null @@ -1,16 +0,0 @@ -output "opensearch_vpc_endpoint_id" { - value = aws_vpc_endpoint.opensearch.id - description = "The ID of the VPC endpoint for OpenSearch" -} - -output "vpc_id" { - value = aws_vpc.main.id -} - -output "subnet_ids" { - value = [aws_subnet.a.id] -} - -output "security_group_ids" { - value = [aws_security_group.lambda_to_opensearch.id] -} \ No newline at end of file diff --git a/operation-team-account/modules/network/variables.tf b/operation-team-account/modules/network/variables.tf deleted file mode 100644 index 5ec9b61e..00000000 --- a/operation-team-account/modules/network/variables.tf +++ /dev/null @@ -1,4 +0,0 @@ -variable "aws_region" { - type = string - description = "AWS Region" -} \ No newline at end of file diff --git a/operation-team-account/modules/opensearch/main.tf b/operation-team-account/modules/opensearch/main.tf index 1ab62f1a..4b8131c3 100644 --- a/operation-team-account/modules/opensearch/main.tf +++ b/operation-team-account/modules/opensearch/main.tf @@ -1,10 +1,3 @@ -variable "domain_name" { type = string } -variable "engine_version" { type = string } -variable "cluster_instance_type" { type = string } -variable "cluster_instance_count"{ type = number } -variable "ebs_volume_size" { type = number } -variable "kms_key_arn" { type = string } - resource "aws_opensearch_domain" "siem" { domain_name = "siem-${var.domain_name}" engine_version = var.engine_version @@ -42,8 +35,4 @@ resource "aws_opensearch_domain" "siem" { output "endpoint" { value = aws_opensearch_domain.siem.endpoint -} - -output "domain_arn" { - value = aws_opensearch_domain.siem.arn } \ No newline at end of file diff --git a/operation-team-account/modules/opensearch/policy.tf b/operation-team-account/modules/opensearch/policy.tf index 389800a2..e0270aea 100644 --- a/operation-team-account/modules/opensearch/policy.tf +++ b/operation-team-account/modules/opensearch/policy.tf @@ -1,8 +1,3 @@ -variable "lambda_role_arn" { - description = "ARN of the Lambda execution role" - type = string -} - resource "aws_opensearch_domain_policy" "siem_policy" { domain_name = aws_opensearch_domain.siem.domain_name @@ -20,7 +15,7 @@ resource "aws_opensearch_domain_policy" "siem_policy" { "es:ESHttpGet" ], Resource = [ - "${aws_opensearch_domain.siem.arn}/security-events-*" + "${aws_opensearch_domain.siem.arn}/security-events-*/*" ], Condition = { StringEquals = { diff --git a/operation-team-account/modules/opensearch/variables.tf b/operation-team-account/modules/opensearch/variables.tf index a3d95905..40c0e058 100644 --- a/operation-team-account/modules/opensearch/variables.tf +++ b/operation-team-account/modules/opensearch/variables.tf @@ -1,8 +1,39 @@ +variable "domain_name" { + description = "OpenSearch domain name (without prefix)" + type = string +} + +variable "engine_version" { + description = "OpenSearch engine version" + type = string +} + +variable "cluster_instance_type" { + description = "Instance type for OpenSearch nodes" + type = string +} + +variable "cluster_instance_count" { + description = "Number of OpenSearch instances" + type = number +} + +variable "ebs_volume_size" { + description = "EBS volume size (GiB) for each OpenSearch node" + type = number +} + +variable "kms_key_arn" { + description = "KMS key ARN to encrypt OpenSearch data at rest" + type = string +} + variable "lambda_role_arn" { - description = "ARN of the Lambda execution role" + description = "IAM Role ARN that Lambda will assume for indexing into OpenSearch" type = string } + variable "vpc_endpoint_id" { - description = "VPC Endpoint ID for OpenSearch" + description = "VPC Endpoint ID for the OpenSearch domain" type = string -} +} \ No newline at end of file diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index d052e42b..d91447df 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -1,33 +1,31 @@ data "aws_caller_identity" "current" {} -variable "bucket_name" { type = string } - resource "aws_kms_key" "cloudtrail" { description = "KMS key for encrypting CloudTrail logs" enable_key_rotation = true policy = jsonencode({ - Version = "2012-10-17", + Version = "2012-10-17" Statement = [ { - Sid = "AllowRootAccountFullAccess", - Effect = "Allow", + Sid = "AllowRootAccountFullAccess" + Effect = "Allow" Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" - }, - Action = "kms:*", + } + Action = "kms:*" Resource = "*" }, { - Sid = "AllowCloudTrailUseOfTheKey", - Effect = "Allow", + Sid = "AllowCloudTrailUseOfTheKey" + Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" - }, + } Action = [ "kms:GenerateDataKey*", "kms:Decrypt" - ], + ] Resource = "*" } ] @@ -68,30 +66,30 @@ resource "aws_s3_bucket_public_access_block" "block" { resource "aws_s3_bucket_policy" "cloudtrail" { bucket = aws_s3_bucket.logs.id policy = jsonencode({ - Version = "2012-10-17", + Version = "2012-10-17" Statement = [ { - Sid = "AllowCloudTrailAclCheck", - Effect = "Allow", - Principal = { Service = "cloudtrail.amazonaws.com" }, - Action = "s3:GetBucketAcl", + Sid = "AllowCloudTrailAclCheck" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:GetBucketAcl" Resource = aws_s3_bucket.logs.arn }, { - Sid = "AllowCloudTrailWrite", - Effect = "Allow", - Principal = { Service = "cloudtrail.amazonaws.com" }, - Action = "s3:PutObject", - Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*", - Condition = { - StringEquals = { - "aws:SourceArn" = "arn:aws:cloudtrail:${var.aws_region}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}" - "s3:x-amz-server-side-encryption" = "aws:kms" - "s3:x-amz-acl" = "bucket-owner-full-control" - } -} - -} + Sid = "AllowCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" + Condition = { + StringEquals = { + # management 계정의 CloudTrail ARN + "aws:SourceArn" = "arn:aws:cloudtrail:${var.aws_region}:${var.management_account_id}:trail/${var.cloudtrail_name}" + "s3:x-amz-server-side-encryption" = "aws:kms" + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + } ] }) } @@ -121,9 +119,4 @@ output "bucket_arn" { output "kms_key_arn" { value = aws_kms_key.cloudtrail.arn -} - -variable "aws_region" { - type = string - description = "AWS region in use" -} +} \ No newline at end of file diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index adee9af8..ac1b552b 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -1,4 +1,19 @@ +variable "bucket_name" { + description = "S3 bucket name for CloudTrail logs" + type = string +} + variable "cloudtrail_name" { + description = "Name of the CloudTrail trail in the management account" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string +} + +variable "management_account_id" { + description = "AWS Account ID of the Management account (CloudTrail producer)" type = string - description = "Name of the CloudTrail trail in management account" } \ No newline at end of file diff --git a/operation-team-account/outputs.tf b/operation-team-account/outputs.tf index 11e3a67a..678aa020 100644 --- a/operation-team-account/outputs.tf +++ b/operation-team-account/outputs.tf @@ -21,4 +21,4 @@ output "kms_key_arn" { output "operation_account_id" { description = "Account ID of the operation account" value = data.aws_caller_identity.current.account_id -} +} \ No newline at end of file diff --git a/operation-team-account/variables.tf b/operation-team-account/variables.tf index dabbada5..2ecf9131 100644 --- a/operation-team-account/variables.tf +++ b/operation-team-account/variables.tf @@ -40,17 +40,6 @@ variable "opensearch_ebs_size" { default = 10 } -variable "alerts_sns_topic" { - description = "SNS topic name for security alerts" - type = string - default = "whs-security-alerts" -} - -variable "management_account_id" { - description = "Management AWS Account ID" - type = string -} - variable "slack_webhook_url" { description = "Slack Webhook URL for notifications" type = string @@ -63,13 +52,21 @@ variable "org_trail_name" { } variable "vpc_id" { - type = string + description = "VPC ID for Interface Endpoint creation" + type = string } variable "subnet_ids" { - type = list(string) + description = "Subnet IDs for Interface Endpoint and Lambda VPC config" + type = list(string) } variable "security_group_ids" { - type = list(string) + description = "Security Group IDs for Interface Endpoint and Lambda VPC config" + type = list(string) +} + +variable "management_account_id" { + description = "AWS Account ID of the Management account (CloudTrail producer)" + type = string } \ No newline at end of file From 3f48cd6f9b91ba4b4475e93495f2a441d62ec57f Mon Sep 17 00:00:00 2001 From: 3olly Date: Sun, 6 Jul 2025 18:26:42 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20s3=5Faws=5Fkms=5Fkey=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lambda/lambda_function.py | 4 + .../lambda/lambda_package.zip | Bin 0 -> 347 bytes operation-team-account/main.tf | 34 +++++--- .../modules/eventbridge/main.tf | 72 ++++++++--------- .../modules/eventbridge/outputs.tf | 1 - operation-team-account/modules/lambda/main.tf | 73 ++++++++++-------- .../modules/opensearch/main.tf | 5 ++ .../modules/opensearch/policy.tf | 7 +- .../modules/opensearch/variables.tf | 11 ++- operation-team-account/modules/s3/main.tf | 31 ++++---- operation-team-account/modules/s3/outputs.tf | 11 +++ operation-team-account/variables.tf | 15 ---- 12 files changed, 143 insertions(+), 121 deletions(-) create mode 100644 operation-team-account/lambda/lambda_function.py create mode 100644 operation-team-account/lambda/lambda_package.zip create mode 100644 operation-team-account/modules/s3/outputs.tf diff --git a/operation-team-account/lambda/lambda_function.py b/operation-team-account/lambda/lambda_function.py new file mode 100644 index 00000000..c3c672da --- /dev/null +++ b/operation-team-account/lambda/lambda_function.py @@ -0,0 +1,4 @@ +def lambda_handler(event, context): + # TODO: Slack 알림, OpenSearch 색인 로직 구현 + print("Event:", event) + return {"status": "ok"} \ No newline at end of file diff --git a/operation-team-account/lambda/lambda_package.zip b/operation-team-account/lambda/lambda_package.zip new file mode 100644 index 0000000000000000000000000000000000000000..8d507740a361194c6f3bce41c48e6c82831b0d6b GIT binary patch literal 347 zcmWIWW@Zs#-~dA3@@G*DNI-}|fgvX`Hz_4CKCLt_xg;|`Pp_b|w1S&~kp-j-OoWE; zGO#xZWMzPGEf9Nq`UaoyJmc%-*>m3GjQ2@T-Af+2-e3 Date: Sun, 6 Jul 2025 20:19:45 +0900 Subject: [PATCH 04/19] =?UTF-8?q?fix:=20kms=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/main.tf | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/management-team-account/main.tf b/management-team-account/main.tf index 063c1b4a..f02272e9 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -1,13 +1,3 @@ -data "terraform_remote_state" "operation" { - backend = "s3" - config = { - bucket = "cloudfence-operation-s3" - key = "monitoring/terraform.tfstate" - region = "ap-northeast-2" - profile = "whs-sso-operation" - } -} - terraform { required_version = ">= 1.1.0" required_providers { @@ -16,6 +6,7 @@ terraform { version = "~> 5.0" } } + backend "s3" { bucket = "cloudfence-management-s3" key = "cloudtrail/terraform.tfstate" @@ -31,9 +22,19 @@ provider "aws" { profile = "whs-sso-management" } +data "terraform_remote_state" "operation" { + backend = "s3" + config = { + bucket = "cloudfence-operation-s3" + key = "monitoring/terraform.tfstate" + region = "ap-northeast-2" + profile = "whs-sso-operation" + } +} + data "aws_caller_identity" "current" {} -resource "aws_cloudtrail" "org" { +resource "aws_cloudtrail" "organization" { name = var.org_trail_name is_organization_trail = true is_multi_region_trail = true @@ -41,8 +42,8 @@ resource "aws_cloudtrail" "org" { enable_log_file_validation = true enable_logging = true - s3_bucket_name = var.destination_s3_bucket_name - kms_key_id = var.s3_kms_key_arn + s3_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name + kms_key_id = data.terraform_remote_state.operation.outputs.kms_key_arn tags = { Name = var.org_trail_name From 1c314febde128c2a76cbb443a2d27d7164271ffd Mon Sep 17 00:00:00 2001 From: 3olly Date: Sun, 6 Jul 2025 20:20:35 +0900 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20s3=20main=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=95=EC=B1=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/main.tf | 1 + operation-team-account/modules/s3/main.tf | 82 +++++++++++++------ .../modules/s3/variables.tf | 5 ++ operation-team-account/variables.tf | 5 ++ 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index 3b70b89a..e0ad3e33 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -55,6 +55,7 @@ module "s3" { cloudtrail_name = var.org_trail_name aws_region = var.aws_region management_account_id = var.management_account_id + organization_id = var.organization_id } # 3) OpenSearch 모듈: 도메인 생성 + 접근 정책 diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 44db225e..ec6bda94 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -1,27 +1,28 @@ data "aws_caller_identity" "current" {} +# KMS 키 (CloudTrail 로그 암호화용) resource "aws_kms_key" "cloudtrail" { description = "KMS key for encrypting CloudTrail logs" enable_key_rotation = true policy = jsonencode({ - Version = "2012-10-17" + Version = "2012-10-17" Statement = [ - - # 1) 루트 계정 (운영 계정) 전 권한 { Sid = "AllowRootAccountFullAccess" Effect = "Allow" Principal = { - AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + AWS = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", # Operation root + "arn:aws:iam::${var.management_account_id}:root", # Management root + ] } Action = "kms:*" Resource = "*" }, - - # 2) CloudTrail 서비스에 필요한 KMS 호출 일체 허용 + # 조직 전체 Organization Trail 전용: CloudTrail 서비스가 이 키를 사용할 수 있도록 허용 { - Sid = "AllowCloudTrailUseOfTheKey" + Sid = "AllowOrgCloudTrailUse" Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" @@ -35,8 +36,30 @@ resource "aws_kms_key" "cloudtrail" { Resource = "*" Condition = { StringEquals = { - "kms:CallerAccount" = var.management_account_id, - "kms:ViaService" = "cloudtrail.${var.aws_region}.amazonaws.com" + "aws:PrincipalOrgID" = var.organization_id, + "kms:ViaService" = "cloudtrail.${var.aws_region}.amazonaws.com" + } + } + }, + # S3 서비스에서 이 키를 사용해 암호화할 수 있도록 허용 + { + Sid = "AllowS3UseOfTheKey" + Effect = "Allow" + Principal = { + Service = "s3.amazonaws.com" + } + Action = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + Resource = "*" + Condition = { + StringEquals = { + "aws:SourceArn" = "arn:aws:s3:::${var.bucket_name}", + "aws:SourceAccount" = data.aws_caller_identity.current.account_id } } } @@ -44,11 +67,12 @@ resource "aws_kms_key" "cloudtrail" { }) } - +# S3 버킷 생성 resource "aws_s3_bucket" "logs" { bucket = var.bucket_name } +# 버전 관리 설정 resource "aws_s3_bucket_versioning" "logs" { bucket = aws_s3_bucket.logs.id @@ -57,6 +81,7 @@ resource "aws_s3_bucket_versioning" "logs" { } } +# SSE-KMS 암호화 설정 resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { bucket = aws_s3_bucket.logs.id @@ -68,6 +93,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { } } +# 공개 접근 차단 resource "aws_s3_bucket_public_access_block" "block" { bucket = aws_s3_bucket.logs.id block_public_acls = true @@ -76,30 +102,37 @@ resource "aws_s3_bucket_public_access_block" "block" { restrict_public_buckets = true } +# CloudTrail에서 로그를 S3에 쓸 수 있도록 정책 설정 resource "aws_s3_bucket_policy" "cloudtrail" { bucket = aws_s3_bucket.logs.id + policy = jsonencode({ Version = "2012-10-17" Statement = [ + # GetBucketAcl 권한 { Sid = "AllowCloudTrailAclCheck" Effect = "Allow" - Principal = { Service = "cloudtrail.amazonaws.com" } - Action = "s3:GetBucketAcl" - Resource = aws_s3_bucket.logs.arn + Principal = { + Service = "cloudtrail.amazonaws.com" + } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.logs.arn }, + # CloudTrail에서 PutObject 허용 (조직 전체 Organization Trail) { - Sid = "AllowCloudTrailWrite" + Sid = "AllowCloudTrailWriteFromOrg" Effect = "Allow" - Principal = { Service = "cloudtrail.amazonaws.com" } - Action = "s3:PutObject" - Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" + Principal = { + Service = "cloudtrail.amazonaws.com" + } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" Condition = { StringEquals = { - # management 계정의 CloudTrail ARN - "aws:SourceArn" = "arn:aws:cloudtrail:${var.aws_region}:${var.management_account_id}:trail/${var.cloudtrail_name}" - "s3:x-amz-server-side-encryption" = "aws:kms" - "s3:x-amz-acl" = "bucket-owner-full-control" + "s3:x-amz-server-side-encryption" = "aws:kms", + "s3:x-amz-acl" = "bucket-owner-full-control", + "aws:PrincipalOrgID" = var.organization_id } } } @@ -107,14 +140,17 @@ resource "aws_s3_bucket_policy" "cloudtrail" { }) } +# 라이프사이클 정책 (30일 후 만료) resource "aws_s3_bucket_lifecycle_configuration" "logs" { bucket = aws_s3_bucket.logs.id rule { id = "expire-logs-after-30-days" status = "Enabled" - - filter { prefix = "" } + + filter { + prefix = "" + } expiration { days = 30 diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index ac1b552b..5697043b 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -16,4 +16,9 @@ variable "aws_region" { variable "management_account_id" { description = "AWS Account ID of the Management account (CloudTrail producer)" type = string +} + +variable "organization_id" { + description = "AWS Organization ID used for cross-account policies" + type = string } \ No newline at end of file diff --git a/operation-team-account/variables.tf b/operation-team-account/variables.tf index bb28c0a5..bc79f532 100644 --- a/operation-team-account/variables.tf +++ b/operation-team-account/variables.tf @@ -54,4 +54,9 @@ variable "org_trail_name" { variable "management_account_id" { description = "AWS Account ID of the Management account (CloudTrail producer)" type = string +} + +variable "organization_id" { + description = "AWS Organization ID used for cross-account policies" + type = string } \ No newline at end of file From 485b4638a192ac19ff937f7c68ba1c063a3cf313 Mon Sep 17 00:00:00 2001 From: 3olly Date: Mon, 7 Jul 2025 07:11:20 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20managemenet-s3=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/modules/s3/main.tf | 169 ++++++++---------- operation-team-account/modules/s3/outputs.tf | 9 +- .../modules/s3/variables.tf | 8 +- 3 files changed, 80 insertions(+), 106 deletions(-) diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index ec6bda94..b7b78ba3 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -1,29 +1,29 @@ data "aws_caller_identity" "current" {} -# KMS 키 (CloudTrail 로그 암호화용) resource "aws_kms_key" "cloudtrail" { description = "KMS key for encrypting CloudTrail logs" enable_key_rotation = true policy = jsonencode({ - Version = "2012-10-17" + Version = "2012-10-17", Statement = [ + # 운영 계정 루트에게 전체 권한 { Sid = "AllowRootAccountFullAccess" Effect = "Allow" Principal = { AWS = [ - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", # Operation root - "arn:aws:iam::${var.management_account_id}:root", # Management root - ] + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", # Operation root + "arn:aws:iam::${var.management_account_id}:root" # Management root + ] } Action = "kms:*" Resource = "*" }, - # 조직 전체 Organization Trail 전용: CloudTrail 서비스가 이 키를 사용할 수 있도록 허용 + # CloudTrail 서비스에서 KMS 키 사용 허용 (management account) { - Sid = "AllowOrgCloudTrailUse" - Effect = "Allow" + Sid = "AllowCloudTrailFromMgmtToUseKMS" + Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } @@ -31,35 +31,15 @@ resource "aws_kms_key" "cloudtrail" { "kms:GenerateDataKey*", "kms:Decrypt", "kms:ReEncrypt*", - "kms:DescribeKey" - ] - Resource = "*" - Condition = { - StringEquals = { - "aws:PrincipalOrgID" = var.organization_id, - "kms:ViaService" = "cloudtrail.${var.aws_region}.amazonaws.com" - } - } - }, - # S3 서비스에서 이 키를 사용해 암호화할 수 있도록 허용 - { - Sid = "AllowS3UseOfTheKey" - Effect = "Allow" - Principal = { - Service = "s3.amazonaws.com" - } - Action = [ + "kms:DescribeKey", + "kms:ListKeys", "kms:Encrypt", - "kms:Decrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:DescribeKey" + "kms:ListAliases" ] Resource = "*" Condition = { StringEquals = { - "aws:SourceArn" = "arn:aws:s3:::${var.bucket_name}", - "aws:SourceAccount" = data.aws_caller_identity.current.account_id + "kms:CallerAccount": "${var.management_account_id}" } } } @@ -67,93 +47,84 @@ resource "aws_kms_key" "cloudtrail" { }) } -# S3 버킷 생성 -resource "aws_s3_bucket" "logs" { - bucket = var.bucket_name +resource "aws_kms_alias" "cloudtrail" { + name = "alias/cloudtrail-logs" + target_key_id = aws_kms_key.cloudtrail.key_id } -# 버전 관리 설정 -resource "aws_s3_bucket_versioning" "logs" { - bucket = aws_s3_bucket.logs.id +resource "aws_s3_bucket" "cloudtrail_logs" { + bucket = var.bucket_name + force_destroy = false + + lifecycle { + prevent_destroy = true + } + + tags = { + Name = var.bucket_name + Environment = "prod" + Owner = "security-team" + } +} +resource "aws_s3_bucket_versioning" "this" { + bucket = aws_s3_bucket.cloudtrail_logs.id versioning_configuration { status = "Enabled" } } -# SSE-KMS 암호화 설정 -resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { - bucket = aws_s3_bucket.logs.id +resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + bucket = aws_s3_bucket.cloudtrail_logs.id rule { apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" kms_master_key_id = aws_kms_key.cloudtrail.arn + sse_algorithm = "aws:kms" } } } -# 공개 접근 차단 -resource "aws_s3_bucket_public_access_block" "block" { - bucket = aws_s3_bucket.logs.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# CloudTrail에서 로그를 S3에 쓸 수 있도록 정책 설정 -resource "aws_s3_bucket_policy" "cloudtrail" { - bucket = aws_s3_bucket.logs.id +resource "aws_s3_bucket_policy" "allow_cloudtrail" { + bucket = aws_s3_bucket.cloudtrail_logs.id policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - # GetBucketAcl 권한 - { - Sid = "AllowCloudTrailAclCheck" - Effect = "Allow" - Principal = { - Service = "cloudtrail.amazonaws.com" + Version: "2012-10-17", + Statement = [ + { + Sid = "AWSCloudTrailWrite", + Effect = "Allow", + Principal = { "Service": "cloudtrail.amazonaws.com" }, + Action = "s3:PutObject", + Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.organization_id}/*", + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" } - Action = "s3:GetBucketAcl" - Resource = aws_s3_bucket.logs.arn - }, - # CloudTrail에서 PutObject 허용 (조직 전체 Organization Trail) - { - Sid = "AllowCloudTrailWriteFromOrg" - Effect = "Allow" + } + }, + { + Sid = "AWSCloudTrailAclCheck", + Effect = "Allow", + Principal = { "Service": "cloudtrail.amazonaws.com" }, + Action = "s3:GetBucketAcl", + Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" + }, + { + Sid = "AllowOperationAndManagementListBucket", + Effect = "Allow", Principal = { - Service = "cloudtrail.amazonaws.com" - } - Action = "s3:PutObject" - Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" - Condition = { - StringEquals = { - "s3:x-amz-server-side-encryption" = "aws:kms", - "s3:x-amz-acl" = "bucket-owner-full-control", - "aws:PrincipalOrgID" = var.organization_id - } - } + AWS = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + "arn:aws:iam::${var.management_account_id}:root" + ] + }, + Action = [ + "s3:ListBucket" + ], + Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" } - ] - }) + ] } - -# 라이프사이클 정책 (30일 후 만료) -resource "aws_s3_bucket_lifecycle_configuration" "logs" { - bucket = aws_s3_bucket.logs.id - - rule { - id = "expire-logs-after-30-days" - status = "Enabled" - - filter { - prefix = "" - } - - expiration { - days = 30 - } - } +) } \ No newline at end of file diff --git a/operation-team-account/modules/s3/outputs.tf b/operation-team-account/modules/s3/outputs.tf index 2781a629..bbf5904e 100644 --- a/operation-team-account/modules/s3/outputs.tf +++ b/operation-team-account/modules/s3/outputs.tf @@ -1,11 +1,14 @@ output "bucket_name" { - value = aws_s3_bucket.logs.bucket + value = aws_s3_bucket.cloudtrail_logs.bucket + description = "S3 bucket name for CloudTrail logs" } output "bucket_arn" { - value = aws_s3_bucket.logs.arn + value = aws_s3_bucket.cloudtrail_logs.arn + description = "ARN of the S3 bucket" } output "kms_key_arn" { - value = aws_kms_key.cloudtrail.arn + value = aws_kms_key.cloudtrail.arn + description = "KMS key ARN for S3 encryption" } \ No newline at end of file diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index 5697043b..2f321475 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -4,21 +4,21 @@ variable "bucket_name" { } variable "cloudtrail_name" { - description = "Name of the CloudTrail trail in the management account" + description = "Name of the CloudTrail (for tag, optional)" type = string } variable "aws_region" { - description = "AWS region" + description = "AWS Region" type = string } variable "management_account_id" { - description = "AWS Account ID of the Management account (CloudTrail producer)" + description = "Management account AWS ID" type = string } variable "organization_id" { - description = "AWS Organization ID used for cross-account policies" + description = "Organization ID (for cross-account policy, optional)" type = string } \ No newline at end of file From 7a6a70973bed7a7485e143a6b222f29f48d44c8a Mon Sep 17 00:00:00 2001 From: 3olly Date: Mon, 7 Jul 2025 07:32:24 +0900 Subject: [PATCH 07/19] =?UTF-8?q?fix:=20terraform=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/main.tf | 32 ++++----- .../modules/eventbridge/main.tf | 38 +++++------ .../modules/eventbridge/outputs.tf | 2 +- operation-team-account/modules/lambda/main.tf | 12 ++-- .../modules/opensearch/policy.tf | 36 +++++----- operation-team-account/modules/s3/main.tf | 67 +++++++++++-------- 6 files changed, 98 insertions(+), 89 deletions(-) diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index e0ad3e33..aa8afbe8 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -50,12 +50,12 @@ data "aws_security_group" "default" { # 2) S3 모듈: CloudTrail 로그 버킷 + KMS module "s3" { - source = "./modules/s3" - bucket_name = var.cloudtrail_bucket_name - cloudtrail_name = var.org_trail_name - aws_region = var.aws_region + source = "./modules/s3" + bucket_name = var.cloudtrail_bucket_name + cloudtrail_name = var.org_trail_name + aws_region = var.aws_region management_account_id = var.management_account_id - organization_id = var.organization_id + organization_id = var.organization_id } # 3) OpenSearch 모듈: 도메인 생성 + 접근 정책 @@ -68,21 +68,21 @@ module "opensearch" { ebs_volume_size = var.opensearch_ebs_size kms_key_arn = module.s3.kms_key_arn lambda_role_arn = module.lambda.lambda_function_role_arn - subnet_ids = [ data.aws_subnets.default.ids[0] ] - security_group_ids = [data.aws_security_group.default.id] + subnet_ids = [data.aws_subnets.default.ids[0]] + security_group_ids = [data.aws_security_group.default.id] } # 4) Lambda 모듈: 로그 파싱 → OpenSearch + Slack 전송 module "lambda" { - source = "./modules/lambda" - lambda_function_name = "cloudtrail-log-processor" - lambda_zip_path = "./lambda/lambda_package.zip" - opensearch_domain_arn = module.opensearch.domain_arn - opensearch_endpoint = module.opensearch.endpoint - slack_webhook_url = var.slack_webhook_url - kms_key_arn = module.s3.kms_key_arn - bucket_arn = module.s3.bucket_arn - lambda_subnet_ids = [ data.aws_subnets.default.ids[0] ] + source = "./modules/lambda" + lambda_function_name = "cloudtrail-log-processor" + lambda_zip_path = "./lambda/lambda_package.zip" + opensearch_domain_arn = module.opensearch.domain_arn + opensearch_endpoint = module.opensearch.endpoint + slack_webhook_url = var.slack_webhook_url + kms_key_arn = module.s3.kms_key_arn + bucket_arn = module.s3.bucket_arn + lambda_subnet_ids = [data.aws_subnets.default.ids[0]] lambda_security_group_ids = [data.aws_security_group.default.id] } diff --git a/operation-team-account/modules/eventbridge/main.tf b/operation-team-account/modules/eventbridge/main.tf index 5ff6b36f..36306150 100644 --- a/operation-team-account/modules/eventbridge/main.tf +++ b/operation-team-account/modules/eventbridge/main.tf @@ -34,11 +34,11 @@ resource "aws_cloudwatch_event_rule" "detect_root_fail" { name = "detect-root-login-failure" description = "Detect failed root console login" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { - eventName = ["ConsoleLogin"] - errorCode = ["FailedAuthentication"] + eventName = ["ConsoleLogin"] + errorCode = ["FailedAuthentication"] userIdentity = { type = ["Root"] } } }) @@ -64,13 +64,13 @@ resource "aws_cloudwatch_event_rule" "detect_permission_change" { name = "detect-iam-permission-change" description = "Detect changes to IAM policies" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { eventName = [ - "AttachUserPolicy","DetachUserPolicy", - "PutUserPolicy","DeleteUserPolicy", - "CreatePolicy","DeletePolicy" + "AttachUserPolicy", "DetachUserPolicy", + "PutUserPolicy", "DeleteUserPolicy", + "CreatePolicy", "DeletePolicy" ] } }) @@ -96,10 +96,10 @@ resource "aws_cloudwatch_event_rule" "detect_iam_delete" { name = "detect-iam-deletion" description = "Detect deletion of IAM users, roles, or login profiles" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { - eventName = ["DeleteUser","DeleteRole","DeleteLoginProfile"] + eventName = ["DeleteUser", "DeleteRole", "DeleteLoginProfile"] } }) event_bus_name = "default" @@ -124,10 +124,10 @@ resource "aws_cloudwatch_event_rule" "detect_cloudtrail_disable" { name = "detect-cloudtrail-disable" description = "Detect when CloudTrail is stopped or deleted" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { - eventName = ["StopLogging","DeleteTrail"] + eventName = ["StopLogging", "DeleteTrail"] } }) event_bus_name = "default" @@ -152,10 +152,10 @@ resource "aws_cloudwatch_event_rule" "detect_mfa_deactivate" { name = "detect-mfa-deactivation" description = "Detect MFA deactivation or deletion" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { - eventName = ["DeactivateMFADevice","DeleteVirtualMFADevice"] + eventName = ["DeactivateMFADevice", "DeleteVirtualMFADevice"] } }) event_bus_name = "default" @@ -180,12 +180,12 @@ resource "aws_cloudwatch_event_rule" "detect_sg_change" { name = "detect-security-group-change" description = "Detect changes to Security Group ingress/egress rules" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { eventName = [ - "AuthorizeSecurityGroupIngress","RevokeSecurityGroupIngress", - "AuthorizeSecurityGroupEgress","RevokeSecurityGroupEgress" + "AuthorizeSecurityGroupIngress", "RevokeSecurityGroupIngress", + "AuthorizeSecurityGroupEgress", "RevokeSecurityGroupEgress" ] } }) @@ -211,10 +211,10 @@ resource "aws_cloudwatch_event_rule" "detect_s3_public" { name = "detect-s3-public-access" description = "Detect when S3 bucket ACL or policy makes it public" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { - eventName = ["PutBucketAcl","PutBucketPolicy","PutPublicAccessBlock"] + eventName = ["PutBucketAcl", "PutBucketPolicy", "PutPublicAccessBlock"] } }) event_bus_name = "default" @@ -239,7 +239,7 @@ resource "aws_cloudwatch_event_rule" "detect_ec2_launch" { name = "detect-ec2-instance-launch" description = "Detect when EC2 instances are launched" event_pattern = jsonencode({ - source = ["aws.cloudtrail"] + source = ["aws.cloudtrail"] "detail-type" = ["AWS API Call via CloudTrail"] detail = { eventName = ["RunInstances"] diff --git a/operation-team-account/modules/eventbridge/outputs.tf b/operation-team-account/modules/eventbridge/outputs.tf index 17c5efcd..cf9967c0 100644 --- a/operation-team-account/modules/eventbridge/outputs.tf +++ b/operation-team-account/modules/eventbridge/outputs.tf @@ -1,3 +1,3 @@ output "event_rule_arn" { - value = aws_cloudwatch_event_rule.s3_object_created.arn + value = aws_cloudwatch_event_rule.s3_object_created.arn } \ No newline at end of file diff --git a/operation-team-account/modules/lambda/main.tf b/operation-team-account/modules/lambda/main.tf index 86cd88e7..8e720b61 100644 --- a/operation-team-account/modules/lambda/main.tf +++ b/operation-team-account/modules/lambda/main.tf @@ -21,9 +21,9 @@ resource "aws_iam_role_policy" "lambda_policy" { Version = "2012-10-17" Statement = [ { - Sid = "AllowCloudWatchLogs" - Effect = "Allow" - Action = [ + Sid = "AllowCloudWatchLogs" + Effect = "Allow" + Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" @@ -31,9 +31,9 @@ resource "aws_iam_role_policy" "lambda_policy" { Resource = "*" }, { - Sid = "AllowOpenSearchAccess" - Effect = "Allow" - Action = [ + Sid = "AllowOpenSearchAccess" + Effect = "Allow" + Action = [ "es:ESHttpPost", "es:ESHttpPut", "es:ESHttpGet" diff --git a/operation-team-account/modules/opensearch/policy.tf b/operation-team-account/modules/opensearch/policy.tf index 7dbe026f..7750df64 100644 --- a/operation-team-account/modules/opensearch/policy.tf +++ b/operation-team-account/modules/opensearch/policy.tf @@ -2,22 +2,22 @@ resource "aws_opensearch_domain_policy" "siem_policy" { domain_name = aws_opensearch_domain.siem.domain_name access_policies = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Effect = "Allow", - Principal = { - AWS = var.lambda_role_arn - }, - Action = [ - "es:ESHttpPut", - "es:ESHttpPost", - "es:ESHttpGet" - ], - Resource = [ - "${aws_opensearch_domain.siem.arn}/security-events-*/*" - ] - } - ] -}) + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = var.lambda_role_arn + }, + Action = [ + "es:ESHttpPut", + "es:ESHttpPost", + "es:ESHttpGet" + ], + Resource = [ + "${aws_opensearch_domain.siem.arn}/security-events-*/*" + ] + } + ] + }) } \ No newline at end of file diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index b7b78ba3..8c6bf902 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -9,8 +9,8 @@ resource "aws_kms_key" "cloudtrail" { Statement = [ # 운영 계정 루트에게 전체 권한 { - Sid = "AllowRootAccountFullAccess" - Effect = "Allow" + Sid = "AllowRootAccountFullAccess" + Effect = "Allow" Principal = { AWS = [ "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", # Operation root @@ -39,7 +39,7 @@ resource "aws_kms_key" "cloudtrail" { Resource = "*" Condition = { StringEquals = { - "kms:CallerAccount": "${var.management_account_id}" + "kms:CallerAccount" : "${var.management_account_id}" } } } @@ -53,7 +53,7 @@ resource "aws_kms_alias" "cloudtrail" { } resource "aws_s3_bucket" "cloudtrail_logs" { - bucket = var.bucket_name + bucket = var.bucket_name force_destroy = false lifecycle { @@ -67,6 +67,15 @@ resource "aws_s3_bucket" "cloudtrail_logs" { } } +resource "aws_s3_bucket_public_access_block" "cloudtrail_logs" { + bucket = aws_s3_bucket.cloudtrail_logs.id + + block_public_acls = true + ignore_public_acls = true + block_public_policy = true + restrict_public_buckets = true +} + resource "aws_s3_bucket_versioning" "this" { bucket = aws_s3_bucket.cloudtrail_logs.id versioning_configuration { @@ -89,29 +98,29 @@ resource "aws_s3_bucket_policy" "allow_cloudtrail" { bucket = aws_s3_bucket.cloudtrail_logs.id policy = jsonencode({ - Version: "2012-10-17", - Statement = [ - { - Sid = "AWSCloudTrailWrite", - Effect = "Allow", - Principal = { "Service": "cloudtrail.amazonaws.com" }, - Action = "s3:PutObject", - Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.organization_id}/*", - Condition = { - StringEquals = { - "s3:x-amz-acl" = "bucket-owner-full-control" + Version : "2012-10-17", + Statement = [ + { + Sid = "AWSCloudTrailWrite", + Effect = "Allow", + Principal = { "Service" : "cloudtrail.amazonaws.com" }, + Action = "s3:PutObject", + Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.organization_id}/*", + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + } } - } - }, - { - Sid = "AWSCloudTrailAclCheck", - Effect = "Allow", - Principal = { "Service": "cloudtrail.amazonaws.com" }, - Action = "s3:GetBucketAcl", - Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" - }, - { - Sid = "AllowOperationAndManagementListBucket", + }, + { + Sid = "AWSCloudTrailAclCheck", + Effect = "Allow", + Principal = { "Service" : "cloudtrail.amazonaws.com" }, + Action = "s3:GetBucketAcl", + Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" + }, + { + Sid = "AllowOperationAndManagementListBucket", Effect = "Allow", Principal = { AWS = [ @@ -124,7 +133,7 @@ resource "aws_s3_bucket_policy" "allow_cloudtrail" { ], Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" } - ] -} -) + ] + } + ) } \ No newline at end of file From 5abfe85d384c9f6a09e191c27f774437051f943a Mon Sep 17 00:00:00 2001 From: 3olly Date: Mon, 7 Jul 2025 11:02:24 +0900 Subject: [PATCH 08/19] =?UTF-8?q?fix:=20s3=20main.tf=20kms=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/outputs.tf | 8 +++ operation-team-account/modules/s3/main.tf | 65 ++++++++++++++--------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/management-team-account/outputs.tf b/management-team-account/outputs.tf index d8f71786..167d71d5 100644 --- a/management-team-account/outputs.tf +++ b/management-team-account/outputs.tf @@ -1,4 +1,12 @@ output "management_account_id" { description = "Account ID of the management account" value = data.aws_caller_identity.current.account_id +} + +output "debug_cloudtrail_s3_bucket_name" { + value = data.terraform_remote_state.operation.outputs.bucket_name +} + +output "debug_cloudtrail_kms_key_arn" { + value = data.terraform_remote_state.operation.outputs.kms_key_arn } \ No newline at end of file diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 8c6bf902..3f6abcae 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -1,5 +1,6 @@ data "aws_caller_identity" "current" {} +# KMS 키 생성 (CloudTrail 로그 암호화용) resource "aws_kms_key" "cloudtrail" { description = "KMS key for encrypting CloudTrail logs" enable_key_rotation = true @@ -7,39 +8,55 @@ resource "aws_kms_key" "cloudtrail" { policy = jsonencode({ Version = "2012-10-17", Statement = [ - # 운영 계정 루트에게 전체 권한 { - Sid = "AllowRootAccountFullAccess" - Effect = "Allow" + Sid = "Enable management & operation root access", + Effect = "Allow", Principal = { AWS = [ - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", # Operation root - "arn:aws:iam::${var.management_account_id}:root" # Management root + "arn:aws:iam::${var.management_account_id}:root", + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" ] - } - Action = "kms:*" + }, + Action = "kms:*", Resource = "*" }, - # CloudTrail 서비스에서 KMS 키 사용 허용 (management account) { - Sid = "AllowCloudTrailFromMgmtToUseKMS" - Effect = "Allow" + Sid = "Allow CloudTrail org trail use of the key", + Effect = "Allow", Principal = { Service = "cloudtrail.amazonaws.com" - } + }, Action = [ "kms:GenerateDataKey*", "kms:Decrypt", - "kms:ReEncrypt*", - "kms:DescribeKey", - "kms:ListKeys", + "kms:DescribeKey" + ], + Resource = "*", + Condition = { + StringEquals = { + "kms:CallerAccount" : "${var.management_account_id}", + "kms:ViaService" : "cloudtrail.${var.aws_region}.amazonaws.com" + } + } + }, + { + Sid = "Allow CloudTrail S3 encryption access", + Effect = "Allow", + Principal = { + Service = "cloudtrail.amazonaws.com" + }, + Action = [ + "kms:GenerateDataKey*", "kms:Encrypt", - "kms:ListAliases" - ] - Resource = "*" + "kms:ReEncrypt*", + "kms:Decrypt", + "kms:DescribeKey" + ], + Resource = "*", Condition = { StringEquals = { - "kms:CallerAccount" : "${var.management_account_id}" + "kms:CallerAccount" : "${var.management_account_id}", + "kms:ViaService" : "s3.${var.aws_region}.amazonaws.com" } } } @@ -78,6 +95,7 @@ resource "aws_s3_bucket_public_access_block" "cloudtrail_logs" { resource "aws_s3_bucket_versioning" "this" { bucket = aws_s3_bucket.cloudtrail_logs.id + versioning_configuration { status = "Enabled" } @@ -98,14 +116,14 @@ resource "aws_s3_bucket_policy" "allow_cloudtrail" { bucket = aws_s3_bucket.cloudtrail_logs.id policy = jsonencode({ - Version : "2012-10-17", + Version = "2012-10-17", Statement = [ { Sid = "AWSCloudTrailWrite", Effect = "Allow", Principal = { "Service" : "cloudtrail.amazonaws.com" }, Action = "s3:PutObject", - Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.organization_id}/*", + Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.management_account_id}/*", Condition = { StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" @@ -128,12 +146,9 @@ resource "aws_s3_bucket_policy" "allow_cloudtrail" { "arn:aws:iam::${var.management_account_id}:root" ] }, - Action = [ - "s3:ListBucket" - ], + Action = ["s3:ListBucket"], Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" } ] - } - ) + }) } \ No newline at end of file From 34f9b3882e31bf3263033a4f4e9539522de64177 Mon Sep 17 00:00:00 2001 From: 3olly Date: Mon, 7 Jul 2025 11:05:40 +0900 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20format=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/modules/s3/main.tf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 3f6abcae..849dbfb3 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -9,7 +9,7 @@ resource "aws_kms_key" "cloudtrail" { Version = "2012-10-17", Statement = [ { - Sid = "Enable management & operation root access", + Sid = "Enable management & operation root access", Effect = "Allow", Principal = { AWS = [ @@ -17,11 +17,11 @@ resource "aws_kms_key" "cloudtrail" { "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" ] }, - Action = "kms:*", + Action = "kms:*", Resource = "*" }, { - Sid = "Allow CloudTrail org trail use of the key", + Sid = "Allow CloudTrail org trail use of the key", Effect = "Allow", Principal = { Service = "cloudtrail.amazonaws.com" @@ -35,12 +35,12 @@ resource "aws_kms_key" "cloudtrail" { Condition = { StringEquals = { "kms:CallerAccount" : "${var.management_account_id}", - "kms:ViaService" : "cloudtrail.${var.aws_region}.amazonaws.com" + "kms:ViaService" : "cloudtrail.${var.aws_region}.amazonaws.com" } } }, { - Sid = "Allow CloudTrail S3 encryption access", + Sid = "Allow CloudTrail S3 encryption access", Effect = "Allow", Principal = { Service = "cloudtrail.amazonaws.com" @@ -56,7 +56,7 @@ resource "aws_kms_key" "cloudtrail" { Condition = { StringEquals = { "kms:CallerAccount" : "${var.management_account_id}", - "kms:ViaService" : "s3.${var.aws_region}.amazonaws.com" + "kms:ViaService" : "s3.${var.aws_region}.amazonaws.com" } } } @@ -146,7 +146,7 @@ resource "aws_s3_bucket_policy" "allow_cloudtrail" { "arn:aws:iam::${var.management_account_id}:root" ] }, - Action = ["s3:ListBucket"], + Action = ["s3:ListBucket"], Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" } ] From 3f9fa65251c0ff68f27b9ffd1b2fce0177d57139 Mon Sep 17 00:00:00 2001 From: 3olly Date: Mon, 7 Jul 2025 12:19:59 +0900 Subject: [PATCH 10/19] =?UTF-8?q?fix:=20lambda=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/main.tf | 2 +- .../{ => modules}/lambda/lambda_function.py | 0 .../{ => modules}/lambda/lambda_package.zip | Bin 3 files changed, 1 insertion(+), 1 deletion(-) rename operation-team-account/{ => modules}/lambda/lambda_function.py (100%) rename operation-team-account/{ => modules}/lambda/lambda_package.zip (100%) diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index aa8afbe8..8e458859 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -76,7 +76,7 @@ module "opensearch" { module "lambda" { source = "./modules/lambda" lambda_function_name = "cloudtrail-log-processor" - lambda_zip_path = "./lambda/lambda_package.zip" + lambda_zip_path = "./modules/lambda/lambda_package.zip" opensearch_domain_arn = module.opensearch.domain_arn opensearch_endpoint = module.opensearch.endpoint slack_webhook_url = var.slack_webhook_url diff --git a/operation-team-account/lambda/lambda_function.py b/operation-team-account/modules/lambda/lambda_function.py similarity index 100% rename from operation-team-account/lambda/lambda_function.py rename to operation-team-account/modules/lambda/lambda_function.py diff --git a/operation-team-account/lambda/lambda_package.zip b/operation-team-account/modules/lambda/lambda_package.zip similarity index 100% rename from operation-team-account/lambda/lambda_package.zip rename to operation-team-account/modules/lambda/lambda_package.zip From 505b2d7bc12e76d0e36861574e22e725c0e618b1 Mon Sep 17 00:00:00 2001 From: 3olly Date: Tue, 8 Jul 2025 09:51:24 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix:cloudtrail->s3=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=A0=84=EC=86=A1=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/main.tf | 7 +- management-team-account/variables.tf | 25 +-- operation-team-account/main.tf | 8 +- operation-team-account/modules/s3/main.tf | 172 +++++------------- operation-team-account/modules/s3/outputs.tf | 9 +- .../modules/s3/variables.tf | 25 +-- 6 files changed, 63 insertions(+), 183 deletions(-) diff --git a/management-team-account/main.tf b/management-team-account/main.tf index f02272e9..7769d7af 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -34,7 +34,7 @@ data "terraform_remote_state" "operation" { data "aws_caller_identity" "current" {} -resource "aws_cloudtrail" "organization" { +resource "aws_cloudtrail" "org" { name = var.org_trail_name is_organization_trail = true is_multi_region_trail = true @@ -42,12 +42,11 @@ resource "aws_cloudtrail" "organization" { enable_log_file_validation = true enable_logging = true - s3_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name - kms_key_id = data.terraform_remote_state.operation.outputs.kms_key_arn + s3_bucket_name = var.cloudtrail_bucket_name tags = { Name = var.org_trail_name Environment = "prod" Owner = "security-team" } -} \ No newline at end of file +} diff --git a/management-team-account/variables.tf b/management-team-account/variables.tf index 00465e80..955da3f0 100644 --- a/management-team-account/variables.tf +++ b/management-team-account/variables.tf @@ -1,31 +1,14 @@ variable "aws_region" { - description = "AWS Region" + description = "AWS 리전" type = string - default = "ap-northeast-2" } variable "org_trail_name" { - description = "Name of the organization trail" + description = "Organization CloudTrail 이름" type = string - default = "org-cloudtrail" } -variable "destination_s3_bucket_name" { - description = "S3 bucket name in operation account" - type = string -} - -variable "destination_s3_bucket_arn" { - description = "ARN of the S3 bucket in operation account" - type = string -} - -variable "s3_kms_key_arn" { - description = "ARN of the KMS key used to encrypt the logs (in operation account)" - type = string -} - -variable "sso_role_name" { - description = "The name of the AWS SSO role to attach policy to" +variable "cloudtrail_bucket_name" { + description = "Operation 계정에서 생성된 CloudTrail 로그용 S3 버킷 이름" type = string } \ No newline at end of file diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index 8e458859..1e38f7dc 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -50,12 +50,8 @@ data "aws_security_group" "default" { # 2) S3 모듈: CloudTrail 로그 버킷 + KMS module "s3" { - source = "./modules/s3" - bucket_name = var.cloudtrail_bucket_name - cloudtrail_name = var.org_trail_name - aws_region = var.aws_region - management_account_id = var.management_account_id - organization_id = var.organization_id + source = "./modules/s3" + bucket_name = var.cloudtrail_bucket_name } # 3) OpenSearch 모듈: 도메인 생성 + 접근 정책 diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 849dbfb3..55feec0b 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -1,154 +1,82 @@ -data "aws_caller_identity" "current" {} - -# KMS 키 생성 (CloudTrail 로그 암호화용) resource "aws_kms_key" "cloudtrail" { - description = "KMS key for encrypting CloudTrail logs" - enable_key_rotation = true - - policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Sid = "Enable management & operation root access", - Effect = "Allow", - Principal = { - AWS = [ - "arn:aws:iam::${var.management_account_id}:root", - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" - ] - }, - Action = "kms:*", - Resource = "*" - }, - { - Sid = "Allow CloudTrail org trail use of the key", - Effect = "Allow", - Principal = { - Service = "cloudtrail.amazonaws.com" - }, - Action = [ - "kms:GenerateDataKey*", - "kms:Decrypt", - "kms:DescribeKey" - ], - Resource = "*", - Condition = { - StringEquals = { - "kms:CallerAccount" : "${var.management_account_id}", - "kms:ViaService" : "cloudtrail.${var.aws_region}.amazonaws.com" - } - } - }, - { - Sid = "Allow CloudTrail S3 encryption access", - Effect = "Allow", - Principal = { - Service = "cloudtrail.amazonaws.com" - }, - Action = [ - "kms:GenerateDataKey*", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:Decrypt", - "kms:DescribeKey" - ], - Resource = "*", - Condition = { - StringEquals = { - "kms:CallerAccount" : "${var.management_account_id}", - "kms:ViaService" : "s3.${var.aws_region}.amazonaws.com" - } - } - } - ] - }) -} - -resource "aws_kms_alias" "cloudtrail" { - name = "alias/cloudtrail-logs" - target_key_id = aws_kms_key.cloudtrail.key_id -} - -resource "aws_s3_bucket" "cloudtrail_logs" { - bucket = var.bucket_name - force_destroy = false - - lifecycle { - prevent_destroy = true - } - - tags = { - Name = var.bucket_name - Environment = "prod" - Owner = "security-team" - } + description = "KMS key for encrypting CloudTrail logs in S3" + deletion_window_in_days = 30 } -resource "aws_s3_bucket_public_access_block" "cloudtrail_logs" { - bucket = aws_s3_bucket.cloudtrail_logs.id - - block_public_acls = true - ignore_public_acls = true - block_public_policy = true - restrict_public_buckets = true +resource "aws_s3_bucket" "logs" { + bucket = "${var.bucket_name}" } -resource "aws_s3_bucket_versioning" "this" { - bucket = aws_s3_bucket.cloudtrail_logs.id +resource "aws_s3_bucket_versioning" "logs" { + bucket = aws_s3_bucket.logs.id versioning_configuration { status = "Enabled" } } -resource "aws_s3_bucket_server_side_encryption_configuration" "this" { - bucket = aws_s3_bucket.cloudtrail_logs.id +resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { + bucket = aws_s3_bucket.logs.id rule { apply_server_side_encryption_by_default { - kms_master_key_id = aws_kms_key.cloudtrail.arn sse_algorithm = "aws:kms" + kms_master_key_id = aws_kms_key.cloudtrail.arn } } } -resource "aws_s3_bucket_policy" "allow_cloudtrail" { - bucket = aws_s3_bucket.cloudtrail_logs.id +resource "aws_s3_bucket_public_access_block" "block" { + bucket = aws_s3_bucket.logs.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_policy" "cloudtrail" { + bucket = aws_s3_bucket.logs.id policy = jsonencode({ - Version = "2012-10-17", + Version = "2012-10-17" Statement = [ + # 1) ACL 확인 허용 + { + Sid = "AllowCloudTrailAclCheck" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.logs.arn + }, + + # 2) 로그 쓰기 + bucket-owner-full-control ACL 조건 { - Sid = "AWSCloudTrailWrite", - Effect = "Allow", - Principal = { "Service" : "cloudtrail.amazonaws.com" }, - Action = "s3:PutObject", - Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.management_account_id}/*", + Sid = "AllowCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" Condition = { StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" } } - }, - { - Sid = "AWSCloudTrailAclCheck", - Effect = "Allow", - Principal = { "Service" : "cloudtrail.amazonaws.com" }, - Action = "s3:GetBucketAcl", - Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" - }, - { - Sid = "AllowOperationAndManagementListBucket", - Effect = "Allow", - Principal = { - AWS = [ - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", - "arn:aws:iam::${var.management_account_id}:root" - ] - }, - Action = ["s3:ListBucket"], - Resource = "${aws_s3_bucket.cloudtrail_logs.arn}" } ] }) +} + +resource "aws_s3_bucket_lifecycle_configuration" "logs" { + bucket = aws_s3_bucket.logs.id + + rule { + id = "expire-logs-after-30-days" + status = "Enabled" + + filter { prefix = "" } + + expiration { + days = 30 + } + } } \ No newline at end of file diff --git a/operation-team-account/modules/s3/outputs.tf b/operation-team-account/modules/s3/outputs.tf index bbf5904e..2781a629 100644 --- a/operation-team-account/modules/s3/outputs.tf +++ b/operation-team-account/modules/s3/outputs.tf @@ -1,14 +1,11 @@ output "bucket_name" { - value = aws_s3_bucket.cloudtrail_logs.bucket - description = "S3 bucket name for CloudTrail logs" + value = aws_s3_bucket.logs.bucket } output "bucket_arn" { - value = aws_s3_bucket.cloudtrail_logs.arn - description = "ARN of the S3 bucket" + value = aws_s3_bucket.logs.arn } output "kms_key_arn" { - value = aws_kms_key.cloudtrail.arn - description = "KMS key ARN for S3 encryption" + value = aws_kms_key.cloudtrail.arn } \ No newline at end of file diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index 2f321475..4fe67a7b 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -1,24 +1 @@ -variable "bucket_name" { - description = "S3 bucket name for CloudTrail logs" - type = string -} - -variable "cloudtrail_name" { - description = "Name of the CloudTrail (for tag, optional)" - type = string -} - -variable "aws_region" { - description = "AWS Region" - type = string -} - -variable "management_account_id" { - description = "Management account AWS ID" - type = string -} - -variable "organization_id" { - description = "Organization ID (for cross-account policy, optional)" - type = string -} \ No newline at end of file +variable "bucket_name" { type = string } \ No newline at end of file From b697303f2f309d18ef3093e82a63793ca805bca2 Mon Sep 17 00:00:00 2001 From: 3olly Date: Tue, 8 Jul 2025 15:52:44 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20SSE-KMS=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/main.tf | 3 +- operation-team-account/main.tf | 1 + operation-team-account/modules/s3/main.tf | 37 ++++++++++++++++++- .../modules/s3/variables.tf | 7 +++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/management-team-account/main.tf b/management-team-account/main.tf index 7769d7af..960e8272 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -42,7 +42,8 @@ resource "aws_cloudtrail" "org" { enable_log_file_validation = true enable_logging = true - s3_bucket_name = var.cloudtrail_bucket_name + s3_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name + kms_key_id = data.terraform_remote_state.operation.outputs.kms_key_arn tags = { Name = var.org_trail_name diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index 1e38f7dc..97ad9699 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -52,6 +52,7 @@ data "aws_security_group" "default" { module "s3" { source = "./modules/s3" bucket_name = var.cloudtrail_bucket_name + aws_region = var.aws_region } # 3) OpenSearch 모듈: 도메인 생성 + 접근 정책 diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 55feec0b..ec99b2ee 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -1,6 +1,39 @@ +data "aws_caller_identity" "current" {} + resource "aws_kms_key" "cloudtrail" { description = "KMS key for encrypting CloudTrail logs in S3" deletion_window_in_days = 30 + + # 키 정책: 계정 루트 + CloudTrail 서비스 허용 + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + # 이 KMS 키를 만든 계정(root)이 모든 작업을 할 수 있도록 + { + Sid = "AllowAccountRootFullAccess" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + + # CloudTrail 서비스가 이 키로 암호화 작업을 할 수 있도록 + { + Sid = "AllowCloudTrailUseOfKey" + Effect = "Allow" + Principal = { + Service = "cloudtrail.amazonaws.com" + } + Action = [ + "kms:GenerateDataKey*", + "kms:Decrypt" + ] + Resource = "*" + } + ] + }) } resource "aws_s3_bucket" "logs" { @@ -40,7 +73,7 @@ resource "aws_s3_bucket_policy" "cloudtrail" { policy = jsonencode({ Version = "2012-10-17" Statement = [ - # 1) ACL 확인 허용 + # ACL 확인 허용 { Sid = "AllowCloudTrailAclCheck" Effect = "Allow" @@ -49,7 +82,7 @@ resource "aws_s3_bucket_policy" "cloudtrail" { Resource = aws_s3_bucket.logs.arn }, - # 2) 로그 쓰기 + bucket-owner-full-control ACL 조건 + # 로그 쓰기 + bucket-owner-full-control ACL 조건 { Sid = "AllowCloudTrailWrite" Effect = "Allow" diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index 4fe67a7b..d3eb0509 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -1 +1,6 @@ -variable "bucket_name" { type = string } \ No newline at end of file +variable "bucket_name" { type = string } + +variable "aws_region" { + description = "Region where the KMS key is created" + type = string +} \ No newline at end of file From c8eb33bf3dd3986f45c4e602a1e49ac2bb1388ed Mon Sep 17 00:00:00 2001 From: 3olly Date: Tue, 8 Jul 2025 18:00:33 +0900 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20cloudtrail=20module=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/main.tf | 23 +++++-------------- .../modules/cloudtrail/main.tf | 17 ++++++++++++++ .../modules/cloudtrail/variables.tf | 14 +++++++++++ management-team-account/outputs.tf | 12 ---------- management-team-account/variables.tf | 11 ++++----- 5 files changed, 41 insertions(+), 36 deletions(-) create mode 100644 management-team-account/modules/cloudtrail/main.tf create mode 100644 management-team-account/modules/cloudtrail/variables.tf delete mode 100644 management-team-account/outputs.tf diff --git a/management-team-account/main.tf b/management-team-account/main.tf index 960e8272..0a7e3d44 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -34,20 +34,9 @@ data "terraform_remote_state" "operation" { data "aws_caller_identity" "current" {} -resource "aws_cloudtrail" "org" { - name = var.org_trail_name - is_organization_trail = true - is_multi_region_trail = true - include_global_service_events = true - enable_log_file_validation = true - enable_logging = true - - s3_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name - kms_key_id = data.terraform_remote_state.operation.outputs.kms_key_arn - - tags = { - Name = var.org_trail_name - Environment = "prod" - Owner = "security-team" - } -} +module "cloudtrail" { + source = "./modules/cloudtrail" + org_trail_name = var.org_trail_name + cloudtrail_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name + cloudtrail_kms_key_arn = data.terraform_remote_state.operation.outputs.kms_key_arn +} \ No newline at end of file diff --git a/management-team-account/modules/cloudtrail/main.tf b/management-team-account/modules/cloudtrail/main.tf new file mode 100644 index 00000000..bd66a554 --- /dev/null +++ b/management-team-account/modules/cloudtrail/main.tf @@ -0,0 +1,17 @@ +resource "aws_cloudtrail" "org" { + name = var.org_trail_name + is_organization_trail = true + is_multi_region_trail = true + include_global_service_events = true + enable_log_file_validation = true + enable_logging = true + + s3_bucket_name = var.cloudtrail_bucket_name + kms_key_id = var.cloudtrail_kms_key_arn + + tags = { + Name = var.org_trail_name + Environment = "prod" + Owner = "security-team" + } +} \ No newline at end of file diff --git a/management-team-account/modules/cloudtrail/variables.tf b/management-team-account/modules/cloudtrail/variables.tf new file mode 100644 index 00000000..0133252c --- /dev/null +++ b/management-team-account/modules/cloudtrail/variables.tf @@ -0,0 +1,14 @@ +variable "org_trail_name" { + description = "Organization CloudTrail name" + type = string +} + +variable "cloudtrail_bucket_name" { + description = "S3 bucket name for CloudTrail logs (from operation account)" + type = string +} + +variable "cloudtrail_kms_key_arn" { + description = "KMS key ARN for CloudTrail SSE-KMS (from operation account)" + type = string +} diff --git a/management-team-account/outputs.tf b/management-team-account/outputs.tf deleted file mode 100644 index 167d71d5..00000000 --- a/management-team-account/outputs.tf +++ /dev/null @@ -1,12 +0,0 @@ -output "management_account_id" { - description = "Account ID of the management account" - value = data.aws_caller_identity.current.account_id -} - -output "debug_cloudtrail_s3_bucket_name" { - value = data.terraform_remote_state.operation.outputs.bucket_name -} - -output "debug_cloudtrail_kms_key_arn" { - value = data.terraform_remote_state.operation.outputs.kms_key_arn -} \ No newline at end of file diff --git a/management-team-account/variables.tf b/management-team-account/variables.tf index 955da3f0..27580262 100644 --- a/management-team-account/variables.tf +++ b/management-team-account/variables.tf @@ -1,14 +1,11 @@ variable "aws_region" { - description = "AWS 리전" + description = "AWS region" type = string + default = "ap-northeast-2" } variable "org_trail_name" { - description = "Organization CloudTrail 이름" - type = string -} - -variable "cloudtrail_bucket_name" { - description = "Operation 계정에서 생성된 CloudTrail 로그용 S3 버킷 이름" + description = "Organization CloudTrail name" type = string + default = "org-cloudtrail" } \ No newline at end of file From 622836fec4de81f89a5b6f640ab7388322224a11 Mon Sep 17 00:00:00 2001 From: 3olly Date: Tue, 8 Jul 2025 21:03:46 +0900 Subject: [PATCH 14/19] fix: backend s3 migration (operation, management) --- management-team-account/main.tf | 2 +- operation-team-account/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management-team-account/main.tf b/management-team-account/main.tf index 0a7e3d44..7fec747e 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -8,7 +8,7 @@ terraform { } backend "s3" { - bucket = "cloudfence-management-s3" + bucket = "cloudfence-management-state" key = "cloudtrail/terraform.tfstate" region = "ap-northeast-2" encrypt = true diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index 97ad9699..05f83dda 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -7,7 +7,7 @@ terraform { } } backend "s3" { - bucket = "cloudfence-operation-s3" + bucket = "cloudfence-operation-state" key = "monitoring/terraform.tfstate" region = "ap-northeast-2" encrypt = true From 350e48380fd04a9f45d39960a8cd66d9cf878a1c Mon Sep 17 00:00:00 2001 From: 3olly Date: Tue, 8 Jul 2025 21:45:47 +0900 Subject: [PATCH 15/19] =?UTF-8?q?chore:=20=EA=B0=95=ED=99=94=EB=90=9C=20s3?= =?UTF-8?q?=20bucket&KMS=20=EC=A0=95=EC=B1=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/main.tf | 8 ++-- operation-team-account/main.tf | 5 +++ operation-team-account/modules/s3/main.tf | 43 ++++++++++++++++--- .../modules/s3/variables.tf | 5 +++ operation-team-account/variables.tf | 10 ----- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/management-team-account/main.tf b/management-team-account/main.tf index 7fec747e..4d8457e7 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -35,8 +35,8 @@ data "terraform_remote_state" "operation" { data "aws_caller_identity" "current" {} module "cloudtrail" { - source = "./modules/cloudtrail" - org_trail_name = var.org_trail_name - cloudtrail_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name - cloudtrail_kms_key_arn = data.terraform_remote_state.operation.outputs.kms_key_arn + source = "./modules/cloudtrail" + org_trail_name = var.org_trail_name + cloudtrail_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name + cloudtrail_kms_key_arn = data.terraform_remote_state.operation.outputs.kms_key_arn } \ No newline at end of file diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index 05f83dda..e001d886 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -48,11 +48,16 @@ data "aws_security_group" "default" { vpc_id = data.aws_vpc.default.id } +data "aws_caller_identity" "management" { + provider = aws.management +} + # 2) S3 모듈: CloudTrail 로그 버킷 + KMS module "s3" { source = "./modules/s3" bucket_name = var.cloudtrail_bucket_name aws_region = var.aws_region + management_account_id = data.aws_caller_identity.management.account_id } # 3) OpenSearch 모듈: 도메인 생성 + 접근 정책 diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index ec99b2ee..46c6125d 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -8,10 +8,23 @@ resource "aws_kms_key" "cloudtrail" { policy = jsonencode({ Version = "2012-10-17" Statement = [ + { + Sid = "DenyInsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.logs.arn, + "${aws_s3_bucket.logs.arn}/*" + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }, # 이 KMS 키를 만든 계정(root)이 모든 작업을 할 수 있도록 { - Sid = "AllowAccountRootFullAccess" - Effect = "Allow" + Sid = "AllowAccountRootFullAccess" + Effect = "Allow" Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } @@ -21,8 +34,8 @@ resource "aws_kms_key" "cloudtrail" { # CloudTrail 서비스가 이 키로 암호화 작업을 할 수 있도록 { - Sid = "AllowCloudTrailUseOfKey" - Effect = "Allow" + Sid = "AllowCloudTrailUseOfKey" + Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } @@ -31,13 +44,31 @@ resource "aws_kms_key" "cloudtrail" { "kms:Decrypt" ] Resource = "*" + Condition = { + StringEquals = { + "kms:ViaService" = "cloudtrail.ap-northeast-2.amazonaws.com" + } + } + }, + { + Sid = "AllowCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/${var.management_account_id}/*" + Condition = { + StringEquals = { + "aws:SourceAccount" = var.management_account_id + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } } ] }) } resource "aws_s3_bucket" "logs" { - bucket = "${var.bucket_name}" + bucket = var.bucket_name } resource "aws_s3_bucket_versioning" "logs" { @@ -105,7 +136,7 @@ resource "aws_s3_bucket_lifecycle_configuration" "logs" { rule { id = "expire-logs-after-30-days" status = "Enabled" - + filter { prefix = "" } expiration { diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index d3eb0509..063dadcd 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -3,4 +3,9 @@ variable "bucket_name" { type = string } variable "aws_region" { description = "Region where the KMS key is created" type = string +} + +variable "management_account_id" { + description = "Account ID of the management account (for S3 bucket policy)" + type = string } \ No newline at end of file diff --git a/operation-team-account/variables.tf b/operation-team-account/variables.tf index bc79f532..b4a294e3 100644 --- a/operation-team-account/variables.tf +++ b/operation-team-account/variables.tf @@ -49,14 +49,4 @@ variable "slack_webhook_url" { variable "org_trail_name" { description = "Name of the organization CloudTrail trail" type = string -} - -variable "management_account_id" { - description = "AWS Account ID of the Management account (CloudTrail producer)" - type = string -} - -variable "organization_id" { - description = "AWS Organization ID used for cross-account policies" - type = string } \ No newline at end of file From 36e876f533e0e92226178a4fc95e5f259f584ebe Mon Sep 17 00:00:00 2001 From: 3olly Date: Tue, 8 Jul 2025 23:18:04 +0900 Subject: [PATCH 16/19] =?UTF-8?q?chore:=20=EA=B0=95=ED=99=94=EB=90=9C=20S3?= =?UTF-8?q?=20=EB=B2=84=ED=82=B7=C2=B7KMS=20=EC=A0=95=EC=B1=85=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- management-team-account/main.tf | 2 +- operation-team-account/main.tf | 1 + operation-team-account/modules/s3/main.tf | 53 ++++++++----------- .../modules/s3/variables.tf | 5 ++ operation-team-account/variables.tf | 7 +++ 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/management-team-account/main.tf b/management-team-account/main.tf index 4d8457e7..8364699f 100644 --- a/management-team-account/main.tf +++ b/management-team-account/main.tf @@ -25,7 +25,7 @@ provider "aws" { data "terraform_remote_state" "operation" { backend = "s3" config = { - bucket = "cloudfence-operation-s3" + bucket = "cloudfence-operation-state" key = "monitoring/terraform.tfstate" region = "ap-northeast-2" profile = "whs-sso-operation" diff --git a/operation-team-account/main.tf b/operation-team-account/main.tf index e001d886..b4470fac 100644 --- a/operation-team-account/main.tf +++ b/operation-team-account/main.tf @@ -57,6 +57,7 @@ module "s3" { source = "./modules/s3" bucket_name = var.cloudtrail_bucket_name aws_region = var.aws_region + kms_alias_name = var.kms_alias_name management_account_id = data.aws_caller_identity.management.account_id } diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 46c6125d..3c3e7c58 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -8,19 +8,6 @@ resource "aws_kms_key" "cloudtrail" { policy = jsonencode({ Version = "2012-10-17" Statement = [ - { - Sid = "DenyInsecureTransport" - Effect = "Deny" - Principal = "*" - Action = "s3:*" - Resource = [ - aws_s3_bucket.logs.arn, - "${aws_s3_bucket.logs.arn}/*" - ] - Condition = { - Bool = { "aws:SecureTransport" = "false" } - } - }, # 이 KMS 키를 만든 계정(root)이 모든 작업을 할 수 있도록 { Sid = "AllowAccountRootFullAccess" @@ -49,24 +36,16 @@ resource "aws_kms_key" "cloudtrail" { "kms:ViaService" = "cloudtrail.ap-northeast-2.amazonaws.com" } } - }, - { - Sid = "AllowCloudTrailWrite" - Effect = "Allow" - Principal = { Service = "cloudtrail.amazonaws.com" } - Action = "s3:PutObject" - Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/${var.management_account_id}/*" - Condition = { - StringEquals = { - "aws:SourceAccount" = var.management_account_id - "s3:x-amz-acl" = "bucket-owner-full-control" - } - } } ] }) } +resource "aws_kms_alias" "cloudtrail" { + name = var.kms_alias_name + target_key_id = aws_kms_key.cloudtrail.key_id +} + resource "aws_s3_bucket" "logs" { bucket = var.bucket_name } @@ -104,6 +83,20 @@ resource "aws_s3_bucket_policy" "cloudtrail" { policy = jsonencode({ Version = "2012-10-17" Statement = [ + # 0) HTTPS 아닌 요청 모두 거부 + { + Sid = "DenyInsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.logs.arn, + "${aws_s3_bucket.logs.arn}/*" + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }, # ACL 확인 허용 { Sid = "AllowCloudTrailAclCheck" @@ -112,17 +105,17 @@ resource "aws_s3_bucket_policy" "cloudtrail" { Action = "s3:GetBucketAcl" Resource = aws_s3_bucket.logs.arn }, - # 로그 쓰기 + bucket-owner-full-control ACL 조건 - { + { Sid = "AllowCloudTrailWrite" Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } Action = "s3:PutObject" - Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/${var.management_account_id}/*" Condition = { StringEquals = { - "s3:x-amz-acl" = "bucket-owner-full-control" + "aws:SourceAccount" = var.management_account_id + "s3:x-amz-acl" = "bucket-owner-full-control" } } } diff --git a/operation-team-account/modules/s3/variables.tf b/operation-team-account/modules/s3/variables.tf index 063dadcd..2a27fe30 100644 --- a/operation-team-account/modules/s3/variables.tf +++ b/operation-team-account/modules/s3/variables.tf @@ -8,4 +8,9 @@ variable "aws_region" { variable "management_account_id" { description = "Account ID of the management account (for S3 bucket policy)" type = string +} + +variable "kms_alias_name" { + description = "KMS key alias for CloudTrail logs" + type = string } \ No newline at end of file diff --git a/operation-team-account/variables.tf b/operation-team-account/variables.tf index b4a294e3..2cad90b1 100644 --- a/operation-team-account/variables.tf +++ b/operation-team-account/variables.tf @@ -49,4 +49,11 @@ variable "slack_webhook_url" { variable "org_trail_name" { description = "Name of the organization CloudTrail trail" type = string + default = "org-cloudtrail" +} + +variable "kms_alias_name" { + description = "KMS key alias for CloudTrail logs" + type = string + default = "alias/cloudtrail-logs" } \ No newline at end of file From abe80b607208daedafe3e2d6ad25ffaec2ef9c3f Mon Sep 17 00:00:00 2001 From: 3olly Date: Wed, 9 Jul 2025 10:44:08 +0900 Subject: [PATCH 17/19] =?UTF-8?q?fix:=20cloudtrail=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EB=A9=88=EC=B6=A4=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/modules/s3/main.tf | 27 ++++++++--------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 3c3e7c58..66461890 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -10,8 +10,8 @@ resource "aws_kms_key" "cloudtrail" { Statement = [ # 이 KMS 키를 만든 계정(root)이 모든 작업을 할 수 있도록 { - Sid = "AllowAccountRootFullAccess" - Effect = "Allow" + Sid = "AllowAccountRootFullAccess" + Effect = "Allow" Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } @@ -21,8 +21,8 @@ resource "aws_kms_key" "cloudtrail" { # CloudTrail 서비스가 이 키로 암호화 작업을 할 수 있도록 { - Sid = "AllowCloudTrailUseOfKey" - Effect = "Allow" + Sid = "AllowCloudTrailUseOfKey" + Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } @@ -31,23 +31,13 @@ resource "aws_kms_key" "cloudtrail" { "kms:Decrypt" ] Resource = "*" - Condition = { - StringEquals = { - "kms:ViaService" = "cloudtrail.ap-northeast-2.amazonaws.com" - } - } } ] }) } -resource "aws_kms_alias" "cloudtrail" { - name = var.kms_alias_name - target_key_id = aws_kms_key.cloudtrail.key_id -} - resource "aws_s3_bucket" "logs" { - bucket = var.bucket_name + bucket = "${var.bucket_name}" } resource "aws_s3_bucket_versioning" "logs" { @@ -83,7 +73,7 @@ resource "aws_s3_bucket_policy" "cloudtrail" { policy = jsonencode({ Version = "2012-10-17" Statement = [ - # 0) HTTPS 아닌 요청 모두 거부 + # HTTPS 아닌 요청 모두 거부 { Sid = "DenyInsecureTransport" Effect = "Deny" @@ -105,8 +95,9 @@ resource "aws_s3_bucket_policy" "cloudtrail" { Action = "s3:GetBucketAcl" Resource = aws_s3_bucket.logs.arn }, + # 로그 쓰기 + bucket-owner-full-control ACL 조건 - { + { Sid = "AllowCloudTrailWrite" Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } @@ -129,7 +120,7 @@ resource "aws_s3_bucket_lifecycle_configuration" "logs" { rule { id = "expire-logs-after-30-days" status = "Enabled" - + filter { prefix = "" } expiration { From 88cc060c36efce2f80ce01d5f43612ba56c584e6 Mon Sep 17 00:00:00 2001 From: 3olly Date: Wed, 9 Jul 2025 10:57:47 +0900 Subject: [PATCH 18/19] =?UTF-8?q?fix:=20cloudtrail=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EB=A9=88=EC=B6=A4=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/modules/s3/main.tf | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/operation-team-account/modules/s3/main.tf b/operation-team-account/modules/s3/main.tf index 66461890..82f3d66a 100644 --- a/operation-team-account/modules/s3/main.tf +++ b/operation-team-account/modules/s3/main.tf @@ -36,6 +36,11 @@ resource "aws_kms_key" "cloudtrail" { }) } +resource "aws_kms_alias" "cloudtrail" { + name = var.kms_alias_name + target_key_id = aws_kms_key.cloudtrail.key_id +} + resource "aws_s3_bucket" "logs" { bucket = "${var.bucket_name}" } @@ -102,11 +107,10 @@ resource "aws_s3_bucket_policy" "cloudtrail" { Effect = "Allow" Principal = { Service = "cloudtrail.amazonaws.com" } Action = "s3:PutObject" - Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/${var.management_account_id}/*" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" Condition = { StringEquals = { - "aws:SourceAccount" = var.management_account_id - "s3:x-amz-acl" = "bucket-owner-full-control" + "s3:x-amz-acl" = "bucket-owner-full-control" } } } From 54dc5d41576819da2a83d09d9a934a50a39294f5 Mon Sep 17 00:00:00 2001 From: luujaiyn Date: Sat, 12 Jul 2025 22:46:21 +0900 Subject: [PATCH 19/19] =?UTF-8?q?feat:=20OpeanSearch=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- operation-team-account/modules/lambda/main.tf | 2 +- .../opensearch/ISM/delete-after-30d.json | 28 +++++++++++++++ .../modules/opensearch/null_resource.tf | 35 +++++++++++++++++++ .../saved_queries/root-login.ndjson | 17 +++++++++ .../saved_queries/s3-public-acl.ndjson | 22 ++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 operation-team-account/modules/opensearch/ISM/delete-after-30d.json create mode 100644 operation-team-account/modules/opensearch/null_resource.tf create mode 100644 operation-team-account/modules/opensearch/saved_queries/root-login.ndjson create mode 100644 operation-team-account/modules/opensearch/saved_queries/s3-public-acl.ndjson diff --git a/operation-team-account/modules/lambda/main.tf b/operation-team-account/modules/lambda/main.tf index 8e720b61..09f1bfae 100644 --- a/operation-team-account/modules/lambda/main.tf +++ b/operation-team-account/modules/lambda/main.tf @@ -83,4 +83,4 @@ resource "aws_lambda_function" "log_processor" { resource "aws_iam_role_policy_attachment" "vpc_access" { role = aws_iam_role.lambda_exec.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" -} \ No newline at end of file +} diff --git a/operation-team-account/modules/opensearch/ISM/delete-after-30d.json b/operation-team-account/modules/opensearch/ISM/delete-after-30d.json new file mode 100644 index 00000000..1f757774 --- /dev/null +++ b/operation-team-account/modules/opensearch/ISM/delete-after-30d.json @@ -0,0 +1,28 @@ +{ + "policy": { + "policy_id": "delete-after-30d", + "description": "Delete indices older than 30 days", + "default_state": "hot", + "states": [ + { + "name": "hot", + "actions": [], + "transitions": [ + { + "state_name": "delete", + "conditions": { + "min_index_age": "30d" + } + } + ] + }, + { + "name": "delete", + "actions": [ + { "delete": {} } + ], + "transitions": [] + } + ] + } +} diff --git a/operation-team-account/modules/opensearch/null_resource.tf b/operation-team-account/modules/opensearch/null_resource.tf new file mode 100644 index 00000000..6c59c266 --- /dev/null +++ b/operation-team-account/modules/opensearch/null_resource.tf @@ -0,0 +1,35 @@ +resource "null_resource" "import_saved_query" { + provisioner "local-exec" { + command = <