コンテンツにスキップ

ペンテストの手順

AWS Security Agent
検証日: 2026-10-18 / リージョン: ap-northeast-1 / 対象: AWS Security Agent(now part of AWS Continuum)

はじめに

本記事は、AWS Security Agent(以下、Security Agent)のペネトレーションテスト(以下、ペンテスト)機能を、意図的に脆弱性を仕込んだ自作の Flask アプリ(以下、対象アプリ)に対して、実環境に近い「インターネット公開 + AWS WAF」の構成で動かす手順である。手順は次の目的の順に進む。

  1. エージェントが攻撃を試みる先として、対象アプリをインターネットに公開する。その手前に WAF を置き、エージェントだけを通して第三者の通信は遮断する。この環境を Terraform で作り、ヘッダーの有無で遮断と到達が分かれることを確かめる(手順 1〜4)
  2. 自分が所有するドメインを Security Agent に検査させてよいことを AWS に示すため、ドメインの所有確認(以下、ドメイン検証)を行い、テストの設定を作る。1 回の実行で使ってよい Task-hour の上限も決めておく(手順 5〜6)
  3. ペンテストを実行して進み方を監視し、検出結果と WAF ログ、アプリのログを取り出して、エージェントが何を送り WAF が何を遮断したかを突き合わせる(手順 7〜8)
  4. WAF の評価順を入れ替えてもう 1 回実行し、マネージドルール群がエージェントの検査をどれだけ妨げるかを比べる(手順 9)
  5. 実行の合間に日をまたぐ場合は、脆弱なアプリを外から届く状態で放置しないよう、アプリ側だけを削除して待つ(手順 10、任意)
  6. 課金と公開を残さないよう、作ったものをすべて削除し、残骸がないことを確かめる(手順 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
VPC10.1.0.0/16。パブリックサブネット 2 つ(ALB 用、2 AZ)とプライベートサブネット 1 つ(対象アプリ用)。プライベート側にインターネットへの経路はない(NAT なし)
ALBHTTPS(443)のみ。ACM 証明書(DNS 検証)。セキュリティグループはインターネットから 443 を受け、選別は WAF に任せる
AWS WAFREGIONAL の Web ACL を ALB に関連付け。既定動作は遮断。ヘッダー許可ルールとマネージドルール群 5 つ。評価順は waf_mode で切り替え。ログは CloudWatch Logs
対象アプリFlask + SQLite で作った小さな Web アプリ。応答する URL のパス(/search、/login、/admin など)は 8 つで、意図的に作り込んだ脆弱性 7 種類の一覧はリポジトリの docs/planted-vulnerabilities.md に記載
対象アプリを動かす EC2t3.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 version
Terraform v1.12.2
on linux_amd64

AWS CLI の版も確認する。securityagent コマンドは 2.34.21 で追加されたため、それより古いと手順 5 以降の確認コマンドが動かない。

aws --version
aws-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、EC2Terraform
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 / 確認コマンド
以下の出力例は実測値を基にしている。実リソース ID・アカウント ID はプレースホルダで表記する。ドメインは筆者が実際に使った lab.infra.jp の配下をそのまま書いているので、コマンドと terraform.tfvars の中のドメインは、読者自身の保有ドメインに置き換える。検証環境は記事末尾の手順で削除しており、記事公開時点で pentest.lab.infra.jp には到達できない。

1. コードの取得

Terraform のコードと対象アプリのソースを、GitHub のリポジトリから手元に取得する。

git clone https://github.com/tatsuyaw/security-agent-lab.git
Cloning 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 init
Initializing 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.tfvars
account_id = "<検証アカウント ID>"   # apply 先のアカウント。これ以外のアカウントには apply できない安全装置になる
profile    = "<プロファイル名>"       # 使う AWS CLI のプロファイル
zone_name  = "lab.infra.jp"        # 委任するサブドメイン。保有ドメインの配下にする
terraform plan -out=tfplan
Plan: 1 to add, 0 to change, 0 to destroy.
terraform apply tfplan
aws_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 レコードの登録(手作業)

この手順の中で、レジストラへの NS レコードの登録だけは Terraform で行わない手作業である。登録してから委任が反映されるまで、本記事の環境では約 20 分かかった。

保有ドメインの DNS 設定画面で、ホスト名 lab、種別 NS、値に上の 4 つのネームサーバーを登録する。これで lab.infra.jp 配下の名前解決が Route 53 の委任ゾーンに向く。

2.3 委任の確認

反映には数分から数十分かかる(本記事の環境では約 20 分だった)。手元から NS を引き、Route 53 のネームサーバーが返ることを確認する。

dig NS lab.infra.jp +short
ns-<数字>.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 へのエイリアス)外からの唯一の入口
WAFWeb 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 init
Initializing provider plugins...
- Installing hashicorp/aws v6.<x>...
- Installing hashicorp/random v3.<x>...

Terraform has been successfully initialized!
cp terraform.tfvars.example terraform.tfvars

terraform.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 wheels
blinker-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.whl

Flask と gunicorn の依存 9 つを含めて 11 個になる。

3.2 plan

terraform plan -out=tfplan
Plan: 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 tfplan
aws_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/healthz
403
TOKEN=$(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/healthz
200

ヘッダーを知らない第三者には 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  /healthz

terminatingRuleId は、ヘッダーありのリクエストが 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 は 2 つに分ける。Pentest はターゲットドメインが VERIFIED でないと作成を拒否され(API が InvalidRequest で「The domain of the endpoint hasn’t been verified」を返す)、検証用 TXT レコードのトークンは Target Domain を作成したときに初めて払い出されるためである。理由の詳細は仕組みと設計を参照。

最初の 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.tfvars
account_id = "<検証アカウント ID>"
profile    = "<プロファイル名>"
create_pentest = false
terraform plan -out=tfplan
Plan: 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.jp
  • aws_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 tfplan
Apply 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=tfplan
Plan: 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 tfplan
Apply 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 である。

最大タスク時間を「制限なし」のまま実行しないこと。設定できる最小値は 20 時間で、それより低く抑えたい場合は手動停止(手順 7.4)をするしかない。

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-1
null

20 時間未満は API が拒否する。画面の選択肢だけでなく API の制約で、6 時間を指定すると次のエラーになる。

aws: [ERROR]: An error occurred (ValidationException) when calling the UpdatePentest operation: Max task hours must be at least 20 hours

6.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 時間を選び、「保存」を押す。

最大タスク時間の選択肢。制限なし、20 時間(最小許容値)、30 時間(推奨)、カスタム

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

設定タブ。最大タスク時間 20h、IAM ロール、CloudWatch ロググループ、VPC は未設定

「設定」タブと、実行後の「実行の概要」タブにある「実行範囲と制限」の欄には、カスタムヘッダーの値が平文で表示される。ウィザードの最初のステップの「高度なネットワークアクセス」でも同じである。この画面を撮影して共有する場合は、値を隠す必要がある(本記事の画像は塗りつぶしている)。一方、実行中のタスクログに出るコマンドでは、値は [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 だった。

実行のモニタリング画面。プリフライトの 3 つのチェックのうち 2 つが完了

プリフライトの到達性チェックは、WAF の 403 でも合格する。エージェントがトップページに送った GET の一部はカスタムヘッダーを付けずに送られ、既定動作で遮断されたが、「HTTP の応答が返る」ことで到達可能と判定されている。カスタムヘッダーの設定を誤ってもプリフライトでは気づけないので、設定が正しいことは実行前に手順 4.1 の curl で確かめておく。実行中にヘッダーが付いているかは、手順 8.2 の WAF ログで確かめる。

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 が加算された
  • 停止までに作られた検出結果は、そのまま残った
手動で停止した場合も、画面上部には「失敗した実行」「The job has been stopped」という赤い表示が出る。異常ではない。

7.5 実行の完了

止めずに回すと、全段階が「Completed」になって完了する。ファイナライズの段階も Task-hour を消費する。この段階でも検出結果を最終確認するリクエストが対象アプリに届いており、取りまとめだけの軽い処理ではない。

agent-first(完走)の完了画面。ステータスは Completed、Task-hour は 5 時間 40 分

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」に変わった。

レポートには再現手順として、対象アプリのテスト用アカウントのパスワードや EC2 の内部ホスト名がそのまま載る(カスタムヘッダーの値は伏せられる)。レポート自身にも「公開しないこと」とあり、扱いには注意が要る。

8. 結果の取り出し

8.1 検出結果の確認

検出結果は、実行の画面の「検出結果」タブと「実行の概要」タブで確認する。概要タブの重大度のグラフは、既定では信頼度が低いものと情報レベルのものを除いて数える。「低信頼度および情報の検出結果を表示する」をオンにすると全件になる。各検出結果には、重大度と CVSS v3.1 のスコアが付く。

agent-first(完走)の「実行の概要」タブ。重大度レベルのグラフは 9 件(重要 2、高 4、中程度 3)を数え、「低信頼度および情報の検出結果を表示する (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-1

agent-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 ファイルに書いて渡す。

EC2 の時刻は UTC なので、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 apply

network-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/healthz
403
TOKEN=$(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/healthz
200

確かめたら、手順 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 destroy
Destroy complete! Resources: 60 destroyed.

10.2 network-app と Pentest の更新

network-app を作り直すと、カスタムヘッダーの秘密値も新しく生成される。WAF と Pentest で値が食い違うので、続けて security-agent も apply して 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 tfplan
Apply 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/healthz
403
TOKEN=$(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/healthz
200

11. クリーンアップ

作成の逆順に削除する。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 attributes
cd ~/security-agent-lab/terraform/public-method/security-agent
terraform plan -destroy -out=tfplan-destroy
terraform apply tfplan-destroy
Plan: 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 の後に行う。

  1. コンソールの AWS Security Agent「統合」で登録を選び、「削除」を押す。確認ダイアログの「削除」で、一覧が「接続された統合 (0)」になる
  2. 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-destroy
Plan: 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-destroy
Plan: 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 が自動作成したロググループは Terraform 管理外なので、destroy の後も残る。ペンテスト用(pt-)とコードレビュー用(cr-)の 2 つで、本記事の検証では合計約 9.4 MB だった。必要なら別途削除する。

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": []
}

参考リンク

Amazonアソシエイトリンク