KMSの難所「キーポリシー」を完全に理解した、から次の一歩
안녕하신게라!パナソニック コネクト株式会社クラウドソリューション部の加賀です。
前回は、AWS KMSのカスタマーマネージドキー(CMK)を選択する理由や、S3連携によるコスト爆発の罠など、インフラ設計面でのポイントを解説しました。
しかし、実務でKMSを使い始めると、ほぼすべてのエンジニアが確実に一度は直面する「謎のエラー」があります。
「AdministratorAccess(IAMフル権限)なのに、KMSでAccessDeniedになるんですけど!?」
AWSのエラーにおいて最も理不尽に感じる瞬間かもしれませんが、これはバグでもなんでもなく、KMSの「ある強固な仕様」によるものです。
今回は、インフラエンジニアが知っておくべき「KMSキーポリシー」の絶対ルール、クロスアカウントの壁、そして絶対にやってはいけないアンチパターン等を掘り下げて解説するTips集です。
前提 【AWSマネージドキーとカスタマーマネージドキー(CMK)の違い】
本記事で解説する「キーポリシーの評価やロックアウトの罠」は、ユーザーが独自に作成するカスタマーマネージドキーを利用する場合のお話です。S3などをデフォルト設定で暗号化する際に使われる「AWSマネージドキー(alias/aws/s3等)」は、キーポリシーの管理をAWS側が行うため、IAMポリシーの権限付与だけで完結します。
「S3を使う時はいつも何も設定していないのに動いているよ」という方は、AWSマネージドキーを利用しているためです。
最重要概念「IAMポリシーとキーポリシーの関係」
この記事の中心テーマであり、KMSを利用する際の最大の難所となるのが「ポリシーの評価ロジック」です。
AWSの一般的なサービス(S3やEC2など)は、IAM(アイデンティティベースのポリシー)側で権限を付与すれば操作できます。しかし、KMSは常に「キーポリシー(リソースベースのポリシー)」との掛け合わせで評価されるため、根本的な考え方が異なります。
同一アカウント内でのKMSのアクセス評価ロジックは、大きく以下の5つのルールに集約されます。
- キーポリシーで明示的なDeny → ブロック(わかる)
- IAMポリシーで明示的なDeny → ブロック(わかる)
- キーポリシーで明示的な許可 → アクセス可能(わかる)
- IAMポリシーで明示的な許可 → ブロック!?(コレガワカラナイ、後述)
- キーポリシーでIAMへ権限委譲 かつ IAMポリシーで明示的な許可 → アクセス可能(後述)
このロジックをフローチャートにすると、以下のようになります。
つまり、KMSにおける絶対の掟は、キーポリシー上で「直接許可」または「IAMへの委譲」が設定されていない限り、どれだけ強力なIAM権限を持っていても一律でアクセス拒否される、ということです。
Tips 【AWS Organizations 環境下のSCP/RCPについて】
親組織側でSCP(サービスコントロールポリシー)やRCP(リソースコントロールポリシー)を利用している場合、明示的な拒否(Deny)がされていないことも確認する必要があります。SCPやRCPでブロックされている場合、キーポリシーやIAMポリシーでどれだけ許可しても通常はAccessDeniedになります(※例外として、SCPは自アカウント内のIAMプリンシパルにかかるため、別のアカウントからアクセスしてくるプリンシパルに対しては、相手側アカウントのSCPが影響します。全社的な統制の設計には留意が必要です)。
なぜAdministratorAccessでも弾かれるのか?
KMSを利用するには、キーそのものに設定されている「キーポリシー」で明示的に許可されている必要があります。
もしあなたがTerraformなどでKMSを作成する際、以下のような記述(委譲)をキーポリシーに書き忘れるとどうなるでしょうか。
// IAMへ権限委譲
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:root"
},
"Action": "kms:*",
"Resource": "*"
}
この記述がない場合、先ほどのフロー図の色違いで上から4番目の分岐の評価自体がNoとなります。
結果としてIAMポリシーの評価自体がされず、AdministratorAccess(フル権限)を持つIAMロール/ユーザであってもキーに一切アクセスできない状態に陥ります。
Tips 【ルート(root)についての誤解とConditionの罠】
ここでの arn:aws:iam::<AccountID>:root は、パスワード等でログインする「ルートユーザ」のことではありません。厳密には「そのAWSアカウント自体(リソースの所有者)を指し、結果としてIAMポリシーによる権限委譲設定を許可する」という特殊な意味を持ちます。
また、良かれと思ってこの委譲ステートメントに対して Condition(特定IPのみ許可など)を設定してはいけません。これを付与すると、IAMポリシー側での許可設定が意図通りに機能しなくなり、デバッグが極めて困難な沼にハマります。
Tips 【VPCエンドポイントポリシーの影響】
閉域網からKMSを利用している場合に見落とされがちなのがVPCエンドポイントポリシーです。IAMとキーポリシーが完璧でも、KMS用のVPCエンドポイント(com.amazonaws.<region>.kms)のポリシー側で適切に許可されていないと同じくAccessDeniedとなります。
また、利用側のネットワーク経路だけでなく、逆にキーポリシー側の Condition で aws:sourceVpce(特定VPCエンドポイントからの通信のみ許可)を厳しく縛っているケースもAccessDeniedの温床になります。評価ロジックの大前提として「ネットワーク経路上のポリシーでもブロックされていないこと」を意識してください。
Terraform等(IaC)とマネジメントコンソールの「冷酷な差」
なぜキーポリシーでつまずく初心者が多いのでしょうか。それは「作成方法によるデフォルトの挙動の違い」にあります。
-
マネジメントコンソールから作成する場合
AWSが気を利かせて、「IAMへの権限委譲」「キー管理者の定義」「キー利用者の定義」を分かりやすいGUIで設定させ、裏で完璧なキーポリシーを自動生成してくれます。 -
TerraformやCloudFormationから作成する場合
IaCツールは良くも悪くも指示通りに冷酷に動作します。Terraformで単純にaws_kms_keyリソースを作成するだけならデフォルト設定が自動付与されますが、カスタムのキーポリシーを定義する場合は「自分で全部責任を持って書いてね」というスタンスになります。
ここで前述の「IAMへの権限委譲」のブロックを含め忘れてterraform applyするとどうなるでしょうか。
通常はAWS側のロックアウト防止機能(Default Lockout Safety Check)によってMalformedPolicyDocumentExceptionエラーとなり、デプロイが弾かれます。しかし、Terraform特有のbypass_policy_lockout_safety_check = trueを誤って有効にして強行したり、CI/CD用の一時的なIAMロールにのみ管理権限を渡して後からロールを削除してしまったりすると、前回の記事で解説した通り、最強の権限を持つはずのアカウントルートであっても手出しできない真のロックアウト(AWSサポートへの依頼が必須)に陥ります。
必ずdata "aws_caller_identity" "current" {}などを利用して、自身のロールを明示的に管理者に加える、またはAWSアカウント(ルート)への委譲を恒久的な形で記述する設計にしましょう。
クロスアカウント利用の壁(ツーウェイ許可)
KMSの強力な利点が「別AWSアカウントへの権限付与」です。
しかし、「キーポリシーで別アカウントをAllowしたのに動かない!」というトラブルが頻発します。
クロスアカウントでKMSを利用する場合、「キーポリシー(鍵の持ち主)」と「IAMポリシー(呼び出し元)」の双方でAllowされている必要があります。
- アカウントA(鍵所持)側のキーポリシー 「アカウントBのRole-Xが使ってもいいよ」
- アカウントB(呼び出し元)側のIAMポリシー 「アカウントAのKMSキーを使っていいよ」
片方だけでは絶対に通信は通りません。これは「セキュリティと権限管理の境界」がアカウントごとに完全に独立しているためです。鍵の持ち主がアクセスを許可(リソース保護)しても、呼び出し元のアカウント側でも「その外部のKMSキーを使ってよい」と許可しない限り、ゼロトラストの原則によって通信が成立しないアーキテクチャになっているからです。
※IAMポリシーで他アカウントのKMSキーを指定する際、Target Resource句にはエイリアス名での指定は機能せず、Key ARN(完全修飾ARN)で指定する必要があります。エイリアスはアカウント固有のリソースであるため、別アカウントからエイリアス名では直接参照できない仕様です。
Tips 【管理者と利用者の分離】
クロスアカウントに限った話ではありませんが、ロールに対して無造作に kms:* を渡してはいけません。AWSのマネジメントコンソールでも自動生成されるように、キーポリシー上では「キー管理者(ポリシー変更やキー削除が可能)」のステートメントと、「キー利用者(kms:Encrypt、kms:Decrypt、kms:GenerateDataKey 等のみに限定)」のステートメントを別々のブロックとして厳格に分離して定義するのが鉄則です。
IaCで自作する場合もこの模範解答に従ってください。
AWSサービス連携の落とし穴「Grant(グラント)」の存在
前回の記事でも触れた通り、EC2(EBS暗号化)やAuto ScalingなどをKMSで暗号化する際、AWSサービスがバックグラウンド(非同期)で、ユーザの代わりにKMSを利用するためにKMS グラント(権限の委譲)という仕組みが使われます。
AWSサービスに対してKMSキーを利用させる場合、キーポリシーや呼び出し元のIAMポリシーで kms:CreateGrant や kms:DescribeKey などの権限を付与しておく必要があります。(また、リソースの削除や切り離し時にグラントをクリーンアップするために kms:RetireGrant も必要になるケースが多いです)
これを忘れると「インフラのデプロイは成功したのに、リソースの起動が静かに失敗し続ける」というデバッグの難しい事象が発生します。Auto ScalingによるEC2の起動失敗などの場合、EC2のコンソール画面等ではなく、CloudTrailで kms:CreateGrant のAccessDeniedを探せば一発で原因が分かるため、トラブルシューティングの視点として覚えておくと時短になります。
なぜ kms:Encrypt だけでは動かないのか?(必須アクションの罠)
前提 【対称キーと非対称キー】
本記事の解説は、AWSで最も一般的に利用される「対称キー(Symmetric)」を前提としています。RSAなどの「非対称キー(Asymmetric)」を利用する場合は、エンベロープ暗号化の仕組みが異なり kms:GetPublicKey などの別アクションが必要になる点にご留意ください。
「データを暗号化させたいから、IAMロールには kms:Encrypt を許可すればOKよね」
というのは、KMS初心者が非常によく陥るトラップです。実際にはこれだけだと、大半のアプリケーションやAWSサービス連携でAccessDeniedが発生します。
ここで権限設計の根拠となるのが、前回の記事でも解説した「エンベロープ暗号化」の仕様です。
実はKMSの kms:Encrypt APIは最大4KBまでのデータしか直接暗号化できないという制約が存在します。そのため、S3やEBSのような大ボリュームのデータや実務の大半のユースケースでは、KMSへ直接データを渡さずに、KMS側で発行した「データキー」を使って暗号化(エンベロープ暗号化)を行う構成になります。
ポリシー設計の鉄則(GenerateDataKeyの必須化)
この「データキーを発行する」ために呼ばれるのが kms:GenerateDataKey というKMSのAPIです。
そのため、アプリケーションのIAMロールや、それを許可するキーポリシーには、単なる kms:Encrypt や kms:Decrypt だけでなく、必ず kms:GenerateDataKey(または kms:GenerateDataKeyWithoutPlaintext)の許可を含める必要があります。
S3やDynamoDBなども裏側でこの機能を利用して透過的暗号化を行っています。AWSマネージドキー(alias/aws/s3等)を使っている分にはこれらをAWSが裏でよしなにやってくれますが、カスタマーマネージドキー(CMK)を連携させてデータを書き込む際には、利用するロール側にこの kms:GenerateDataKey 権限が不可欠になります。
ただし、S3にファイルを保存(s3:PutObject)するロールには kms:GenerateDataKey が必要ですが、S3からファイルを読み取る(s3:GetObject)だけのロールには kms:Decrypt のみで十分です。
暗号化する際は自らデータキーを新規作成(kms:GenerateDataKey)してもらう必要がありますが、復号する際は暗号化ファイルに付随している暗号化されたデータキーをKMSに渡し、KMS側にそのデータキーを復号(kms:Decrypt)してもらうだけで済むためです。アクション単位での最小権限の原則を意識して設計してください。
必須のデバッグ手法(CloudTrailの活用)
権限設定で「AccessDenied」に陥った際は、推測でポリシーを書き直すのではなくCloudTrailのイベント履歴を確認するのが一番の近道です。KMSのエラーログには、「どのアクションが不足して拒否されたのか」が明確に記録されるため、解決策が一発で分かります。
また、ログを見て初めて「意図したキーとは違う KMS Key ID を呼び出そうとしていた」という初歩的な設定ミス(環境変数の渡し忘れなど)に気づくケースも非常に多いです。
絶対に避けるべきアンチパターン(バッドノウハウ)
Conditionなしの "Principal": "*" と安易な全開放
「なんだかAccessDeniedばかりで面倒だから、全部通しちゃえ!」と、以下のように設定しようとしてしまうケースです。
// APIでブロックされる最悪のアンチパターン
{
"Effect": "Allow",
"Principal": { "AWS": "*" },
"Action": "kms:*",
"Resource": "*"
}
実はKMSの場合、S3のバケットポリシーとは異なり、Condition(条件句)のない "AWS": "*" のAllowはAWSのAPI側(MalformedPolicyDocumentException)で強固にブロックされ、保存すらできないセーフティネットが働きます。KMSでは、すべてのAWSアカウントからのアクセスを許可してしまうような潜在的なデータ漏洩(パブリックアクセス)を防ぐため、"AWS": "*" を指定する場合は kms:CallerAccount、aws:PrincipalOrgID、aws:PrincipalAccount などの条件キーを併用することが義務付けられています。
しかし、だからといって適当なConditionを付けて強引に通すような「意図しない広範な許可」は、重大なセキュリティインシデント(外部からの不正アクセス)を招くため絶対にやめましょう。
IAMユーザなどからS3経由での操作のみにKMSの利用を限定したい場合は、kms:ViaService を活用した以下のような絞り込みがベストプラクティスです。(※注 EBSやAuto Scalingなど、Grantを使用して非同期に暗号化を行うサービスに対しては、この kms:ViaService 条件は適用できません)
// S3経由の操作のみにKMS利用を限定する例(kms:ViaServiceの活用)
{
"Effect": "Allow",
"Principal": { "AWS": "*" }, // 全てのプリンシパルをいったん対象にする
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.ap-northeast-1.amazonaws.com", // S3を通じた暗号化/復号のみ許可(マルチリージョン環境等では適宜読み替えてください)
"aws:PrincipalAccount": "111122223333" // 自アカウントからの呼び出しに限定
}
}
}
Tips 【鉄壁の防衛策とOrganizations環境での応用】
kms:ViaService が使えないケースでも、最低限 "aws:PrincipalAccount": "<自分のAWSアカウントID>" をCondition(条件句)に含める癖をつけてください。「万一、キーポリシーをガバガバにしてしまっても、外部のAWSアカウントからの不正利用だけはブロックできる」という最後の命綱になります。
さらに、AWS Organizationsを利用して複数アカウントを運用しているエンタープライズ環境であれば、アカウントIDの代わりに "aws:PrincipalOrgID": "o-xxxxxxx" を条件句に指定するのがベストプラクティスです。組織外からのアクセスを強固にシャットアウトしつつ、組織内でのセキュアなキー共有が可能になります。
復号(Decrypt)と管理者権限(ScheduleKeyDeletion等)の混同
アプリケーションの稼働に必要なIAMロール(Lambda等)に対して、キーに対する完全な管理者権限 (kms:*) を渡すのはNGです。
アプリケーションの脆弱性を突かれた際に、データの復号だけでなくキー自体の削除や無効化まで実行されてしまい、システム全体が破壊されるだけでなく、暗号化したデータを復号して盗難されるリスクがあります。
アプリケーションロールには原則として、kms:Encrypt、kms:Decrypt、kms:GenerateDataKey 等、必要最小限のアクション許可のみを付与するように徹底してください。
まとめ
KMSは強固なセキュリティを提供するからこそ、その扉(キーポリシー)の管理には厳格なルールが存在します。
- IAMフル権限でもキーポリシーの許可(IAMへの委譲)がなければ手出しできない
- IaCでのデプロイ時は「IAM委譲」を絶対に忘れない(忘れるとロックアウト)
- クロスアカウント利用時は「キーポリシー」と「IAMポリシー」の両方が必要
- AWSサービスが非同期で使う場合は
kms:CreateGrantが必要 - エンベロープ暗号化の性質上、権限には
kms:GenerateDataKeyがほぼ必須になる "Principal": "*"は絶対に使わず、Conditionで厳密に絞り込む
KMSキーポリシーの制約は一見すると面倒で複雑ですが、この「二重の鍵(IAMとキーポリシー)」こそが、クラウド上のデータを守る最強の盾となります。
次に真っ赤なAccessDenied画面に遭遇した際、この記事が調査の糸口(あるいはKMSの沼から抜け出す一助)になれば嬉しいです!
お断り
記事内容は個人の見解であり、所属組織の立場や戦略・意見を代表するものではありません。
あくまでエンジニアとしての経験や考えを発信していますので、ご了承ください。