こんにちは、とらのあなラボのはっとりです。
EC2インスタンスへのSSH接続は日常的な作業ですが、セキュリティと監査の観点から、誰がいつ接続したのかを把握することは非常に重要です。AWS Systems Manager Session Managerを使えば、セキュリティグループでSSHポート(22番)を開けることなく、安全にインスタンスへ接続できます。
今回は、Session ManagerでのSSH接続イベントをトリガーに、AWS ChatBot (※) 経由でSlackに通知する仕組みをTerraformで構築する方法をご紹介します。
※現在はAmazon Q Developer in chat applicationsという名前ですが、名前が長いため本記事ではChatBotと表記させていただきます。
構築する構成
- EC2インスタンスとIAM: Session Managerで接続される対象のEC2インスタンスと、接続に必要なIAMロールを作成します。
- CloudTrail (オプション): Session ManagerのAPI呼び出し履歴 (SSH接続開始イベント) を記録するために設定します。すでに設定済みの場合は不要です。
- 通知設定 (EventBridge, SNS): CloudTrailのログから特定のSSH接続イベントを検知し、SNSトピックに発行します。
- AWS ChatBot (手動設定): SNSトピックへの発行をトリガーに、指定したSlackチャンネルへ通知します。
Terraformで管理するのはSNSへの配信のみで、Slack通知部分(ChatBot)は手動設定が必要です。
前提条件
- AWSアカウントを持っていること
- Terraformの基本的な知識があること (インストール、
init
,apply
など) - Slackワークスペースと通知を受け取りたいチャンネルがあること
Terraformによる設定
今回はTerraformのコードを3つのファイルに分けて説明します。
- EC2インスタンスとIAM関連 (
ec2_iam.tf
) - CloudTrail関連 (
cloudtrail.tf
) - オプション - 通知設定関連 (
notification.tf
)
1. EC2インスタンスとIAM関連
provider "aws" { } # 以下のリソースは必要だが、ここでは省略 # - "aws_vpc" # - "aws_subnet" # - "aws_internet_gateway" # - "aws_route_table" # - "aws_route_table_association" # EC2インスタンスのリソース resource "aws_instance" "example" { ami = data.aws_ssm_parameter.amazonlinux_2023.value instance_type = "t2.micro" subnet_id = /* 省略 */ # associate_public_ip_address = false associate_public_ip_address = true vpc_security_group_ids = [ aws_security_group.example.id, ] iam_instance_profile = aws_iam_instance_profile.ec2_example.id tags = { Name = "example-instance" } lifecycle { ignore_changes = [ ami, ] } } resource "aws_security_group" "example" { vpc_id = aws_vpc.example.id name = "example-security-group" # 必要に応じて ingress も設定 # ingress { # from_port = 443 # to_port = 443 # protocol = "tcp" # cidr_blocks = ["0.0.0/0"] # } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_iam_instance_profile" "ec2_example" { name = aws_iam_role.ec2_example.name role = aws_iam_role.ec2_example.name } resource "aws_iam_role" "ec2_example" { name = "ExampleInstanceIamRole" description = "Role for EC2 instance" assume_role_policy = data.aws_iam_policy_document.ec2_example.json } data "aws_iam_policy_document" "ec2_example" { version = "2012-10-17" statement { effect = "Allow" actions = [ "sts:AssumeRole", ] principals { type = "Service" identifiers = [ "ec2.amazonaws.com", ] } } } resource "aws_iam_role_policy_attachment" "ec2_example_ssm_managed_instance_core" { role = aws_iam_role.ec2_example.name policy_arn = data.aws_iam_policy.ssm_managed_instance_core.arn } data "aws_iam_policy" "ssm_managed_instance_core" { name = "AmazonSSMManagedInstanceCore" } # 最新の Amazon Linux 2023 の AMI ID を取得 data "aws_ssm_parameter" "amazonlinux_2023" { name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64" }
ここでは、Session Managerで接続する対象のEC2インスタンスを作成します。重要なのはIAM関連の設定です。
ここでのポイントは
AmazonSSMManagedInstanceCore
というAWS管理ポリシーをアタッチします。このポリシーにより、EC2インスタンスはSSMエージェントを通じてSystems Managerサービスと通信できるようになり、Session Managerでの接続が可能になります。- EC2インスタンスに適用するセキュリティグループは。SSHポート(22番)を開ける必要はありません。
また、このインスタンスに接続するためのIAMグループを作っておきます。
# SSHでインスタンスにアクセスするためのIAMグループ resource "aws_iam_group" "ssh_instance_for_developer" { name = "SshInstanceForDeveloper" } resource "aws_iam_group_policy" "ssh_instance_for_developer" { name = "SshInstanceForDeveloper" group = aws_iam_group.ssh_instance_for_developer.name policy = data.aws_iam_policy_document.ssh_instance_for_developer.json } data "aws_iam_policy_document" "ssh_instance_for_developer" { version = "2012-10-17" statement { effect = "Allow" actions = [ "ec2:DescribeInstances", ] resources = [ "*", ] } statement { effect = "Allow" actions = [ "ec2-instance-connect:SendSSHPublicKey", "ssm:StartSession", "ssm:DescribeSessions", ] resources = [ aws_instance.example.arn, ] } statement { effect = "Allow" actions = [ "ssm:StartSession", ] resources = [ "arn:aws:ssm:*:*:document/AWS-StartPortForwardingSession", "arn:aws:ssm:*:*:document/AWS-StartSSHSession", "arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell", ] } statement { effect = "Allow" actions = [ "ssm:DescribeSessions", "ssm:TerminateSession", ] resources = [ "*", ] } }
このIAMグループに所属させたIAMユーザーで以下のようにSSHが可能になります。
※ Session Manager プラグイン のインストールが必要になります。
aws ssm start-session --target (i-から始まるEC2インスタンスのID)
2. CloudTrail関連
履歴通知のためにはCloudTrailの設定(管理イベント)が必要になります。Systems Managerを利用するだけならば不要です。
Session Managerの接続開始イベント (StartSession
) はAPI呼び出しとして記録されます。このAPI呼び出し履歴を取得するためにCloudTrailが必要です。
もしTerraformで設定する場合は、以下のリソースを作成します。
もし、すでに対象リージョン・アカウントで管理イベントを記録するCloudTrailが有効になっている場合は、このTerraformコードは不要です。 Webコンソールなどで設定済みか確認してください。
data "aws_caller_identity" "current" { } data "aws_region" "current" {} resource "aws_s3_bucket" "cloudtrail_logs" { bucket = "cloudtrail-logs-example-${random_string.cloudtrail_logs_bucket_suffix.result}" } resource "aws_s3_bucket_public_access_block" "cloudtrail_logs" { bucket = aws_s3_bucket.cloudtrail_logs.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "random_string" "cloudtrail_logs_bucket_suffix" { length = 32 special = false upper = false } locals { cloudtrail_name = "management-event" } resource "aws_cloudtrail" "example" { depends_on = [ aws_s3_bucket_policy.cloudtrail_logs_bucket_policy, ] name = local.cloudtrail_name s3_bucket_name = aws_s3_bucket.cloudtrail_logs.id is_multi_region_trail = true include_global_service_events = true advanced_event_selector { name = "Managed Event Selector" field_selector { field = "eventCategory" equals = ["Management"] } } } resource "aws_s3_bucket_policy" "cloudtrail_logs_bucket_policy" { bucket = aws_s3_bucket.cloudtrail_logs.id policy = data.aws_iam_policy_document.cloudtrail_logs_bucket_policy.json } data "aws_iam_policy_document" "cloudtrail_logs_bucket_policy" { statement { principals { type = "Service" identifiers = ["cloudtrail.amazonaws.com"] } actions = [ "s3:GetBucketAcl", ] resources = [ aws_s3_bucket.cloudtrail_logs.arn, ] condition { test = "StringEquals" variable = "AWS:SourceArn" values = [ "arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.cloudtrail_name}", ] } } statement { principals { type = "Service" identifiers = ["cloudtrail.amazonaws.com"] } actions = [ "s3:PutObject", ] resources = [ "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*", ] condition { test = "StringEquals" variable = "AWS:SourceArn" values = [ "arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.cloudtrail_name}", ] } condition { test = "StringEquals" variable = "s3:x-amz-acl" values = [ "bucket-owner-full-control", ] } } }
3. 通知設定関連
ここが今回のメイン設定です。CloudTrailに記録されたSession Managerの接続開始イベントを検知し、SNSトピックへ連携します。
# SSHの履歴を通知するためのEventBridgeルール resource "aws_cloudwatch_event_rule" "ssh_notify_rule" { name = "SshNotifyRule" event_pattern = jsonencode({ source = [ "aws.ssm", ] detail-type = [ "AWS API Call via CloudTrail", ] detail = { eventSource = [ "ssm.amazonaws.com", ] eventName = [ "StartSession", ] requestParameters = { target = [ aws_instance.example.id, ], } } }) } resource "aws_cloudwatch_event_target" "ssh_notify_target" { target_id = "SshNotify" rule = aws_cloudwatch_event_rule.ssh_notify_rule.name arn = aws_sns_topic.ssh_notify.arn role_arn = aws_iam_role.ssh_notify_role.arn input_transformer { input_paths = { "userName" = "$.detail.userIdentity.userName", "roleName" = "$.detail.userIdentity.sessionContext.sessionIssuer.userName", "sessionId" = "$.detail.responseElements.sessionId", } input_template = <<-EOF { "version": "1.0", "source": "custom", "content": { "title": "インスタンスへのSSH通知", "description": "接続しました。\nユーザー: <userName> <roleName>\nセッションID: <sessionId>" } } EOF } } resource "aws_sns_topic" "ssh_notify" { name = "SshNotifyTopic" } resource "aws_iam_role" "ssh_notify_role" { name = "SshNotifyIamRole" assume_role_policy = data.aws_iam_policy_document.ssh_notify_assume_role.json } data "aws_iam_policy_document" "ssh_notify_assume_role" { version = "2012-10-17" statement { effect = "Allow" actions = [ "sts:AssumeRole", ] principals { type = "Service" identifiers = [ "events.amazonaws.com", ] } condition { test = "StringEquals" variable = "aws:SourceAccount" values = [ data.aws_caller_identity.current.account_id, ] } condition { test = "StringEquals" variable = "aws:SourceArn" values = [ aws_cloudwatch_event_rule.ssh_notify_rule.arn, ] } } } resource "aws_iam_role_policy" "ssh_notify_role" { name = aws_iam_role.ssh_notify_role.name role = aws_iam_role.ssh_notify_role.name policy = data.aws_iam_policy_document.ssh_notify_role.json } data "aws_iam_policy_document" "ssh_notify_role" { version = "2012-10-17" statement { effect = "Allow" actions = [ "sns:Publish", ] resources = [ aws_sns_topic.ssh_notify.arn, ] } }
aws_cloudwatch_event_rule
(ssh_notify_rule
): 特定のイベントを検知するためのEventBridgeルールです。event_pattern
:source
:aws.ssm
(Systems Managerからのイベント)detail-type
:AWS API Call via CloudTrail
(CloudTrail経由のAPI呼び出し)detail.eventSource
:ssm.amazonaws.com
detail.eventName
:StartSession
(接続開始イベント)detail.requestParameters.target
: [aws_instance.example.id] (特定のEC2インスタンスIDを指定)
aws_sns_topic
(ssh_notify
): EventBridgeルールが検知したイベントを発行する先のSNSトピック。aws_cloudwatch_event_target
(ssh_notify_target
): EventBridgeルールとSNSトピックを接続するターゲット設定です。arn
: 作成したSNSトピックのARNを指定します。role_arn
: EventBridgeがSNSトピックに発行するための権限を持つIAMロールのARNを指定します。input_transformer
: Slackに通知するメッセージの内容を整形します。input_paths
: イベントJSONから抽出したい情報を定義します (接続ユーザー名、引き受けたロール名、セッションID)。input_template
: 抽出した情報を使って、ChatBotが解釈できる形式 (今回はSlack向けのJSON) でメッセージテンプレートを作成します。
aws_iam_role
(ssh_notify_role
) と関連ポリシー: EventBridgeがSNSトピックへ発行 (sns:Publish
) する権限を持つIAMロールです。信頼ポリシーでEventBridgeサービス (events.amazonaws.com
) を信頼し、SourceArn
とSourceAccount
で呼び出し元を制限しています。
Terraformの適用
上記3つのファイル (または ec2_iam.tf と notification.tf、必要なら cloudtrail.tf) を同じディレクトリに配置し、Terraformコマンドを実行します。
terraform init terraform apply
これで、EC2インスタンス、IAMロール、(必要ならCloudTrail)、EventBridgeルール、SNSトピックが作成されます。
AWS ChatBotの手動設定
Terraformでのリソース作成後、AWS ChatBotの設定を行います。(ChatBot自体の設定はTerraformで完全に自動化するのが少し難しい部分があるため、ここでは手動設定とします。)
- AWSマネジメントコンソールを開き、Amazon Q Developer in chat applications(旧ChatBot)サービスに移動します。
- 「チャットクライアント」で「Slack」を選択し、「設定」ボタンをクリックします。
- Slackワークスペースへのアクセス許可を求められるので、指示に従って認証・承認します。(初回のみ)
- Webコンソール上で登録したSlackワークスペースを開き、「新しいチャネルを設定」をクリックします。
- 設定名: 任意の名前を入力します (例:
ec2-ssh-notify
)。 - ログ記録 (オプション): 必要であれば設定します。
- Slackチャネル:
- 設定の詳細: わかりやすい名前を入力します。チャンネル名などが良いでしょう。
- チャネルタイプ: 「パブリック」または「プライベート」を選択します。
- パブリックチャネル名 or チャネルID: 通知を送りたいSlackチャンネルのIDを入力します。(チャンネル名を右クリックして「リンクをコピー」し、URLの末尾の方にある文字列を取得します。)
- IAMアクセス許可:
- 「ロール設定」で「テンプレートを使用してIAMロールを作成する」を選択します。
- ロール名: 任意の名前を入力します。
- ポリシーテンプレート: デフォルトのままでよいです。
- チャネルガードレールポリシー: デフォルトのままでよいです。
- 通知 - オプション:
- 「リージョン」でTerraformリソースを作成したリージョンを選択します。
- 「SNSトピック」で、Terraformで作成したSNSトピック
SshNotifyTopic
を選択します。
- 「設定」ボタンをクリックして保存します。
動作確認
AWSマネジメントコンソールまたはAWS CLIから、Terraformで作成したEC2インスタンスに対してSession Managerを使用して接続します。
aws ssm start-session --target (i-から始まるEC2インスタンスのID)
接続が成功すると、数秒〜数十秒後に設定したSlackチャンネルに以下のような通知が届けば成功です。
インスタンスへのSSH通知 接続しました。 ユーザー: <接続に使用したIAMユーザー名 or ロール名> セッションID: <sm-xxxxxxxxxxxxxxxxx>
まとめ
今回は、Terraformを使い、Session ManagerによるEC2インスタンスへのSSH接続イベントを、Slackにリアルタイムで通知する仕組みを構築する方法をご紹介しました。
CloudTrailのログを直接監視する代わりにSlackへ通知することで、誰かがインスタンスに接続したことをより迅速かつ容易に把握できます。特に、Amazon Linux 2やAmazon Linux 2023など、SSM Agentがプリインストール済みのAMIを利用すれば、インスタンス側の設定も不要です。
この仕組みを導入することで、意図しない接続の早期発見や、チーム内での接続状況の可視化に繋がります。