検証用AWSアカウントでGuardDutyと闘う話 〜不用意にポート開けるな編〜

# はじめに

前回の話: [検証用AWSアカウントでGuardDutyと闘う話 〜S3バケット編〜](https://qiita.com/kikeyama/items/8582e015f4bd6e5c57da)

2021年師走、街はクリスマスのイルミネーションが点灯し、世間は年越しに向けてせわしなくなり始めたある日、1通のSlack DMが届いた。

例の如くインシデントレスポンスからだった。

英語のメッセージだったが、要約するとこう↓いうことである。

> お前のインスタンスがポートスキャンされて8089ポートにアクセスされたぞ。なんでこんなポート開けてるんだ？とりあえずインスタンス止めといたンだわ

あーやっちまったかな、と思いつつGuardDutyを確認すると、確かに某国から `8089` で**Port Probing**を検知したとのイベントが上がっていた。

# なぜ8089ポートを開けていたのか？

**結論**：ただの不注意

検証用にSplunkのインスタンスを立てて、あろうことか管理用ポート8089を `0.0.0.0/0` に開けてしまっていたのである。
しかもわざわざPublic Subnetにインスタンスを作っていた。

前職では検証用アカウントについてはここまでセキュリティ監視されていなかったので、ついうっかり何も考えずにPublic Subnetにインスタンス作成してポートを開けてしまっていた。
だって、Public Subnetにあった方がSSHでアクセスしやすいし。

でも、これは完全に自分の不注意・不手際・不作法だった。

時は2021年12月、勘の良い方ならお気づきだろう。当時Log4jの脆弱性で騒がれていたので、インシデントレスポンスチームとしても少しピリピリしていたようだ。

申し訳ないと返信しつつ、Terraformを修正していく。

# Terraform修正して再デプロイしてみる

修正対象のリソースはざっくり↓
（細かいところ言うともっとあるけど、割愛します）

- VPC Subnet
- Security Group
- EC2 Instance
- ALB
- Route53

セキュリティグループだけ直しとけばいいかとも考えたけど、せっかくなのでSubnetごと変えることにした。
インスタンスも作り替えることになるけど、検証用だしまあいいか、ということで。

AWSとTerraformに関しては完全に素人の独学なので、間違っているところあればコメントでご指摘いただけると有り難いです。

## VPCとSubnet

### 自分のVPCにPrivate Subnet作成

自分用のVPCを `/16` で作成

```hcl:VPC
resource "aws_vpc" "my_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.prefix}-vpc"
  }
}
```

Private SubnetをAZごとに一つずつ作成していく
EC2インスタンスはこのSubnetに作成する（後述）
名前は `${var.prefix}-private-subnet-1,2,3` とする

```hcl:Private Subnet
data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "private_subnet" {
  for_each          = toset(data.aws_availability_zones.available.names)
  availability_zone = each.value

  vpc_id            = aws_vpc.my_vpc.id
  cidr_block        = cidrsubnet(aws_vpc.my_vpc.cidr_block, 8, index(data.aws_availability_zones.available.names, each.value) + 1)

  tags = {
    Name = "${var.prefix}-private-subnet-${index(data.aws_availability_zones.available.names, each.value) + 1}"
  }
}
```

SplunkインストーラーをDLする際にインターネットへのEgressトラフィックは必要になるので、NAT Gatewayを作成
Private Subnetと言いながらPublic NATを使うというわけである
参考: [Amazon VPC でプライベートサブネット用の NAT ゲートウェイをセットアップするにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/nat-gateway-vpc-private-subnet/)

この場合Public Subnetも必要になるが、それはこの後のステップで作成しよう

NAT GatewayにはElastic IPが必要になるので、それも追加

```hcl:Elastic IP & NAT Gateway
resource "aws_eip" "nat_eip" {
  vpc              = true
  public_ipv4_pool = "amazon"
  for_each         = aws_subnet.private_subnet
  tags = {
    Name = "${var.prefix}-nat-eip-${each.value.id}"
  }
}

resource "aws_nat_gateway" "nat" {
  for_each          = aws_subnet.public_subnet
  subnet_id         = each.value.id
  connectivity_type = "public"
  allocation_id     = aws_eip.nat_eip[each.key].id
  tags = {
    Name = "${var.prefix}-public-nat-${each.value.id}"
  }
}

```

仕上げはRoute Tableを作成してPrivate Subnetに関連付け

```hcl:Route Table
resource "aws_route_table" "private_rtb" {
  for_each = aws_subnet.private_subnet
  vpc_id   = aws_vpc.my_vpc.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat[each.key].id
  }

  tags = {
    Name = "${var.prefix}-private-rtb-${each.value.id}"
  }
}

resource "aws_route_table_association" "private_rtb_a" {
  for_each       = aws_subnet.private_subnet
  subnet_id      = each.value.id
  route_table_id = aws_route_table.private_rtb[each.key].id
}
```

### ALBとNAT Gateway用にPublic Subnet作成

Private Subnetと同様にPublic Subnetも作成
インターネットアクセスが必要になるロードバランサーはここに配置

Private Subnetとの主な違いは...

- Internet GatewayでRoute Table作成
- `map_public_ip_on_launch` でパブリックIPを自動生成

```hcl:Subnet, Internet Gateway, and Route Table
resource "aws_subnet" "public_subnet" {
  for_each          = toset(data.aws_availability_zones.available.names)
  availability_zone = each.value

  vpc_id                  = aws_vpc.my_vpc.id
  cidr_block              = cidrsubnet(aws_vpc.my_vpc.cidr_block, 8, index(data.aws_availability_zones.available.names, each.value) + 101)
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.prefix}-public-subnet-${index(data.aws_availability_zones.available.names, each.value) + 1}"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.my_vpc.id

  tags = {
    Name = "${var.prefix}-igw"
  }
}

resource "aws_default_route_table" "public_rtb" {
  default_route_table_id = aws_vpc.my_vpc.default_route_table_id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.prefix}-public-rtb"
  }
}

resource "aws_route_table_association" "public_rtb_a" {
  for_each       = aws_subnet.public_subnet
  subnet_id      = each.value.id
  route_table_id = aws_default_route_table.public_rtb.id
}
```

## セキュリティグループ

ここが肝心

共通セキュリティグループとして自身からのアクセスは全ポート許可して、且つ、ELBのセキュリティグループからのアクセスも全ポート許可するように設定

SSHのingressを `0.0.0.0/0` に開けているが、後にこれによって別のインシデントが引き起こされた。その経緯と対処についてはまた別の記事で。**良い子は真似しないように。**

```hcl:全インスタンスに共通のセキュリティグループ
resource "aws_security_group" "base_sg" {
  vpc_id = aws_vpc.my_vpc.id
  name   = "${var.prefix}-base-sg"
  description = "Base Security Group"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "SSH"
  }

  ingress {
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    self        = true
    description = "ingress traffic from myself"
  }

  ingress {
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    security_groups = [aws_security_group.elb_sg.id, aws_security_group.home_sg.id]
    description = "ingress traffic from load balancers"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.prefix}-base-sg"
  }
}
```

```hcl:ALB用のセキュリティグループ
resource "aws_security_group" "elb_sg" {
  vpc_id = aws_vpc.my_vpc.id
  name   = "${var.prefix}-elb-sg"
  description = "Security Group for Load Balancers"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.prefix}-elb-sg"
  }
}
```

```hcl:ALB用のセキュリティグループ（家のIPアドレスだけ許可）
resource "aws_security_group" "home_sg" {
  vpc_id = aws_vpc.my_vpc.id
  name   = "${var.prefix}-home-sg"
  description = "Security Group from Home"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["${var.home_cidr}"]
    description = "HTTP from Home"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["${var.home_cidr}"]
    description = "HTTPS from Home"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.prefix}-home-sg"
  }
}
```

正直なところ、アクセスをIPアドレスで制限するなんてナンセンスすぎるが、当時は少しでもアクセスを制限しようと必死だった。
実際運用してみると家のIPアドレスがコロコロ変わる（月に数回くらい）ので、正直めんどくさいことになったんだよね。

セキュリティグループについては整理する必要あるね、かなり。

## Splunk用EC2インスタンス

まずはインスタンスのイメージを作成
やっぱUbuntuだよね

```hcl:Ubuntuイメージの定義
data "aws_ami" "ubuntu" {
  owners      = ["099720109477"]
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-20211122"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}
```

続いてSSHキーを作成して自分の端末に保存、キーペアとして登録

```hcl:キー作成 & ローカル保存 & AWSキーペア登録
locals {
  public_key_file  = "${var.key_file_path}/${var.key_name}.id_rsa.pub"
  private_key_file = "${var.key_file_path}/${var.key_name}.id_rsa.pem"
}

resource "tls_private_key" "key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "local_file" "private_key_pem" {
  filename = "${local.private_key_file}"
  content  = "${tls_private_key.key.private_key_pem}"

  provisioner "local-exec" {
    command = "chmod 600 ${local.private_key_file}"
  }
}

resource "local_file" "public_key_openssh" {
  filename = "${local.public_key_file}"
  content  = "${tls_private_key.key.public_key_openssh}"

  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}

resource "aws_key_pair" "keypair" {
  key_name   = "${var.prefix}-key"
  public_key = tls_private_key.key.public_key_openssh
}
```

そしてEC2インスタンスをPrivate Subnetに作成

ちなみにこれはAuth0テナントのログをHTTP Event Collectorで受け取ってインデックス＆可視化する用のインスタンス
Auth0の [アクセス元IPアドレスのWhitelistは公開されている](https://auth0.com/docs/secure/security-guidance/data-security/allowlist) ので、HEC用のALBもこのIPアドレスに絞れば良かったけど、今後他のIPアドレスからもIngestするかもと思って、当時は制限していなかった（今は制限してるよ）

このインスタンスはPrivate Subnetに作成したわけだが、SSHで入りたいときにはPublic Subnetに作成した別のインスタンスを踏み台にしてSSH Agentを使ってこのインスタンスに入ることにした。

```hcl:Private SubnetにSplunkインスタンス作成
resource "random_integer" "az_index" {
  min = 0
  max = length(data.aws_availability_zones.available.names) - 1
}

resource "random_password" "splunk_password" {
  length           = 8
  lower            = true
  upper            = true
  number           = true
  special          = true
  override_special = "!@#"
}

resource "aws_instance" "ec2_splunk" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.small"
  key_name               = aws_key_pair.keypair.key_name
  vpc_security_group_ids = [aws_security_group.base_sg.id]
  subnet_id              = aws_subnet.private_subnet[data.aws_availability_zones.available.names[random_integer.az_index.result]].id
  user_data              = <<-EOF
    #! /bin/bash
    # Set hostname
    hostnamectl set-hostname ${var.prefix}-splunk
    # Install Splunk
    echo "install splunk"
    SPLUNK_FILE="${var.splunk_download_file}"
    SPLUNK_VERSION=`echo $${SPLUNK_FILE} | sed 's/-/ /g' | awk '{print $2}'`
    SPLUNK_URL="https://download.splunk.com/products/splunk/releases/$${SPLUNK_VERSION}/linux/$${SPLUNK_FILE}"
    mkdir /home/splunk
    groupadd -g 501 splunk
    useradd -u 501 -g 501 splunk -d /home/splunk -s /bin/bash
    chown splunk:splunk /home/splunk
    echo "Download Splunk from $${SPLUNK_URL}"
    wget -nv -O /opt/$${SPLUNK_FILE} $${SPLUNK_URL}
    tar zxf /opt/$${SPLUNK_FILE} -C /opt/
    chown -R splunk:splunk /opt/splunk
    echo "splunk pw: ${random_password.splunk_password.result}"
    # Start Splunk
    su - splunk -c "/opt/splunk/bin/splunk start --accept-license --answer-yes --seed-passwd ${random_password.splunk_password.result}"
    # Configure Splunk
    su - splunk -c "/opt/splunk/bin/splunk set default-hostname ${var.prefix}-splunk -auth admin:${random_password.splunk_password.result}"
    su - splunk -c "/opt/splunk/bin/splunk set servername ${var.prefix}-splunk -auth admin:${random_password.splunk_password.result}"
    # Create index and sourcetype
    su - splunk -c "/opt/splunk/bin/splunk add index auth0 -app search  -auth admin:${random_password.splunk_password.result}"
    su - splunk -c "echo '[auth0]' > /opt/splunk/etc/apps/search/local/props.conf"
    # Enable Web SSL
    su - splunk -c "/opt/splunk/bin/splunk enable web-ssl -auth admin:${random_password.splunk_password.result}"
    # Enable boot start
    /opt/splunk/bin/splunk enable boot-start -user splunk
    # Restart Splunk
    su - splunk -c "/opt/splunk/bin/splunk restart"
    # Enable HEC
    su - splunk -c "/opt/splunk/bin/splunk http-event-collector enable -uri https://localhost:8089 -enable-ssl 1 -auth admin:${random_password.splunk_password.result}"
    # Create HEC endpoint
    su - splunk -c "/opt/splunk/bin/splunk http-event-collector create new-token -uri https://localhost:8089 -name auth0 -disabled 0 -index auth0 -sourcetype auth0 -auth admin:${random_password.splunk_password.result}"
  EOF

  root_block_device {
    volume_size = 32
  }

  tags = {
    Name = "${var.prefix}-splunk"
  }

  volume_tags = {
    Name = "${var.prefix}-splunk"
  }

  lifecycle {
    ignore_changes = [user_data]
  }
}
```

## インターネットアクセス用のロードバランサー（ALB）

インスタンスは一つだけなので、はっきり言ってロードバランサーは要らない。
けどPrivate Subnetにインスタンス作っちゃったし、でも、ブラウザとかSplunkのHTTP Event Collectorでのアクセスにはインターネット経由でのアクセス必要だし、ということでALBを追加する。

Nginxとかでリバースプロキシ作ることも考えたけど、セキュリティグループとか証明書とか設定の管理とか、いろいろとめんどくさそうなのでALBにしたい。
セキュリティインシデントについては社内のチームから指摘されるけど、コストについては特に何も言われないから、細かいことは気にせずALB作っちゃおう。

ということで、まずはACMでHTTPS用の証明書発行

```hcl:ACMで証明書発行 & Route53にValidation用レコード作成
resource "aws_acm_certificate" "mydomain_com_acm" {
  domain_name       = "*.mydomain.com"
  validation_method = "DNS"

  tags = {
    Name = "${var.prefix}-acm"
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "acm_dvo_mydomain_com" {
  for_each = {
    for dvo in aws_acm_certificate.mydomain_com_acm.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = aws_route53_zone.mydomain_com.zone_id
}

resource "aws_acm_certificate_validation" "mydomain_com_acm_validation" {
  certificate_arn         = aws_acm_certificate.mydomain_com_acm.arn
  validation_record_fqdns = [for record in aws_route53_record.acm_dvo_mydomain_com : record.fqdn]
}
```

続いてブラウザでのSplunkアクセス用とHECエンドポイント用に2つのALBを作成
Webアクセスについては家のIPアドレスからだけ許可

それぞれAZごとに作成したPublic Subnetに展開

```hcl:Webアクセス用ALB
resource "aws_lb" "alb_splunk_web" {
  name               = "${var.prefix}-alb-splunk-web"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.home_sg.id]
  subnets            = [for subnet in aws_subnet.public_subnet : subnet.id]

  tags = {
    Name = "${var.prefix}-alb-splunk-web"
  }
}

resource "aws_lb_target_group" "tg_splunk_web" {
  name        = "${var.prefix}-tg-splunk-web"
  target_type = "instance"
  port        = 8000
  protocol    = "HTTPS"
  vpc_id      = aws_vpc.my_vpc.id

  health_check {
    enabled             = true
    healthy_threshold   = 5
    interval            = 30
    matcher             = "200"
    path                = "/en-US/account/login"
    protocol            = "HTTPS"
    timeout             = 5
    unhealthy_threshold = 2
  }

  tags = {
    Name = "${var.prefix}-tg-splunk-web"
  }
}

resource "aws_lb_target_group_attachment" "tg_attachment_splunk_web" {
  target_group_arn = aws_lb_target_group.tg_splunk_web.arn
  target_id        = aws_instance.ec2_splunk.id
  port             = 8000
}

resource "aws_lb_listener" "alb_listener_splunk_web" {
  load_balancer_arn = aws_lb.alb_splunk_web.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.mydomain_com_acm.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg_splunk_web.arn
  }

  tags = {
    Name = "${var.prefix}-alb-listener-splunk-web"
  }
}
```

```hcl:HEC用ALB
resource "aws_lb" "alb_splunk_hec" {
  name               = "${var.prefix}-alb-splunk-hec"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.elb_sg.id]
  subnets            = [for subnet in aws_subnet.public_subnet : subnet.id]

  tags = {
    Name = "${var.prefix}-alb-splunk-hec"
  }
}

resource "aws_lb_target_group" "tg_splunk_hec" {
  name        = "${var.prefix}-tg-splunk-hec"
  target_type = "instance"
  port        = 8088
  protocol    = "HTTPS"
  vpc_id      = aws_vpc.my_vpc.id

  health_check {
    enabled             = true
    healthy_threshold   = 5
    interval            = 30
    matcher             = "200"
    path                = "/services/collector/health"
    protocol            = "HTTPS"
    timeout             = 5
    unhealthy_threshold = 2
  }

  tags = {
    Name = "${var.prefix}-tg-splunk-hec"
  }
}

resource "aws_lb_target_group_attachment" "tg_attachment_splunk_hec" {
  target_group_arn = aws_lb_target_group.tg_splunk_hec.arn
  target_id        = aws_instance.ec2_splunk.id
  port             = 8088
}

resource "aws_lb_listener" "alb_listener_splunk_hec" {
  load_balancer_arn = aws_lb.alb_splunk_hec.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.mydomain_com_acm.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg_splunk_hec.arn
  }

  tags = {
    Name = "${var.prefix}-alb-listener-splunk-hec"
  }
}
```

## Route53でALBの名前解決

まずはゾーンを作成

```hcl:ゾーン作成
resource "aws_route53_zone" "mydomain_com" {
  name = "mydomain.com"

  tags = {
    Name = "${var.prefix}-com-zone"
  }
}
```

Splunk WebとHECの2レコード登録
最初はCNAMEしてたけど、ALBだしエイリアス使っちゃおうかな

```hcl:Route53レコード
resource "aws_route53_record" "splunk_mydomain_com" {
  zone_id = aws_route53_zone.mydomain_com.zone_id
  name    = "splunk.mydomain.com"
  type    = "A"

  alias {
    name                   = aws_lb.alb_splunk_web.dns_name
    zone_id                = aws_lb.alb_splunk_web.zone_id
    evaluate_target_health = true
  }
}

resource "aws_route53_record" "splunk_http_inputs_mydomain_com" {
  zone_id = aws_route53_zone.mydomain_com.zone_id
  name    = "splunk-http-inputs.mydomain.com"
  type    = "A"

  alias {
    name                   = aws_lb.alb_splunk_hec.dns_name
    zone_id                = aws_lb.alb_splunk_hec.zone_id
    evaluate_target_health = true
  }
}
```

## リソースを展開

あとはこれを `terraform apply` して展開すれば各リソースが作成される。
変数の定義とか端折っているけど、適宜 `variable` を追加すれば問題ないはず。[変数ファイルを作成してapply](https://www.terraform.io/language/values/variables#variable-definitions-tfvars-files)した方が楽でしょう。

# おわりに

[前回S3でアラート上がってから](https://qiita.com/kikeyama/items/8582e015f4bd6e5c57da)1ヶ月もしないうちに今回またGuardDutyで検知してしまった。
今回は前回とは別の人だったけど、何度もアラートあげてしまい、本当に申し訳ないことをした。

正直なところ、今回の構成（特にセキュリティグループ）は検証用としてはあまり良くない状態なので、注意してご利用いただきたい。

このEC2インスタンスにSSHログインするためにPublic Subnetに適当に作った踏み台インスタンスとSSHポート開放によって別のインシデントが起きたわけだが、それは次の記事で書こう。乞うご期待。

# 関連リンク
第1回: [検証用AWSアカウントでGuardDutyと闘う話 〜S3バケット編〜](https://qiita.com/kikeyama/items/8582e015f4bd6e5c57da)
第2回: 検証用AWSアカウントでGuardDutyと闘う話 〜不用意にポート開けるな編〜（当記事）
第3回: [検証用AWSアカウントでGuardDutyと闘う話 〜怒りのVPN編〜](https://qiita.com/kikeyama/items/53c66cccd53e1cd6f6ff)