본문 바로가기

AWS

[AWS] Multi Account에서 IAM 내역 감사하기 #2 - 테라폼으로 인프라 프로비저닝

반응형

1. 개요

Multi Account 환경에서 IAM 관련 Action에 대한 감사를 하기 위해서는 Log들을 Security 관련 Account에서 수집해야 한다.

 

이에 대한 인프라 프로비저닝은 Terraform을 사용하였다.

 

(부분 부분 웹 콘솔로 진행한 부분도 있기 때문에 참고만 하면 좋을 것이다.)

 

2. 구조

프로젝트의 폴더 구조는 아래와 같다.

 

별도의 모듈은 굳이 사용하지 않았으며, resource, data, variable, local 등을 활용하여 리소스를 Code로써 표현하였다.

 

# another-accounts

EventBridge에서 기 정의한 IAM Action이 Pattern에 걸릴 경우 Security Account의 EventBus로 전달한다.

 

# security-accounts

IAM Audit 내역을 중앙에서 수집하고, 중앙에서 수집한 뒤 1) Lambda를 활용한 Slack Notification, 2) OpenSearch를 활용한 Logging 중앙화 및 시각화

3. Terraform

## Security Account

 

1) _backend.tf

: Backend를 S3로 정의하고, tflock을 위해 DynamoDB를 설정한다.

각 폴더 별로 S3의 폴더 객체를 만들어서 Backend State를 관리하는 것을 자동화하기 위해 Terragrunt를 사용할 수 있지만 굳이 그렇게 하진 않았다.

 

2) _data.tf

: IAM Policy를 설정하기 위해 data block을 사용하였다.

 

eventBus는 같은 OrganizationID일 경우에만 받을 수 있다.

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "eventBusPolicy" {
  statement {
    sid       = "allow_all_accounts_from_organization_to_put_events"
    effect    = "Allow"
    resources = ["arn:aws:events:${local.region}:${local.account_id}:event-bus/${aws_cloudwatch_event_bus.this.name}"]
    actions   = ["events:PutEvents"]

    condition {
      test     = "StringEquals"
      variable = "aws:PrincipalOrgID"
      values   = [local.org_id]
    }

    principals {
      type        = "*"
      identifiers = ["*"]
    }
  }
}

data "aws_iam_policy_document" "es_access_policy" {
  statement {
    sid       = ""
    effect    = "Allow"
    resources = ["arn:aws:es:${local.region}:${local.account_id}:domain/${local.prefix}-domain/*"]
    actions   = ["es:*"]

    principals {
      type        = "*"
      identifiers = ["*"]
    }
  }
}

data "aws_iam_role" "security-service-profile-iam-role" {
  name = "security-service-profile-iam-role"
}

 

 

3) _provider.tf

: Provider(AWS)를 설정하기 위한 파일이다.

 

4) _shared_vars.tf

: 전체 폴더에 공통적으로 사용되는 변수들을 모아놓은 변수 파일이다.

  현재는 각각 따로 생성해주어 복붙했지만, 추후 Terragrunt나 스크립트 파일을 별도 만들어 관리하는 것이 좀 더 좋을 것으로 보인다.

# Shared Variables

variable "env" {}
#variable "owner" {}
variable "region" {}
variable "account" {}
variable "cred_file" {}
variable "tags" {}
variable "team" {}

 

5) _vars.tf

: 해당 폴더에서 필요한 변수들을 모아놓은 파일이다.

# EventBus
variable "org_id" {}

# CloudWatch Logs
variable "retention_in_days" {}

variable "vpc_id" {}
variable "subnet_ids" {}
variable "security_group_ids" {}
variable "es_version" {}
variable "es_instance_type" {}
variable "es_instance_count" {}

variable "es_volume_size" {}
variable "es_volume_type" {}

variable "es_custom_endpoint_enabled" {}
variable "acm_arn" {}
variable "es_custom_endpoint_domain_name" {}
variable "es_custom_endpoint_tls_policy" {}

# Route 53
variable "route53_zone_id" {}

 

6) _terraform.auto.tfvars

terraform apply를 수행할 시 자동으로 이 파일을 읽어들이게 된다.

auto를 붙이지 않을 경우 추가 옵션을 넣어주어야 한다.

env       = "xxxx"
team      = "xxxx"
account   = "xxxx"
cred_file = "~/.aws/credentials"
region    = "us-east-1"
tags      = {}

org_id = "o-xxxxxxxxx"

retention_in_days = 7

vpc_id             = "vpc-xxxxxxxxxx"
subnet_ids         = ["subnet-xxxxxxxxx"]
security_group_ids = ["sg-xxxxxxxxx"]
es_instance_type   = "t3.medium.elasticsearch"
es_instance_count  = 1
es_version         = "OpenSearch_1.2" # Before : 7.10

es_volume_size = 20
es_volume_type = "gp2"

acm_arn                        = "arn:aws:acm:us-east-1:xxxxxxx:certificate/xxxxxxxxxx"
es_custom_endpoint_enabled     = true
es_custom_endpoint_domain_name = "xxxxx.security.xxxx.xxxx"
es_custom_endpoint_tls_policy  = "Policy-Min-TLS-1-0-2019-07"

route53_zone_id = "xxxxxxxxxxx"

 

 

7) cwLogs.tf

아래의 Error 문구를 보면 CloudWatch Logs에서 Subscription Filter로 ES를 설정할 경우, Resource가 생성되지 않는다.

이는 내부적으로 Lambda를 구성하기 때문에 Terraform으로 관리하기 위해서는 별도 Lambda 및 소스 코드를 Terraform으로 관리해야 한다.

 

필자는 이 부분은 웹 콘솔에서 진행

resource "aws_cloudwatch_log_group" "this" {
  name              = "/aws/events/${local.prefix}-logs"
  retention_in_days = local.retention_in_days

  tags = merge(
    local.tags,
    {
      Name = "${local.prefix}-logs"
    }
  )
}

#│ Error: Error creating Cloudwatch log subscription filter: InvalidParameterException: PutSubscriptionFilter operation cannot work with destinationArn for vendor es
# Error가 발생. 수동으로 생성
#resource "aws_cloudwatch_log_subscription_filter" "this" {
#  name            = "${local.prefix}-to-es-subs-filter"
#  role_arn        = data.aws_iam_role.security-service-profile-iam-role.arn
#
#  log_group_name  = aws_cloudwatch_log_group.this.name
#  filter_pattern  = ""
#
#  destination_arn = aws_elasticsearch_domain.this.arn # 5.10
#}

 

8) elasticSearch.tf

: OpenSearch를 생성하기 위한 테라폼 파일이다.

 

resource "aws_iam_service_linked_role" "es" {
  aws_service_name = "es.amazonaws.com"
  description      = "AWSServiceRoleForAmazonElasticsearchService Service-Linked Role"
}

resource "aws_elasticsearch_domain" "this" {
  domain_name = "${local.prefix}-domain"

  elasticsearch_version = local.es.version

  auto_tune_options {
    desired_state       = "DISABLED"
    rollback_on_disable = "NO_ROLLBACK"
  }

  cluster_config {
    instance_count         = local.es.instance_count
    instance_type          = local.es.instance_type
    zone_awareness_enabled = false
  }

  ebs_options {
    ebs_enabled = true
    volume_size = local.es.volume_size
    volume_type = local.es.volume_type
  }

  vpc_options {
    subnet_ids         = local.es.subnet_ids
    security_group_ids = local.es.security_group_ids
  }

  domain_endpoint_options {
    enforce_https       = true
    tls_security_policy = local.es.custom_endpoint.tls_policy

    custom_endpoint_enabled         = local.es.custom_endpoint.enabled
    custom_endpoint_certificate_arn = local.es.custom_endpoint.enabled ? local.acm_arn : null
    custom_endpoint                 = local.es.custom_endpoint.enabled ? local.es.custom_endpoint.domain_name : null
  }

  access_policies = data.aws_iam_policy_document.es_access_policy.json

  tags = merge(
    local.tags,
    {
      Name = "${local.prefix}-domain"
    }
  )

  depends_on = [aws_iam_service_linked_role.es]

  lifecycle {
    ignore_changes = [
      auto_tune_options
    ]
  }

}

 

9) eventBridge.tf

event_bus_name은 해당 폴더에서 생성한 Custom EventBus를 지정해주었다. 지정해주지 않으면 default eventBus를 바라보게 될 것이다.

 

Target으로는 기 생성한 CloudWatch Logs로 보낼 것이다.

(Slack Noti 용도의 Lambda 타겟은 웹 콘솔에서 진행하였음)

resource "aws_cloudwatch_event_rule" "this" {
  name           = "${local.prefix}-event-rule"
  description    = "Capture IAM API Actions for security audit"
  event_bus_name = aws_cloudwatch_event_bus.this.name

  event_pattern = <<EOF
{
  "source": ["aws.iam"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["iam.amazonaws.com"]
  }
}
  EOF

  depends_on = [
    aws_cloudwatch_event_bus.this
  ]

  #  lifecycle {
  #    create_before_destroy = true
  #  }
}

resource "aws_cloudwatch_event_target" "this" {
  arn            = aws_cloudwatch_log_group.this.arn
  rule           = aws_cloudwatch_event_rule.this.name
  event_bus_name = aws_cloudwatch_event_bus.this.name
  target_id      = "SendToCloudWatchLogs"
}

 

10) eventBus.tf

: default eventBus와 별개로 IAM Audit 내역만 받아내기 위한 EventBus를 생성하였다.

resource "aws_cloudwatch_event_bus" "this" {
  name = "${local.prefix}-eventbus"

  tags = merge(
    local.tags,
    {
      Name = "${local.prefix}-eventbus"
    }
  )
}

resource "aws_cloudwatch_event_bus_policy" "this" {
  event_bus_name = aws_cloudwatch_event_bus.this.name
  policy         = data.aws_iam_policy_document.eventBusPolicy.json
}

 

11) _local.tf

한번 더 감싸서 관리하기 위해 locals 블록을 사용하였다.

locals {
  prefix = format("%s-%s-audit", var.account, var.env)

  account   = var.account
  cred_file = var.cred_file
  region    = var.region
  env       = var.env

  tags = merge(var.tags,
    {
      #      Owner       = var.owner,
      Environment = var.env
      Team        = var.team
  })

  org_id     = var.org_id
  account_id = data.aws_caller_identity.current.account_id

  retention_in_days = var.retention_in_days

  es = {
    security_group_ids = var.security_group_ids,
    subnet_ids         = var.subnet_ids,
    vpc_id             = var.vpc_id,
    version            = var.es_version,
    instance_type      = var.es_instance_type
    instance_count     = var.es_instance_count

    volume_size = var.es_volume_size
    volume_type = var.es_volume_type

    custom_endpoint = {
      enabled     = var.es_custom_endpoint_enabled
      domain_name = var.es_custom_endpoint_domain_name
      tls_policy  = var.es_custom_endpoint_tls_policy
    }
  }

  acm_arn = var.acm_arn

  route53_zone_id = var.route53_zone_id
}

 

 

여기까지 security-account의 Terraform 코드였고, 그 외 다른 어카운트들에 대한 테라폼 코드는 다음 글에서 작성하도록 할 예정이다.

 

생략한 내용들이 좀 있지만, 위의 큰 순서대로 작성한 뒤에 terraform fmt, terraform validate -> terraform plan , terraform apply를 수행하면 인프라가 프로비저닝이 된다.

 

 

EvnetBridge -> SNS -> Lambda 부분은 따로 빼서 글을 작성해 볼 생각이다.

 

반응형