はじめに
前回の話: 検証用AWSアカウントでGuardDutyと闘う話 〜S3バケット編〜
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
で作成
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
とする
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 ゲートウェイをセットアップするにはどうすればよいですか?
この場合Public Subnetも必要になるが、それはこの後のステップで作成しよう
NAT GatewayにはElastic IPが必要になるので、それも追加
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に関連付け
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を自動生成
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
に開けているが、後にこれによって別のインシデントが引き起こされた。その経緯と対処についてはまた別の記事で。良い子は真似しないように。
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"
}
}
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"
}
}
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だよね
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キーを作成して自分の端末に保存、キーペアとして登録
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は公開されている ので、HEC用のALBもこのIPアドレスに絞れば良かったけど、今後他のIPアドレスからもIngestするかもと思って、当時は制限していなかった(今は制限してるよ)
このインスタンスはPrivate Subnetに作成したわけだが、SSHで入りたいときにはPublic Subnetに作成した別のインスタンスを踏み台にしてSSH Agentを使ってこのインスタンスに入ることにした。
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用の証明書発行
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に展開
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"
}
}
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の名前解決
まずはゾーンを作成
resource "aws_route53_zone" "mydomain_com" {
name = "mydomain.com"
tags = {
Name = "${var.prefix}-com-zone"
}
}
Splunk WebとHECの2レコード登録
最初はCNAMEしてたけど、ALBだしエイリアス使っちゃおうかな
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した方が楽でしょう。
おわりに
前回S3でアラート上がってから1ヶ月もしないうちに今回またGuardDutyで検知してしまった。
今回は前回とは別の人だったけど、何度もアラートあげてしまい、本当に申し訳ないことをした。
正直なところ、今回の構成(特にセキュリティグループ)は検証用としてはあまり良くない状態なので、注意してご利用いただきたい。
このEC2インスタンスにSSHログインするためにPublic Subnetに適当に作った踏み台インスタンスとSSHポート開放によって別のインシデントが起きたわけだが、それは次の記事で書こう。乞うご期待。
関連リンク
第1回: 検証用AWSアカウントでGuardDutyと闘う話 〜S3バケット編〜
第2回: 検証用AWSアカウントでGuardDutyと闘う話 〜不用意にポート開けるな編〜(当記事)
第3回: 検証用AWSアカウントでGuardDutyと闘う話 〜怒りのVPN編〜