diff --git a/operation-team-account/.gitignore b/operation-team-account/.gitignore new file mode 100644 index 00000000..e5ab9a69 --- /dev/null +++ b/operation-team-account/.gitignore @@ -0,0 +1,22 @@ +# Terraform 관련 파일 무시 +.terraform/ # terraform init 시 생성되는 폴더 +*.tfstate # 상태 파일 (리소스 실제 정보 포함) +*.tfstate.* # 상태 파일 백업 +*.tfvars +*.tfstate.backup +.terraform.lock.hcl +terraform.tfvars # 민감 정보 입력용 파일 +crash.log # Terraform 충돌 로그 +.terraform +# AWS CLI 자격 증명 +.aws/ # ~/.aws/credentials, config 등 + +# 시스템 자동 생성 파일 (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..d6d9a6be --- /dev/null +++ b/operation-team-account/main.tf @@ -0,0 +1,28 @@ +module "s3" { + source = "./modules/s3" +} + +module "opensearch" { + source = "./modules/opensearch" + sso_role_name = var.sso_role_name + sso_user_name = var.sso_user_name + firehose_role_arn = module.firehose.firehose_role_arn +} + +module "firehose" { + source = "./modules/firehose" + opensearch_domain_arn = module.opensearch.domain_arn + s3_bucket_arn = module.s3.bucket_arn +} + +module "cloudwatch" { + source = "./modules/cloudwatch" + + subscription_filter_name = "cw-to-firehose" + log_group_name = var.log_group_name + firehose_arn = module.firehose.delivery_stream_arn + role_arn = module.firehose.firehose_role_arn +} +module "securitylake" { + source = "./modules/securitylake" +} diff --git a/operation-team-account/modules/cloudwatch/main.tf b/operation-team-account/modules/cloudwatch/main.tf new file mode 100644 index 00000000..bbe3e6e7 --- /dev/null +++ b/operation-team-account/modules/cloudwatch/main.tf @@ -0,0 +1,33 @@ +resource "aws_cloudwatch_log_subscription_filter" "logs_to_firehose" { + name = var.subscription_filter_name + log_group_name = var.log_group_name + filter_pattern = "{ $.level = \"ERROR\" }" # ERROR 레벨만 전달 + destination_arn = var.firehose_arn + role_arn = var.role_arn +} + +resource "aws_cloudwatch_log_group" "target" { + name = var.log_group_name + retention_in_days = 14 # 14일 지나면 자동 삭제 +} + +resource "aws_cloudwatch_metric_filter" "error_count" { + name = "ErrorCount" + log_group_name = aws_cloudwatch_log_group.target.name + pattern = "\"ERROR\"" + metric_transformation { + name = "ErrorCount" + namespace = "MyApp/SIEM" + value = "1" + } +} + +resource "aws_cloudwatch_metric_alarm" "error_alarm" { + alarm_name = "HighErrorCount" + namespace = "MyApp/SIEM" + metric_name = aws_cloudwatch_metric_filter.error_count.metric_transformation[0].name + threshold = 100 + comparison_operator = "GreaterThanThreshold" + evaluation_periods = 1 + alarm_actions = [aws_sns_topic.alerts.arn] +} diff --git a/operation-team-account/modules/cloudwatch/variables.tf b/operation-team-account/modules/cloudwatch/variables.tf new file mode 100644 index 00000000..254047c4 --- /dev/null +++ b/operation-team-account/modules/cloudwatch/variables.tf @@ -0,0 +1,19 @@ +variable "subscription_filter_name" { + description = "CloudWatch Logs Subscription Filter 이름" + type = string +} + +variable "log_group_name" { + description = "대상 CloudWatch 로그 그룹 이름" + type = string +} + +variable "firehose_arn" { + description = "로그를 전송할 Kinesis Firehose ARN" + type = string +} + +variable "role_arn" { + description = "CloudWatch Logs → Firehose 연결용 IAM Role ARN" + type = string +} diff --git a/operation-team-account/modules/firehose/main.tf b/operation-team-account/modules/firehose/main.tf new file mode 100644 index 00000000..f1dd71d1 --- /dev/null +++ b/operation-team-account/modules/firehose/main.tf @@ -0,0 +1,97 @@ +resource "aws_iam_role" "firehose_role" { + name = "siem-firehose-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + Service = "firehose.amazonaws.com" + }, + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role_policy" "firehose_policy" { + name = "firehose-access" + role = aws_iam_role.firehose_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + # 1) OpenSearch 접근 권한: 도메인 자체와 색인 아래 객체만 허용 + { + Effect = "Allow" + Action = [ + "es:DescribeElasticsearchDomain", + "es:DescribeElasticsearchDomains", # Note: 이 액션은 리소스 지정이 불가능하여 "*" 사용 필요 + "es:DescribeElasticsearchDomainConfig", + "es:ESHttpPost", + "es:ESHttpPut", + "es:ESHttpGet", + ] + Resource = [ + var.opensearch_domain_arn, + "${var.opensearch_domain_arn}/*", + ] + }, + + # 2) S3 버킷 접근 권한: 버킷 메타데이터 조회와 객체 Put만 해당 버킷으로 제한 + { + Effect = "Allow" + Action = [ + "s3:ListBucket", # 버킷 목록 조회(버킷 이름에 한정) + "s3:GetBucketLocation", # 리전 조회 + ] + Resource = var.s3_bucket_arn + }, + { + Effect = "Allow" + Action = [ + "s3:PutObject", # 객체 업로드 + ] + Resource = "${var.s3_bucket_arn}/*" + } + ] + }) +} + + +resource "aws_kinesis_firehose_delivery_stream" "to_opensearch" { + name = "siem-delivery" + destination = "opensearch" + + opensearch_configuration { + domain_arn = var.opensearch_domain_arn + role_arn = aws_iam_role.firehose_role.arn + index_name = "cw-logs" + type_name = "log" + + s3_backup_mode = "FailedDocumentsOnly" + + s3_configuration { + role_arn = aws_iam_role.firehose_role.arn + bucket_arn = var.s3_bucket_arn + buffering_interval = 300 + buffering_size = 5 + compression_format = "GZIP" + prefix = "backup/" + } + } +} +# S3 버킷에 Lifecycle 정책 붙이기 +resource "aws_s3_bucket_lifecycle_configuration" "firehose_backup" { + bucket = aws_s3_bucket.firehose_backup.id + + rule { + id = "ExpireBackup" + status = "Enabled" + + filter { prefix = "backup/" } + + expiration { days = 7 } # 7일 지난 백업은 자동 삭제 + } +} \ No newline at end of file diff --git a/operation-team-account/modules/firehose/outputs.tf b/operation-team-account/modules/firehose/outputs.tf new file mode 100644 index 00000000..02f54d53 --- /dev/null +++ b/operation-team-account/modules/firehose/outputs.tf @@ -0,0 +1,9 @@ +output "delivery_stream_arn" { + value = aws_kinesis_firehose_delivery_stream.to_opensearch.arn + description = "ARN of the Firehose delivery stream" +} + +output "firehose_role_arn" { + value = aws_iam_role.firehose_role.arn + description = "ARN of the Firehose IAM role" +} diff --git a/operation-team-account/modules/firehose/variables.tf b/operation-team-account/modules/firehose/variables.tf new file mode 100644 index 00000000..93dd87a1 --- /dev/null +++ b/operation-team-account/modules/firehose/variables.tf @@ -0,0 +1,7 @@ +variable "opensearch_domain_arn" { + type = string +} + +variable "s3_bucket_arn" { + type = string +} \ 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..fdede2c7 --- /dev/null +++ b/operation-team-account/modules/opensearch/main.tf @@ -0,0 +1,50 @@ +data "aws_caller_identity" "current" {} + +resource "aws_opensearch_domain" "siem" { + domain_name = "siem-domain" + engine_version = "OpenSearch_2.9" + + cluster_config { + instance_type = "t3.small.search" + instance_count = 1 + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } + + domain_endpoint_options { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } + + node_to_node_encryption { + enabled = true + } + + encrypt_at_rest { + enabled = true + } + + access_policies = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = [ + "arn:aws:sts::${data.aws_caller_identity.current.account_id}:assumed-role/${var.sso_role_name}/${var.sso_user_name}", + var.firehose_role_arn, + ] + } + Action = "es:*" + # 도메인 및 그 하위 인덱스·도큐먼트 전체 리소스 + Resource = [ + aws_opensearch_domain.siem.arn, + "${aws_opensearch_domain.siem.arn}/*", + ] + } + ] + }) +} diff --git a/operation-team-account/modules/opensearch/outputs.tf b/operation-team-account/modules/opensearch/outputs.tf new file mode 100644 index 00000000..5272c407 --- /dev/null +++ b/operation-team-account/modules/opensearch/outputs.tf @@ -0,0 +1,3 @@ +output "domain_arn" { + value = aws_opensearch_domain.siem.arn +} \ 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..449a8fe3 --- /dev/null +++ b/operation-team-account/modules/opensearch/variables.tf @@ -0,0 +1,15 @@ +variable "sso_role_name" { + description = "SSO에서 부여된 IAM 역할 이름" + type = string +} + +variable "sso_user_name" { + description = "SSO 세션 사용자 이름 (예: 이메일)" + type = string +} + +variable "firehose_role_arn" { + description = "ARN of the Firehose IAM role to allow write access" + 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..b0977c44 --- /dev/null +++ b/operation-team-account/modules/s3/main.tf @@ -0,0 +1,35 @@ +resource "random_id" "bucket_id" { + byte_length = 4 +} + +resource "aws_s3_bucket" "firehose_backup" { + bucket = "siem-firehose-backup-${random_id.bucket_id.hex}" + force_destroy = true +} + +resource "aws_s3_bucket_public_access_block" "block" { + bucket = aws_s3_bucket.firehose_backup.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_kms_key" "s3_cmk" { + description = "KMS key for encrypting Firehose backup S3 bucket" + deletion_window_in_days = 10 + enable_key_rotation = true +} + +# 암호화 설정 +resource "aws_s3_bucket_server_side_encryption_configuration" "default" { + bucket = aws_s3_bucket.firehose_backup.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + kms_master_key_id = aws_kms_key.s3_cmk.arn + } + } +} diff --git a/operation-team-account/modules/s3/outputs.tf b/operation-team-account/modules/s3/outputs.tf new file mode 100644 index 00000000..f4bd5c26 --- /dev/null +++ b/operation-team-account/modules/s3/outputs.tf @@ -0,0 +1,3 @@ +output "bucket_arn" { + value = aws_s3_bucket.firehose_backup.arn +} \ No newline at end of file diff --git a/operation-team-account/modules/securitylake/main.tf b/operation-team-account/modules/securitylake/main.tf new file mode 100644 index 00000000..5f77218b --- /dev/null +++ b/operation-team-account/modules/securitylake/main.tf @@ -0,0 +1,42 @@ +# 1. 메타스토어 관리자 IAM 역할 생성 +resource "aws_iam_role" "securitylake_manager" { + name = "securitylake-meta-store-manager" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { Service = "securitylake.amazonaws.com" }, + Action = "sts:AssumeRole" + } + ] + }) +} + +# 2. 조직 단위 Security Lake 활성화 (조직 전체 설정) +resource "aws_securitylake_organization_configuration" "org_config" { + auto_enable { + all_regions = false + regions = ["ap-northeast-2"] + } +} + +# 3. 특정 리전(ap-northeast-2)에서 Data Lake 활성화 및 라이프사이클 설정 +resource "aws_securitylake_data_lake" "this" { + meta_store_manager_role_arn = aws_iam_role.securitylake_manager.arn + + configuration { + region = "ap-northeast-2" + + lifecycle_configuration { + expiration { + days = 30 # 데이터 30일 보관 + } + transition { + days = 7 + storage_class = "ONEZONE_IA" # 7일 후 저비용 스토리지로 전환 + } + } + } +} diff --git a/operation-team-account/outputs.tf b/operation-team-account/outputs.tf new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/operation-team-account/outputs.tf @@ -0,0 +1 @@ + diff --git a/operation-team-account/providers.tf b/operation-team-account/providers.tf new file mode 100644 index 00000000..f0ae24f2 --- /dev/null +++ b/operation-team-account/providers.tf @@ -0,0 +1,4 @@ +provider "aws" { + region = "ap-northeast-2" + profile = "my-profile" +} \ No newline at end of file diff --git a/operation-team-account/variables.tf b/operation-team-account/variables.tf new file mode 100644 index 00000000..d532342d --- /dev/null +++ b/operation-team-account/variables.tf @@ -0,0 +1,14 @@ +variable "sso_role_name" { + description = "SSO에서 부여된 IAM 역할 이름" + type = string +} + +variable "sso_user_name" { + description = "SSO 세션 사용자 이름" + type = string +} + +variable "log_group_name" { + description = "전송할 CloudWatch Log Group 이름" + type = string +}