CloudWatchでSSH接続を検知する
概要
RHELに対するSSHパスワード認証失敗・時間外のSSH接続を検知する処理を構築することになったため、その備忘録として「CloudWatch Logsに/var/log/secureを転送→メトリクスフィルタで該当ログをフィルタリング→CloudWatch Alarmで拾ってメール通知」という流れをTerraformで実装しました。
構成図
実装
Terraformコードは下記GitHubに記載しています。本記事では一部を抜粋して紹介させていただきますので、詳細はGitHubをご確認ください。
├─ environments
│ ├─ prd
│ ├─ stg
│ └─ dev
│ ├─ main.tf
│ ├─ local.tf
│ ├─ variable.tf
│ └─ (terraform.tfvars)
│
├─ files
│ ├─ cloudwatch_agent.json(CloudWatch Agent設定用JSON)
│ └─ script.sh(RHEL初期構築用のUserdata)
│
└─ modules(各Moduleにmain.tf,variable.tf,output.tfが存在)
├─ ec2(EC2関連のリソース)
├─ initializer(tfstate用S3バケット作成)
├─ monitoring(CloudWatch Alarmやメトリクスフィルタなど監視関連のリソース)
└─ network(VPC、Subnet、VPNなどネットワーク全般のリソース)
SSHパスワード認証失敗検知
今回使用したRHEL9.4では、SSHパスワード認証が失敗するたびに/var/log/secureに以下のようなログが出力されます。そこで、[Mon, day, timestamp, ip, id, msg1= Failed, msg2 = password, ...]
というフィルタパターンをメトリクスフィルタにおいて設定することで、SSHパスワード認証失敗という事象を表現するメトリクスを出力するように構成しています。
Dec 16 13:35:11 ip-172-16-1-238 sshd[1918]: Failed password for test-user from 172.16.2.46 port 52167 ssh2
アラームを発報する閾値に関してですが、今回のSSHパスワード認証失敗検知はブルートフォース攻撃を想定したものであるため、「1分間に10回」認証失敗したらメール通知するように設定しました。操作ミスで1分間に10回も失敗することはないと思いますので、割といい塩梅なのかなと思っています。
# Define CloudWatch Logs Metric Filter
resource "aws_cloudwatch_log_metric_filter" "ssh_failures" {
for_each = { for i, s in var.ec2.instance_ids : i => s }
name = "${var.common.env}-metrics-filter-ssh-failures-${each.key}"
pattern = "[Mon, day, timestamp, ip, id, msg1= Failed, msg2 = password, ...]"
log_group_name = "/var/log/secure-${each.key}"
metric_transformation {
name = "SSH-Failures-${each.key}"
namespace = "CWAgent"
value = "1"
default_value = "0"
}
}
# Define CloudWatch Alarm for SSH Failures
resource "aws_cloudwatch_metric_alarm" "ssh_failures" {
for_each = { for i, s in aws_cloudwatch_log_metric_filter.ssh_failures : i => s }
alarm_name = "${var.common.env}-alarm-ssh-failures-${each.key}"
namespace = "CWAgent"
metric_name = "SSH-Failures-${each.key}"
statistic = "Sum"
period = 60
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 10
evaluation_periods = 1
datapoints_to_alarm = 1
treat_missing_data = "notBreaching"
alarm_description = "This metric monitors SSH failures."
alarm_actions = [aws_sns_topic.main.arn]
tags = {
Name = "${var.common.env}-alarm-ssh-failures-${each.key}"
}
}
時間外のSSH接続検知
業務時間外(21:00-7:00:JST)にSSH接続することはないとのことで、業務時間外にSSH公開鍵認証が成功した場合には即座にアラームを発報する構成としました。SSH公開鍵認証が成功するたびに/var/log/secureに以下のようなログが出力されます。そこで、[Mon, day, timestamp=%1[2-9]\:[0-5][0-9]\:[0-5][0-9]% || timestamp=%2[0-1]\:[0-5][0-9]\:[0-5][0-9]%, ip, id, msg1= Accepted, msg2 = publickey, ...]
というフィルタパターンをメトリクスフィルタにおいて設定することで、業務時間外のSSH公開鍵認証成功という事象を表現するメトリクスを出力するように構成しています。ログのタイムスタンプがUTCのため、検出する時間帯を12:00:00-21:59:59と設定しています。
Dec 16 13:39:24 ip-172-16-1-238 sshd[1994]: Accepted publickey for ec2-user from 172.16.2.46 port 52470 ssh2: RSA SHA256:xxxxxxxxxx
# Define CloudWatch Metric Filter for SSH during non-business hours
resource "aws_cloudwatch_log_metric_filter" "ssh_non_business_hours" {
for_each = { for i, s in var.ec2.instance_ids : i => s }
name = "${var.common.env}-metrics-filter-ssh-during-non-business-hours-${each.key}"
pattern = "[Mon, day, timestamp=%1[2-9]\\:[0-5][0-9]\\:[0-5][0-9]% || timestamp=%2[0-1]\\:[0-5][0-9]\\:[0-5][0-9]%, ip, id, msg1= Accepted, msg2 = publickey, ...]"
log_group_name = "/var/log/secure-${each.key}"
metric_transformation {
name = "SSH-during-non-business-hours-${each.key}"
namespace = "CWAgent"
value = "1"
default_value = "0"
}
}
# Define CloudWatch Alarm for SSH during non-business hours
resource "aws_cloudwatch_metric_alarm" "ssh_non_business_hours" {
for_each = { for i, s in aws_cloudwatch_log_metric_filter.ssh_non_business_hours : i => s }
alarm_name = "${var.common.env}-alarm-ssh-non-business-hours-${each.key}"
namespace = "CWAgent"
metric_name = "SSH-during-non-business-hours-${each.key}"
statistic = "Sum"
period = 60
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 1
evaluation_periods = 1
datapoints_to_alarm = 1
treat_missing_data = "notBreaching"
alarm_description = "This metric monitors SSH logins during non-business hours."
alarm_actions = [aws_sns_topic.main.arn]
tags = {
Name = "${var.common.env}-alarm-ssh-non-business-hours-${each.key}"
}
}
リソース使用率監視
また、SSHパスワード認証失敗の検知に加えて、各種リソース使用率も監視しています。
CloudWatch Alarm閾値は以下のように設定しました。
- CPU使用率:15分内の2データポイントのCPUUtilization≧80
- Memory使用率:15分内の2データポイントのmem_used_percent≧80
- Disk使用率:5分内の1データポイントのdisk_used_percent≧80
# Define CloudWatch Alarm for CPU Utilization
resource "aws_cloudwatch_metric_alarm" "cpu_utilization" {
for_each = { for i, s in var.ec2.instance_ids : i => s }
alarm_name = "${var.common.env}-alarm-cpu-utilization-${each.key}"
namespace = "AWS/EC2"
metric_name = "CPUUtilization"
dimensions = {
InstanceId = each.value
}
statistic = "Average"
period = 300
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 80
evaluation_periods = 3
datapoints_to_alarm = 2
treat_missing_data = "breaching"
alarm_description = "This metric monitors EC2 instance CPU utilization."
alarm_actions = [aws_sns_topic.main.arn]
tags = {
Name = "${var.common.env}-alarm-cpu-utilization-${each.key}"
}
}
# Define CloudWatch Alarm for Memory Utilization
resource "aws_cloudwatch_metric_alarm" "memory_utilization" {
for_each = { for i, s in var.ec2.instance_ids : i => s }
alarm_name = "${var.common.env}-alarm-memory-utilization-${each.key}"
namespace = "CWAgent"
metric_name = "mem_used_percent"
dimensions = {
InstanceId = each.value
}
statistic = "Average"
period = 300
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 80
evaluation_periods = 3
datapoints_to_alarm = 2
treat_missing_data = "breaching"
alarm_description = "This metric monitors EC2 instance memory utilization."
alarm_actions = [aws_sns_topic.main.arn]
tags = {
Name = "${var.common.env}-alarm-memory-utilization-${each.key}"
}
}
# Define CloudWatch Alarm for Disk Utilization
resource "aws_cloudwatch_metric_alarm" "disk_utilization" {
for_each = { for i, s in var.ec2.instance_ids : i => s }
alarm_name = "${var.common.env}-alarm-disk-utilization-${each.key}"
namespace = "CWAgent"
metric_name = "disk_used_percent"
dimensions = {
InstanceId = each.value
device = "nvme0n1p4"
fstype = "xfs"
path = "/"
}
statistic = "Average"
period = 300
comparison_operator = "GreaterThanOrEqualToThreshold"
threshold = 80
evaluation_periods = 1
datapoints_to_alarm = 1
treat_missing_data = "breaching"
alarm_description = "This metric monitors EC2 instance disk utilization."
alarm_actions = [aws_sns_topic.main.arn]
tags = {
Name = "${var.common.env}-alarm-disk-utilization-${each.key}"
}
}
動作確認
SSHパスワード認証失敗検知
Userdataとしてscript.shに記載したコマンドを定義しているため、terraform apply
コマンドを実行することで、テスト用ユーザ(testuser)を作成し、SSHパスワード認証を許可まで自動で設定されます。あとは頑張って1分間に10回パスワード認証を失敗するだけです。
ロググループに転送された/var/log/secureを確認すると、Failed password for testuser
というログが複数確認できると思います。
そして、CloudWatch Alarmを見てみると、SSH-Failures-0
のメトリクス値が10を超えたタイミングでアラーム状態に遷移しました!これにて「SSHパスワード失敗認証検知」の動作確認は完了です!
時間外のSSH接続検知
こちらもterraform apply
コマンドを実行した後に、21:00-7:00(JST)の間に公開鍵認証によりSSH接続してみます。/var/log/secureにAccepted publickey from ec2-user
というログが確認できるはずです。
こちらはCloudWatch Alarmの閾値を1と設定しているため、SSH-during-non-working-hours-0
のメトリクス値が1を超えた段階で、即座にアラーム状態に遷移していることを確認できます。以上で、「時間外のSSH接続検知」の動作確認も完了です!
最後に
なんと1週間前にDevelopersIO様の方で全く同じような記事がアップされていました...
トホホ..
参考
CloudWatch Agent設定
メトリクスフィルタパターン
Discussion