はじめに
Akamai の WAF/WAAP である App & API Protector の Security Configuration を、Terraform で管理する際に、Terraformの運用のみに絞ることができれば変更も矛盾が起きずに管理することができます。
しかし、攻撃を受けており緊急を要する実装、サポートエンジニアの操作などが必要になるケースでは、ACC(Akamai Control Center)からのWAFの設定変更の操作が行われることもあります。
そのため、Security Config は Terraform だけの持ち物にならないケースがあり、変更経路を Terraform だけに一本化するという運用ポリシーは現実的ではありません。
本記事は、Terraformからの設定変更とACCから手動で変更が入る前提での運用をまとめます。
Akamai App & API Protector — Akamai の WAF/WAAP
App & API Protector 開発者向けドキュメント
環境情報
本記事での検証環境は次のとおりです。
- Akamai CLI 2.0.5(
appsec/terraformプラグイン) - Terraform provider
akamai/akamai10.3.0 - Terraform 1.15.8
- 1つのSecurity ConfigにSecurity Policyが2つある設定を利用
既存の Security Config を Terraform に取り込む
まずは対象となるACC で作成済みの Security Config を1つ、Terraform の管理下に取り込みます。
今回はAkamaiが開発している CLI プラグイン cli-terraform の export-appsec を活用します。
このCLIを利用することにより既存の Security Config を HCL として書き出すことができます。
akamai terraform --section default export-appsec "example-waf-config"
実行すると、modules/security/ と modules/activate-security/ の HCL 一式、そして state へ取り込むための appsec-import.sh が生成されます。rate policy や custom rule といった設定オブジェクトは jsonencode(...) のインライン HCL として書き出され、外部の .json ファイルは作られません。
$ find . -name '*.json' | wc -l
0
インライン HCL なので、Git の差分は行単位で確認できます。コメントも書き込めます。
resource "akamai_appsec_rate_policy" "example_rate_policy" {
config_id = local.config_id
rate_policy = jsonencode(
{
"name" : "Example Rate Policy",
"averageThreshold" : 100,
"burstThreshold" : 200
}
)
}
生成された appsec-variables.tf には、contract_id と group_name を手で入力します。appsec の CLI/API はこの2つを返さないため、export しても空のまま出力されます。値は ACC もしくは PAPI/IAM 側で確認して入力してください。
variable "contract_id" { default = "C-XXXXXX" }
variable "group_name" { default = "Example Group" }
state への取り込みは、生成された appsec-import.sh を実行するだけです。
terraform init
sh ./appsec-import.sh
terraform plan
このスクリプトが流す terraform import は、config 全体を対象にします。config レベルの設定と、2つの Security Policy それぞれの設定が、下図のようにカバーされます。
config 全体が百数十リソースの規模でも、この全体をひとまとまりで Terraform 化することができます。
ひとつ注意点があります。import は逐次実行になります。appsec-import.sh は生成された terraform import を1件ずつ回し、そのたびに state 全体を読み込み、API を呼び、state を書き戻します。state が大きくなるほど1件あたりの処理は重くなりますが、これは初回のみのコストです。import 自体は読み取り専用で、実際の設定を変更することはありません。
初回 import 後の plan は No changes にならない
import スクリプトを流して初回の terraform plan を回すと、No changes にはなりません。import 後に plan がクリーンになるとは公式手順に書かれていないので、実際に出た残差を残します。
$ terraform plan
Plan: 1 to add, 2 to change, 0 to destroy.
出た残差は3件でした。
-
activation(
akamai_appsec_activations)の新規作成(1 to add)。appsec-import.shは activation を import しません。activation は設定ではなく反映操作で、import の対象外だからです。state に無いので、plan では新規作成に見えます。apply すれば staging へ実際に反映が走ります(10.3.0 で確認)。 -
akamai_appsec_advanced_settings_request_bodyのrequest_body_inspection_limit_override = true。export が.tfに書いたこのフィールドを、import が state に戻せていません。原因は公式ドキュメントに記載がなく未確定です。差分が出る点は再現します。 -
akamai_appsec_configuration.configのcontract_id/group_id/description。contract と group は appsec の API が値を返さず、import できません(appsec-variables.tfに手入力した値なので、中身は正しいです)。descriptionは export が既定の"Created by Terraform"を書き込むためで、そのまま apply すると ACC 上の説明文を上書きします。
初回は plan を必ず読み、意図しない差分は apply の前に処理します。description が困るなら .tf を実際の説明文に直してから、activation の 1 to add は反映してよいか確認してから、apply します。
version と activation の仕組み
Security Config は version を持ち、staging / production にどの version が反映されているかが分かれています。現状は次のように確認できます。
$ akamai appsec configs --section default --json | jq -r \
'.. |objects|select(.name?=="example-waf-config")
|"latest=\(.latestVersion) staging=\(.stagingVersion) production=\(.productionVersion)"'
latest=10 staging=9 production=9
ここで押さえておくべき仕様があります。activate 済みの version は編集できません(公式ドキュメント: "You can't edit a Security Configuration that has been activated, even if you deactivate it.")。設定を変更したいときは、新しい version を作成して編集し、activate する。これが基本の流れです。新しい version を作ると version 番号は1つ上がります(v31 を clone すると v32 になります)。
Terraform も同じ流儀に従います。設定変更(apply)とネットワーク反映(activation)は別リソースに分かれ、terraform apply で version を編集し、akamai_appsec_activations で staging / production へ反映します。provider は、最新 version が activate 済みならその場では編集できないため、自動で新しい version を作ってから編集します。
- 最新 version が activate 済みなら、新しい version を作って編集します(version 番号が +1 されます)。
- 編集中でまだ activate していない version があれば、それをそのまま編集します(version 番号は増えません)。
未 activate の v10 を編集した場合、version は増えません。
$ terraform apply -auto-approve
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
$ akamai appsec configs ...
latest=10 staging=9 production=9 # latest は 10 のまま
v10 を staging に activate してから編集すると、新しい version が作られます。
$ akamai appsec configs ...
latest=10 staging=10 production=9 # v10 を staging に反映済み
# この状態で rate policy を編集して apply
$ akamai appsec configs ...
latest=11 staging=10 production=9 # 新しい v11 が作られた
activate する version は明示する(latest_version 追従の落とし穴)
activation の version に data source の latest_version を渡すと、常に「アカウントで最新の version」を activate しようとします。
resource "akamai_appsec_activations" "staging" {
config_id = data.akamai_appsec_configuration.this.config_id
version = data.akamai_appsec_configuration.this.latest_version
network = "STAGING"
# notification_emails などの必須引数は省略
}
activation の version を latest_version にすると、ACC で誰かが version を clone しただけ(activate していなくても)で latest_version はそこへ動き、plan はその version を staging に反映しにいきます。後述の precondition も止められません。precondition が見張るのは staging で稼働中の version で、この場合それは動いていないからです。
# akamai_appsec_activations.staging will be updated in-place
~ version = 12 -> 15 # 15 は ACC 側で clone しただけの version
なので、この記事の運用では、activate する version は latest_version に任せず明示します。番号を手で書くのではなく、terraform apply が作った version(apply の出力や state に出る番号)を渡します。こうすれば、外部で作られた version を誤って反映することはありません。
ACC 側の変更は plan に映る — ただし管理下だけ
Terraform を通さない変更が入ったときの挙動を確認します。ACC の UI での変更に相当する操作として、CLI で rate policy を直接変更します(以下のサブコマンド書式は一例で、実際のオプションは CLI のバージョンで確認してください)。
akamai appsec rate-policy --config <config-id> --rate-policy <rate-policy-id> --json \
| jq '.averageThreshold = 250' > modified.json
akamai appsec modify-rate-policy @modified.json \
--config <config-id> --rate-policy <rate-policy-id>
この変更は Terraform の state に入っていないため、terraform plan -detailed-exitcode が差分として検知します。
$ terraform plan -detailed-exitcode
# module.security.akamai_appsec_rate_policy.example_rate_policy will be updated in-place
~ averageThreshold = 250 -> 100
Plan: 0 to add, 1 to change, 0 to destroy.
$ echo $?
2 # 0=差分なし / 2=差分あり / 1=エラー
-detailed-exitcode は差分があると exit code 2 を返します。~ averageThreshold = 250 -> 100 は、ACC 側で 250 になっている値を config の 100 に上書きする差分です。Terraform は既定で、ACC 側の変更を config の内容で上書きします。差分を確認せずに apply すると、ACC 側の変更が消えてしまいます。
ここで押さえるのは、管理下のフィールド変更は plan に差分として映ること、そして既定では config の内容で上書きされることの2点です。採用するか戻すかの判断と、実際の直し方は後半の「直す」節にまとめます。
ただし plan が見せるのは管理下のリソースだけです。コードに書いているプロパティの値変更(Drift)は plan に出ますが、コードに書いてない新設リソースやバージョンのズレは plan だけではわかりません。
ACC 側の変更に気づく — version 番号が信号になる
activate 済みの config を変更すると、必ず新しい version が作られ、version 番号が1つ上がります。この version 番号の動きが、「ACC 側で誰かが変更した」ことに気づく信号になります。
手作業なら、akamai appsec configs で latest / staging / production の version を確認し、Terraform が最後に把握していた version とズレていないかを見ます。毎回やるのは手間で、忘れることもあります。
この突き合わせは Terraform の中に組み込めます。ただし前提が1つあります。Terraform の data source が version について返すのは、番号(latest / staging / production の version)と staging / production の状態だけです。「誰が変更したか」は返しません。そのため、Terraform の中でできる検知は version 番号の突き合わせのみになります。
入れ方は check(警告)と precondition(停止)の2つです。どちらも「対象の環境が、想定している version のままか」を確認します。肝心なのは、突き合わせる相手を「これから activate する新しい version」ではなく、「今その環境に載っているはずの version(基準)」にすることです。基準は、反映の直前に akamai appsec configs で読んだ現在の version を渡します。番号を .tf に固定で書くのではありません。
次は staging を対象にした例です(staging_version を production_version に、変数名を替えれば本番も同じ形になります。検証はすべて staging で行いました)。
data "akamai_appsec_configuration" "this" {
name = "example-waf-config"
}
variable "expected_staging_version" {
# staging に載っているはずの version。反映の直前に現在の version を読んで渡す
type = number
}
variable "target_staging_version" {
# activate する version。terraform apply が作った version を渡す
type = number
}
resource "akamai_appsec_activations" "staging" {
config_id = data.akamai_appsec_configuration.this.config_id
version = var.target_staging_version # activate する version。latest_version 任せにしない
network = "STAGING"
# notification_emails などは省略
lifecycle {
precondition {
# 突き合わせるのは「今の基準」。これから activate する新しい version ではない
condition = data.akamai_appsec_configuration.this.staging_version == var.expected_staging_version
error_message = "AppSec drift: staging の version=${data.akamai_appsec_configuration.this.staging_version} が想定(${var.expected_staging_version})とズレています。中身を確認してから apply してください。"
}
}
}
version が想定どおりなら precondition は通り、apply は指定した version を activate します。誰かが別の version を activate していれば、version が想定とズレ、plan の時点で止まります。実際の出力です。
# 想定=現在の version → 通る。指定した version(13)へ進む。止まらない
$ terraform plan -var expected_staging_version=12 -var target_staging_version=13
~ version = 12 -> 13
Plan: 0 to add, 1 to change, 0 to destroy.
# 想定とズレている → plan の時点で止まる
$ terraform plan -var expected_staging_version=11 -var target_staging_version=13
│ Error: Resource precondition failed
│ AppSec drift: staging の version=12 が想定(11)とズレています。...
$ echo $?
1
止めずに警告だけ出したいなら、同じ条件を check ブロックに書きます。check は plan を止めず、警告だけを出します(check ブロックの仕様です。Terraform 1.5 以降。precondition は Terraform 1.2 以降)。
check "appsec_no_drift" {
assert {
condition = data.akamai_appsec_configuration.this.staging_version == var.expected_staging_version
error_message = "AppSec DRIFT: staging の version=${data.akamai_appsec_configuration.this.staging_version} ≠ 想定=${var.expected_staging_version}"
}
}
version 番号だけでは足りない — 二段で検知する
ここまでの check / precondition は、自分が plan / apply する瞬間に version 番号のズレを警告・停止する、その場のガードでした。ただし自分が apply しなければ働きませんし、version 番号は「何かが動いた」ことまでしか教えません。誰も触らなくても気づき、新しく足された定義まで拾うには、検知を別に用意してスケジュールで回します。
version 番号の突き合わせも terraform plan も、Terraform が管理しているものしか見ません。ACC で新しく custom rule などのオブジェクトを追加しても、それは .tf に無く state にも無いため、plan には一切出ません。
実際に、既存の custom rule を1つ Terraform 管理下に置き、別の新しい custom rule を ACC 側で作ってから plan を回すと、管理下のルールは plan に現れる一方、新しく作ったルールは plan に一度も現れませんでした。version 番号の突き合わせも、既存の管理下リソースが変わらなければ反応しません。
つまり、version 番号だけでは「何かが動いた」ことしか分かりません。何が足されたかまで拾うには、検知を二段にします。
-
版の内容を比べます。
export-appsecで現在の設定を HCL に書き出し、リポジトリに保存した基準(baseline)と diff します。export は決定的で、同じ設定を2回書き出すとterraform fmt後に完全に一致しました(実測)。だから差分がそのまま変更箇所になります。rate policy の値、ポリシーに割り当てた custom rule、WAF mode などはここで拾えます。 -
config レベルのオブジェクト一覧を比べます。未割り当ての custom rule 定義は version の export に出ません。そこで custom rules / rate policies の一覧を API で取得し、baseline と diff します。custom rules の一覧 API は概要しか返さないので、各ルールを個別に取得して conditions まで突き合わせます。ここを一覧だけで済ませると、条件の書き換えも、後述の取り込み用コードも取りこぼしてします。
この2つを合わせることで、version に入る変更も、version に出ない定義も検知できます。
検知を GitHub Actions に載せる
上の二段検知を GitHub Actions のワークフローにして、公開テンプレートとして切り出しました。毎時(または手動で)検知を回し、drift があれば Issue で知らせます。
Terraform を手作業で運用している場合、検知結果を Pull Request にしても実益は薄いです。マージしても apply は走らないので、通知は Issue にしました。Issue には次が載ります。
- 何がどう変わったかの差分
- それを手元の
.tfに取り込むためのサンプルresource(jsonencode込み) - 新規オブジェクトを管理下に入れる
terraform importコマンド
## AppSec ドリフト検知(example-waf-config)
### 取り込み用のサンプル Terraform
custom_rules id=xxxxx を追加(未割り当ての新規ルール)
resource "akamai_appsec_custom_rule" "block_bad_bot_xxxxx" {
config_id = local.config_id
custom_rule = jsonencode({
"conditions": [ { "type": "requestMethodMatch", "value": ["PATCH"] } ],
"name": "block-bad-bot",
"tag": ["bot"]
})
}
取り込み: terraform import akamai_appsec_custom_rule.block_bad_bot_xxxxx <config_id>:xxxxx
(例は読みやすさのため簡略化しています。実際の Issue では JSON は縦に展開され、差分の節も付きます。)
人はこの Issue を読んで、採用するならサンプルを .tf に反映して apply、不要なら ACC 側を戻します。落ち着いたら baseline を更新して Issue を閉じます。「気づく」をワークフローに肩代わりさせ、「どうするか」は人が決める、という切り分けです。
WAF ルールの中身が Issue に載るので、動かすリポジトリは private にします。細かい所では、ルールの値に ${...} が入っても生成 HCL が壊れないよう、HCL の補間シーケンスを無効化しています。Log4Shell を検知するルールは値に ${jndi: を含めるため、この無効化が要ります。
直す — Issue の内容を取り込んで apply
気づいた後は、version 番号ではなく中身を単位に直します。Issue に出た差分とサンプルを見て、採用するか戻すかを決めます。
採用するなら、サンプルの resource を手元の .tf に反映します(既存の値を直す、新しいルールのブロックを足す)。新規オブジェクトは、Issue に添えた terraform import で state に取り込みます。戻すなら .tf はそのままにして、ACC 側を元に戻します。緊急で入れた本番の変更を確認せずに消してしまわないよう、中身を読んでから決めます。
反映は terraform apply です。config を編集して apply すると、provider が新しい version を採番します(番号は手で書きません。apply の出力や state に出ます)。その version を activation に渡して反映します(前述のとおり latest_version には任せず、出す version を明示します)。
落ち着いたら baseline を更新します。これで次の検知は、その変更を正常として扱います。
運用フロー
普段の変更と、ACC 側の変更が入ったときの対応を、次の流れで回します。
普段の変更は Terraform で行います。
git switch -c waf/tune-rate
# .tf を編集
terraform plan -out=tfplan
terraform apply tfplan
terraform apply -target=akamai_appsec_activations.staging # staging で確認
terraform apply -target=akamai_appsec_activations.production # 本番へ反映
ACC 側の変更は、二段検知を拾って Issue にします。人はその Issue を見て、採用なら取り込んで apply、不要なら戻します。version 番号のズレを apply の直前で確実に止めたいときは、前述の precondition を activation に足しておきます。検知(気づく)と precondition(止める)は役割が違い、両方あると隙が減ります。
さらに、誰がどういう場合に ACC を触るかを事前に合意しておくと、Issue が来たときの判断が速くなります。
まとめ
Terraform と ACC の二重管理は、変更経路を一本化するのではなく、変更に気づいて取り込む運用にすれば回せます。気づくのに必要なのは、version 番号だけでなく二段の検知でした。設定を変えると必ず新しい version ができるので version 番号のズレは信号になりますが、それだけだと ACC で新しく足されたオブジェクトを取りこぼします。version の内容の diff と、config レベルのオブジェクト一覧の diff を合わせて、初めて塞げます。
これを GitHub Actions に載せて Issue で知らせ、Issue に取り込み用の Terraform まで添える——ここまでを公開テンプレートにしました。手作業で Terraform を運用しているチームでも、Pull Request や自動反映を前提にせず、「気づいて、必要なら取り込む」だけで回せます。
最初に押さえておくのは、初回 import 後の残差3件と、activate する version を明示すること(latest_version 任せにしない)くらいです。あとは、気づいて必要なら取り込む——この流れで回せます。お手元の Security Config でぜひ試してみてください。
Akamai はCDN、セキュリティ、クラウドサービスを通じ、オンラインライフの力となり守っています。本稿でご紹介したような課題やご相談があれば、お気軽にお問い合わせください
