![[アップデート] GuardDuty 拡張脅威検出がEKSに対応しました](https://images.ctfassets.net/ct0aopd36mqt/6vZd9zWZvlqOEDztYoZCro/7349aaad8d597f1c84ffd519d0968d43/eyecatch_awsreinforce2025_1200x630-crunch.png)
[アップデート] GuardDuty 拡張脅威検出がEKSに対応しました
はじめに
皆様こんにちは、あかいけです。
re:Inforce にて、GuardDuty 拡張脅威検出が EKS をサポートしたことが発表されました。
今回私は AWS re:inforce 2025 に参加していませんが、検証してブログを書きます。
なお本記事の資材は以下に格納しているため、実際に検証したい場合はよければご利用ください。
アップデート概要
まず GuardDuty 拡張脅威検出自体は、2024 年 12 月に GA されています。
その上で今回のアップデートで EKS もサポートされるようになりました。
本記事では実際に 拡張脅威検出 で EKS が検出されるところまで検証します。
なお検出結果のサンプルを作成できるため、お手元で実際に確認したい場合はこちらのご利用もご検討ください。
(わざわざ EKS を作成して検出させるのは大変だと思うので……)
具体的には以下のコマンドで EKS の拡張脅威検出の結果 (AttackSequence:EKS/CompromisedCluster) だけを作成できます。
aws guardduty create-sample-findings \
--detector-id $(aws guardduty list-detectors --query 'DetectorIds' --output text) \
--finding-types 'AttackSequence:EKS/CompromisedCluster'
事前準備
まず 拡張脅威検出にてEKSを検出できるよう設定します。
拡張脅威検出の英語版ドキュメントにはEKSの記載があり、日本語化してみると以下の内容でした。
GuardDuty は、EKS 監査ログ、プロセスの実行時の動作、AWS API アクティビティにわたる複数のセキュリティシグナルを相関させ、高度な攻撃パターンを検出します。
EKS の拡張脅威検出を利用するには、EKS Protection または Runtime Monitoring(EKS アドオンを使用)のいずれか少なくとも 1 つの機能を有効にする必要があります。
EKS Protection は監査ログを通じてコントロールプレーンのアクティビティを監視し、Runtime Monitoring はコンテナ内の動作を観察します。
とのことなので、
今回は EKS Protection 、Runtime Monitoring の両方を有効化します。
リソース作成
今回は Terraform でまとめて作成します、作成しているリソースは以下の通りです。
作成完了まで大体 25 分ぐらいかかります。
- GuardDuty
- GuardDuty 有効化
- EKS Protection 有効化
- EKS Runtime Monitoring 有効化
- EKS
- ネットワーク関連リソース
- EKS 用 IAM ロール (クラスターロール、ノードグループロール)
- EKS クラスター関連リソース (クラスター、ノードグループ、アドオン)
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {
Environment = "test"
Project = "guardduty-eks"
}
}
variable "app_name" {
description = "Application name"
type = string
default = "guardduty-eks"
}
variable "cluster_version" {
description = "EKS cluster version"
type = string
default = "1.33"
}
variable "node_group_instance_types" {
description = "EC2 instance types for EKS managed node group"
type = list(string)
default = ["t3.medium"]
}
variable "node_group_scaling_config" {
description = "Scaling configuration for EKS managed node group"
type = object({
desired_size = number
max_size = number
min_size = number
})
default = {
desired_size = 2
max_size = 3
min_size = 1
}
}
locals {
eks_addons = [
"vpc-cni",
"coredns",
"kube-proxy",
"aws-ebs-csi-driver",
]
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_guardduty_detector" "main" {
enable = true
finding_publishing_frequency = "SIX_HOURS"
tags = {
Name = "${var.app_name}-guardduty-detector"
}
}
resource "aws_guardduty_detector_feature" "eks_protection" {
detector_id = aws_guardduty_detector.main.id
name = "EKS_AUDIT_LOGS"
status = "ENABLED"
}
resource "aws_guardduty_detector_feature" "eks_runtime_monitoring" {
detector_id = aws_guardduty_detector.main.id
name = "RUNTIME_MONITORING"
status = "ENABLED"
additional_configuration {
name = "EKS_ADDON_MANAGEMENT"
status = "ENABLED"
}
additional_configuration {
name = "ECS_FARGATE_AGENT_MANAGEMENT"
status = "DISABLED"
}
additional_configuration {
name = "EC2_AGENT_MANAGEMENT"
status = "DISABLED"
}
}
resource "aws_iam_role" "eks_cluster" {
name = "${var.app_name}-cluster-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "eks.amazonaws.com"
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.eks_cluster.name
}
resource "aws_iam_role" "eks_node_group" {
name = "${var.app_name}-node-group-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.eks_node_group.name
}
resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.eks_node_group.name
}
resource "aws_iam_role_policy_attachment" "eks_container_registry_readonly" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.eks_node_group.name
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, {
Name = "${var.app_name}-vpc"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(var.tags, {
Name = "${var.app_name}-igw"
})
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.app_name}-public-subnet-${count.index + 1}"
"kubernetes.io/role/elb" = "1"
})
}
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 10}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = merge(var.tags, {
Name = "${var.app_name}-private-subnet-${count.index + 1}"
"kubernetes.io/role/internal-elb" = "1"
})
}
resource "aws_eip" "nat" {
count = 2
domain = "vpc"
tags = merge(var.tags, {
Name = "${var.app_name}-nat-eip-${count.index + 1}"
})
depends_on = [aws_internet_gateway.main]
}
resource "aws_nat_gateway" "main" {
count = 2
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(var.tags, {
Name = "${var.app_name}-nat-gateway-${count.index + 1}"
})
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = merge(var.tags, {
Name = "${var.app_name}-public-route-table"
})
}
resource "aws_route_table" "private" {
count = 2
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
tags = merge(var.tags, {
Name = "${var.app_name}-private-route-table-${count.index + 1}"
})
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
resource "aws_security_group" "eks_cluster" {
name_prefix = "${var.app_name}-cluster-sg"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(var.tags, {
Name = "${var.app_name}-cluster-sg"
})
}
resource "aws_security_group" "eks_nodes" {
name_prefix = "${var.app_name}-node-sg"
vpc_id = aws_vpc.main.id
ingress {
description = "Node to node communication"
from_port = 0
to_port = 65535
protocol = "tcp"
self = true
}
ingress {
description = "Cluster API to node groups"
from_port = 1025
to_port = 65535
protocol = "tcp"
security_groups = [aws_security_group.eks_cluster.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(var.tags, {
Name = "${var.app_name}-node-sg"
})
}
# 現在のリージョンを取得するためのデータソース
data "aws_region" "current" {}
# VPC エンドポイント用セキュリティグループ
resource "aws_security_group" "vpc_endpoints" {
name_prefix = "${var.app_name}-vpc-endpoints-sg"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(var.tags, {
Name = "${var.app_name}-vpc-endpoints-sg"
})
}
# Interface型VPCエンドポイント
locals {
interface_endpoints = {
guardduty_data = {
service_name = "guardduty-data"
name_suffix = "guardduty-data-endpoint"
}
ecr_api = {
service_name = "ecr.api"
name_suffix = "ecr-api-endpoint"
}
ecr_dkr = {
service_name = "ecr.dkr"
name_suffix = "ecr-dkr-endpoint"
}
eks = {
service_name = "eks"
name_suffix = "eks-endpoint"
}
ec2 = {
service_name = "ec2"
name_suffix = "ec2-endpoint"
}
sts = {
service_name = "sts"
name_suffix = "sts-endpoint"
}
logs = {
service_name = "logs"
name_suffix = "logs-endpoint"
}
monitoring = {
service_name = "monitoring"
name_suffix = "monitoring-endpoint"
}
elasticloadbalancing = {
service_name = "elasticloadbalancing"
name_suffix = "elb-endpoint"
}
}
}
resource "aws_vpc_endpoint" "interface" {
for_each = local.interface_endpoints
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${data.aws_region.current.name}.${each.value.service_name}"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = merge(var.tags, {
Name = "${var.app_name}-${each.value.name_suffix}"
})
}
# Gateway型VPCエンドポイント
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = aws_route_table.private[*].id
tags = merge(var.tags, {
Name = "${var.app_name}-s3-endpoint"
})
}
resource "aws_eks_cluster" "main" {
name = var.app_name
role_arn = aws_iam_role.eks_cluster.arn
version = var.cluster_version
vpc_config {
subnet_ids = concat(aws_subnet.public[*].id, aws_subnet.private[*].id)
security_group_ids = [aws_security_group.eks_cluster.id]
endpoint_private_access = true
endpoint_public_access = true
public_access_cidrs = ["0.0.0.0/0"]
}
enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
depends_on = [
aws_iam_role_policy_attachment.eks_cluster_policy,
aws_cloudwatch_log_group.eks_cluster,
]
tags = merge(var.tags, {
Name = "${var.app_name}-cluster"
GuardDutyManaged = "true"
})
}
resource "aws_cloudwatch_log_group" "eks_cluster" {
name = "/aws/eks/${var.app_name}/cluster"
retention_in_days = 7
tags = var.tags
}
resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${var.app_name}-node-group"
node_role_arn = aws_iam_role.eks_node_group.arn
subnet_ids = aws_subnet.private[*].id
scaling_config {
desired_size = var.node_group_scaling_config.desired_size
max_size = var.node_group_scaling_config.max_size
min_size = var.node_group_scaling_config.min_size
}
instance_types = var.node_group_instance_types
ami_type = "AL2023_x86_64_STANDARD"
capacity_type = "ON_DEMAND"
disk_size = 20
update_config {
max_unavailable = 1
}
depends_on = [
aws_iam_role_policy_attachment.eks_worker_node_policy,
aws_iam_role_policy_attachment.eks_cni_policy,
aws_iam_role_policy_attachment.eks_container_registry_readonly,
]
tags = merge(var.tags, {
Name = "${var.app_name}-node-group"
GuardDutyManaged = "true"
})
}
resource "aws_eks_addon" "main" {
for_each = toset(local.eks_addons)
cluster_name = aws_eks_cluster.main.name
addon_name = each.value
resolve_conflicts_on_update = "OVERWRITE"
tags = var.tags
}
作成が完了したら kubeconfig を設定して、情報が取得できることを確認します。
aws eks update-kubeconfig --region ap-northeast-1 --name $CLUSTER_NAME
% kubectl get node
NAME STATUS ROLES AGE VERSION
ip-XX-XX-XX-XX.ap-northeast-1.compute.internal Ready <none> 4m22s v1.33.0-eks-XXXXXXX
ip-XX-XX-XX-XX.ap-northeast-1.compute.internal Ready <none> 4m19s v1.33.0-eks-XXXXXXX
また GuardDuty の ランタイムモニタリング にて、
作成した EKS クラスターと EC2 のカバレッジステータスが healthy になっていれば準備 OK です。
検出させてみた
さて、本題の検出についてですが、
こちらも拡張脅威検出の英語版ドキュメントに EKS の記載を日本語化してみます。
GuardDuty は、これらの関連イベントを AttackSequence:EKS/CompromisedCluster という単一の重大度レベルの検出結果として表します。
両方の保護プランを有効にすると、攻撃シーケンスの検出結果は次の脅威シナリオをカバーします。
このことから、EKS Protection や EKS Runtime Monitoring で検出された結果を、
最終的に AttackSequence:EKS/CompromisedCluster
という攻撃シーケンスで時系列順にまとめてくれるようです。
そのため今回は、Runtime Monitoring、EKS Protection で複数の検出を発生させ、
最終的に AttackSequence:EKS/CompromisedCluster
を出すことを目指します。
なお Runtime Monitoring、EKS Protection の検出タイプは以下をご参照ください。
試験内容
試験用とはいえ検出が発生するスクリプトをそのままブログに載せるのはアレな気がしたので、
以下リポジトリに置いています。
使い方はリポジトリの README でご確認ください。
なお後述の検出結果で記載していますが、
めちゃめちゃ検出が発生するので、個人の検証アカウントなどでお使いください。
検出結果
私の環境ではスクリプト実行から 50 分ぐらいで AttackSequence:EKS/CompromisedCluster
が検出されました。
ただこれは GuardDuty の気分次第なのと、もっとうまくスクリプトを作ればもう少し早く検出される気がするので、あくまで参考値とさせてください。
結果は以下の通りです。
一連の検出結果を時系列順にまとめてくれているので、視認性が非常に高くていいですね。
-
概要
-
シグナル
-
リソース
またこの画面をそのまま見るのもいいですが、
個人的には結果を生成 AI に渡してレポートを作成させるのもありかなと思いました。
拡張脅威検出の結果はアクションから JSON 形式でエクスポートできます。
以下はエクスポートを元に、 Claude Code で生成したレポートの例です。
(ガチなクレデンシャル情報は伏せ字にしています)
# GuardDuty EKSクラスター侵害検出レポート
## 概要
2025年6月22日、EKSクラスター「guardduty-eks」において重大なセキュリティ侵害が検出されました。攻撃者はクラスターの管理者権限を取得し、暗号通貨マイニング活動を実行しました。
## 検出サマリー
| 項目 | 値 |
|------|-----|
| 検出時刻 | 2025-06-22 10:14:22 JST |
| 攻撃期間 | 2025-06-22 09:29:31 ~ 10:26:12 JST(約1時間) |
| 検出シグナル数 | 43件 |
| 最高脅威レベル | 8(High) |
| 対象EKSクラスター | guardduty-eks |
## 主要な脅威
### 1. 権限昇格攻撃(Privilege Escalation)
- **内容**: cluster-admin権限への不正な昇格
- **手法**: comprehensive-attack-bindingロールバインディングの作成
- **MITRE ATT&CK**: T1098.006 - Account Manipulation: Additional Container Cluster Roles
### 2. 暗号通貨マイニング(CryptoCurrency Mining)
- **検出件数**: 30+ DNS/通信イベント
- **マイニングプール接続先**:
- gulf.moneroocean.stream
- xmr.pool.minergate.com
- pool.supportxmr.com
- pool.hashvault.pro
- **実行プロセス**: xmrig(暗号通貨マイナー)
### 3. 悪意のあるファイル実行(Malicious File Execution)
- **検出**: CoinMiner:Linux/Xmrig.Gen
- **実行場所**: /tmp/xmrig-6.21.0/xmrig
- **MITRE ATT&CK**: T1204.002 - User Execution: Malicious File
### 4. 匿名アクセス許可(Anonymous Access)
- **内容**: 匿名ユーザーへのAPI権限付与
- **検出回数**: 7回
- **脅威レベル**: High(8)
## 関与したアクター
### IAMロール
- **ユーザー**: XXX.XXX
- **ロール**: XXXXXXXXXXXXXXXXXX
- **クレデンシャル**: XXXXXXXXXXXXXXXXXX
### 悪意のあるプロセス
1. **cryptominer** (/tmp/cryptominer)
2. **xmrig** (/tmp/xmrig-6.21.0/xmrig)
3. **nc.openbsd** (netcat - 通信ツール)
4. **nslookup** (DNS検索)
## ネットワーク通信
### 疑わしい接続元IP
- **104.28.211.105**: Cloudflare VPN/匿名化ネットワーク
- **157.20.104.252**: 暗号通貨マイニング関連IP
- **49.12.80.40**: 暗号通貨マイニング関連IP
- **15.235.221.117**: 暗号通貨マイニング関連IP
## 対応策
### 即座に実行すべき対応
1. **アクセス権限の無効化**
# 関与したIAMロールの一時停止
aws iam attach-role-policy --role-name <role-name> --policy-arn arn:aws:iam::aws:policy/AWSDenyAll
2. **不正なロールバインディングの削除**
kubectl delete clusterrolebinding comprehensive-attack-binding
3. **悪意のあるプロセスの停止**
# マイニングプロセスの強制終了
kubectl exec -it <pod-name> -- pkill -f xmrig
kubectl exec -it <pod-name> -- rm -rf /tmp/xmrig-6.21.0
kubectl exec -it <pod-name> -- rm -f /tmp/cryptominer
4. **ネットワークアクセス制御**
- 暗号通貨マイニングプールへのアウトバウンド通信をブロック
- セキュリティグループでIP制限を強化
### 中長期的な対策
1. **RBAC強化**
- cluster-admin権限の最小化
- 定期的な権限監査の実施
- ロールバインディングの承認プロセス導入
2. **コンテナセキュリティ**
- Pod Security Standardsの適用
- イメージスキャンの義務化
- ランタイムセキュリティ監視の強化
3. **ネットワークセキュリティ**
- Network Policyの適用
- DNS監視の強化
- アウトバウンド通信の制限
4. **監視・検知の強化**
- GuardDuty EKS Protection有効化継続
- CloudTrail監査ログの詳細化
- 異常な権限昇格の自動検知
## 調査継続項目
1. 攻撃の侵入経路の特定
2. 他のクラスターへの水平展開の有無
3. データ漏洩の範囲調査
4. インシデント対応プロセスの見直し
## 結論
今回の侵害は、EKSクラスターの管理者権限を悪用した組織的な攻撃です。暗号通貨マイニングが主目的でしたが、クラスター全体への完全なアクセス権を取得されており、深刻なセキュリティ侵害として対処する必要があります。
**緊急度**: 最高
**影響範囲**: EKSクラスター全体
**推奨対応**: 即座のクラスター隔離とセキュリティ強化
さいごに
以上、GuardDuty 拡張脅威検出(EKS)の検証結果でした。
大量に発生した検出に相関性を見出すには、ある程度の経験や専門性が必要となるため、EKSのような複雑になりがちなサービスでも利用できるのは非常に有効だと感じました。
また、検出結果をJSON形式でエクスポートできるため生成AIに連携しやすく、分析作業を大幅に効率化できる点もとても良いと感じました。
是非とも、他のAWSサービスに対しても拡張脅威検出の対応が広がって欲しいですね。