アクセスキー漏洩による AWS アカウント制限と、その解除までの記録
IAM のアクセスキーが漏洩し、AWS からアカウント制限を受けた。攻撃自体は全て失敗していたが、本番は 6 日 20 時間止まった。CloudTrail と Cost Explorer での被害の切り分け方、制限が Lambda に限定されていたことの見分け方、そして通知の指示どおり対処してもなお解除されなかった制限をどう解いたかの記録。
通知は、届いていた
朝の 10 時 48 分、AWS から件名 [Action Required] Suspicious activity in your AWS account のメールが届いた。個人で運用している AWS アカウントに対する、アクセスキー漏洩の通知だった。
同じタイミングで、AWS はアカウントに対して サービス利用の制限 をかけていた。あとから調べたら、Lambda のログはその日の 10 時 31 分から 10 時 47 分にかけて、全ての関数で途切れていた。通知の送信時刻と、分単位で一致していた。
つまりこの瞬間、本番環境は止まっていた。
私がそのメールを見たのは、その日の夜だ。開発はしていなかった。ごく普通の日常を送っていた。すぐに対応できる状況ではなく、見はしたが動けなかった。
見立ては合っていた。外したのは優先度だった
見た瞬間に思ったのは「キーを作り直せば終わる話だろう」だった。
この見立て自体は、結果的に間違っていない。 実際にやることの中心は、漏洩したキーの無効化だった。
まずかったのは、そこから先だ。「鍵のローテーションなら、手が空いたときにやればいい」と優先度を落とした。
これが一番大きい判断ミスだった。鍵の入れ替えは、こちらが着手するまで何も進まないし、着手するまで何も壊れない——そういう性質の作業に見える。だから「作業できるタイミングでいいや」と後ろに置いた。
実際には、AWS はもう本番を止めていた。
通知が届いた時刻と、Lambda のログが途切れた時刻は分単位で一致していた。私が「あとでやる作業」に分類したその瞬間、サービスは既に落ちていた。 私はそれを知らないまま、丸一日を過ごした。
気づけなかった要因はもうひとつある。5〜6 通がまとめて届いたこと、そしてその形式が、ふだんの AWS のアナウンスと区別できなかったこと。
AWS はこの手の通知を 1 通にまとめてくれない。漏洩したキーごとにサポートケースが立ち、それぞれについて通知が飛ぶ。 件名も似ている。
[Action Required] Suspicious activity in your AWS account
[Action Required] Please review your AWS Account and credentials
似た件名が束で並ぶと「同じ内容の重複だろう」と処理してしまう。実際に開いたのは 1 通だけで、それも軽く目を通した程度だった。メンテナンス予告やサービス更新の案内と並ぶと、見た目では区別がつかない。 件名の [Action Required] も通常の案内で使われることがある以上、それだけでは緊急性の判別に使えない。
PC で見ていたことも、良い方向には働かなかった。 一覧で束をまとめて見渡せてしまうぶん、1 通ずつ開かずに、並びの印象だけで処理できてしまう。
本文には、やるべきことが Step 1 から Step 4 まで明記されていて、対応期限も書かれていた。そして「アカウントに制限をかけた」ことも書かれていた。
だからここでの教訓は「ちゃんと読もう」ではない。もっと具体的に言える。
クラウド事業者からのセキュリティ通知は、「自分が作業するまで何も起きない依頼」ではない。通知が届いた時点で、事業者側の措置は既に実行されている可能性がある。 作業量の見積もりで優先度を決めてはいけない。まず確認すべきは、自分が何をするかではなく、もう何かが起きていないかのほうだった。
自分のサービスが使えなくなって、ようやく動いた
状況が変わったのは翌日の夕方だ。自分で運用している Web サービスを、普通に利用者として使おうとした。
使えなかった。
ここで初めて緊急度が上がった。予定を後回しにして対応を始めたのは、さらにその翌日の朝。通知が届いてから、丸一日以上が経っていた。
最初に疑ったのは、AI の操作が AWS 側の何かを踏んだ可能性だった
対応を始めるにあたって、最初に頭に浮かんだ可能性はこうだった。
私は AI コーディングエージェントに、AWS を調査させるための読み取り専用 IAM ユーザーを与えている。ログを見たり、リソース構成を確認したりするためのものだ。
疑ったのは権限を与えたこと自体ではない。読み取り専用に絞る設計は自分で決めてやっていることなので、そこに疑う余地は無かった。
疑ったのは、その権限の範囲内でエージェントが実行した操作が、AWS 側の何かに引っかかったのではないか、というほうだった。読み取り専用なら壊しようが無いとはいえ、短時間に大量の API を叩くような動き方をすれば、AWS の側から見て異常に映ることはありうる。「悪いことはしていないが、AWS のルールに触れる動き方をしてしまったのではないか」という感覚に近い。
結果から言えば、これは外れだった。制限の原因は、AI とはまったく関係のない、放置されていたアクセスキーの漏洩だった。
ただ、疑いの向け方としては間違っていなかったと思っている。 自分のアカウントで直近に増えた変数は何か、と考えれば、エージェントに権限を渡したことは真っ先に挙がる。そこを最初に潰しにいくのは順当だ。
だから調査もそのままエージェントに投げた。疑わしいから問い詰めたのではなく、無実であればそのまま調査までやってくれるだろう、という前提だった。実際そうなった。読み取り専用の IAM ユーザーが実際に何を叩いていたかは CloudTrail に全部残っているので、「シロであること」を自分で証明させられる。そういう使い方ができるのは、権限を絞ってあったからでもある。
root にログインできない
対応の最初でつまずいた。root アカウントに、いつものパスワードと二段階認証でログインできなかった。パスワードの変更を求められた。
「アカウント関連の何かだろうな」とは思ったが、この時点でもまだ事の重大さには気づいていない。パスワードをリセットしてログインし、まず DynamoDB を見た。データは消えていなかった。 ここで少し安心した。
再デプロイすれば治る、と思っていた
次にやったのは、CI から本番デプロイを流すことだった。「ひとつずつ再デプロイしていけば元に戻るだろう」くらいの感覚だった。
なぜか成功しない。
違和感が明確になったのはここからだ。デプロイが通らないのは、コードの問題でもパイプラインの問題でもなく、アカウントそのものに制限がかかっているからだった。
調べたら、本物だった
腰を据えて CloudTrail を確認した。漏洩したとされるキーは、実際に使われていた。
| 日時 (UTC) | 操作 | 結果 |
|---|---|---|
| 08-27 06:04 | iam:CreateRole |
AccessDenied |
| 08-27 14:17 | iam:CreateRole |
AccessDenied |
| 08-29 09:16 | iam:CreateUser |
AccessDenied |
| 08-29 12:15 | sts:GetFederationToken |
AccessDenied |
| 08-29 18:22 | ec2:RunInstances |
Client.VPCIdNotSpecified |
決め手は User-Agent だった。
Boto3/1.43.80 md/Botocore#1.43.80 ua/2.1
os/linux#6.18.12+kali-cloud-amd64 md/arch#x86_64 lang/python#3.13.12
kali-cloud-amd64。Kali Linux のクラウドイメージから Boto3 で叩かれていた。 送信元も、普段 CI が使っているクラウドのレンジとはまったく別のホスティング業者の IP だった。
行動も教科書どおりだった。まず永続化を狙って CreateRole と CreateUser、次に GetFederationToken で権限昇格、最後に RunInstances で計算リソースの確保。キーを盗んだあとにやることが、綺麗に並んでいる。
正直なところ、この時点で強い焦りはなかった。「ついに自分のアカウントにも来たか」という気持ちのほうが近い。Cost Explorer を早い段階で見て不審な課金がないことは確認していたし、データも消えていなかったからだ。
全部、失敗していた
5 回の試行のうち 4 回は素直に AccessDenied で終わっている。
漏洩したのは、かなり前に使っていて放置されていた IAM ユーザーのキーだった。そのユーザーに付いていた権限は、CloudWatch Logs、Secrets Manager、DynamoDB の 3 つ。IAM も STS も EC2 も持っていなかった。 だから攻撃者は、何ひとつ実行できなかった。
唯一 AccessDenied ではなかった RunInstances も、EC2 側がパラメータ検証で先に弾いた形で終わっていた。Cost Explorer で確認したところ、EC2 のコンピュート課金はその期間ずっと 0 ドルだった。インスタンスは一台も起動していない。
Secrets Manager への読み取りアクセスも、記録上は一件も無かった。権限としては持っていたのに、そこへ到達する前にキーが無効化されたのだと思う。
助かった理由は、対応の速さではない。 対応は明確に遅かった。助かったのは、そのキーが最小権限に絞られていたからだ。
調査でつまずいたところ
ここからは、同じ状況になった人向けの技術メモ。
CloudTrail の Event history には落とし穴が三つある
一つ目は期間指定。 コンソールの Event history をそのままエクスポートすると、開いた時点のフィルタ(デフォルトでは直近)がそのまま効く。私は最初、事件の三日前の 24 時間分をエクスポートしてしまい、「不審な操作は一件も無い」という誤った結論に一度たどり着いた。当然だ、その期間を見ていないのだから。必ず時間範囲を明示的に設定すること。
二つ目は絞り込み条件。 Event history は検索属性を 一つしか指定できない。ユーザー名とアクセスキーの両方で同時に絞ることはできないので、調べたいキーごとにエクスポートを分ける必要がある。ユーザー名よりも、アクセスキー ID で引くほうが確実だった。
三つ目は記録範囲。 Event history に載るのは 管理イベントだけ だ。S3 のオブジェクト読み取りや DynamoDB の項目取得といったデータイベントは、専用の証跡を別途設定していない限り記録されない。つまり「テーブルの中身を読まれたかどうか」は、この経路では分からない。 テーブルの作成・削除といった管理操作が無かったことは確認できるので、破壊されていないことだけは言える。
なお Event history の保持期間は 90 日ある。事件から数日経っていても、記録は残っている。
Cost Explorer で被害額を見るときは、フィルタ名を検証する
不正利用による課金の有無を調べるなら、リージョンごとに見て回るのではなく Cost Explorer を使う。悪用は複数リージョンに散らされるのが普通だからだ。
aws ce get-cost-and-usage --region us-east-1 \
--time-period Start=2026-08-20,End=2026-08-31 \
--granularity DAILY --metrics UnblendedCost \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["Amazon Bedrock"]}}' \
--group-by Type=DIMENSION,Key=REGION
Cost Explorer API のエンドポイントは us-east-1 固定 だが、返ってくるデータは全リージョン分だ。
ここで注意したいのは、「ヒットが 0 件」を「課金が無い」と読んでいいかどうか。サービス名の指定が間違っていても同じく 0 件になる。私は念のため、フィルタを外してサービス別の内訳を取り直し、使用実績ゼロのサービスまで一覧に出てくる中に、目的のサービスが項目として存在しないことを確認した。そこまでやって初めて「本当に使われていない」と言える。
もう一点、Cost Explorer のデータは 約 24 時間遅れる。直近の数字は数日後にもう一度見たほうがいい。
get-access-key-last-used は、拒否された呼び出しも記録する
漏洩したキーの最終利用を調べたとき、こういう結果が返ってきた。
最終使用: 2026-08-29T23:49:00Z bedrock us-east-1
生成 AI のサービスが出てきたので、一瞬「モデルを回されて高額請求か」と身構えた。だが Cost Explorer での課金はゼロだった。
このコマンドが返すのは 最後に認証された呼び出し で、ポリシーで拒否されたものも含む。つまり「キーが有効か試された」痕跡であって、課金が発生したという意味ではない。サービス名だけを見て被害を判断してはいけない。
GitHub の Secret scanning が出すものと、AWS が言っているものは別物のことがある
漏洩経路を探すために、GitHub のリポジトリで Secret scanning のアラートを初めて開いた。3 件出てきた。
ところがそれらは全て ASIA で始まるキー ID だった。ASIA は STS の一時認証情報で、AWS が通知してきた AKIA で始まる恒久キーとは別物だ。過去の Pull Request に混入していたもので、中身を見ても今回の件とは関係が無かった。
つまり 恒久キーがどこから漏れたのかは、いまだに特定できていない。キー自体は削除済みなので当座の危険は無いが、経路が分からないままだと再発しうる。「Secret scanning にアラートが出ているから、これが原因だろう」と早合点しなくてよかったと思っている。
指示どおり対処しても、制限は解除されなかった
ここからが、今回いちばん長かった。
AWS からの通知には、やるべきことが Step 1 から Step 4 まで書かれている。鍵の無効化、CloudTrail の確認、リソースの確認、そして Step 4 として「完了した旨をサポートケースに書面で返信する」。この最後が抜けると制限は解除されない。自動では閉じない。キーごとにケースが別々に立つ点にも注意がいる。私の場合は 3 件立っていた。
全部やった。8 月 30 日に完了報告まで返信した。
それでも Lambda は動かないままだった。
まず、制限の範囲を特定する
漠然と「アカウントが止まっている」と思っていたが、実際に叩いてみると違った。
lambda:ListFunctions
-> AccessDeniedException(理由の記載なし)
logs / iam / cloudtrail / ce / dynamodb / ec2 / s3 / events / cloudformation
-> すべて成功
止まっていたのは Lambda だけだった。 EventBridge のルールは有効なまま時刻どおり発火していて、その先の Lambda 起動だけが弾かれる。だから「スケジュールは動いているのに何も起きない」という見え方になっていた。
ここで効いたのが、エラーの形の違いだ。
- IAM ポリシーによる拒否は、拒否されたアクション名とリソースの ARN を名指しする
- AWS 側のアカウント制限は、
AccessDeniedExceptionとだけ返して理由を返さない
理由が空のまま拒否される、しかも特定のサービスだけ。これは自分の権限設定では起こせない形で、AWS 側で止められている証拠になる。自分の設定を疑って延々と IAM を眺めるより先に、ここを確認したほうがいい。
この切り分けは、サポートに状況を伝えるときの材料としても効いた。
サポートが動かない
そこからが厳しかった。
- 3 件のケースに返信したが、反応が無い
- ライブチャットを開いても繋がらない。繋がっても、担当者が調べている間に10 分無操作で自動的に切断される
- Basic プランなので電話は選べない
- 一晩経っても、二晩経っても状況が変わらない
焦りよりも「これ、いつまで続くんだ」という感覚に近かった。
AWS re:Post で分かった、本当の理由
行き詰まって、AWS の公式 Q&A サイトである AWS re:Post で同じ症状を探した。ほぼ同一のスレッドが複数見つかり、そこに核心が書かれていた。
アカウントに付与された「compromised」という属性が残っている限り、Lambda は使えない。この属性を外せるのはセキュリティチームだけで、通常のテクニカルサポートのケースでは解決できない。
1 年前のスレッドのベストアンサーも、解決経路は同じだった。「1 時間待ってライブチャットに繋がり → 担当者がセキュリティチームに連絡 → セキュリティチームが compromised ラベルを外した」。その 8 ヶ月後のコメントで、別の人がまったく同じ症状(ケースが放置される、チャットが繋がらない、Basic では電話が使えない)を報告していた。
つまり、待っていても自動では解けない。そして解除まで数日かかるのは異常ではない。5 日経っても解除されなかった例が挙がっていた。
これが分かって、待ち方が変わった。ケースを増やさず(重複ケースはむしろ遅延を招くと明記されていた)、セキュリティチームへ取り次いでもらう一点に絞った。
効いたのは「業務影響を具体的に答えること」だった
6 日目、ようやくライブチャットが繋がった。
そこで聞かれたのは、技術的な質問ではなかった。
- これはビジネスにどう影響していて、なぜ急ぐ必要があるのか
- 現在、何人のユーザーが影響を受けているのか
この 2 問に答えられるかどうかが、社内エスカレーションが起動するかどうかの分かれ目だった。
数を盛る必要はない。私は正直に「5 人程度」と答えた。個人アカウントなので絶対数は小さい。そのうえで、書いたのは比率のほうだ。
- 稼働中のサービスは全て Lambda の上にあり、100% が停止している。無事な部分もフォールバック先も無い
- 公開している Web アプリは全ページがエラー
- スケジュール実行のバッチは 8 月 28 日から一度も走っていない
- デプロイもできないので、切り戻すことすらできない
- 停止はすでに 7 日目
「規模が小さいから後回し」ではなく、「規模は小さいが全損で、自力では復旧できない」と伝わる形にした。この直後にエスカレーションが動いた。
待っている間に何度も返信を重ねるより、聞かれた 2 問に事実で答えるほうがずっと効いた。
最後に追加のガードレールを要求される
エスカレーションと引き換えに、復旧の条件として 3 点を要求された。
- セキュリティ用の代替連絡先の登録(アカウント設定の「代替の連絡先」→ セキュリティ)
- コンソールにログインできる全 IAM ユーザーの MFA 有効化
- IAM ベストプラクティス文書の確認と了承
2 番は、実は最初から満たしていた。17 個ある IAM ユーザーのうち、コンソールログイン(ログインプロファイル)を持つものは 1 つも無かった。全てプログラムアクセス専用だったからだ。
これは推測ではなく、コマンドで確認できる。
# root の MFA が有効かどうか(AccountMFAEnabled が 1 なら有効)
aws iam get-account-summary --query 'SummaryMap.{MFA:AccountMFAEnabled,RootKeys:AccountAccessKeysPresent}'
# 各ユーザーにコンソールログインがあるか(NoSuchEntity ならログイン不可)
aws iam get-login-profile --user-name <user>
「満たしている」ことを、根拠つきで即答できると話が速い。 私は上の結果をそのまま返信に書いた。1 番の代替連絡先だけ設定して報告し、その数時間後に制限は解除された。
停止から、6 日 20 時間だった。
結局、何がまずかったのか
技術的な備えは、結果的には機能した。最小権限に絞っていたおかげで、攻撃者は永続化も計算リソースの確保もできなかった。攻撃そのものによる被害はゼロだった。
それでも本番は 6 日 20 時間止まった。止めたのは攻撃者ではない。AWS のアカウント制限と、その解除にかかった時間だ。
そして根本の原因は、キーが漏れたことですらない。使わなくなった IAM ユーザーとアクセスキーを消していなかったことだ。 漏れたのは、かなり前に使うのをやめて、そのまま放置していたユーザーのキーだった。消していれば、通知も制限も停止も、何ひとつ起きていない。
今回いちばん確実に効く再発防止はこれで、監視でも権限設計でもない。使わなくなったものを消すことに尽きる。あとから数えたら、この時点で 1 年以上使っていないアクセスキーが他にも複数あった。
停止時間の内訳も、分けておく。
- 初動の遅れ(約 1 日): 通知は届いていた。だが「鍵のローテーションなら手が空いたときでいい」と優先度を落とした。既にサービスが止まっていることに気づいたのは、自分で触って壊れているのを見てからだった
- 制限の解除待ち(約 6 日): 通知の指示は 2 日目に完了して報告済み。それでも解除されず、ライブチャットでセキュリティチームに取り次いでもらうまで動かなかった
初動を詰めても、後半の 6 日は短くならなかった可能性が高い。この事象は、気づいた瞬間から数日単位で止まることを前提にしたほうがいい。
なお、本番が止まっていること自体には、最後まで気づく手段が無かった。利用者が多くないサービスなので、外部から死活監視をかけるところまでは今も考えていない。そこは過剰だと判断している。
同じ状況になった人へ
順番に、これだけやればいい。
- 通知が届いたら、まず「もう止まっていないか」を確認する。 作業量で優先度を決めない。束で届いていたら 1 通ではなく全部開く。本文に Step 1〜4 と対応期限があれば、それは通常のアナウンスではない
- キーを無効化し、CloudTrail で実際に何をされたかを確認する。 期間指定と検索属性の落とし穴に注意(前述)
- Cost Explorer で課金を確認する。 「0 件」がフィルタ名の間違いでないことまで検証する
- Step 4 の完了報告をケースに返信する。 これを出さないと制限は解除されない。ケースはキーごとに立つ
- 制限の範囲を特定する。 理由の記載が無い
AccessDeniedExceptionが特定サービスだけに出るなら、それは自分の IAM 設定ではなく AWS 側の制限 - それでも解除されないなら、ライブチャットに繋ぐ。 ケースを増やしてはいけない。重複はむしろ遅延を招く
- 繋がったら、業務影響を具体的に答える。 人数を盛る必要はない。「規模は小さいが全損で、自力では復旧できない」と伝わる形にする
そして知っておくべきなのは、アカウントの「compromised」属性を外せるのはセキュリティチームだけで、通常のサポートケースでは解けないということ。解除まで数日かかるのは異常ではない。だから、待っている間に返信を重ねるより、セキュリティチームへ取り次いでもらう一点に絞ったほうがいい。
最後に、いちばん短い教訓を書いておく。
使わなくなった鍵は、その日のうちに消す。 それができていれば、この記事は存在していない。