検証用AWSアカウントでGuardDutyと闘う話 〜怒りのVPN編〜

# はじめに

前回の話: [検証用AWSアカウントでGuardDutyと闘う話 〜不用意にポート開けるな編〜](https://qiita.com/kikeyama/items/8d694725a43272bed3e8)

2022年春、桜は散り新緑が眩しくなってきたある日、1通のSlack DMが届いた。月曜の朝の出来事だった。

メッセージの送り主はそう、インシデントレスポンスチームからである。

英語のメッセージで捲し立てられたが、要約するとこう↓いうことである。

> SSHのアクセスがパブリックになってるせいでSSH Brute Force受けたぞ。セキュリティグループのSSH消しといたンだわ。こっちはまだ日曜の夕方なんだ。変なアラート出さすなよ。

誠にごめんなさい。

[前回の記事](https://qiita.com/kikeyama/items/8d694725a43272bed3e8)で書いたPublic SubnetのSSH `0.0.0.0/0` が原因で攻撃を受けたらしい。

GuardDutyを確認してみると、これまた某国からの攻撃だった。
**SSH Brute Force**か...鍵は自分しか持ってないから大丈夫なんじゃないの？という反論を飲み込んでGuardDutyのイベントに目を通す。

そして追加のメッセージも送られてきた。

> 俺がGuardDuty確認するときはタグを見るから、Purposeとかのタグに「本番データやコードは含まない」と書いとけば次からは見逃してやる。他の奴はどうするか知らんけど。

なるほど、ここらへん個人の運用の裁量に依存しそうだけど、確かにそういうタグをつけておけばアラートを受けた側もわかりやすいかも。

そして最後に↓言われた。

> パブリックアクセスは止めろ。ダメ、絶対。

休み中なのに申し訳ないと返信して、とりあえず対処することにした。

# とりあえず検証用であることを示すタグを追加

これは簡単
Terraform AWS Providerの `default_tags` にPurposeを追加するだけなので、メッセージに書いてあった文章をそのままコピペ

```hcl
provider "aws" {
  region  = var.region
  default_tags {
    tags = {
      Owner     = "${var.email}"
      ManagedBy = "terraform"
      Purpose   = "testing no prod code or customer data"
    }
  }
}
```

これをapplyするだけで全リソースにタグが適用される。Terraform使っててほんとに良かった。

# SSHのパブリックアクセスを禁止させる

「パブリック」ってどういう意味で捉えればいいんだろう？
とりあえずセキュリティグループのInboundを自宅のIPアドレスに限定してみたけど、これでいいのだろうか？

- 自宅のIPアドレスといっても同じISP使ってたら他にも同じIPアドレスの人いるし
- IPアドレスなんてコロコロ変わるから都度書き換えるのめんどくさいし
- そもそもこの設定でパブリックアクセスを禁止していると言えるのか？

```hcl:SSHのInboundを自宅のIPアドレスに限定
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 = ["${var.home_cidr}"]
    description = "SSH from Home"
  }

  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"
  }
}
```

しかもこれ、セキュリティグループを限定しているとはいえ、全ポートを開放しているの良くないよな。
検証用とはいえ、流石にこれは怖くなってきた。

## じゃあどうしよう？

「パブリック許可すな」っていうのはつまり「インターネット経由すな」って意味なのか？
そんなんSSHするときインターネット経由でのアクセス必要になるじゃん。無理じゃん。どうしろって言うんだよ！？

仕事柄VPN使うことないからVPNのアクセス権を会社から支給されてないし。

ん？じゃあ自分でVPN作れば良くない？

何かいい方法ないかな・・・あっ、まさに **[これ](https://docs.aws.amazon.com/ja_jp/vpn/latest/clientvpn-admin/scenario-restrict.html)** ができるといいな！

**Client VPN Endpoint**にアタッチしたセキュリティグループからのみSSHを許可すれば、パブリックアクセスじゃないと言い切れるよね。

## TerraformでClient VPNをデプロイする

ステップとしてはこちら↓

1. 証明書を作成してACM登録
1. Client VPN Endpoint用のセキュリティグループを作成
1. Route53のPrivate ZoneとResolverを作成
1. Log GroupとLog Streamを作成
1. Client VPN Endpointを作成
1. Mac用Client VPNをダウンロードして設定

うーん、けっこうあるね。一つずつ見ていこうか。

### 証明書を作成してACM登録

まずはオレオレ認証局を立ててサーバー証明書とクライアント証明書を発行

```hcl:証明書発行
locals {
  ca_private_key_file     = "${var.key_file_path}/ca/${var.prefix}-ca-private-key.id_rsa.pem"
  ca_cert_file            = "${var.key_file_path}/ca/${var.prefix}-ca.cer"

  server_private_key_file = "${var.key_file_path}/server/${var.prefix}-server-private-key.id_rsa.pem"
  server_csr_file         = "${var.key_file_path}/server/${var.prefix}-server.csr"
  server_cert_file        = "${var.key_file_path}/server/${var.prefix}-server.pem"
  server_cert_chain_file  = "${var.key_file_path}/server/${var.prefix}-server-chain.pem"

  client_private_key_file = "${var.key_file_path}/client/${var.prefix}-client-private-key.id_rsa.pem"
  client_csr_file         = "${var.key_file_path}/client/${var.prefix}-client.csr"
  client_cert_file        = "${var.key_file_path}/client/${var.prefix}-client.pem"
}

# ----------------------------------------------------------------
# Private CA
# ----------------------------------------------------------------

resource "tls_private_key" "ca_private_key" {
  algorithm = "RSA"
}

resource "local_file" "ca_private_key_pem" {
  filename = local.ca_private_key_file
  content  = tls_private_key.ca_private_key.private_key_pem

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

resource "tls_self_signed_cert" "ca_cert" {
  private_key_pem       = tls_private_key.ca_private_key.private_key_pem
  validity_period_hours = 87600  # 10 Years
  is_ca_certificate     = true

  subject {
    common_name  = "Private CA"
    country      = "JP"
    locality     = "Tokyo"
    organization = "Private CA"
  }

  allowed_uses = [
    "cert_signing"
  ]
}

resource "local_file" "ca_cert_pem" {
  filename = local.ca_cert_file
  content  = tls_self_signed_cert.ca_cert.cert_pem

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

# ----------------------------------------------------------------
# Server Cert
# ----------------------------------------------------------------

resource "tls_private_key" "server_private_key" {
  algorithm = "RSA"
}

resource "local_file" "server_private_key_pem" {
  filename = local.server_private_key_file
  content  = tls_private_key.server_private_key.private_key_pem

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

resource "tls_cert_request" "server_csr" {
  private_key_pem = tls_private_key.server_private_key.private_key_pem

  subject {
    common_name  = "*.mydomain.internal"
    organization = "Internal"
  }
}

resource "local_file" "server_csr_pem" {
  filename = local.server_csr_file
  content  = tls_cert_request.server_csr.cert_request_pem

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

resource "tls_locally_signed_cert" "server_cert" {
  cert_request_pem   = tls_cert_request.server_csr.cert_request_pem
  ca_private_key_pem = tls_private_key.ca_private_key.private_key_pem
  ca_cert_pem        = tls_self_signed_cert.ca_cert.cert_pem

  validity_period_hours = 43800  # 5 Years

  allowed_uses = [
    "key_encipherment",
    "digital_signature",
    "server_auth",
  ]
}

resource "local_file" "server_cert_pem" {
  filename = local.server_cert_file
  content  = tls_locally_signed_cert.server_cert.cert_pem

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

# ----------------------------------------------------------------
# Client Cert
# ----------------------------------------------------------------

resource "tls_private_key" "client_private_key" {
  algorithm = "RSA"
}

resource "local_file" "client_private_key_pem" {
  filename = local.client_private_key_file
  content  = tls_private_key.client_private_key.private_key_pem

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

resource "tls_cert_request" "client_csr" {
  private_key_pem = tls_private_key.client_private_key.private_key_pem

  subject {
    common_name  = "kikeyama"
    organization = "Internal"
  }
}

resource "local_file" "client_csr_pem" {
  filename = local.client_csr_file
  content  = tls_cert_request.client_csr.cert_request_pem

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

resource "tls_locally_signed_cert" "client_cert" {
  cert_request_pem   = tls_cert_request.client_csr.cert_request_pem
  ca_private_key_pem = tls_private_key.ca_private_key.private_key_pem
  ca_cert_pem        = tls_self_signed_cert.ca_cert.cert_pem

  validity_period_hours = 43800  # 5 Years

  allowed_uses = [
    "key_encipherment",
    "digital_signature",
    "client_auth",
  ]
}

resource "local_file" "client_cert_pem" {
  filename = local.client_cert_file
  content  = tls_locally_signed_cert.client_cert.cert_pem

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

※ 眠かったので `allowed_uses` はかなり適当。いつかは見直したい。

そしてサーバー証明書をACMに登録
同じCAから発行された場合はクライアント証明書は登録不要らしい

```hcl:証明書とプライベートキーを登録
resource "aws_acm_certificate" "internal_acm" {
  private_key       = tls_private_key.server_private_key.private_key_pem
  certificate_body  = tls_locally_signed_cert.server_cert.cert_pem
  certificate_chain = tls_self_signed_cert.ca_cert.cert_pem
}
```

### Client VPN Endpoint用のセキュリティグループを作成

[このブログ記事](https://dev.classmethod.jp/articles/how-to-configure-security-groups-for-aws-client-vpn/)によるとInboundは不要らしい。
なるほど、ありがとうクラメソさん！

最初はClient VPN Endpointの設定に合わせて `UDP 443` を設定していたけど削除した

```hcl:Outboundのみのセキュリティグループ
resource "aws_security_group" "client_vpn_sg" {
  vpc_id = aws_vpc.my_vpc.id
  name   = "${var.prefix}-client-vpn-sg"
  description = "Client VPN Security Group"

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

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

### Route53のPrivate ZoneとResolverを作成

仮に自分の端末から直接SSHでPrivate IP指定してSSHできるようになったとしても、各インスタンスのIPアドレスなんて覚えられないから、名前解決させたいよね

ということで、まずはPrivate Zone

```hcl:VPC内にプライベートゾーンを作るよ
resource "aws_route53_zone" "mydomain_internal" {
  name = "mydomain.internal"

  vpc {
    vpc_id = aws_vpc.my_vpc.id
  }

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

[前回の話](https://qiita.com/kikeyama/items/8d694725a43272bed3e8)の続きとして、SplunkインスタンスのプライベートFQDNはこれで登録

```hcl:Aレコードを作成
resource "aws_route53_record" "splunk_mydomain_internal" {
  zone_id = aws_route53_zone.mydomain_internal.zone_id
  name    = "splunk.mydomain.internal"
  type    = "A"
  ttl     = "60"
  records = [aws_instance.ec2_splunk.private_ip]
}
```

VPNから名前解決させるにはDNS Resolver Endpointが必要らしい
セキュリティグループのInboundはVPNのセキュリティグループを指定

各AZのサブネットに作成するので、[`dynamic`ブロック](https://www.terraform.io/language/expressions/dynamic-blocks)と`for_each`を使用

```hcl:Resolverとセキュリティグループ作成
resource "aws_route53_resolver_endpoint" "resolver" {
  name      = "${var.prefix}-resolver"
  direction = "INBOUND"

  security_group_ids = [
    aws_security_group.dns_resolver_sg.id
  ]

  dynamic "ip_address" {
    for_each = aws_subnet.private_subnet
    content {
      subnet_id = ip_address.value.id
    }
  }

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

resource "aws_security_group" "dns_resolver_sg" {
  vpc_id = aws_vpc.my_vpc.id
  name   = "${var.prefix}-dns-resolver-sg"
  description = "DNS Resolver Security Group"

  ingress {
    from_port       = 53
    to_port         = 53
    protocol        = "udp"
    security_groups = [aws_security_group.client_vpn_sg.id]
    description     = "DNS Resolver UDP"
  }

  ingress {
    from_port       = 53
    to_port         = 53
    protocol        = "tcp"
    security_groups = [aws_security_group.client_vpn_sg.id]
    description     = "DNS Resolver TCP"
  }

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

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

### Log GroupとLog Streamを作成

Client VPN Endpointのログ配信用にCloudWatchのロググループとログストリームを作成

```hcl:Log Group名とStream名は適当
resource "aws_cloudwatch_log_group" "client_vpn_lg" {
  name              = "/aws/client-vpn-endpoint/${var.prefix}"
  retention_in_days = 90

  tags = {
    Name = "${var.prefix}-client-vpn-lg"
  }
}

resource "aws_cloudwatch_log_stream" "client_vpn_ls" {
  name           = "${var.prefix}-client-vpn-endpoint"
  log_group_name = aws_cloudwatch_log_group.client_vpn_lg.name
}
```

### Client VPN Endpointを作成

そしていよいよClient VPN Endpointである
事前に色々作らなきゃいけないものが多かったね

躓いたポイントは↓

- CIDRブロックは最低でも `/22` にしないといけない（最初 `/24` でやったらコケた）
- Resolver Endpointを各サブネットに登録してあるから、`dns_servers` には `for` で全て登録

```hcl:いよいよEndpoint作るよ
resource "aws_ec2_client_vpn_endpoint" "client_vpn_endpoint" {
  description            = "my client vpn endpoint"
  server_certificate_arn = aws_acm_certificate.internal_acm.arn
  client_cidr_block      = "10.0.200.0/22"
  transport_protocol     = "udp"
  vpc_id                 = aws_vpc.my_vpc.id
  vpn_port               = "443"
  security_group_ids     = [aws_security_group.client_vpn_sg.id]

  dns_servers            = [
    for r in aws_route53_resolver_endpoint.resolver.ip_address : r.ip
  ]

  authentication_options {
    type                       = "certificate-authentication"
    root_certificate_chain_arn = aws_acm_certificate.internal_acm.arn
  }

  connection_log_options {
    enabled               = true
    cloudwatch_log_group  = aws_cloudwatch_log_group.client_vpn_lg.name
    cloudwatch_log_stream = aws_cloudwatch_log_stream.client_vpn_ls.name
  }

  tags = {
    Name = "${var.prefix}-client-vpn-endpoint"
  }
}
```

Client VPN EndpointのRoute TableにPrivate Subnetを付与

```hcl
resource "aws_ec2_client_vpn_network_association" "client_vpn_association" {
  for_each               = aws_subnet.private_subnet
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client_vpn_endpoint.id
  subnet_id              = each.value.id
}
```

Route TableにインターネットEgressを付与

```hcl
resource "aws_ec2_client_vpn_route" "client_vpn_internet_route" {
  for_each               = aws_subnet.private_subnet
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client_vpn_endpoint.id
  destination_cidr_block = "0.0.0.0/0"
  target_vpc_subnet_id   = each.value.id
}
```

Authorization Rules追加
各サブネット（前半部分）とインターネットEgress（後半部分）の2つを作成

```hcl
resource "aws_ec2_client_vpn_authorization_rule" "client_vpn_authorization" {
  for_each               = aws_subnet.private_subnet
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client_vpn_endpoint.id
  target_network_cidr    = each.value.cidr_block
  authorize_all_groups   = true
}

resource "aws_ec2_client_vpn_authorization_rule" "client_vpn_internet_authorization" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client_vpn_endpoint.id
  target_network_cidr    = "0.0.0.0/0"
  authorize_all_groups   = true
}
```

これらをapplyすればClient VPN Endpointは作成される

### Mac用Client VPNをダウンロードして設定

そしてクライアント側の設定に移る

まずはコンソールから設定ファイルをダウンロード
![aws_client_vpn_configuration_download.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174837/e512d416-0bcd-0b9d-8faf-a00a57782f62.jpeg)

ダウンロードした `.ovpn` 設定ファイルをいじる

- ドメイン名に適当なサブドメインを追加
- `<cert></cert>` と `<key></key>` を追加して保存

```
...
remote asdfa.cvpn-endpoint-xxxxxxxxxxxxxxxxx.prod.clientvpn.ap-northeast-1.amazonaws.com 443
...

<ca>
-----BEGIN CERTIFICATE-----
Root CAの証明書
-----END CERTIFICATE-----

</ca>

# ここから追加
<cert>
-----BEGIN CERTIFICATE-----
作成したクライアント証明書
-----END CERTIFICATE-----

</cert>

<key>
-----BEGIN RSA PRIVATE KEY-----
クライアント証明書のプライベートキー
-----END RSA PRIVATE KEY-----

</key>
```

そしてAWS VPN Clientをダウンロード
https://aws.amazon.com/jp/vpn/client-vpn-download/

編集した設定ファイルをVPN Clientのプロファイルに追加

[File] > [Manage Profiles]から**Add Profile**
![aws_client_vpn_add_profile.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174837/d1368d28-25de-2c24-1268-bb081e86db43.jpeg)

この状態でConnectをクリックするとVPNつながるよ

## EC2にアタッチするセキュリティグループを編集

最後にEC2のセキュリティグループを編集しよう

- EC2インスタンス間でのSSH用に自分自身のセキュリティグループのInboundを許可
- Client VPN EndpointからのSSHを許可
- 特定セキュリティグループからの全ポート開放は怖いから削除

```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"
    self        = true
    description = "SSH from This Security Group"
  }

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.client_vpn_sg.id]
    description     = "SSH from VPN Client"
  }

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

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

このセキュリティグループを各インスタンスにアタッチすればいいね。

## うまくいったかな？

VPNつないだ状態で自分の端末からSplunkのインスタンスに接続してみよう。

```bash
$ ssh ubuntu@splunk.mydomain.internal
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1059-aws x86_64)
...
Last login: Mon May  2 13:55:20 2022 from 10.0.x.xx
ubuntu@kikeyama-splunk:~$
```

うんうん、うまくいった！

更に [WhatIsMyIp](https://www.whatismyip.com/) にアクセスするとNAT GatewayにアタッチしたElastic IPになっていることが確認できるよ。

# おわりに

今回はけっこう大がかりなもので正直めんどくさかった。

「パブリックアクセス止めろ」って言われてもどうしたらいいんだという困惑と、どうしようもなくね！？という憤りも混じった複雑な感情になりながらも解決策に辿り着いたので、かなり満足。

ここまでやればもう大丈夫だろうという思いはあるものの、いかんせん多数のリソースを自分1人で管理しているので、何かしらの抜け漏れがありそう。

これ以上インシデントレスポンスチームに迷惑をかけたくないので、もう一度セキュリティグループを見直そうかな。

2022年5月現在、GuardDutyにはこれ以上何も出ていないが、もしかしたらまた何らかの攻撃を受けるかもしれない。
そのときはこのシリーズの記事を追加投稿しよう。

# 関連リンク

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