ペンテストの手順
はじめに
本記事は、AWS Security Agent(以下、Security Agent)のペネトレーションテスト(以下、ペンテスト)機能を、意図的に脆弱性を仕込んだ自作の Flask アプリ(以下、対象アプリ)に対して、実環境に近い「インターネット公開 + AWS WAF」の構成で動かす手順である。手順は次の目的の順に進む。
- エージェントが攻撃を試みる先として、対象アプリをインターネットに公開する。その手前に WAF を置き、エージェントだけを通して第三者の通信は遮断する。この環境を Terraform で作り、ヘッダーの有無で遮断と到達が分かれることを確かめる(手順 1〜4)
- 自分が所有するドメインを Security Agent に検査させてよいことを AWS に示すため、ドメインの所有確認(以下、ドメイン検証)を行い、テストの設定を作る。1 回の実行で使ってよい Task-hour の上限も決めておく(手順 5〜6)
- ペンテストを実行して進み方を監視し、検出結果と WAF ログ、アプリのログを取り出して、エージェントが何を送り WAF が何を遮断したかを突き合わせる(手順 7〜8)
- WAF の評価順を入れ替えてもう 1 回実行し、マネージドルール群がエージェントの検査をどれだけ妨げるかを比べる(手順 9)
- 実行の合間に日をまたぐ場合は、脆弱なアプリを外から届く状態で放置しないよう、アプリ側だけを削除して待つ(手順 10、任意)
- 課金と公開を残さないよう、作ったものをすべて削除し、残骸がないことを確かめる(手順 11)
各手順は、実際に動かしたコマンドと出力例で示す。
Security Agent のペンテストには、公開されたアプリに対して AWS 側から攻撃を試みる「公開方式」と、利用者の VPC の中にテストを実行する AI(以下、エージェント)を置いて閉域で行う「Private VPC 方式」(以下、閉域方式)がある。本記事は公開方式を対象とし、閉域方式は扱わない。
本記事で使う用語は次のとおり。
| 用語 | 意味 |
|---|---|
| Web アプリ | Security Agent の Web アプリケーション。AWS 側でホストされる専用画面で、Pentest の実行と結果の確認に使う |
| Pentest | テストの設定 1 件。ターゲット URL、カスタムヘッダー、認証情報などを定義する。実行するたびに Run(実行)が 1 つできる |
| カスタムヘッダー | Pentest に登録し、エージェントの全リクエストに付けさせるヘッダー。本記事のヘッダー名は X-Pentest-Token |
| ヘッダー許可ルール | カスタムヘッダーの値が一致したリクエストだけを許可する WAF のルール |
| マネージドルール群 | AWS が提供する WAF のルールセット(AWS マネージドルール)。本記事では 5 つ使う |
| 検出結果 | Security Agent が確定した脆弱性 1 件ごとの記録(英語の画面や API では Finding) |
| Task-hour | ペンテストの課金単位。並列に動く全タスクの稼働時間の合計 |
このページの位置づけ
本ページは手順だけを扱う。Security Agent の機能と料金、リソースの階層と IAM ロール、ヘッダー許可ルールで既定動作を遮断にする設計の理由は、ペンテストの仕組みと設計で説明している。実行して分かった所要時間、Task-hour、検出結果、WAF の防御効果は、ペンテストとコードレビューの検証結果にまとめている。同じ Agent Space に GitHub のリポジトリを接続してコードセキュリティレビューを行う手順は、GitHub 連携とコードレビューの手順(後日公開)にある。
検証環境
| 配置 | 内容 |
|---|---|
| アカウント | Organizations に属さない検証専用アカウント(本番データなし) |
| リージョン | ap-northeast-1(東京)。Security Agent の AI 推論は東京発なら日本国内で処理される。コード修正機能だけは EU で処理される(公式の仕様) |
| ドメイン | 筆者の保有ドメイン infra.jp の配下に委任したサブドメイン(lab.infra.jp)を Route 53 のパブリックホストゾーンで管理。レジストラ側の作業は NS レコードの登録 1 回だけ。対象アプリのホスト名は pentest.lab.infra.jp |
| VPC | 10.1.0.0/16。パブリックサブネット 2 つ(ALB 用、2 AZ)とプライベートサブネット 1 つ(対象アプリ用)。プライベート側にインターネットへの経路はない(NAT なし) |
| ALB | HTTPS(443)のみ。ACM 証明書(DNS 検証)。セキュリティグループはインターネットから 443 を受け、選別は WAF に任せる |
| AWS WAF | REGIONAL の Web ACL を ALB に関連付け。既定動作は遮断。ヘッダー許可ルールとマネージドルール群 5 つ。評価順は waf_mode で切り替え。ログは CloudWatch Logs |
| 対象アプリ | Flask + SQLite で作った小さな Web アプリ。応答する URL のパス(/search、/login、/admin など)は 8 つで、意図的に作り込んだ脆弱性 7 種類の一覧はリポジトリの docs/planted-vulnerabilities.md に記載 |
| 対象アプリを動かす EC2 | t3.micro。パブリック IP なし、インスタンスメタデータは v2 のみ許可(IMDSv2 必須)、接続は Session Manager のみ(以下、SSM 接続)。セキュリティグループは ALB からの TCP 80 のみ受信許可 |
| VPC エンドポイント | S3 のゲートウェイ型エンドポイントと、ssm・ssmmessages・ec2messages の 3 つのインターフェース型エンドポイント。EC2 にはインターネットへの経路がないため、OS パッケージは S3 エンドポイント経由で Amazon Linux のリポジトリから取り、対象アプリの Python ライブラリは手元の端末でダウンロードして Terraform で S3 バケットにアップロードしたものを、EC2 が同じエンドポイント経由で取り出す。SSM 接続は 3 つのインターフェース型エンドポイントを通る |
| 観測 | WAF ログ(ルール別の許可・遮断件数、送信元 IP)、対象アプリ用サブネットの VPC フローログ、対象アプリ自身のアクセスログ(journald) |
WAF のルール構成(ヘッダー許可ルール、マネージドルール群 5 つ、既定動作の遮断)、評価順を入れ替える 2 つのモード(agent-first、managed-first)、秘密値の扱いは、仕組みと設計の「公開方式でエージェントだけを通す設計」で説明している。本記事では、Terraform の変数 waf_mode でモードを切り替える(既定は agent-first)。
結果
本記事の手順は、検証で 3 回実行して通した最終形である。WAF の既定動作を遮断にしたまま、ドメイン検証からレポートの生成まで全段階が通った。所要時間と Task-hour、仕込んだ脆弱性 7 件の検出状況、managed-first での WAF の防御効果は、ペンテストとコードレビューの検証結果を参照。
Terraform の構成
本記事では、AWS 側の設定変更をすべて Terraform で行う。実行は GitHub Actions などの CI からではなく、手元の端末から terraform apply する。1 人で短期間に作って壊す検証環境であり、state(後述)もローカルに置く。
リポジトリの中の Terraform のファイルのうち、本記事で使うものは次の階層になっている。
terraform/
├── modules/ 共通部品
│ ├── vulnapp/ 対象アプリの EC2 一式
│ ├── vpc-endpoints/ VPC エンドポイント
│ ├── subnet-flow-logs/ フローログ
│ └── security-agent-roles/ Security Agent の IAM ロール
└── public-method/ 公開方式
├── dns-zone/ 1. 委任先のホストゾーン
├── network-app/ 2. VPC、EC2、証明書、ALB、WAF
└── security-agent/ 3. Security Agent の設定本文で使う用語を補足する。
| 用語 | 意味 |
|---|---|
| ディレクトリ(ルートモジュール) | terraform apply を実行する単位。上の階層の末端のディレクトリ(network-app/ など)がこれに当たり、その中の .tf ファイルに書かれたリソースが 1 回の apply でまとめて作られる(別のディレクトリのリソースは作られない) |
| state | そのディレクトリで「今 AWS に何を作ってあるか」を Terraform が覚えておくファイル(terraform.tfstate)。リソースの ID や、今回なら WAF の秘密値も入るので、リポジトリには置かない |
| モジュール | 複数のディレクトリから呼び出せる共通部品。同じものを 2 か所に書かないために使う |
本記事のコードは 3 つのディレクトリに分かれていて、番号順に apply する。分けているのは、1 と 2 の間にレジストラへの NS 登録という手作業が挟まることと、3 が 2 の出力(ターゲット URL と秘密値)を使うためである。さらに 3 の中では、Pentest がドメイン検証の済んだ Target Domain にしか作れないため、apply が 2 回に分かれ、その間にドメイン検証の実行が挟まる(詳細は仕組みと設計の「ディレクトリの分割」)。
1. dns-zone/ 委任先のホストゾーンを作る
│
▼
(手作業) 出力されたネームサーバーをレジストラに NS レコードとして登録
│
▼
2. network-app/ VPC、EC2、ACM 証明書、ALB、WAF、ヘッダーの秘密値を作る
│ (自動) 2 の出力(ターゲット URL、秘密値)を 3 が読む
▼
3. security-agent/ 1 回目の apply: IAM ロール、Application、Agent Space、Target Domain、
検証用 TXT レコードを作る
│
▼
(CLI) ドメイン検証を実行する(Terraform では行えない操作)
│
▼
3. security-agent/ 2 回目の apply: 変数 create_pentest を true に変えて、Pentest を作る共通部品(モジュール)は terraform/modules/ に 4 つある。対象アプリの EC2 一式(vulnapp)、VPC エンドポイント(vpc-endpoints)、フローログ(subnet-flow-logs)、Security Agent の IAM ロール(security-agent-roles)で、閉域方式の記事のコードとも共用している。
前提条件
- 検証専用の AWS アカウントと、その管理者権限を持つ CLI プロファイル
- 自分が管理する保有ドメイン。レジストラの管理画面で NS レコードを追加できること
- Terraform 1.12 以降(本記事は 1.12.2)。プロバイダは aws 6 系(6.69.1)、awscc 1.78.0 以降(1.106.0。Security Agent の 4 リソースは 1.78.0 で追加された)、random 3 系(3.9.1)、time 0.13 以降(0.14.2)。括弧内は本記事で実際に使った版で、
.terraform.lock.hclに固定している - AWS CLI v2 2.34.21 以降(本記事は 2.38.1)。
securityagentコマンドはこの版で追加されたので、古い CLI では手順 5.4 以降の確認コマンドが「Invalid choice」になる - Python 3 と pip、
dig(名前解決の確認)、Git(コードの取得) - 対象リージョンが Security Agent 対応リージョンであること(Resilience の一覧)
始める前に、使うアカウントとツールの版を確認する。
aws sts get-caller-identity --query '{Account:Account,Arn:Arn}'{
"Account": "<検証アカウント ID>",
"Arn": "arn:aws:iam::<検証アカウント ID>:user/<ユーザー名>"
}terraform versionTerraform v1.12.2
on linux_amd64AWS CLI の版も確認する。securityagent コマンドは 2.34.21 で追加されたため、それより古いと手順 5 以降の確認コマンドが動かない。
aws --versionaws-cli/2.38.1 Python/3.14.6 Linux/6.6.87.2-microsoft-standard-WSL2 exe/x86_64.ubuntu.22検証の流れ
flowchart LR
A[1. コードの取得] --> B[2. ドメインの委任]
B --> C[3. network-app apply]
C --> D[4. 到達性の確認]
D --> E[5. security-agent apply<br>とドメイン検証]
E --> F[6. 最大タスク時間の設定]
F --> G[7. 実行の開始と監視]
G --> H[8. 結果の取り出し]
H --> I[9. 評価順の切り替え<br>managed-first]
I --> J[10. 日をまたぐ待機<br>任意]
J --> K[11. クリーンアップ]
| ステップ | 内容 | 主な手段 |
|---|---|---|
| 1. コードの取得 | リポジトリを手元に取得する | git |
| 2. ドメインの委任 | 委任先ホストゾーンを作り、レジストラに NS を登録する | Terraform / レジストラの管理画面 |
| 3. ネットワーク・ALB・WAF・対象アプリの構築 | VPC、証明書、ALB、WAF、EC2 | Terraform |
| 4. 到達性の確認 | ヘッダーなしは遮断、ヘッダーありは到達することを確認 | curl / SSM |
| 5. Security Agent の構築とドメイン検証 | IAM ロール、Application、Agent Space、Target Domain、Pentest。Verify の実行 | Terraform(apply は 2 つ)/ CLI |
| 6. 最大タスク時間の設定 | Terraform で設定できない最大タスク時間を 20 時間にする | CLI(代替: 画面操作) |
| 7. 実行の開始と監視 | 実行、監視、手動停止(任意)、レポートの生成 | 画面操作 / 確認コマンド |
| 8. 結果の取り出し | 検出結果、WAF ログ、アプリログ | 画面操作 / 確認コマンド |
| 9. 評価順の切り替え | waf_mode を managed-first にして apply し、7 と 8 を繰り返す | Terraform / 画面操作 |
| 10. 日をまたぐ待機 | 実行の合間に network-app だけを削除し、作り直す | Terraform / 確認コマンド |
| 11. クリーンアップ | 逆順の destroy、NS レコードの削除、残骸の確認 | Terraform / 確認コマンド |
lab.infra.jp の配下をそのまま書いているので、コマンドと terraform.tfvars の中のドメインは、読者自身の保有ドメインに置き換える。検証環境は記事末尾の手順で削除しており、記事公開時点で pentest.lab.infra.jp には到達できない。1. コードの取得
Terraform のコードと対象アプリのソースを、GitHub のリポジトリから手元に取得する。
git clone https://github.com/tatsuyaw/security-agent-lab.gitCloning into 'security-agent-lab'...
Receiving objects: 100% (122/122), 42.82 KiB | 3.57 MiB/s, done.
Resolving deltas: 100% (40/40), done.以降の手順は、ホームディレクトリに取得した前提で ~/security-agent-lab を起点にする。
2. ドメインの委任
公開方式には、自分が管理するドメインが要る(ACM 証明書発行と、Security Agent のドメイン検証の両方で DNS レコードを置くため)。保有ドメイン全体を Route 53 に移すのではなく、サブドメイン 1 つだけを Route 53 に委任する。レジストラ側の作業は NS レコードの登録 1 回で済み、以降のレコードはすべて Terraform が作る。
2.1 委任先ホストゾーンの作成
cd ~/security-agent-lab/terraform/public-method/dns-zone
terraform initInitializing provider plugins...
- Installing hashicorp/aws v6.<x>...
Terraform has been successfully initialized!terraform.tfvars は、読者が自分の環境に合わせた値を書くファイルで、3 つのディレクトリそれぞれに用意する。リポジトリには値がプレースホルダのままの見本(terraform.tfvars.example)だけを各ディレクトリに置いてあり、それをコピーして実値を書く。実アカウント ID が入るため terraform.tfvars は Git 管理外にしている。
cp terraform.tfvars.example terraform.tfvarsaccount_id = "<検証アカウント ID>" # apply 先のアカウント。これ以外のアカウントには apply できない安全装置になる
profile = "<プロファイル名>" # 使う AWS CLI のプロファイル
zone_name = "lab.infra.jp" # 委任するサブドメイン。保有ドメインの配下にするterraform plan -out=tfplanPlan: 1 to add, 0 to change, 0 to destroy.terraform apply tfplanaws_route53_zone.lab: Creating...
aws_route53_zone.lab: Still creating... [00m10s elapsed]
aws_route53_zone.lab: Still creating... [00m20s elapsed]
aws_route53_zone.lab: Still creating... [00m30s elapsed]
aws_route53_zone.lab: Creation complete after 32s [id=<委任ゾーンの ID>]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
name_servers = tolist([
"ns-<数字>.awsdns-<数字>.com",
"ns-<数字>.awsdns-<数字>.net",
"ns-<数字>.awsdns-<数字>.org",
"ns-<数字>.awsdns-<数字>.co.uk",
])
zone_id = "<委任ゾーンの ID>"
zone_name = "lab.infra.jp"Terraform の出力だけで終わらせず、AWS CLI でホストゾーンの実体を確認する。get-hosted-zone はホストゾーンの属性を返すコマンドで、--query で 3 つに絞っている。Name がゾーン名(末尾にドットが付く)、Private が false ならインターネットから引けるパブリックゾーン、NameServers がこのゾーンに割り当てられたネームサーバー(次の手順でレジストラに登録する値。Terraform の出力と同じ 4 つ)である。
aws route53 get-hosted-zone --id <委任ゾーンの ID> \
--query '{Name:HostedZone.Name,Private:HostedZone.Config.PrivateZone,NameServers:DelegationSet.NameServers}'{
"Name": "lab.infra.jp.",
"Private": false,
"NameServers": [
"ns-<数字>.awsdns-<数字>.com",
"ns-<数字>.awsdns-<数字>.net",
"ns-<数字>.awsdns-<数字>.co.uk",
"ns-<数字>.awsdns-<数字>.org"
]
}2.2 レジストラ側での NS レコードの登録(手作業)
保有ドメインの DNS 設定画面で、ホスト名 lab、種別 NS、値に上の 4 つのネームサーバーを登録する。これで lab.infra.jp 配下の名前解決が Route 53 の委任ゾーンに向く。
2.3 委任の確認
反映には数分から数十分かかる(本記事の環境では約 20 分だった)。手元から NS を引き、Route 53 のネームサーバーが返ることを確認する。
dig NS lab.infra.jp +shortns-<数字>.awsdns-<数字>.com.
ns-<数字>.awsdns-<数字>.net.
ns-<数字>.awsdns-<数字>.org.
ns-<数字>.awsdns-<数字>.co.uk.4 つが返るまで次へ進まない(証明書の DNS 検証が委任ゾーンで行われるため)。
3. ネットワーク・ALB・WAF・対象アプリの構築(network-app)
このステップの apply で作るものは次のとおり。インターネットに向けて開くのは ALB の 443 番だけで、WAF の既定動作が遮断なので、カスタムヘッダーのないリクエストはアプリに届かない。
| 分類 | リソース | 役割 |
|---|---|---|
| ネットワーク | VPC、パブリックサブネット 2 つ(2 AZ)、プライベートサブネット 1 つ、IGW、ルートテーブル | パブリック側だけがインターネットへの経路を持つ。プライベート側のルートテーブルにはデフォルトルート(0.0.0.0/0)がなく、NAT も IGW も経由しない |
| VPC エンドポイント | S3(ゲートウェイ型)、ssm / ssmmessages / ec2messages(インターフェース型) | インターネットに出られない EC2 が、OS パッケージと Python ライブラリを S3 から取り、SSM 接続を受けるための出入口。S3 のゲートウェイ型は、プライベート側のルートテーブルに S3 宛てのルートを自動で追加する。インターフェース型はサブネット内の ENI として作られ、VPC 内の宛先として届くのでルートの追加は要らない |
| 対象アプリ | 手元でダウンロードした Python ライブラリを EC2 に渡すための S3 バケット、EC2 t3.micro、IAM ロール(SSM 接続と、そのバケットの読み取りだけを許可)、セキュリティグループ(ALB からの 80 のみ) | 検査対象。パブリック IP なし |
| 外部公開 | ACM 証明書(DNS 検証用 CNAME を委任ゾーンに作成)、ALB(HTTPS 443 のみ)、ターゲットグループ、委任ゾーンの A レコード(ALB へのエイリアス) | 外からの唯一の入口 |
| WAF | Web ACL(REGIONAL、既定動作: 遮断)、ヘッダー許可ルール、マネージドルール群 5 つ、ALB への関連付け、ログ用ロググループ | エージェントだけを通す |
| 秘密値 | random_password(32 文字) | ヘッダー許可ルールと、手順 5 で Pentest のカスタムヘッダーに使う |
| 観測 | 対象アプリ用サブネットの VPC フローログ | 送信元 IP の記録 |
3.1 init、変数ファイル、wheel の取得
cd ~/security-agent-lab/terraform/public-method/network-app
terraform initInitializing provider plugins...
- Installing hashicorp/aws v6.<x>...
- Installing hashicorp/random v3.<x>...
Terraform has been successfully initialized!cp terraform.tfvars.example terraform.tfvarsterraform.tfvars に 2.1 と同じ account_id と profile を記入する。waf_mode は既定の agent-first のまま。動作確認は手順 4 で curl にカスタムヘッダーを付けて行う。
EC2 にはインターネットへの経路がないので、対象アプリの Python ライブラリ(Flask と gunicorn)は、手元の端末で pip download を使って Python の配布形式である wheel(拡張子 .whl)として先に取得し、Terraform が S3 に置き、EC2 は S3 ゲートウェイエンドポイント経由で取得してオフラインで導入する。取得するライブラリは、リポジトリの app/requirements.txt でバージョンと SHA-256 ハッシュを固定している。--require-hashes を付けると、pip はこのハッシュと一致するファイル以外を受け付けないので、手元での取得時と EC2 での導入時の両方で、中身が差し替わっていないことを機械的に確かめられる。Python のライブラリの配布物には 2 種類ある。ビルド済みで、展開するだけで使える wheel と、ソースコードのまま配られて導入する側でビルドが要るソース配布(sdist)である。ソース配布は、pip が依存関係を調べる段階でパッケージに含まれるビルド用のスクリプトを実行するため、取得しただけでそのパッケージのコードが手元で動く。--only-binary=:all: は、すべてのライブラリについて wheel だけを受け付け、ソース配布を使わない指定である。
importlib-metadata と zipp は Flask が Python 3.9 でだけ要求する依存で、手元の Python が 3.10 以上だと pip download が取りこぼし、EC2(Python 3.9)側の pip install で「見つからない」と失敗することがあるため、requirements.txt に明示している。
python3 -m pip download -d wheels --only-binary=:all: --python-version 3.9 \
--implementation cp --abi cp39 --platform manylinux2014_x86_64 \
--require-hashes -r ../../../app/requirements.txt
ls wheelsblinker-1.9.0-py3-none-any.whl
click-8.1.8-py3-none-any.whl
flask-3.1.3-py3-none-any.whl
gunicorn-23.0.0-py3-none-any.whl
importlib_metadata-8.7.1-py3-none-any.whl
itsdangerous-2.2.0-py3-none-any.whl
jinja2-3.1.6-py3-none-any.whl
markupsafe-3.0.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
packaging-26.3-py3-none-any.whl
werkzeug-3.1.9-py3-none-any.whl
zipp-3.23.1-py3-none-any.whlFlask と gunicorn の依存 9 つを含めて 11 個になる。
3.2 plan
terraform plan -out=tfplanPlan: 60 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ target_fqdn = "pentest.lab.infra.jp"
+ target_url = "https://pentest.lab.infra.jp"
+ waf_mode = "agent-first"plan の出力では、Web ACL の各ルールの中身(名前、優先度)が見えない。ヘッダー許可ルールにはヘッダーの秘密値が入っており、Terraform は秘密値を含むブロックの中身を出力に出さない。その際、次のように rule ブロックごと伏せられ、秘密値を含まない他のルールも同じ扱いになる。
+ rule {
# At least one attribute in this block is (or was) sensitive,
# so its contents will not be displayed.
}既定動作とルールの順番は、apply 後に 3.4 の get-web-acl で確認する。
3.3 apply
ACM 証明書は委任ゾーンに置いた CNAME で検証される。委任が効いていれば検証は数秒で終わる。時間がかかるのは ALB(約 2 分半)と SSM 用のインターフェース型エンドポイント(約 1 分半)で、全体で 5 分前後かかる。
terraform apply tfplanaws_vpc.lab: Creating...
...
aws_acm_certificate.app: Creation complete after 8s [id=arn:aws:acm:ap-northeast-1:<検証アカウント ID>:certificate/<証明書 ID>]
aws_route53_record.acm_validation["pentest.lab.infra.jp"]: Creation complete after 32s [id=...]
aws_acm_certificate_validation.app: Creation complete after 1s [id=<検証完了時刻>]
module.endpoints.aws_vpc_endpoint.ssm["ec2messages"]: Creation complete after 1m24s [id=vpce-<ID>]
module.vulnapp.aws_instance.app: Creation complete after 13s [id=<対象アプリのインスタンス ID>]
aws_lb.app: Creation complete after 2m22s [id=arn:aws:elasticloadbalancing:ap-northeast-1:<検証アカウント ID>:loadbalancer/app/sa-pub-alb/<ID>]
aws_lb_listener.https: Creation complete after 0s [id=...]
aws_route53_record.app: Creation complete after 32s [id=<委任ゾーンの ID>_pentest.lab.infra.jp_A]
aws_wafv2_web_acl.app: Creation complete after 2s [id=<Web ACL の ID>]
aws_wafv2_web_acl_association.alb: Creation complete after 39s [id=...]
Apply complete! Resources: 60 added, 0 changed, 0 destroyed.
Outputs:
alb_dns_name = "sa-pub-alb-<数字>.ap-northeast-1.elb.amazonaws.com"
app_instance_id = "<対象アプリのインスタンス ID>"
custom_header_name = "X-Pentest-Token"
custom_header_value = <sensitive>
flow_log_group_name = "/vpc/flow-logs/sa-pub-app"
target_fqdn = "pentest.lab.infra.jp"
target_url = "https://pentest.lab.infra.jp"
waf_log_group_name = "aws-waf-logs-sa-pub-app"
waf_mode = "agent-first"
web_acl_arn = "arn:aws:wafv2:ap-northeast-1:<検証アカウント ID>:regional/webacl/sa-pub-app/<Web ACL の ID>"3.4 実体の確認
証明書が発行されたので、ホスト名が証明書の透明性ログに載っていることを自分で確かめる。検索サイト crt.sh にホスト名で問い合わせると、ACM が発行した証明書の記録が返る。DNS を誰にも教えていなくてもホスト名が公開情報になることを、ここで確かめられる。
curl -sf --retry 3 --retry-all-errors "https://crt.sh/?q=pentest.lab.infra.jp&output=json" | python3 -c 'import sys,json; [print(e["not_before"], e["issuer_name"][:40], e["name_value"]) for e in json.load(sys.stdin)]'<発行時刻> C=US, O=Amazon, CN=Amazon RSA 2048 M0x pentest.lab.infra.jp反映まで数分から数十分かかることがある。また crt.sh は混雑時に、応答を返さなかったりエラーページを返したりすることがある。その場合は python3 側が JSONDecodeError: Expecting value で止まるので、少し待って再実行する。-f と --retry は、HTTP エラーのときに curl を失敗させて 3 回まで再試行するための指定である。
Web ACL の既定動作とルールの順番を確認する。
aws wafv2 list-web-acls --scope REGIONAL \
--query 'WebACLs[?Name==`sa-pub-app`].{Name:Name,Id:Id,ARN:ARN}' \
--region ap-northeast-1[
{
"Name": "sa-pub-app",
"Id": "<Web ACL の ID>",
"ARN": "arn:aws:wafv2:ap-northeast-1:<検証アカウント ID>:regional/webacl/sa-pub-app/<Web ACL の ID>"
}
]aws wafv2 get-web-acl --scope REGIONAL --name sa-pub-app --id <Web ACL の ID> \
--query 'WebACL.{Default:DefaultAction,Rules:Rules[].{P:Priority,Name:Name}}' \
--region ap-northeast-1{
"Default": {
"Block": {}
},
"Rules": [
{ "P": 0, "Name": "allow-pentest-agent-header" },
{ "P": 10, "Name": "AWSManagedRulesCommonRuleSet" },
{ "P": 11, "Name": "AWSManagedRulesKnownBadInputsRuleSet" },
{ "P": 12, "Name": "AWSManagedRulesSQLiRuleSet" },
{ "P": 13, "Name": "AWSManagedRulesLinuxRuleSet" },
{ "P": 14, "Name": "AWSManagedRulesAdminProtectionRuleSet" }
]
}Web ACL が ALB に関連付いていることを確認する。
aws wafv2 get-web-acl-for-resource \
--resource-arn <ALB の ARN> \
--query 'WebACL.Name' \
--region ap-northeast-1"sa-pub-app"4. 到達性の確認
4.1 インターネットからの到達(WAF の動作)
ヘッダーなしのリクエストは WAF の既定動作で遮断され、ALB が 403 を返す。ヘッダーを付けると届く。秘密値は Terraform の出力から取る(画面に表示されるので扱いに注意)。
curl -s -o /dev/null -w '%{http_code}\n' https://pentest.lab.infra.jp/healthz403TOKEN=$(terraform output -raw custom_header_value)
curl -s -o /dev/null -w '%{http_code}\n' -H "X-Pentest-Token: $TOKEN" https://pentest.lab.infra.jp/healthz200ヘッダーを知らない第三者には 403、ヘッダーを持つエージェントには 200。この差が本記事の構成の要点である。
200 が返らない場合は、まず ALB のターゲットグループのヘルスチェックを見る。user_data の完了前は initial や unhealthy になる。
aws elbv2 describe-target-health \
--target-group-arn <ターゲットグループの ARN> \
--query 'TargetHealthDescriptions[].{Target:Target.Id,State:TargetHealth.State,Reason:TargetHealth.Reason}' \
--region ap-northeast-1[
{
"Target": "<対象アプリのインスタンス ID>",
"State": "healthy",
"Reason": null
}
]4.2 WAF ログの確認
上の 2 回の curl が WAF ログに記録されていることを確かめる。Security Agent の実行前に、ログの収集とクエリが動く状態にしておく目的もある。ログの到着まで数分かかる。
aws logs start-query \
--log-group-name aws-waf-logs-sa-pub-app \
--start-time $(date -d '-15 minutes' +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, action, terminatingRuleId, httpRequest.uri | sort @timestamp desc | limit 10' \
--query queryId --output text \
--region ap-northeast-1<クエリ ID>aws logs get-query-results --query-id <クエリ ID> --query 'results[*][*].value' --output text --region ap-northeast-1出力は 1 行が 1 リクエストで、時刻、判定、終了したルール、パスの順に並ぶ。
<時刻> ALLOW allow-pentest-agent-header /healthz
<時刻> BLOCK Default_Action /healthzterminatingRuleId は、ヘッダーありのリクエストが allow-pentest-agent-header、ヘッダーなしが Default_Action になる。
4.3 アプリ自体の確認(SSM)
EC2 に SSH は開けていない。Systems Manager の Run Command(send-command)で EC2 上にコマンドを送り、アプリのサービスが起動していることと、EC2 自身から見た応答を確認する。対話型の Session Manager でも同じことはできるが、手順を記事に残しやすい非対話の方法にする。NAT がなくても届くのは、SSM 用のインターフェース型エンドポイントを VPC に置いているためである。
aws ssm send-command \
--instance-ids <対象アプリのインスタンス ID> \
--document-name AWS-RunShellScript \
--parameters 'commands=["systemctl is-active vulnapp","curl -s http://localhost/healthz"]' \
--query 'Command.CommandId' --output text \
--region ap-northeast-1<コマンド ID>数秒待ってから結果を取り出す。
aws ssm get-command-invocation \
--command-id <コマンド ID> \
--instance-id <対象アプリのインスタンス ID> \
--query '{Status:Status,Output:StandardOutputContent}' \
--region ap-northeast-1{
"Status": "Success",
"Output": "active\nok\n"
}Status が Success で、出力の 1 行目が active(systemd のサービスが起動中)、2 行目が ok(アプリの /healthz の応答)であればよい。user_data の完了前に実行すると activating や接続エラーになるので、その場合は数分おいて再実行する。
5. Security Agent の構築とドメイン検証(security-agent)
最初の apply(5.2)で IAM ロール、Application、Target Domain、検証用 TXT レコード、Agent Space を作り、ドメイン検証(5.4)のあと 2 つ目の apply(5.5)で Pentest だけを作る。Pentest は、Target Domain の検証が済んでいないと作成を拒否されるため、同じコードを 1 回の apply で全部作ることができない。そこで本記事の Terraform では、create_pentest という変数を独自に定義し、この値で Pentest を作るか作らないかを切り替えている。既定は false(作らない)で、1 回目の apply はこのまま行う。ドメイン検証の後に terraform.tfvars で true に変えて 2 回目の apply を行うと、Pentest だけが追加で作られる(5.5)。Security Agent 側の機能ではなく、本記事のコードの都合で設けたものである。
5.1 init、変数ファイル、plan
cd ~/security-agent-lab/terraform/public-method/security-agent
terraform init
cp terraform.tfvars.example terraform.tfvarsコピーした terraform.tfvars をエディタで開き、account_id と profile を 2.1 で書いたものと同じ値に書き換える。create_pentest は見本のとおり false のままにしておき、5.5 で true に変える。書き換えた後の内容を表示して確かめる。
cat terraform.tfvarsaccount_id = "<検証アカウント ID>"
profile = "<プロファイル名>"
create_pentest = falseterraform plan -out=tfplanPlan: 10 to add, 0 to change, 0 to destroy.plan で確認する点。
awscc_securityagent_target_domain.appのverification_methodがDNS_TXT、target_domain_nameがpentest.lab.infra.jpaws_route53_record.domain_verificationのname、type、recordsが(known after apply)になっている(Target Domain の返す TXT レコード名とトークンを参照するため、apply 時に確定する)awscc_securityagent_pentestが plan に含まれないこと(create_pentestが既定の false なので、Pentest は作られない)
5.2 apply(Target Domain と Agent Space まで)
terraform apply tfplanApply complete! Resources: 10 added, 0 changed, 0 destroyed.
Outputs:
agent_space_id = "as-<ID>"
application_domain = "app-<ID>.securityagent.global.app.aws"
application_id = "app-<ID>"
domain_verification_record = "_aws_securityagent-challenge.pentest.lab.infra.jp"
service_role_arn = "arn:aws:iam::<アカウント ID>:role/sa-pub-securityagent-pentest"
target_domain_id = "td-<ID>"
target_domain_verification_status = "PENDING"application_domain が Web アプリの URL のホスト名である。Target Domain の作成から TXT レコードの作成までが 1 回の apply で済んでいる。
5.3 TXT レコードの確認
Terraform が委任ゾーンに作った TXT レコードが、インターネットから引けることを確認する。
dig TXT _aws_securityagent-challenge.pentest.lab.infra.jp +short"aws-securityagent-domain-verification=<トークン>"5.4 ドメイン検証の実行
Target Domain に対して検証を実行する。Terraform のリソースではなく API の操作なので、CLI の verify-target-domain で行う。コンソールでも、Agent Space の画面にある Target Domains の一覧でドメインを選んで「Verify」を押せば同じ操作になる(公式ドキュメントの手順。本記事では CLI で行い、コンソールの操作は試していない)。
aws securityagent verify-target-domain --target-domain-id td-<ID> --region ap-northeast-1{
"targetDomainId": "td-<ID>",
"domainName": "pentest.lab.infra.jp",
"createdAt": "...",
"updatedAt": "...",
"verifiedAt": "...",
"status": "VERIFIED",
"verificationStatusReason": "Verification successful for domain pentest.lab.infra.jp. This domain can be used in pentests"
}TXT レコードが引ければ即時に VERIFIED が返る。DNS の反映待ちで FAILED になった場合は、数分おいて再実行する。状態を後から確認するコマンドは次のとおり。1 件取得のコマンドはなく、複数 ID をまとめて引く batch-get-target-domains を使う。
aws securityagent batch-get-target-domains --target-domain-ids td-<ID> \
--query 'targetDomains[0].{Name:domainName,Status:verificationStatus,Method:verificationDetails.method}' \
--region ap-northeast-1{
"Name": "pentest.lab.infra.jp",
"Status": "VERIFIED",
"Method": "DNS_TXT"
}5.5 Pentest の作成(2 つ目の apply)
terraform.tfvars の create_pentest を true に変えて plan と apply を行う。
sed -i 's/^create_pentest = false/create_pentest = true/' terraform.tfvars
terraform plan -out=tfplanPlan: 1 to add, 0 to change, 0 to destroy.plan で確認する点。
awscc_securityagent_pentest.vulnapp[0]のassets.endpoints[0].uriがhttps://pentest.lab.infra.jp、network_traffic_config.custom_headersのnameがX-Pentest-Tokenでvalueが(sensitive value)。値は network-app の output をterraform_remote_stateで受け取っており、その経路では sensitive 属性が外れるため、コード側でsensitive()を付けて plan に平文が出ないようにしているvpc_configは設定していない(公開方式なのでエージェントは VPC に入らない)。ただし計算属性のため plan には(known after apply)と表示される
terraform apply tfplanApply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
agent_space_id = "as-<ID>"
application_domain = "app-<ID>.securityagent.global.app.aws"
application_id = "app-<ID>"
domain_verification_record = "_aws_securityagent-challenge.pentest.lab.infra.jp"
pentest_id = "pt-<ID>"
service_role_arn = "arn:aws:iam::<アカウント ID>:role/sa-pub-securityagent-pentest"
target_domain_id = "td-<ID>"
target_domain_verification_status = "VERIFIED"Pentest の作成は数秒で終わる。target_domain_verification_status は 5.4 の結果を反映して VERIFIED になる。
6. 最大タスク時間の設定
最大タスク時間(Max task hours)は、1 回の実行で消費してよい Task-hour の上限である。Terraform では設定できない(理由は仕組みと設計の「最大タスク時間の設定」)。CLI の update-pentest --max-task-hours で 20 時間に設定する。Agent Space の ID と Pentest の ID は、5.5 の出力の agent_space_id と pentest_id である。
6.1 CLI での設定
aws securityagent update-pentest \
--agent-space-id as-<ID> \
--pentest-id pt-<ID> \
--max-task-hours 20 \
--query '{maxTaskHours:maxTaskHours}' \
--region ap-northeast-1null20 時間未満は API が拒否する。画面の選択肢だけでなく API の制約で、6 時間を指定すると次のエラーになる。
aws: [ERROR]: An error occurred (ValidationException) when calling the UpdatePentest operation: Max task hours must be at least 20 hours6.2 設定の確認
aws securityagent batch-get-pentests \
--agent-space-id as-<ID> \
--pentest-ids pt-<ID> \
--query 'pentests[0].{id:pentestId,maxTaskHours:maxTaskHours}' \
--region ap-northeast-1{
"id": "pt-<ID>",
"maxTaskHours": 20.0
}maxTaskHours が 20.0 であればよい。CLI で設定してもほかの設定は変わらない。
6.3 Web アプリでの設定(代替)
CLI の代わりに Web アプリでも設定できる。Web アプリ(開き方は手順 7.1)で vulnapp-public-waf を開き、右上の「詳細の変更」を押す。5 ステップのウィザードの最後(その他の設定)に最大タスク時間がある。選択肢は「制限なし」「20 時間(最小許容値)」「30 時間(推奨)」「カスタム」の 4 つである。20 時間を選び、「保存」を押す。

保存すると「ペネトレーションテストが更新されました」と表示される。vulnapp-public-waf の「設定」タブで、最大タスク時間が 20h、VPC とサブネットが未設定、認証情報なし、カスタム HTTP ヘッダーに X-Pentest-Token があることを確認する。

[REDACTED] と伏せられる。7. 実行の開始と監視
最初の実行は agent-first(ヘッダー許可ルールが先)の構成で行う。この手順は Web アプリの画面操作が中心で、コンソールも Web アプリも日本語で表示される。
7.1 実行の開始
コンソールの Security Agent で Agent Space sa-pub-vulnapp を開く。Web アプリへのサインイン方式は IAM Identity Center(SSO)と IAM のみの 2 つがあり、本記事は IAM のみである(違いは仕組みと設計の「2 つの操作画面」を参照)。コンソールにサインインした状態で「ウェブアプリ」タブの「管理者アクセス」を押すと、Web アプリが別タブに開く。Web アプリのホームの「最近のプロジェクト」に、Terraform で作った vulnapp-public-waf がペネトレーションテストとして載っている。
vulnapp-public-waf を開くと「ペネトレーションテストの実行」タブに実行の一覧があり、右上に「詳細の変更」「継続的なペンテストを有効にする」「スケジュール」「実行を開始する」のボタンがある。「実行を開始する」を押す。右上と、実行が 1 件もないときに一覧の中央に出るボタンは同じものである。確認ダイアログには「テストを開始すると、それ以上設定を変更することはできません」とある。

押すと「ペネトレーションテストが開始されました」と表示され、実行の一覧に 1 行が加わる。一覧の「タスク時間」の列が、その実行で消費した Task-hour である。一覧は自動では更新されないので、更新アイコンかブラウザの再読み込みで最新にする。
7.2 実行の画面の構成
実行の詳細は右上の「実行のモニタリング」で開く。上段に プリフライト → 静的分析 → ペネトレーションテスト → ファイナライズ中 の 4 段階、下に 実行の概要 / プリフライト / ログ / 検出結果 のタブがある。「ログ」タブには、エージェントが起こしたタスクが種類ごとに一覧になり、選ぶと右側にそのタスクのログ(エージェントの推論と、実行したコマンドとその結果)が出る。ログに出るコマンドでは、カスタムヘッダーの値やパスワードは [REDACTED] と伏せられる。
Web アプリはタスクログを Application ロールで読む。本記事のコードでは、Application ロールにペンテスト用ロググループ(/aws/securityagent/*)の読み取りを許可するインラインポリシーを付けてある。AWS 管理ポリシーだけでは読めない理由は仕組みと設計の「公式ドキュメントのロール定義との違い」を参照。
プリフライトでは「Logging Infrastructure Setup」「Endpoint Accessibility Validation」「Setup Testing Environment」の 3 つのチェックが順に行われる。プリフライトの間の Task-hour は 0 だった。

7.3 実行状態の確認(CLI)
実行中の段階と状態は CLI でも追える。list-pentest-jobs-for-pentest で実行(ジョブ)の ID と状態を、batch-get-pentest-jobs で段階ごとの状態を取得する。
aws securityagent list-pentest-jobs-for-pentest \
--agent-space-id as-<ID> \
--pentest-id pt-<ID> \
--query 'pentestJobSummaries[].{JobId:pentestJobId,Status:status,Created:createdAt}' \
--region ap-northeast-1[
{
"JobId": "pj-<ID>",
"Status": "IN_PROGRESS",
"Created": "<作成時刻>"
}
]aws securityagent batch-get-pentest-jobs \
--agent-space-id as-<ID> \
--pentest-job-ids pj-<ID> \
--query 'pentestJobs[0].{status:status,steps:steps[].join(`=`,[name,status])}' \
--region ap-northeast-1手動停止(7.4)が完了した後の出力は次のとおりだった。
{
"status": "STOPPED",
"steps": [
"PREFLIGHT=COMPLETED",
"STATIC_ANALYSIS=COMPLETED",
"PENTEST=COMPLETED",
"FINALIZING=STOPPED"
]
}batch-get-pentest-jobs の出力には、カスタムヘッダーの値が平文で含まれる。上の例のように --query で必要な項目だけに絞るとよい。7.4 手動停止(任意)
終わる時刻は事前に分からない。リスクの種類を順に回り切るか、最大タスク時間(本記事は 20 時間)に達すると終わるが、残りの種類の数は画面に出ない。Task-hour を最大タスク時間より低く抑えたい場合は、画面で Task-hour の消費を見て、止める線を先に決めておき、手動で停止する。
停止は実行の画面右上の「実行を停止」から行う。確認ダイアログで「実行を停止」と入力させる方式で、停止した実行は再開できない。

停止後の挙動は次のとおりだった。
- ステータスは「Stopping」を経て約 1 分で「Stopped」になった。ペネトレーションテストの段階は「Completed」扱いになり、ファイナライズの段階が走り始めてから停止した
- 停止を押した後も、停止処理の分の Task-hour が加算された
- 停止までに作られた検出結果は、そのまま残った
7.5 実行の完了
止めずに回すと、全段階が「Completed」になって完了する。ファイナライズの段階も Task-hour を消費する。この段階でも検出結果を最終確認するリクエストが対象アプリに届いており、取りまとめだけの軽い処理ではない。

7.6 レポートの生成
停止後や完了後の実行の画面に「レポートを生成」のボタンが出る。押すと、レポートに含める検出結果を絞り込む画面になる。既定では信頼度が High のものだけが含まれ、情報レベルも外れている。全件を含めたい場合は、リスクレベル、信頼度レベル、リスクタイプをそれぞれ「すべて選択」にする。「コンプライアンス対応レポートを生成する」をオンにすると、方法論、テスターの独立性、検査対象の脆弱性の種類の一覧、実行されたタスクの一覧などが加わる。

「生成してダウンロード」で PDF がダウンロードされる。レポートは英語である。完走した実行では、タスクステータスの絞り込みは Completed だけになる。各検出結果に CVSS v3.1 のスコアとベクトル、再現手順、根拠、修正案が付く。レポートの扱いについては、公式ドキュメントの Exporting a penetration test report に次の注意がある。レポートは証跡の保全と関係者への共有のためのもので、レポート自体が準拠や認証を示すものではなく、監査要件を満たすかどうかは監査人が判断する。生成時に「コンプライアンス対応レポート」をオンにすると、方法論、テスターの独立性、テスト期間、脆弱性カバレッジなどの節が加わり、SOC 2、ISO/IEC 27001:2022、HIPAA セキュリティルールの監査の補助証跡として使える(本記事のレポートはオンにして生成した)。一方で、Security Agent は専門のペンテストサービスでも PCI DSS の QSA や ASV でもなく、レポートは PCI DSS 11.4 と FedRAMP のペンテスト成果物にはならない、と明記されている。
レポートの生成で Task-hour は増えなかった。また、手動停止した実行では、生成の後にファイナライズの段階が「Stopped」から「Completed」に変わった。
8. 結果の取り出し
8.1 検出結果の確認
検出結果は、実行の画面の「検出結果」タブと「実行の概要」タブで確認する。概要タブの重大度のグラフは、既定では信頼度が低いものと情報レベルのものを除いて数える。「低信頼度および情報の検出結果を表示する」をオンにすると全件になる。各検出結果には、重大度と CVSS v3.1 のスコアが付く。

仕込んだ脆弱性 7 件との対応は検証結果にまとめている。
8.2 WAF ログの集計
WAF ログはルールごとの判定と送信元 IP を 1 リクエスト 1 行で記録する。見るのは「エージェントの通信がヘッダー許可ルールで通ったか」と「第三者の到達が遮断されたか」の 2 点である。CloudWatch Logs Insights で、実行の時間帯の判定を集計する。
aws logs start-query \
--log-group-name aws-waf-logs-sa-pub-app \
--start-time <実行開始の epoch 秒> \
--end-time <実行終了の epoch 秒> \
--query-string 'stats count(*) as requests by action, terminatingRuleId | sort requests desc' \
--query queryId --output text \
--region ap-northeast-1<クエリ ID>数秒待ってから結果を取り出す。
aws logs get-query-results --query-id <クエリ ID> \
--query 'results[*][*].value' --output text \
--region ap-northeast-1agent-first(手動停止)の実行の時間帯を集計した出力は次のとおりだった。
ALLOW allow-pentest-agent-header 4370
BLOCK Default_Action 23
BLOCK AWSManagedRulesCommonRuleSet 5出力の読み方は次のとおり。
ALLOWとallow-pentest-agent-headerの行が、カスタムヘッダーを付けてヘッダー許可ルールで通過したエージェントの通信であるBLOCKとDefault_Actionの行は、カスタムヘッダーを持たずに既定動作で遮断された通信である。第三者からの到達と、エージェントがヘッダーを付けずにトップページに送った GET が含まれるBLOCKとマネージドルール群の名前の行は、カスタムヘッダーを持たない通信のうち、既定動作に達する前にマネージドルール群で止まったものである。agent-first の構成でも、カスタムヘッダーを持たない通信にはマネージドルール群が既定動作より先に評価されるためである
既定動作で遮断された送信元は、次のクエリで集計できる。エージェントの送信元も、ALLOW 側の httpRequest.clientIp から分かる。どちらも公表されていない値なので、本記事には個々の IP を載せない。
aws logs start-query \
--log-group-name aws-waf-logs-sa-pub-app \
--start-time <実行開始の epoch 秒> \
--end-time <実行終了の epoch 秒> \
--query-string 'filter action = "BLOCK" | stats count(*) as requests by httpRequest.clientIp | sort requests desc | limit 20' \
--query queryId --output text \
--region ap-northeast-1集計した件数と送信元の内訳は検証結果にまとめている。
8.3 User-Agent とカスタムヘッダーの確認(アプリのログ)
対象アプリは、受け取ったリクエストごとに User-Agent とカスタムヘッダーの有無を JSON で記録している(ヘッダーの値そのものは記録しない)。SSM の Run Command で、実行の時間帯のログから「エージェントのリクエスト数」と「そのうちカスタムヘッダー付きの数」を数える。引用符が入り組むので、コマンドの一覧は JSON ファイルに書いて渡す。
journalctl の --since と --until に渡す時間帯は UTC で指定する。cat > run1-count.json <<'JSON'
{"commands":[
"journalctl -u vulnapp --since '<開始 UTC>' --until '<終了 UTC>' --no-pager -o cat > /tmp/run1.log",
"grep -c '\"user_agent\": \"securityagent\"' /tmp/run1.log",
"grep -c '\"user_agent\": \"securityagent\", \"x_pentest_token_present\": true' /tmp/run1.log"
]}
JSON
aws ssm send-command \
--instance-ids <対象アプリのインスタンス ID> \
--document-name AWS-RunShellScript \
--parameters file://run1-count.json \
--query 'Command.CommandId' --output text \
--region ap-northeast-1<コマンド ID>aws ssm get-command-invocation \
--command-id <コマンド ID> \
--instance-id <対象アプリのインスタンス ID> \
--query '{Status:Status,Output:StandardOutputContent}' \
--region ap-northeast-1{
"Status": "Success",
"Output": "4367\n4367\n"
}出力の 1 行目がエージェントのリクエスト数、2 行目がそのうちカスタムヘッダー付きの数である。2 つが一致していれば、アプリに届いたエージェントのリクエストはすべてカスタムヘッダー付きである。WAF の ALLOW の件数とは、集計する時間帯の境目の違いで数件ずれることがある。公開方式では ALB を経由するので、アプリから見た送信元(remote_addr)は ALB のプライベート IP になる。エージェントの送信元は WAF ログ側で見る。
9. 評価順の切り替え(managed-first)
WAF の評価順を入れ替え、マネージドルール群を先に評価する構成で実行する。agent-first の結果と比べることで、WAF の効果を見る。設定の違いは評価順だけで、最大タスク時間は同じ 20 時間である。
9.1 waf_mode の切り替えと apply
network-app の terraform.tfvars に waf_mode = "managed-first" を加えて apply し、続けて security-agent を apply する。次の例は、手順 10 で network-app を削除して待っていた状態から、network-app を新しく作る流れである。network-app を作り直すとカスタムヘッダーの秘密値も新しくなるので、security-agent の apply で Pentest に新しい値を渡す。
cd ~/security-agent-lab/terraform/public-method/network-app
cat >> terraform.tfvars <<'EOF2'
waf_mode = "managed-first"
EOF2
terraform apply
cd ../security-agent
terraform applynetwork-app を残したまま切り替える場合は、-replace=module.vulnapp.aws_instance.app を付けて apply する。評価順の更新と、対象アプリの EC2 の作り直し(データベースを初期状態に戻すため)を、1 回で行える。
9.2 ルールの順番の確認
apply の後、get-web-acl でルールの順番を確かめる。
aws wafv2 get-web-acl --scope REGIONAL --name sa-pub-app --id <Web ACL の ID> \
--query 'WebACL.{Default:DefaultAction,Rules:Rules[].{P:Priority,Name:Name}}' \
--region ap-northeast-1{
"Default": { "Block": {} },
"Rules": [
{ "P": 10, "Name": "AWSManagedRulesCommonRuleSet" },
{ "P": 11, "Name": "AWSManagedRulesKnownBadInputsRuleSet" },
{ "P": 12, "Name": "AWSManagedRulesSQLiRuleSet" },
{ "P": 13, "Name": "AWSManagedRulesLinuxRuleSet" },
{ "P": 14, "Name": "AWSManagedRulesAdminProtectionRuleSet" },
{ "P": 50, "Name": "allow-pentest-agent-header" }
]
}ヘッダー許可ルールが最後に来ていればよい。
9.3 到達確認と再実行
ヘッダーなしが 403、ヘッダーありが 200 になることを確かめる。
cd ~/security-agent-lab/terraform/public-method/network-app
curl -s -o /dev/null -w '%{http_code}\n' https://pentest.lab.infra.jp/healthz403TOKEN=$(terraform output -raw custom_header_value)
curl -s -o /dev/null -w '%{http_code}\n' -H "X-Pentest-Token: $TOKEN" https://pentest.lab.infra.jp/healthz200確かめたら、手順 7 の操作で実行を開始し、完了後に手順 8 の操作で結果を取り出す。
10. 日をまたぐ待機(任意)
実行と実行の間で日をまたぐ場合は、万一に備えて network-app だけを destroy して待つ。脆弱なアプリを外から届く状態のまま放置しないためである。security-agent と dns-zone は残す。Security Agent のリソースは置いておくだけでは課金されず、残しておけばドメイン検証と最大タスク時間の設定もやり直さずに済む。
10.1 network-app の削除
cd ~/security-agent-lab/terraform/public-method/network-app
terraform destroyDestroy complete! Resources: 60 destroyed.10.2 network-app と Pentest の更新
cd ~/security-agent-lab/terraform/public-method/network-app
terraform apply
cd ../security-agent
terraform plan -out=tfplan # awscc_securityagent_pentest.vulnapp[0] will be updated in-place
Plan: 0 to add, 1 to change, 0 to destroy.Pentest は作り直しではなく、その場で更新される。Pentest の ID も実行の履歴も残る。
terraform apply tfplanApply complete! Resources: 0 added, 1 changed, 0 destroyed.手順 6 で設定した最大タスク時間が保たれていることを、CLI で確かめる。
aws securityagent batch-get-pentests \
--agent-space-id as-<ID> \
--pentest-ids pt-<ID> \
--query 'pentests[0].{id:pentestId,maxTaskHours:maxTaskHours}' \
--region ap-northeast-1{
"id": "pt-<ID>",
"maxTaskHours": 20.0
}最後に、ヘッダーなしが 403、ヘッダーありが 200 になることを確かめてから、Web アプリで「実行を開始する」を押す。
cd ~/security-agent-lab/terraform/public-method/network-app
curl -s -o /dev/null -w '%{http_code}\n' https://pentest.lab.infra.jp/healthz403TOKEN=$(terraform output -raw custom_header_value)
curl -s -o /dev/null -w '%{http_code}\n' -H "X-Pentest-Token: $TOKEN" https://pentest.lab.infra.jp/healthz20011. クリーンアップ
作成の逆順に削除する。GitHub 連携とコードセキュリティレビューを行った場合は、security-agent の削除(11.1)の後、network-app の削除(11.3)の前に、GitHub 連携の登録と GitHub App を消す(11.2)。
11.1 security-agent の削除
手順 10 で network-app を削除したままの状態では、security-agent の destroy は次のエラーで失敗する。security-agent のコードが network-app の出力(ターゲットの FQDN と秘密値)を state 経由で参照しており、network-app の state が空だと評価できないためである。この場合は、先に network-app を 手順 3 と同じ plan と apply で作り直してから進む(本記事の検証では約 5 分だった)。
Error: Unsupported attribute
on security_agent.tf line 15, in resource "awscc_securityagent_target_domain" "app":
15: target_domain_name = local.net.target_fqdn
│ local.net is object with no attributescd ~/security-agent-lab/terraform/public-method/security-agent
terraform plan -destroy -out=tfplan-destroy
terraform apply tfplan-destroyPlan: 0 to add, 0 to change, 11 to destroy.
...
awscc_securityagent_pentest.vulnapp[0]: Destruction complete after 6s
awscc_securityagent_agent_space.lab: Destruction complete after 5s
awscc_securityagent_application.lab[0]: Destruction complete after 5s
aws_route53_record.domain_verification: Destruction complete after 32s
awscc_securityagent_target_domain.app: Destruction complete after 6s
Apply complete! Resources: 0 added, 0 changed, 11 destroyed.Application(アカウントに 1 つのリソース)も、この destroy で削除できる。Agent Space は、GitHub 連携でリポジトリを接続したままでも削除できた。
11.2 GitHub 連携の登録と GitHub App の削除(GitHub 連携を行った場合)
GitHub 連携の登録と GitHub App のインストールは Terraform の管理外なので、destroy では消えない。どちらも画面操作で、次の順に行う。登録の削除は、それを使う Agent Space がなくなってからしかできない(確認ダイアログにその旨が出る)ので、11.1 の後に行う。
- コンソールの AWS Security Agent「統合」で登録を選び、「削除」を押す。確認ダイアログの「削除」で、一覧が「接続された統合 (0)」になる
- github.com の「Settings」→「Applications」→「Installed GitHub Apps」で「AWS Security Agent」の「Configure」を開き、画面下の「Danger zone」の「Uninstall」を押す。AWS 側の登録を消しても GitHub 側のインストールは残るので、両方を消す
削除後に登録が消えたことを確かめる。
aws securityagent list-integrations --region ap-northeast-1{
"integrationSummaries": []
}11.3 network-app と dns-zone の削除
cd ../network-app
terraform plan -destroy -out=tfplan-destroy
terraform apply tfplan-destroyPlan: 0 to add, 0 to change, 60 to destroy.
...
Apply complete! Resources: 0 added, 0 changed, 60 destroyed.dns-zone を消す前に、委任ゾーンに NS と SOA 以外のレコードが残っていないことを確かめる。security-agent の TXT レコードと network-app の A レコード、ACM 検証用の CNAME は、それぞれの destroy で消えている。
aws route53 list-resource-record-sets --hosted-zone-id <委任ゾーンの ID> --query 'ResourceRecordSets[].Type'[
"NS",
"SOA"
]cd ../dns-zone
terraform plan -destroy -out=tfplan-destroy
terraform apply tfplan-destroyPlan: 0 to add, 0 to change, 1 to destroy.
aws_route53_zone.lab: Destruction complete after 31s
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.11.4 NS レコードの削除
最後にレジストラ側の NS レコード(手順 2.2)を削除する。これを忘れると、委任先のホストゾーンは消えたのに委任だけが残る状態になる。Route 53 では誰でも任意の名前のホストゾーンを作れるため、第三者が同じ名前のゾーンを作ってネームサーバーが一致すれば、サブドメインの名前解決を乗っ取られる。
削除がレジストラのネームサーバーに反映されるまでは、親ゾーンがまだ委任を返す。反映されたことは、親ゾーンの権威サーバーに直接問い合わせて、応答に NS レコードが含まれなくなったことで確かめる。
dig NS infra.jp +short
dig NS lab.infra.jp @<上で返った親ゾーンの権威サーバー> +norecurse;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 46194
;; flags: qr aa; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1応答が NXDOMAIN になり、NS レコードが含まれなくなれば反映済みである(AUTHORITY に出るのは親ゾーンの SOA)。本記事の検証では、削除の約 1 分後に親ゾーンの権威サーバー 4 台すべてで消えた。手元のリゾルバのキャッシュには TTL(86,400 秒)の間残ることがあるので、確認は権威サーバーに直接行う。
11.5 残骸の確認
Terraform の 3 ディレクトリの state が空であることに加えて、AWS 側に残っていないことを確かめる。
aws wafv2 list-web-acls --scope REGIONAL --query 'WebACLs[].Name' --region ap-northeast-1
aws acm list-certificates --query 'CertificateSummaryList[].DomainName' --region ap-northeast-1
aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerName' --region ap-northeast-1
aws ec2 describe-vpcs --query 'Vpcs[?IsDefault==`false`].VpcId' --region ap-northeast-1
aws iam list-roles --query 'Roles[?starts_with(RoleName, `sa-pub`)].RoleName'
aws s3api list-buckets --query 'Buckets[?starts_with(Name, `sa-pub`)].Name'
aws route53 list-hosted-zones --query 'HostedZones[].Name'[]
[]
[]
[]
[]
[]
[]aws logs describe-log-groups --log-group-name-prefix /aws/securityagent/ --query 'logGroups[].logGroupName' --region ap-northeast-1[
"/aws/securityagent/sa-pub-vulnapp/cr-<ID>",
"/aws/securityagent/sa-pub-vulnapp/pt-<ID>"
]Security Agent の各リソースが消えたことは、次の 3 つが空の一覧を返すことで確かめる(Target Domain は Agent Space の配下なので、Agent Space が消えれば一覧の対象もなくなる)。
aws securityagent list-applications --region ap-northeast-1
aws securityagent list-agent-spaces --region ap-northeast-1
aws securityagent list-integrations --region ap-northeast-1{
"applicationSummaries": []
}
{
"agentSpaceSummaries": []
}
{
"integrationSummaries": []
}参考リンク
- What is AWS Security Agent (now part of AWS Continuum)?
- Quickstart: Run a penetration test
- Enable an application domain for penetration testing
- Create a penetration test
- Troubleshooting(User-Agent による許可)
- Pricing
- Create the IAM roles for AWS Security Agent
- AWS WAF: AWS Managed Rules rule groups
- ACM: Certificate Transparency Logging、crt.sh(透明性ログの検索)