SNSをalias/aws/snsで暗号化するとCloudWatchアラームが届かない

CloudWatchアラームの通知先のSNSトピックをalias/aws/snsで暗号化すると、アラームがALARMに遷移しても、アラームからのpublishをトピックは受理しない。同じトピックでも、KMSの権限を持つ同じアカウントのIAMの主体からのpublishは受理されるため、手動でpublishして試すと成功し、アラームの通知だけが届かない。購読者の数に関係なく、アラームの通知は誰にも配信されない。
自社の監視基盤ではアラームの通知が、構築から2週間以上、一度も配信されていなかった。トピックの購読者が0件だったため、当初は購読者を追加すれば解決すると考えた。追加する前にNumberOfMessagesPublishedを確認すると、アラームからのpublish自体が記録されていなかった。
アラームの通知とSNSの暗号化の仕組み
通知が届くまでの2段階
CloudWatchアラームの通知は2段階で届く。アラームが状態の変化をSNSトピックへpublishし、トピックが購読者(メール、Slack連携など)へ配信する。
購読者の追加や設定で直るのは2段階目だけである。今回の不具合は1段階目で起きていた。
SNSのサーバー側暗号化
SNSのサーバー側暗号化(SSE)は、トピックにKMSキーを指定し、受信したメッセージを暗号化して保存し、配信の時点で復号する機能である。暗号化したトピックにpublishする主体には、そのKMSキーに対するkms:GenerateDataKey*とkms:Decryptの権限が必要になる。KMSキーを使えない主体のpublishは、トピックポリシーで許可していても成功しない。
KMSキーの種類
| 種類 | 例 | キーポリシー | 費用 |
|---|---|---|---|
| AWS管理キー | alias/aws/sns |
AWSのサービスが管理し、利用者は閲覧のみ。キーと同じアカウントのIAMの主体だけが利用できる | キーの保持は無料 |
| カスタマー管理キー | 自分で作成したキー | 利用者が編集でき、任意の主体に利用を許可できる | キー1本あたり月1ドルとAPIリクエストの料金 |
publishする主体
アカウントのIAMユーザーやIAMロールがaws sns publishを実行するとき、publishの主体はそのIAMの主体である。CloudWatchアラームが通知するときの主体は、サービスプリンシパルのcloudwatch.amazonaws.comで、アカウントのIAMの主体ではない。
以上を組み合わせると、alias/aws/snsで暗号化したトピックには、KMSの権限を持つ同じアカウントのIAMの主体はpublishでき、CloudWatchアラームはpublishできない。AWS管理キーのキーポリシーは編集できないため、AWS管理キーをサービスプリンシパルに使わせる手段もない。2つの主体の結果が分かれるのは、KMSのキーを使う段階と考えられる。
(alias/aws/sns で暗号化) participant K as AWS管理キー participant S as 購読者 alt 同じアカウントのIAMの主体 P->>T: Publish T->>K: GenerateDataKey* / Decrypt(呼び出し元はIAMの主体) K-->>T: 許可 T-->>P: 成功(NumberOfMessagesPublished が増える) T->>S: 配信 else CloudWatchアラーム(cloudwatch.amazonaws.com) P->>T: Publish T->>K: GenerateDataKey* / Decrypt(呼び出し元はサービスプリンシパル) K-->>T: 拒否と考えられる(AWS管理キーは同じアカウントの主体しか使えない) Note over T,K: 推定。アラームの履歴とCloudTrailでは未確認 T-->>P: 失敗(NumberOfMessagesPublished は増えない) Note over S: 購読者には何も届かない end
初期構築でalias/aws/snsを設定していた
トピックの暗号化は初期構築のTerraformで設定していた。
resource "aws_sns_topic" "alerts" {
name = "alerts"
kms_master_key_id = "alias/aws/sns"
}
Security HubのコントロールSNS.1は、KMSキーで暗号化していないトピックを指摘する。alias/aws/snsは鍵を新規に作成せずにこの指摘を解消できる値で、設定の時点ではエラーにならない。terraform applyは成功し、トピックの属性にも異常は表示されない。コンソールで暗号化を有効にした場合も、デフォルトで選択されるキーはalias/aws/snsである。
自分のトピックが該当するかは、トピックの属性のKmsMasterKeyIdで確認できる。
aws sns get-topic-attributes --topic-arn <topic-arn> \
--query 'Attributes.KmsMasterKeyId' --output text
出力がalias/aws/snsかそのエイリアスのARNなら該当する。暗号化していないトピックにはKmsMasterKeyIdの属性がないため、--output textではNoneと表示される。
NumberOfMessagesPublishedでpublishの成否を判別する
通知の不達は、トピックへのpublishか、トピックから購読者への配信の、どちらかの段階で発生する。SNSのメトリクスNumberOfMessagesPublishedは、トピックが受理したメッセージの数である。冒頭の節の1段階目(アラームからトピックへのpublish)の成否を表す。アラームの遷移後もデータポイントが出なければ、トピックはメッセージを受理していない。アラームがpublishしていない場合(アクションの無効など)もここに含まれるので、後述の表で区別する。
アラームはset-alarm-stateで手動で遷移させられる。メトリクスアラームで通知のアクションが実行されるのは状態が変化したときだけなので、すでにALARMのアラームは先にOKへ戻す。手動で設定した状態は次の評価で実際の状態に戻る。
aws cloudwatch set-alarm-state --alarm-name <alarm-name> \
--state-value OK --state-reason "reset"
aws cloudwatch set-alarm-state --alarm-name <alarm-name> \
--state-value ALARM --state-reason "delivery test"
遷移の数分後に、トピックのメトリクスを取得する。<start-time>と<end-time>には、遷移の前後を含む時間帯をISO 8601形式のUTCで指定する。
aws cloudwatch get-metric-statistics --namespace AWS/SNS \
--metric-name NumberOfMessagesPublished \
--dimensions Name=TopicName,Value=<topic-name> \
--start-time <start-time> --end-time <end-time> \
--period 60 --statistics Sum
観測結果は次のとおり。
| 暗号化 | publishの主体 | 操作 | NumberOfMessagesPublished |
|---|---|---|---|
alias/aws/sns |
CloudWatchアラーム | アラームをOKからALARMに遷移させる | 0 |
alias/aws/sns |
同じアカウントのIAMの主体 | aws sns publishを実行する |
1 |
| なし | CloudWatchアラーム | 暗号化の解除後に、同じアラームをOKからALARMに遷移させる | 1 |
同じアカウントのIAMの主体からのpublishは成功し、アラームからのpublishだけが記録されない。暗号化を外すと、同じアラームのpublishが記録された。
NumberOfMessagesPublishedが増えない原因はKMS以外にもある。暗号化を外す前に、次の順で区別する。
購読の確認状態・購読先の設定] B -->|増えない| D{アラームの ActionsEnabled と
AlarmActions のARNは正しいか} D -->|誤り| E[アラームの設定を直す] D -->|正しい| F{トピックポリシーで
アラームのpublishを許可しているか} F -->|していない| G[トピックポリシーを直す] F -->|している| H{KmsMasterKeyId の値} H -->|AWS管理キー
エイリアス・ARN・キーID| I[アラームの履歴で
アクションの失敗を確認する] I --> I2[暗号化の解除か
カスタマー管理キーへ] H -->|カスタマー管理キー| J[キーポリシーで
CloudWatchのサービスプリンシパルに
GenerateDataKey* と Decrypt を許可しているか確認] H -->|未設定| K[KMSは関係しない
アラームの履歴で失敗の理由を確認する]
各項目の確認場所は次のとおり。
| 原因 | 確認する場所 |
|---|---|
| アラームのアクションが無効 | describe-alarmsのActionsEnabled |
| 通知先のARNが別のトピック、別のリージョン | describe-alarmsのAlarmActions |
| トピックポリシーで許可していない | get-topic-attributesのPolicy |
| KMSで拒否された | アラームの履歴(describe-alarm-history --history-item-type Action)。CloudTrailのGenerateDataKeyのイベントにも拒否が記録される可能性がある(今回は未確認) |
今回はアラームの履歴とCloudTrailの記録を残していない。KMSを原因と判断したのは、上の対照の観測と、冒頭の節で説明したAWS管理キーのキーポリシーの仕組みによる。
観測結果と仕組みの対応
観測の表の結果は、冒頭の節の仕組みで説明できる。暗号化した状態のアラームはcloudwatch.amazonaws.comとしてpublishし、AWS管理キーを使えずに失敗したと考えられる。IAMの主体からのpublishは、同じアカウントの主体が実行したため、AWS管理キーを利用できた。
SNSのドキュメントにある、AWSのサービスを暗号化したトピックのイベントソースにする手順は、カスタマー管理キーの使用が第1段階になっている。Budgetsの通知にも同様の手順があり、同じ条件で失敗すると考えられる。今回、暗号化した状態でのBudgetsの失敗は観測していない。暗号化の解除後には、Budgetsの予測超過の通知がトピックに記録された。
暗号化の解除とカスタマー管理キーへの変更
対処は2種類ある。
| 方法 | 必要なもの | 適する場合 |
|---|---|---|
| 暗号化を解除する | kms_master_key_idの削除 |
通知の本文に機微な情報を含めない |
| カスタマー管理キーに変更する | キーの作成、キーポリシーの管理、キーの料金 | 通知の本文に機微な情報を含める、または組織の規定で暗号化が必須 |
カスタマー管理キーを使用する場合は、キーポリシーにサービスプリンシパルを追加する。次はSNSのドキュメントの例をCloudWatchアラームに当てはめたステートメントで、自分たちは適用していない。キーポリシーのStatement配列に追加し、アカウントのrootに管理を許可するデフォルトのステートメントは削除しない。
{
"Effect": "Allow",
"Principal": { "Service": "cloudwatch.amazonaws.com" },
"Action": ["kms:GenerateDataKey*", "kms:Decrypt"],
"Resource": "*",
"Condition": {
"StringEquals": { "aws:SourceAccount": "<account-id>" }
}
}
ほかのサービスから通知する場合も、そのサービスのプリンシパルについて同じステートメントを追加する。たとえばBudgetsはbudgets.amazonaws.com、EventBridgeはevents.amazonaws.comである。EventBridgeから暗号化したトピックへのpublishでは、KMSのポリシーでaws:SourceAccountやaws:SourceArnの条件を使用できない。EC2 Auto ScalingやAWS Configなどは、サービスプリンシパルではなくIAMロールをキーポリシーに指定する。
サービスプリンシパルをキーポリシーに追加] C -->|必須| B C -->|必須ではない| D[暗号化を解除する]
今回は暗号化を解除した。SSEはSNSに保存中のメッセージ本文を暗号化する機能で、購読者には平文が届く。暗号化を外しても、SNSはデフォルトでディスクの暗号化を使用してメッセージを保存する。このトピックで送信するのはアラーム名、閾値、費用額、リソースIDで、保存中の内容をKMSのキーポリシーで保護する必要はない。月1ドルとキーポリシーの管理の負担に対して、得られる保護は限定的である。機微な内容を送信する必要が生じたら、カスタマー管理キーに変更する。
Security Hubを有効にした環境では、暗号化の解除によってSNS.1が失敗になる。このトピックは、理由を記録して指摘を抑制する扱いにする。IaCのスキャナも同様で、tfsecは暗号化していないトピックをaws-sns-enable-topic-encryptionで、AWS管理キーで暗号化したトピックをaws-sns-topic-encryption-use-cmkで指摘する。
暗号化の再設定をterraform testで禁止する
alias/aws/snsは、スキャナの指摘を解消する目的で再び設定されるおそれがある。後で誰かが再設定すると、同じ不達が再発する。そこでterraform testの不変条件で、トピックの暗号化の再設定を禁止した。
現在の不変条件は、kms_master_key_idが未設定でもplanの時点で値が確定しないと考えて、定義のファイルにkms_master_key_idという文字列があれば失敗にしている。この形ではカスタマー管理キーへの変更も禁止になる。AWS providerのこの属性は、4.x以降のスキーマでは値を算出しない属性で、未設定ならplanの時点でnullになる。値で判定でき、次の形に置き換えられる。terraform testはTerraform 1.6以降で使える。
run "alerts_topic_invariants" {
command = plan
assert {
condition = (
aws_sns_topic.alerts.kms_master_key_id == null ||
!can(regex("alias/aws/sns$", aws_sns_topic.alerts.kms_master_key_id))
)
error_message = "The alerts topic must not use the AWS managed key alias/aws/sns; service principals cannot publish to it."
}
}
この条件は、未設定と、planの時点で値が確定しているカスタマー管理キーのARNを通し、alias/aws/snsとエイリアスのARNを失敗にする。同じ構成で作るキーのARNをaws_kms_key.alerts.arnのように参照すると、planの時点では値が確定せず、command = planのassertは評価できずに失敗する(Unknown condition value)。カスタマー管理キーへ変更するときは、このrunをmock_provider(Terraform 1.7以降)のもとでcommand = applyにすると、参照先に仮の値が入り条件を評価できる。AWS管理キーをエイリアスではなくキーIDで直接指定した場合は検出できず、その書き方での再設定はテストを通過する。
通知の到達を定期的に試験する
SNSには購読者への配信の失敗数を示すNumberOfNotificationsFailedがあり、これにもアラームを設定した。ただし通知先がこのトピックだけの間は、そのアラーム自体も同じトピックを通過する。publishの段階で失敗する今回の不具合では、配信の失敗数は増加せず、仮に増えても失敗のアラームの通知が同じ段階で止まる。
(NumberOfNotificationsFailed) participant T as SNSトピック participant S as 購読者 A->>T: Publish T--xA: 拒否(KMS、推定) Note over T,F: 配信まで進まないので
NumberOfNotificationsFailed は増えない opt 仮に NumberOfNotificationsFailed が増えた場合 F->>T: Publish(同じトピックを通る) T--xF: 拒否(KMS、推定) end Note over S: どちらの通知も届かない
このため四半期に一度、アラームを手動でALARMに遷移させ、通知の到達を人が確認する運用にした。この頻度では、不達に気付くまで最大で約3か月かかる。通知が来なければ、前述のNumberOfMessagesPublishedの確認と原因の区別の表を使って、失敗した段階を判別する。


