
S3への書き込み権限なしでAWS DevOps AgentにAthenaを使って調査をお願いしてみた
こんにちは。クラウド事業統括本部コンサルティング1部の桑野です。
AWS DevOps Agent に Athena のクエリを実行させようとして、権限で弾かれたことはありませんか?
私はあります。というより、チームメンバーから相談を受けたのがきっかけでした。
クエリの実行権限を足しても、結果を S3 に書き込む段階で止まってしまいます。書き込み操作は DevOps Agent のガードレールに阻まれるため、権限を追加する方向では解決しません。
今回は、この制約をどのように回避したか、そしてエージェントがどこまで調査できたかを共有します。
先に結論
Managed Query Results を有効にした Athena ワークグループを作り、調査対象のアカウントのエージェントロールに以下の権限を追加すれば、s3:PutObject なしで Athena での調査を任せられます。いずれも AIDevOpsAgentAccessPolicy には含まれていないため、別途付与が必要です。
| アクション | 用途 |
|---|---|
athena:StartQueryExecution |
クエリの実行 |
athena:StopQueryExecution |
クエリの停止 |
athena:GetQueryExecution |
実行状態の確認 |
athena:GetQueryResults |
結果の取得(Managed Query Results から読み出す) |
s3:GetObject |
スキャン対象のログの読み取り |
s3:ListBucket |
ログバケットの一覧取得 |
ポリシーの例です。リージョン・アカウント ID・ワークグループ名・バケット名は環境に合わせて置き換えてください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AthenaQueryExecution",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:StopQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults"
],
"Resource": "arn:aws:athena:<リージョン>:<アカウントID>:workgroup/<ワークグループ名>"
},
{
"Sid": "S3ReadLogBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<バケット名>",
"arn:aws:s3:::<バケット名>/*"
]
}
]
}
調査を依頼するときは、使ってほしいワークグループ名を明示するのがポイントです。
背景・課題
きっかけは、チームのメンバーからこんな相談を受けたことです。
昨日お試しで監査用のアカウントから本番アカウントの WAF のログチェックさせたかったのですが、Athena のテーブルを用意しても権限不足でレポーティングできませんでした。。。
クエリ実行権限等付与は本番環境ということもありますしクイックには解決しなさそうですか?
(何か抜け道があれば教えて欲しかったり)
エージェントスペースを置いている監査用のアカウントから、セカンダリアカウントとして登録した本番アカウントの WAF ログを Athena で調査させたい、という話です。テーブルを用意したけど権限不足で止まってしまった、という状況です。
まず前提として、DevOps Agent にデフォルトで付与される AIDevOpsAgentAccessPolicy には、Athena のクエリ実行に必要な athena:StartQueryExecution や athena:GetQueryResults が含まれていないため、追加の権限付与なしにはクエリを実行できません。
Athena がログを読むための s3:GetObject と s3:ListBucket も必要です。ここまでは追加すれば使えるようになります。
ただし通常の Athena はクエリ結果を S3 に書き出すため、s3:PutObject も必要になります。
これは IAM ポリシーに追加してもエージェントは実行できません。DevOps Agent には IAM ロールとは別にパーミッションガードレールという上限があり、書き込み操作はその範囲外に置かれています。ロールに付与しても実行時に弾かれます。
つまり Athena の実行権限も S3 の読み取り権限もエージェントの権限に追加はできるのですが、結果の書き込みだけはどうにもならないという感じです。
この制約について調べていたところ、たかやまさんが 2026年7月時点で検証されている記事を見つけました。
ガードレールが書き込み操作を通さない設計になっている以上、s3:PutObject の追加を待つというのはあまり期待できないかなと考えています。
そこで発想を変えて、Athena 側で結果の保存先を変えられないかと考えました。
さらに調べてみると、AWS の公式ドキュメントに Managed Query Results という機能がありました。
Managed Query Results はクエリ結果を AWS 管理ストレージに保存し、athena:GetQueryResults で読み出します。S3 バケットを用意する必要がなく、s3:PutObject も発生しません。
なお、S3 への書き込みが不要になるのは結果の保存だけです。スキャン対象のログを読む部分は従来どおり S3 に対して行われるので、s3:GetObject と s3:ListBucket は引き続き必要になります。
これなら書き込みが発生しないので、ガードレールに引っかからずに Athena で調査できそうです。ということで実際に試してみました。
イメージとしては以下の通りです。

前提
以下の形で検証をしています。
- エージェントスペースが作成済み
- Managed Query Results を有効にした Athena ワークグループが作成済み
- セカンダリアカウントに検証用のデータとテーブルを配置し、セカンダリアカウント用のロールで調査させる構成
ワークグループの作成手順は、あしざわさんの記事を参考にさせていただきました。
なお本記事の検証は、実際の WAF は構築せず、WAF ログのスキーマに準拠した合成データを使っています。
また、2026年8月20日時点での検証結果である点にご注意ください。
DevOps Agent の権限モデル
検証に入る前に、権限がどう決まるのかを整理しておきます。
DevOps Agent の実効権限は、次の2つの論理積です。
| レイヤ | 誰が決めるか | 役割 |
|---|---|---|
| IAM ロールのポリシー | 利用者 | エージェントにやらせたいことを定義 |
| パーミッションガードレール | AWS DevOps Agent | エージェントが絶対に超えられない上限 |
AWS のドキュメントでは、以下のように説明されています。
A permission must be present in both layers to take effect. If you add a permission to your role that is not included in the guardrail, the agent cannot use it.
冒頭で触れた s3:PutObject が使えない理由は、ここを見れば良さそうですね。
では、ガードレールは何を許可しているんでしょうか。
ReadOnlyAccess マネージドポリシーの全アクションに加えて、以下の2つが挙げられています。
| サービス | アクション | 用途 |
|---|---|---|
| Amazon Athena | athena:StartQueryExecution, athena:StopQueryExecution |
データカタログに対する Athena クエリの実行 |
| AWS KMS | kms:Decrypt |
S3 オブジェクトなど暗号化リソースの復号 |
athena:StartQueryExecution が含まれているのが気になりました。これは SQL 文字列を渡して実行する API です。保存済みクエリを呼ぶだけなら athena:GetNamedQuery で足りるはずなので、エージェントが自分で SQL を書いて投げる前提になっているように読み取れました。
この点は後の検証で実際に確かめてみました。
AIDevOpsAgentAccessPolicy の Athena 権限を確認する
背景で触れた「デフォルトではクエリを実行できない」という点を、具体的に見ておきます。
デフォルトで付与される AIDevOpsAgentAccessPolicy に含まれる Athena 権限は、以下の通りです。
athena:GetCapacityAssignmentConfiguration
athena:GetCapacityReservation
athena:GetDataCatalog
athena:GetNamedQuery
athena:GetPreparedStatement
athena:GetWorkGroup
athena:List*
ポリシードキュメントの全文はこちらで確認できます。
参照系だけで、athena:StartQueryExecution と athena:GetQueryResults が入っていませんね。StartQueryExecution はガードレール側の「追加アクション」に含まれるので、別途ポリシーを足せば使えそうです。GetQueryResults も ReadOnlyAccess に含まれるためガードレールは通すが、AIDevOpsAgentAccessPolicy が許可していないので同様に足す必要がありそうです。
どちらも追加すれば実行できる権限ですね。一方 s3:PutObject はガードレールの範囲外なので、同じように追加したとしても実行時に弾かれてしまいます。
検証環境の準備
想定するシナリオ
WAF ログの調査で実際に確認したいのは、攻撃が来ていないか、誤検知でユーザーを弾いていないか、といったところだと思います。それらをエージェントが判断できるかを検証したいので、以下のシナリオを想定しました。
| シナリオ | 件数 | ユニーク IP | データの特徴 |
|---|---|---|---|
| 正常トラフィック | 44,300 | 多数 | 調査をする上ではノイズとなるデータ 日中にピークがある波を想定 |
| SQLi 攻撃(14:00-14:15) | 3,300 | 3 | 3 つの IP が 15 分間に集中してリクエスト 1 IP あたり約 1,000 件 |
| COUNT モードルール | 1,200 | - | action は ALLOWマッチ情報は nonTerminatingMatchingRules の配列内に格納 |
| Rate-based(03:20-03:35) | 800 | 3 | SQLi とは別の時刻に発生 |
| 誤検知の疑い | 400 | 25 | 25 の IP が 24 時間に分散してリクエスト 1 IP あたり約 16 件 |
このデータで確認できそうなことを整理すると、以下になります。
攻撃と誤検知の切り分け
攻撃側はユニーク IP が 3、誤検知側は 25 になっています。実際の運用でも、攻撃は少数の IP から大量に来る一方、誤検知は多数の正規ユーザーから少量ずつ発生します。この分布の違いから両者を区別できるかを見たいところです。
複数インシデントの識別
SQLi と Rate-based が別の時刻に発生しています。ひとまとめにせず、2つの独立した事象として報告してくれるでしょうか。
誤検知の原因説明
誤検知シナリオのレコードは requestBodySize が 10,000〜20,000 で、requestBodySizeInspectedByWAF は 8,192 になっています。WAF が検査しきれずにサイズ制限で弾いた状態なので、この差分から原因まで説明できると理想的です。
ネストした配列の扱い
COUNT モードのルールは nonTerminatingMatchingRules という配列の中に入っています。CROSS JOIN UNNEST で展開しないと中身が見えないため、ここが一番のハードルになりそうです。
サンプルデータの用意
実際に生成された WAF ログを対象にしたかったのですが、そのために Web ACL や CloudFront を用意して実トラフィックを流すのは面倒です。
WAF ログの形式でサンプルデータを作って S3 に配置し、それらを Athena から読みとる形にしました。
1レコードは以下の形式です。SQLi 攻撃のシナリオを想定して生成したものです。
サンプルレコード(ヘッダーは一部省略)
{
"timestamp": 1787148000225,
"formatVersion": 1,
"webaclId": "arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/devops-agent-test/a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"terminatingRuleId": "AWS-AWSManagedRulesSQLiRuleSet",
"terminatingRuleType": "MANAGED_RULE_GROUP",
"action": "BLOCK",
"terminatingRuleMatchDetails": [
{
"conditionType": "SQL_INJECTION",
"sensitivityLevel": "HIGH",
"location": "QUERY_STRING",
"matchedData": ["1", "=", " or "]
}
],
"httpSourceName": "CF",
"httpSourceId": "123456789012:distribution/EXAMPLE0000",
"ruleGroupList": [
{
"ruleGroupId": "AWS-AWSManagedRulesSQLiRuleSet",
"terminatingRule": {
"ruleId": "SQLiRule_QUERYARGUMENTS",
"action": "BLOCK",
"ruleMatchDetails": [
{
"conditionType": "SQL_INJECTION",
"sensitivityLevel": "HIGH",
"location": "QUERY_STRING",
"matchedData": ["1", "=", " or "]
}
]
},
"nonTerminatingMatchingRules": [],
"excludedRules": null
}
],
"rateBasedRuleList": [],
"nonTerminatingMatchingRules": [],
"requestHeadersInserted": null,
"responseCodeSent": "403",
"httpRequest": {
"clientIp": "192.0.2.44",
"country": "CN",
"headers": [
{ "name": "Host", "value": "www.example.com" },
{ "name": "User-Agent", "value": "python-requests/2.31.0" }
],
"uri": "/login",
"args": "q=admin%27--",
"httpVersion": "HTTP/1.1",
"httpMethod": "GET",
"requestId": "6B02033D6D744D28BBFE3B794EB05AC1",
"fragment": "",
"scheme": "https",
"host": "www.example.com"
},
"labels": [
{ "name": "awswaf:managed:aws:sql-database:SQLi_QueryArguments" }
],
"captchaResponse": null,
"challengeResponse": null,
"ja3Fingerprint": "e7d705a3286e19ea42f587b344ee6865",
"ja4Fingerprint": "t13d1516h2_8daaf6152771_02713d6af862",
"oversizeFields": null,
"requestBodySize": 0,
"requestBodySizeInspectedByWAF": 0
}
配置先のパスは、WAF が実際にログを出力する際の構造に合わせます。
AWSLogs/123456789012/WAFLogs/cloudfront/devops-agent-test/2026/08/19/HH/mm/waf-logs-000000000001.json
データの生成
上記の形式でデータを作るスクリプトを用意しました。Python 3 の標準ライブラリのみで動き、seed=42 で乱数を固定しているので同じデータが再現できます。
約 700 行あるので本記事には掲載しませんが、シナリオごとの件数や IP の分布は前掲の表のとおりに生成しています。処理の要点は以下です。
- 1 日を 5 分刻みの 288 バケットに区切り、時間帯ごとの重みを付けて正常トラフィックを配分する(深夜は少なく、昼休みと夜にピークを作る)
- SQLi は 14:00〜14:15、Rate-based は 03:20〜03:35 のバケットに集中させ、時系列でスパイクが見えるようにする
- SQLi は主犯 1 IP に 3,000 件、共犯 2 IP に各 150 件を割り当て、3 IP に同一の JA3 フィンガープリントを持たせる
- 誤検知シナリオは 25 の IP に 24 時間へ分散させ、
requestBodySizeを 10,000〜20,000、requestBodySizeInspectedByWAFを 8,192 に固定する - COUNT モードのルールは
nonTerminatingMatchingRulesの配列に格納し、actionは ALLOW のままにする - 各バケットの内容を NDJSON(1 行 1 レコード)で書き出し、WAF の出力パス構造に合わせたディレクトリに配置する
S3 にアップロードする
バケットを作成し、生成した AWSLogs フォルダをアップロードします。

s3://<バケット>/AWSLogs/... という階層になっていれば大丈夫です。
Athena のワークグループを確認する
Managed Query Results が有効になっているかを確認しておきます。

「クエリ結果の場所」が Athena マネージドストレージ になっていれば OK です。
ワークグループ一覧を見ると、デフォルトの primary と今回作成した test-managed-query-results が並んでいます。

データベースとテーブルを作成する
まず調査用のデータベースを作ります。
CREATE DATABASE の全文
CREATE DATABASE IF NOT EXISTS waf_verification;
続いて WAF ログ用のテーブルを作ります。スキーマは AWS 公式ドキュメントの DDL に準拠しています。
CREATE EXTERNAL TABLE の全文
CREATE EXTERNAL TABLE IF NOT EXISTS waf_verification.waf_logs (
`timestamp` bigint,
`formatversion` int,
`webaclid` string,
`terminatingruleid` string,
`terminatingruletype` string,
`action` string,
`terminatingrulematchdetails` array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>,
`httpsourcename` string,
`httpsourceid` string,
`rulegrouplist` array<struct<rulegroupid:string,terminatingrule:struct<ruleid:string,action:string,rulematchdetails:array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>>,nonterminatingmatchingrules:array<struct<ruleid:string,action:string,overriddenaction:string,rulematchdetails:array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>,challengeresponse:struct<responsecode:string,solvetimestamp:string>,captcharesponse:struct<responsecode:string,solvetimestamp:string>>>,excludedrules:string>>,
`ratebasedrulelist` array<struct<ratebasedruleid:string,limitkey:string,maxrateallowed:int>>,
`nonterminatingmatchingrules` array<struct<ruleid:string,action:string,rulematchdetails:array<struct<conditiontype:string,sensitivitylevel:string,location:string,matcheddata:array<string>>>,challengeresponse:struct<responsecode:string,solvetimestamp:string>,captcharesponse:struct<responsecode:string,solvetimestamp:string>>>,
`requestheadersinserted` array<struct<name:string,value:string>>,
`responsecodesent` string,
`httprequest` struct<clientip:string,country:string,headers:array<struct<name:string,value:string>>,uri:string,args:string,httpversion:string,httpmethod:string,requestid:string,fragment:string,scheme:string,host:string>,
`labels` array<struct<name:string>>,
`captcharesponse` struct<responsecode:string,solvetimestamp:string,failurereason:string>,
`challengeresponse` struct<responsecode:string,solvetimestamp:string,failurereason:string>,
`ja3fingerprint` string,
`ja4fingerprint` string,
`oversizefields` string,
`requestbodysize` int,
`requestbodysizeinspectedbywaf` int
)
PARTITIONED BY (
`log_time` string
)
ROW FORMAT SERDE
'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT
'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
's3://<バケット名>/AWSLogs/123456789012/WAFLogs/cloudfront/devops-agent-test/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.log_time.type' = 'date',
'projection.log_time.format' = 'yyyy/MM/dd/HH/mm',
'projection.log_time.range' = '2026/08/19/00/00,2026/08/20/00/00',
'projection.log_time.interval' = '5',
'projection.log_time.interval.unit' = 'MINUTES',
'storage.location.template' = 's3://<バケット名>/AWSLogs/123456789012/WAFLogs/cloudfront/devops-agent-test/${log_time}'
);
projection.log_time.interval は 5 分にしています。公式サンプルでは 1 分になっていますが、今回のデータは 5 分刻みで配置しているので、それに合わせました。
実行し、以下のように表示されると成功です。

件数を確認します。

50,000 件返ってきました。パーティションプロジェクションも問題なく動いています。
これで検証の準備ができました。
| 項目 | 内容 |
|---|---|
| S3 バケット | 288 ファイル / 約 73MB / 50,000 レコード |
| Athena テーブル | waf_verification.waf_logs(パーティションプロジェクション) |
| ワークグループ | Managed Query Results 有効 |
| エージェント用のロール | エージェントスペース作成時の構成のまま(Athena 用の権限は未追加) |
次のセクションから、実際にエージェントに調査を依頼していきます。
検証1: 追加権限なしでできることを確認してみた
まずは何も権限を追加していない状態で調査を依頼してみます。
依頼した内容は以下の通りです。
xxxxxxxxxxxx の WAF ログを 調査して欲しいです。
Athena テーブルがあると思うのでチェックしてもらうと捗る気がします。
調査をお願いしてもいいでしょうか?
エージェント自身がどのように調査するかも気になったため、あえてテーブル名やワークグループ名は伝えないように指示しました。

テーブルはちゃんと見つけてくれましたね。
良いニュース: Athena テーブルはバッチリ見つかったでヤンス!
- データベース:
waf_verification.waf_logs(ap-northeast-1)- 対象ログ: CloudFront WAFログ(
devops-agent-testWebACL)- データ範囲: 2026/08/19〜20 のパーティションあり
AIDevOpsAgentAccessPolicy には athena:GetDataCatalog や athena:List*、Glue の読み取り権限が含まれているので、メタデータ層は問題なく辿れています。CloudFront 向けのログだという判別まで済んでいます。
ただ、権限が足りていないのでクエリの実行や S3 バケット内のデータの読み取りまではできませんでした。
ブゥーンなニュース: エージェントロール
DevOpsAgentRole-SecondaryAccountに Athena クエリ実行と S3 データ読み取りの権限が足りなくて、実際のログ内容まで踏み込めなかったでヤンス…!
ツール実行ログを展開してみると、権限不足を報告するまでに以下を順番に試していました。
| # | 試した手段 | 結果 |
|---|---|---|
| 1 | S3 への直接アクセス(s3:ListObjectsV2 / ListBuckets) |
s3:ListBucket が権限不足 |
| 2 | 既存のクエリ実行履歴を流用(ListQueryExecutions → BatchGetQueryExecution) |
履歴は見えたが結果は UNAUTHORIZED |
| 3 | CloudWatch Logs 経由(logs:DescribeLogGroups) |
WAF ログの配信なし |
| 4 | WAFv2 API を直接(wafv2:ListWebACLs) |
実際の WebACL が無いため空 |
| 5 | 自身の IAM 権限を確認(iam:GetRole / ListRolePolicies) |
一部取得、GetPolicyVersion は失敗 |
単に失敗を報告するのではなく、代替経路を探してくれていますね。
そして最後に、必要な権限を JSON で提示してくれました。

{
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults",
"athena:StopQueryExecution"
],
"Resource": "arn:aws:athena:ap-northeast-1:xxxxxxxxxxxx:workgroup/test-managed-query-results"
}
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::<バケット名>",
"arn:aws:s3:::<バケット名>/*"
]
}
要求されたのは Athena の実行権限と S3 の読み取り権限だけで、s3:PutObject は含まれていません。Managed Query Results のワークグループを使う場合は結果の書き込みが発生しないので、それに沿った内容になっています。
検証2: 必要な権限を追加して再調査させてみた
エージェントが提示した6つのアクションを追加してみました。

付与するポリシーは以下です。検証のため、リソースの絞り込みは緩めに設定しています。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AthenaQueryExecutionAnyWorkgroupInThisAccount",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:StopQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults"
],
"Resource": "arn:aws:athena:*:xxxxxxxxxxxx:workgroup/*"
},
{
"Sid": "S3ReadAnyBucketOwnedByThisAccount",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"s3:ResourceAccount": "xxxxxxxxxxxx"
}
}
}
]
}
Athena は ARN にアカウント ID が入るのでそれで絞れます。S3 は ARN にアカウント ID が含まれないため、s3:ResourceAccount 条件で自アカウント所有のバケットに限定しています。
適用しました。

3つのポリシーが並んでいます。上2つは元からあったもので、athena-exec が今回追加した分です。
権限を追加したことを伝えて、再度調査してもらいます。

完走しました。結果を見ていきます。

action count ALLOW 45,500 BLOCK 4,500
- 総リクエスト数: 50,000件
- ブロック率: 9.0%
用意したデータと完全に一致しています。
攻撃元の特定もできていました。
192.0.2.44が突出:4,500件中3,000件(66.7%)を1IPが占有。明らかな集中攻撃源

発動ルールの内訳も正確です。
順位 Rule ID Count 1 AWS-AWSManagedRulesSQLiRuleSet3,300 2 RateLimitRule800 3 SizeRestrictions_BODY400
想定したシナリオには含めていなかった、URI と User-Agent のクロス集計も出てきました。
URI User-Agent(抜粋) Count /api/searchpython-requests/2.31.0236 /api/productspython-requests/2.31.0226 /api/searchScrapy/2.11224
httprequest.headers は array<struct<name,value>> なので、User-Agent を取り出すには配列から該当要素を抽出する処理が必要です。それを自分で書いています。
想定したシナリオのうち「攻撃と誤検知の切り分け」については、この時点で誤検知の可能性に触れていました。
/api/uploadと/api/profile/updateはブラウザUAからも少数ブロック(正規ユーザーの誤ブロックの可能性も)
エージェントが自分で SQL を書いていたのかどうかも気になって調べてみました。
ツール実行ログを見ると、呼ばれていたのは以下の3つだけでした。
athena.start_query_execution
athena.get_query_execution
athena.get_query_results
GetNamedQuery や ListNamedQueries は一度も呼ばれていません。この時点では保存済みクエリの権限を付けていなかったので、そもそも参照できない状態でした。
なので、6本程度のクエリをすべて自分で書いて実行したということになりますね。す、すごい。。。
進め方にも段取りがありました。
| 挙動 | 内容 |
|---|---|
| 疎通確認を先行 | 軽いクエリで StartQueryExecution が通るか確かめてから本番へ |
| スキーマ確認を挟む | カラム構造を把握してから分析クエリを設計 |
| 並行実行 | 複数クエリを同時に投げ、まとめてステータス確認と結果取得 |
事前にクエリを用意しておかなくても調査してもらえるので、ユーザー目線では一次調査を始めるハードルがかなり低くなるんじゃないかと感じました。
検証3: どこまで深掘りできるのか試してみた
検証2 の結果は正確でしたが、用意したシナリオのうち COUNT モードのルール(1,200件)には触れていませんでした。
これは nonTerminatingMatchingRules という配列の中に入っているデータです。CROSS JOIN UNNEST で展開しないと中身が見えません。
一方で、エージェントはスキーマ確認の段階で配列の存在を認識していました。追加調査の提案にもこう書かれています。
SQLi実際のペイロード確認:
terminatingrulematchdetails[0].matcheddataを展開してどんなSQLiが仕込まれているか確認できるでヤンス
つまり展開できること自体はエージェントも分かっているのですが、初回調査には含めなかったということになります。能力の問題なのか、それとも優先順位の問題かが気になったので、追加で依頼してみました。
terminatingrulematchdetails を展開して、実際のSQLiペイロードを確認してください。
あと、COUNTモードで動いているルールがあれば、本番投入したときの影響も評価して欲しいです。

SQLi のペイロードを特定できたのか、調査結果を共有してくれました。
検出トークン:
or,=,1— 典型的な1 or 1=1パターン
location:BODY,QUERY_STRING,HEADERを横断的にスキャン
COUNT モードのルールも3つすべて見つけてくれました。
ruleid action 件数 XSS_BODY_COUNT_MODE COUNT 593 GenericLFI_QUERYARGUMENTS_COUNT COUNT 416 NoUserAgent_HEADER_COUNT COUNT 191
用意した件数と一致しています。指示すれば UNNEST も書けることが分かりました。初回調査時に行わなかったのは優先順位の問題なのかもしれないです。
途中の試行錯誤も記録されていました。
| 場面 | 挙動 |
|---|---|
| スキーマ確認 | matcheddata が array<struct> ではなく array<string> と気づき、クエリを調整 |
| 空振り | action = 'COUNT' は 0 件、rulegrouplist 内も空と判明し、nonterminatingmatchingrules に狙いを変更 |
| 列名エラー | overriddenaction の列名違いでクエリが失敗、修正して再投入 |
| LIMIT の副作用 | LIMIT 50 に特定のルールが含まれないことに気づき、ルールごとに個別クエリを追加 |
スキーマの読み違いやクエリのエラーがあっても、エージェント自身が気づき、修正して投げ直してくれています。取得できた結果が足りていないことにも気づいて追加で調べているので頼もしいですね。
COUNT モードルールの評価も返してくれています。

ルール 判定 推奨アクション XSS_BODY_COUNT_MODE 要追加調査 詳細確認後に判断 GenericLFI_QUERYARGUMENTS_COUNT 誤検知疑い ルールスコープ見直し後にBLOCK検討 NoUserAgent_HEADER_COUNT ルール定義の矛盾 WebACLのルール定義を直接確認
件数を並べるだけでなく、BLOCK モードに切り替えたときに正規ユーザーを弾いてしまわないかまで踏み込んで判断してくれました。
根拠として挙げていたのは「多数の異なる IP が各1件ずつという分散パターン」です。想定したシナリオの「攻撃と誤検知の切り分け」で見たかった観点を、COUNT ルールに対しても当てはめてくれています。
誤検知の判定
もう一歩踏み込んで、誤検知シナリオそのものを判定できないか聞いてみました。
SizeRestrictions_BODY でブロックされた400件について、
本当に攻撃なのか誤検知なのか判断してください。判断根拠も示してください。

しっかりと誤検知と見抜いていますね。根拠として6つの観点を並べていました。

評価観点 本データの結果 判定 User-Agent Chrome / Edge / Safari / iPhone / Android — ボット・スクリプト系はゼロ件 正規 送信元IP 25の独立IP、1IPあたり最大22件と均等分散 正規 時間帯 24時間フラット(最大21件/h・最小10件/h、標準偏差≒2.9) 正規 エンドポイント /api/upload(54.5%)・/api/profile/update(45.5%)のみ正規 認証状態 Cookieヘッダーが76.8%に存在 → セッション済み認証ユーザーの可能性が高い 正規 Referer 59.8%にRefererあり → ブラウザからの正規ページ遷移 正規 JA3/JA4 全件NULL(TLS終端構成の影響) 判定不能 Content-Type WAFログに非記録 判定不能
Cookie や Referer が付いているかどうかを、ログイン済みのユーザーからのアクセスかを判断する材料に使っていました。出現率の数値もデータと一致しています。
また、判断材料にならないものも教えてくれました。
JA3/JA4 NULL = 攻撃の証拠にも正規ユーザーの証拠にもならない
terminatingrulematchdetails が空配列だった件についても、AWS WAF の仕様として説明していました。
SizeConstraintStatementがサイズ数値で評価するため、マッチした文字列を記録しないAWS WAFの既知の動作でヤンス
判定だけでなく、ルールをどのように直すと良いかの改善案も挙げてくれました。

推奨1:ScopeDownStatement で対象URIを除外(最優先・即対応)
推奨3:アップロード系のサイズ制限はALB/CloudFrontレベルで制御
WAFのサイズ制限はカスタムルールでも最大8KBでヤンス。/api/uploadの上限制御はALBやCloudFrontの設定で行うのがベストプラクティスでヤンス。
推奨3 では、WAF 側のサイズ制限には上限があるので ALB や CloudFront で制御するよう提案されています。ログの分析だけでなく、構成の話まで考慮してくれています。
最後に全ルールの対応方針が表でまとめられていました。
ルール 判定 推奨アクション AWS-AWSManagedRulesSQLiRuleSet 本物の攻撃 このままBLOCK継続 RateLimitRule 本物の攻撃 このままBLOCK継続 SizeRestrictions_BODY 全件誤検知 ScopeDownStatement 追加が急務 XSS_BODY_COUNT_MODE 要追加調査 詳細確認後に判断 GenericLFI_QUERYARGUMENTS_COUNT 誤検知疑い ルールスコープ見直し後にBLOCK検討 NoUserAgent_HEADER_COUNT ルール定義の矛盾 WebACLのルール定義を直接確認
検証4: そもそも Managed Query Results は必要だったのか調べてみた
ここまでで一通り調査してもらえて満足していたのですが、ふと疑問が浮かびました。
そもそも test-managed-query-results を使ってくださいと伝えていませんでした。
本当に Managed Query Results のワークグループでクエリを実行していたのか心配になってきたのでエージェントに聞いてみました。

今回の調査では、クエリ実行時のワークグループを自分では直接コントロールしていないでヤンス。調査を担当したサブエージェント(Context Gatherer)が Athena クエリを実行していたでヤンスが、どのワークグループを使ったかの詳細まではこちらには返ってきていないでヤンス。
ただ、一般的な挙動として Athena はワークグループを明示指定しない場合 primary を使うでヤンス。
primary で実行されていたとすると、Managed Query Results は関係なかったことになります。ただ primary にはクエリ結果の出力先を設定していないので、そもそもクエリが通るのかも分かりません。そのためprimary の設定を確認してもらいました。

項目 primary test-managed-query-results クエリ結果の保存先 未設定( ResultConfiguration: {})Athena マネージドストレージ( ManagedQueryResultsConfiguration: Enabled: true)ワークグループ設定の強制 false false スキャン上限 なし 約 1PB
primary は結果の保存先が未設定でした。
ここで気になったのが、出力先が未設定のワークグループでクエリは実行できるのか、という点です。公式ドキュメントを確認したところ、こう書かれていました。
To run a query, you must specify the query results location using either a client-side setting for individual queries or a location specified by the workgroup. If no query location is set, Athena issues an error.
出力先はワークグループかクエリ実行時のどちらかで指定する必要があって、どちらも無ければエラーになるようです。以前は aws-athena-query-results-<アカウントID>-<リージョン> というバケットが自動で作られていたそうですが、今は作られないとのことでした。
とはいえ EnforceWorkGroupConfiguration が false なので、クエリ実行時に出力先を渡せば primary でも動くかもしれません。エージェントも同じことを言っていました。
EnforceWorkGroupConfiguration: falseなので、クライアント側から出力先S3を渡せばprimaryでも実行できる可能性はあるでヤンス。ただ今回の調査エージェントがどう実行したかは不明でヤンス…ブゥーンな感じでヤンス。
はっきりさせるために、primary を指定して調査してもらいました。
xxxxxxxxxxxx の WAF ログを調査して欲しいです。
Athena テーブル waf_verification.waf_logs があります。ただし今回は、ワークグループに primary を指定して実行してください。
test-managed-query-results は使わないでください。

結果として、実行できませんでした。

primaryワークグループでクエリを実行しようとしましたが、IAM 権限不足 でクエリ結果を S3 に書き込めないでヤンス。
DevOpsAgentRole-SecondaryAccountのathena-execインラインポリシーにs3:PutObjectが欠落 しているでヤンス
エージェントは自分でバケットを探して出力先に指定し、athena-results/ と query-results/ の2つのパスを試していました。いずれも書き込みで拒否され、原因を IAM ポリシーまで遡って特定しています。
出力先を明示的に渡してもなお s3:PutObject で拒否されたということは、Athena のクエリ結果の書き込みが呼び出し元プリンシパルの権限で実行されているということになります。Athena サービス自身の権限で書かれるなら、エージェントのロールに s3:PutObject が無くても通るはずです。
したがって s3:PutObject を実行できない DevOps Agent は、通常のワークグループでは Athena クエリを完了できません。Managed Query Results を有効にしたワークグループが必要だと確認できました。
このときのツール実行ログは以下の通りです。
| # | エージェントが試したこと | 結果 |
|---|---|---|
| 1 | primary に出力先を自分で指定 | s3:PutObject 拒否 |
| 2 | 別パス(query-results/)を試す |
同じく拒否 |
| 3 | athena:UpdateWorkGroup でワークグループ設定を変更 |
失敗(承認が必要) |
| 4 | iam:PutRolePolicy で自分に s3:PutObject を追加 |
失敗 |
| 5 | S3 から生の JSON を直接読んで分析 | 成功 |
3 と 4 でリソースの変更をしようとしていますね。ワークグループの設定変更と、自分の IAM ポリシーの書き換えという、行き詰まりを自力で解消する方向まで試していました。いずれもロールに該当の権限が無いため失敗しています。
Athena を諦めた後は、s3:GetObject で生ログを直接読んで分析を始めていました。
サンプルデータ確認済み(1ファイル: 2026/08/19 00:00)
- 35レコード
- ALLOW: 33件 (Default_Action)
- BLOCK: 2件 (SizeRestrictions_BODY →
/api/upload)- COUNT(非終端): LFI, XSS, NoUserAgent 等のマッチあり
実データと照合し、この集計も正確でした。
自身の権限の範囲内で調査する姿勢は評価できますが、大量の WAF ログを S3 直読で集計するのはかなり時間がかかります。 エージェントの稼働時間が膨れて DevOps Agent のコストが膨らむリスクは考慮する必要がありますね。
ただし、エージェント自身も「今の目標はまず Athena クエリを成功させること」と述べて、恒久策として IAM の修正か Managed Query Results の使用を提案していました。
Managed Query Results の制約
ここまでで、s3:PutObject を実行できない DevOps Agent が Athena を使うには Managed Query Results が必要だと分かりました。従来の S3 出力との違いを整理しておきます。
| 項目 | 従来(S3 に出力) | Managed Query Results |
|---|---|---|
s3:PutObject |
必要なため DevOps Agent で利用不可 | 不要 なため DevOps Agent で利用可能 |
| 結果の保存先 | 自分で用意した S3 バケット | AWS 管理ストレージ |
| 結果の読み出し | S3 から直接、または GetQueryResults |
GetQueryResults のみ(ワークグループ単位の権限が必要) |
| クエリ結果の再利用 | 対応(既定 60 分・最大 7 日) | 非対応 |
| 結果の保持期間 | バケットのライフサイクル次第 | 24 時間で自動削除 |
| 200MB 超のダウンロード | 可能 | 不可(UNLOAD は S3 書き込みが必要) |
この中で影響が大きいのは、クエリ結果の再利用が使えない点です。
通常の Athena には Query result reuse という機能があり、同一のクエリを再実行したときに前回の結果を返してスキャン課金をゼロにできます。有効期間は既定で 60 分、最長 7 日まで指定できます。Managed Query Results はこれに非対応です。
差が出るのは、同じクエリを繰り返し実行する場面です。たとえば定型の集計を毎日同じ内容で確認する場合や、複数人が同じ調査を並行して行う場合が該当します。従来なら 2 回目以降のスキャン課金が発生しませんが、Managed Query Results では都度スキャンが走ります。実行回数が増えるほど想定よりコストが膨らむ可能性があります。緩和策は、クエリ側でパーティションを絞ってスキャン量そのものを減らすことです。
結果が 24 時間で削除される点も、調査結果を後から見返す運用とは相性が良くありません。必要なら取得した時点で別途保存しておくことになります。
検証5: 保存済みクエリを使わせることはできるのか試してみた
ここまではエージェントが自分でクエリを書いていました。では、あらかじめ用意したクエリを使わせたらどうなるのでしょうか。
事前に7本のクエリを test-managed-query-results に保存しておきました。

| クエリ名 | 内容 |
|---|---|
| Q1-overview | 全体像の把握(action × ルールの分布) |
| Q2-timeline-spike | 時系列でのスパイク検出 |
| Q3-attacker-identification | 攻撃元の特定(JA3 での関連付けを含む) |
| Q4-unnest-match-details | ルールマッチ詳細の展開 |
| Q5-count-mode-impact | COUNT モードルールの影響評価 |
| Q6-false-positive | 誤検知候補の抽出 |
| Q7-cte-multistage | CTE による多段分析(統計的な異常検知) |
最初、保存済みクエリの存在を伝えて調査を依頼したところ、以下の通り返ってきました。

ワークグループ 保存済みクエリ 参照可否 test-managed-query-results7件あり 全件 UNAUTHORIZED primary0件 —
しっかりと見つけてくれましたが、保存済みクエリの中身を読む権限も足りていないことがわかりました。
{
"Sid": "AthenaNamedQueryReadAnyWorkgroupInThisAccount",
"Effect": "Allow",
"Action": [
"athena:GetNamedQuery",
"athena:BatchGetNamedQuery"
],
"Resource": "arn:aws:athena:*:xxxxxxxxxxxx:workgroup/*"
}
athena:GetNamedQuery は AIDevOpsAgentAccessPolicy の一覧に記載があるのですが、実際には UNAUTHORIZED になっていました。理由は特定できていませんが、上記のポリシーを明示的に追加することで読めるようになりそうですね。早速追加してみます。

権限を追加したら、7本すべての内容を読めるようになりました。説明文も取得できています。
ワークグループを明示して依頼する
改めて、ワークグループ名を明示して依頼します。
xxxxxxxxxxxx のワークグループ test-managed-query-results に保存済みクエリ(Q1〜Q7)があります。
それを使ってWAFログを調査してください。

Q1〜Q7 をすべて実行して、結果をまとめてきました。
自力でクエリを書いていたときは、スキーマの確認やクエリの書き直しに試行を費やしていました。保存済みクエリを使う場合はその工程が省けるので、探索のコストは下がっていそうです。
そして調査の内容も保存済みのクエリを使っているので、ある程度コントロールができています。
自力調査では届いていなかった JA3 フィンガープリントによる関連付けも、保存済みのクエリで調査をしてくれています。

重要な発見:
192.0.2.44(CN/RU)は同一JA3フィンガープリント(e7d705a3286e19ea42f587b344ee6865)を持ち、192.0.2.45・192.0.2.46も同じJA3でヤンス。同一ツール・同一オペレーターによる組織的な攻撃の可能性が高いでヤンスよ!
生成データでは、攻撃元の3つの IP に同一の JA3 を持たせていました。Q3 にその列を含めておいたので、エージェントは IP をまたいだ関連付けに到達できています。
SQLi のペイロードも具体的に出てきました。

注入箇所 代表的なペイロード QUERY_STRING 1' OR '1'='1QUERY_STRING 1 AND SLEEP(5)--QUERY_STRING UNION SELECT null,username,password FROM usersQUERY_STRING '; DROP TABLE users;--
Q7 の統計的な異常検知も機能していました。

14:00〜14:14 UTC の全分が z_score = 8.55〜10.28 で統計的に極めて異常でヤンス!
時刻 BLOCK数 z_score 主攻撃者 14:11 UTC 240 10.28 192.0.2.44 z_score 3超え=3σ超え が15分間連続。通常トラフィックではあり得ない水準の攻撃が持続していたでヤンスよ。
CTE を積んだ多段分析は自力では行わなかったのですが、用意しておけばしっかりと活用してくれました。
誤検知の判定も、ユニークIP数と1IPあたりの件数を並べて根拠を示しています。
ルール URI 国 BLOCK件数 ユニークIP IP1件あたり 活動時間 判定 SizeRestrictions_BODY /api/upload JP 218 25 8.72 24h 誤検知疑い RateLimitRule /api/search US 800 3 266.67 1h 正検知(攻撃)
「1IPあたり8.72件で24時間に分散」と「1IPあたり266件で1時間に集中」の対比です。設計した判定軸がそのまま機能しました。
比較
まとめるとこうなりました。
| 項目 | 自力でクエリ作成 | 保存済みクエリを利用 |
|---|---|---|
| JA3 による攻撃者の関連付け | 未到達 | 到達 |
| 地理的分布の集計 | 未到達 | 到達 |
| 統計的な異常検知 | 未実施 | z_score 10.28 を算出 |
| 独自の切り口 | URI × User-Agent のクロス集計 | (用意したクエリの範囲) |
クエリを用意しておくことで、到達できる分析の深さが上がりました。
ただし自力調査では、事前に用意していない URI × User-Agent の集計を出してきていました。用意したクエリだけに縛られると、こうした発想は出てこない可能性があります。両方を組み合わせるのが良さそうです。
そして運用上の注意として、ワークグループ名は明示的に指示する必要があるという点は押さえておきたいところですね。指定しないと primary を見にいってしまいます。
番外編
検証中、Athena の調査とは別に気になった挙動があったので紹介します。
クエリの改善提案をしてくれた
検証5 のあと、用意したクエリの出来について意見を求めてみました。
用意したクエリについてフィードバックをもらえますか?
そうしたら、7本すべてに星評価を付けて改善点を示してくれました。さらに追加すべきクエリを4本提案してくれました。

まず Q1 についてです。
日時範囲のフィルタがないので、パーティションを絞らず全期間を集計してしまうでヤンス
これは的確な指摘でした。パーティションプロジェクションを設定しておきながら、クエリ側で log_time を絞っていませんでした。毎回フルスキャンになる作りです。
前述のとおり Managed Query Results はクエリ結果の再利用に対応していないため、スキャン量の無駄がそのままコストに乗ります。指摘された影響は小さくありません。
他の指摘も妥当なものでした。
| クエリ | 指摘内容 |
|---|---|
| Q2 | ORDER BY blocked DESC だと時系列として読めない。攻撃の開始から終息を追うなら時刻順にすべき |
| Q4 | args を URL デコードしないと、同じペイロードが別の行として分散する |
| Q5 | r.action は全行 COUNT なので GROUP BY のキーとして機能していない |
| Q7 | 標準偏差がほぼ 0 になるケースへの最低サンプル数のガードがない |
Q2 は「スパイク検出」を優先して件数の降順にしていたのですが、確かに攻撃の推移を追うなら時刻順のほうが読めます。
Q4 の URL デコードは有用な指摘でした。id=1%27%20OR... のような生の値で GROUP BY していたので、同じ攻撃パターンでもエンコードが異なれば別の行に分かれてしまいます。デコードを挟めば同一パターンとして集約できます。
追加を提案されたクエリ
さらに4本の新規クエリを提案されました。いくつかありますが、その中でも Q11 は着眼点が異なるので取り上げます。

-- COUNTルールにマッチしながらALLOWされた件数が多いIPを抽出
-- → WAFの穴を突いてすり抜けている可能性がある攻撃者候補
WITH count_matched AS (
SELECT
httprequest.clientip AS client_ip,
r.ruleid AS matched_count_rule,
count(*) AS cnt
FROM waf_verification.waf_logs
CROSS JOIN UNNEST(nonterminatingmatchingrules) AS t(r)
WHERE action = 'ALLOW'
GROUP BY 1, 2
)
SELECT
client_ip,
matched_count_rule,
cnt AS allowed_but_matched,
-- 同じIPのBLOCK件数と並べて比較
coalesce((
SELECT count(*) FROM waf_verification.waf_logs b
WHERE b.httprequest.clientip = cm.client_ip
AND b.action = 'BLOCK'
), 0) AS also_blocked
FROM count_matched cm
ORDER BY allowed_but_matched DESC
LIMIT 30;
提案の理由も添えられていました。
Q5は「COUNTルールが何件ヒットしたか」の集計でヤンスが、「BLOCKされずに通過しているIPの中に悪意のあるものが混じっていないか」 という視点が抜けているでヤンス。WAFバイパス検出の第一歩になるでヤンすよ。
同じ COUNT モードのデータを「影響評価」ではなく「すり抜けた攻撃者の検出」という視点で使い直しています。
他の3本も挙げておきます。
| クエリ | 内容 | 評価 |
|---|---|---|
| Q8 | エンドポイント × HTTP メソッドの分析 | 妥当。Q1 はルール軸、Q3 は IP 軸で、エンドポイント軸が抜けていた |
| Q9 | 国別 × 時間帯のヒートマップ | 妥当。地理分布の観点を補える |
| Q10 | リピート攻撃者の持続性追跡 | 1日分のデータでは成立しないが、複数日あれば有効 |
全体の評価
最後にまとめの表が出てきました。

クエリ 評価 主な改善点 Q1 overview ★★★☆☆ 時間絞り・全体比率の追加 Q2 timeline ★★★★☆ ORDER BY を時系列に変更 Q3 attacker ★★★★★ country の重複対応 Q4 unnest ★★★★☆ URLデコード適用 Q5 count-mode ★★★★☆ 不要なGROUP BY列の除去 Q6 false-positive ★★★★★ CASE式でスコアリング追加 Q7 cte-multistage ★★★★★ 最小サンプル数ガード追加
クエリを渡せば批評して改善案まで返してくれるというのは、Athena を使う上でありがたい挙動だと思いました。
データの矛盾を指摘してきた
検証を通して、合成データの不自然な点を4回指摘されました。
| # | 指摘内容 |
|---|---|
| 1 | NoUserAgent_HEADER_COUNT ルールなのに、ログには User-Agent が記録されている矛盾 |
| 2 | 送信元 IP が 203.0.113.x に集中しており、RFC 5737 のドキュメント用アドレス |
| 3 | ログパスのアカウント番号が 123456789012(テスト用)になっている |
| 4 | 192.0.2.44 も RFC 5737 の予約アドレスのため、テストデータの可能性がある |
1 番目は実際に生成スクリプトの不備でした。COUNT ルールを機械的に割り当てたため、User-Agent を持つリクエストに NoUserAgent ルールが付く状態になっていました。
エージェントは原因の候補を3つ挙げて、「WebACL のルール定義を直接確認すべき」と提言していました。
4 番目については、こう書かれていました。
192.0.2.44は RFC5737 のドキュメント用予約アドレスのため、テストデータの可能性があるでヤンス。本番環境での実攻撃であれば実際の IP が記録されているはずでヤンス
合成データであることを見抜いていますね。予約アドレスやテスト用アカウント番号といった外形的な特徴に加えて、1番目のようなルールとログの論理的な矛盾まで検出できています。
まとめ
s3:PutObject を実行できない DevOps Agent でも、Athena の Managed Query Results を使えば WAF ログの調査をしてもらえます。
調査自体はテーブル名を伝えるだけで、エージェントがクエリを自分で組み立ててくれます。
保存済みクエリを渡すことで、調査の方向性をある程度コントロールすることもできそうです。その場合はワークグループ名も明示してあげると、遠回りせずに活用して調査してくれます。
Athena のクエリ実行ができることによって DevOps Agent の調査の幅は間違いなく広がると感じたので、すでに Athena を活用している方や、これから Athena を活用しようとしている方、DevOps Agent にどうやって調査を任せようか悩んでいる方の参考になれば幸いです。
最後までご覧いただきありがとうございました。





