CloudFormation Guardとは
オープンソースのPolicy as Codeを表現し、JSONおよびYAML形式のデータを検証するためのドメイン固有言語です。
上記の説明だといまいち頭に入ってきませんが、簡単に書くとCloudFormationのテンプレートが組織や自身で定義したルールに則って作られていますよ ということを確認します。
また、以下の流れでテストを行います。
- CloudFormationのテンプレートを作成
- Guard ルールファイルを作成
- Guard test コマンドを使用して、ルールが意図したとおりに動作することを確認
- Guard validate コマンドを使用して、テンプレートを検証
試してみる
インストール
公式ドキュメントにインストール方法の記載があるため、案内に従いインストールします。
今回はWSLにインストールするため、Linux環境の方でインストールを行います。
Windows用の手順もあるので、自身の環境に合った方法でインストールしましょう。
# curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh
# export PATH=~/.guard/bin:$PATH
# cfn-guard --version
ルールの作成
cfn-guardのインストールが完了したら、ルールの作成をしていきます。
既存のテンプレートから自動作成する方法と、自身でカスタマイズする方法があります。
今回は1から作成し、構文についてもまとめたいと思います。
なお、既存のテンプレートから自動で作成したい場合は、以下のコマンドを実行します。
# cfn-guard rulegen --output rules.guard --template template.yml
これで、template.ymlの定義内容を基にrules.guardというルールファイルが作成されます。
自動作成する方法は!Subや!Refといった短縮形の関数があるとエラーになりますので、実行前に置き換えておきましょう。
構文
ルールを記述する際の構文は以下の通りです。
<query> <operator> [query|value literal] [custom message]
- query
テンプレート内のリソースやプロパティをドット (.) 区切り式で表したパスです。
クエリの結果を更にフィルタリングして使用することができます。# すべての Resources セクションのリソースを取得 let all_resources = Resource.* # リソースの中から IAM 関連のものを抽出 let iam_resources = %resources[ Type == /IAM/ ] # 更にその中から ManagedPolicy のみを抽出 let managed_policies = %iam_resources[ Type == /ManagedPolicy/ ] # すべての ManagedPolicy を順に処理 %managed_policies { # 各ポリシーに対する評価を記述 }
- operator
クエリの状態確認に使用する演算子を入れます。
サポートされている演算子は以下の通りです。
==,!=,>,>=,<,<=,IN,exists,empty,is_string,is_list,is_struct,not(!)
- [query|value literal]
query に対して比較対象となる値を指定します。
範囲を表現する場合は少し特殊な書き方となります。-
r[<lower_limit>, <upper_limit>]はlower_limit <= k <= upper_limitに変換されます -
r(<lower_limit>, <upper_limit>)はlower_limit < k < upper_limitに変換されます
-
- custom message
validateおよび test コマンドの詳細出力に表示されるメッセージです。
必須ではないため省略可能となります。
記載例
例としてS3バケットのルールをいくつかのパターンで作成します。
ルールファイルの拡張子は任意のため、今回は.guardとして作成します。
S3バケットのパブリックアクセスをブロック
# S3Bucketのリソースを変数に格納
let s3_bucket_resources = Resources.*[
Type == "AWS::S3::Bucket"
]
# 存在するパラメータがすべてtrueであることを確認
rule check_backet_public_access_block when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.PublicAccessBlockConfiguration exists {
%s3_bucket_resources.Properties.PublicAccessBlockConfiguration.* == true
}
}
# 指定したパラメータがtrueであることを確認 ※パラメータがない場合はエラー
rule check_backet_public_access_block_2 when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.PublicAccessBlockConfiguration exists {
%s3_bucket_resources.Properties.PublicAccessBlockConfiguration {
BlockPublicAcls == true
RestrictPublicBuckets == true
IgnorePublicAcls == true
RestrictPublicBuckets == true
}
}
}
# 存在するパラメータがfalseではないことを確認
rule check_backet_public_access_block_3 when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.PublicAccessBlockConfiguration exists {
%s3_bucket_resources.Properties.PublicAccessBlockConfiguration.* != false
<<パブリックアクセスブロックを有効化する必要があります>>
}
}
S3バケット名の末尾が-bucketであることを確認
let s3_bucket_resources = Resources.*[
Type == "AWS::S3::Bucket"
]
# !emptyでリソースが存在する場合に評価 かつ existsでパラメータが存在する場合に評価
rule check_bucket_naming when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.BucketName exists {
%s3_bucket_resources.Properties.BucketName == /^.*-bucket$/
}
}
ライフサイクルルールで有効期限が30日以上365日以下であることを確認
let s3_bucket_resources = Resources.*[
Type == "AWS::S3::Bucket"
]
rule check_backet_lefecycle_expiration when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.LifecycleConfiguration exists {
%s3_bucket_resources.Properties.LifecycleConfiguration.Rules[*].ExpirationInDays == r[30, 365]
}
}
ライフサイクルルールのStatusがEnabledまたはDisabledであることを確認
let s3_bucket_resources = Resources.*[
Type == "AWS::S3::Bucket"
]
rule check_backet_lefecycle_status when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.LifecycleConfiguration exists {
%s3_bucket_resources.Properties.LifecycleConfiguration.Rules[*].Status IN ["Enabled", "Disabled"]
<<StatusはEnabledとDisabledのみ許可されています>>
}
}
以下のGitHubに色々な例がありますので、こちらを参考にするとよいかと思います。
Guard ルールのテスト
実際のテンプレートに対して検証を行う前に、ルールが期待通りに動くかユニットテストを行えます。
ユニットテストを実施することで、ルールの正当性を事前に検証できるため、実際のテンプレートに適用する前に安心してルールを運用することができます。
Guardルールのテストを行うにはユニットテストファイルを作成します。
推奨されるファイル名はGuardルールのファイル名に_testsプレフィックスを付与する形となります。
例)
Guardルールファイル:s3_rules.guard
ユニットテストファイル:s3_rules_tests.yml
作成したユニットテストファイルは下記の通りです。
---
- name: Success Test # テスト名
# テストケースで利用するCFnのリソース定義
input:
Resources:
Success:
Type: AWS::S3::Bucket
Properties:
BucketName: SuccessBucket
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
# inputで定義したリソースをルールファイルで評価した際の期待値
# PASS, FAIL, SKIPから選択
expectations:
rules:
check_backet_public_access_block: PASS
check_backet_public_access_block_2: PASS
check_backet_public_access_block_3: PASS
check_backet_lefecycle_status: SKIP
check_backet_object_lock_enabled: SKIP
check_backet_lefecycle_expiration: SKIP
guardルールファイルとユニットテストファイルの作成が完了したら、以下のコマンドでテストを行います。
$ cfn-guard test --rules-file rules.guard --test-data rules_tests.yml
実行結果は以下のようになります。
期待値通りのルールとそうではないルールが一目でわかるようになっています。
$ cfn-guard test --rules-file s3_rules.guard --test-data s3_rules_tests.yml
Test Case #1
Name: Success Test
FAIL Rules:
check_backet_lefecycle_expiration: Expected = PASS, Evaluated = [SKIP]
PASS Rules:
check_backet_object_lock_enabled: Expected = SKIP
check_backet_public_access_block_3: Expected = PASS
check_backet_lefecycle_status: Expected = SKIP
check_backet_public_access_block: Expected = PASS
check_backet_public_access_block_2: Expected = PASS
FAIL Rulesの箇所に期待通りではないルールがまとめて表示されています。
今回はPASSになることを期待していたルールが、実際にはSKIPとなっているためFAILと判定されました。
FAIL Rules:
check_backet_lefecycle_expiration: Expected = PASS, Evaluated = [SKIP]
Guardルールファイルを利用したテンプレートの検証
ユニットテストが完了したら、実際のCloudFormationテンプレートに対して検証を実施します。
guardルールファイルとCloudFormationのテンプレートを用意して以下のコマンドを実行すると検証を行えます。
$ cfn-guard validate --rules s3_rules.guard --data s3.yml -S all
デフォルトだと実行結果にFailしか表示されないため、-S allオプションですべてのサマリを表示すると結果がわかりやすいです。
実際にファイルを用意して出力結果を確認してみましょう。
まずは、以下のCloudFormationのテンプレートを用意しました。
AWSTemplateFormatVersion: "2010-09-09"
Description: S3 Resource
Parameters:
Environment:
Type: String
Default: Dev
AllowedValues:
- Prod
- Dev
- DR
Mappings:
BucketName:
Environment:
Prod: prod
Test: test
Dev: dev
Resources:
GuardBucketSuccess:
Type: AWS::S3::Bucket
Properties:
BucketName:
!Join
- "-"
- - !FindInMap [BucketName, Environment, !Ref Environment]
- "guard"
- "bucket"
- !Ref AWS::Region
- !Ref AWS::AccountId
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: LifeCycleRule
Status: Enabled
Prefix: tmp/
ExpirationInDays: 365
Tags:
- Key: Env
Value: !Ref Environment
Guardルールファイルは以下のように作成しました。
let s3_bucket_resources = Resources.*[
Type == "AWS::S3::Bucket"
]
rule check_backet_public_access_block when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.PublicAccessBlockConfiguration exists {
%s3_bucket_resources.Properties.PublicAccessBlockConfiguration.* == true
}
}
rule check_backet_object_lock_enabled when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.ObjectLockEnabled exists {
%s3_bucket_resources.Properties.ObjectLockEnabled == false
}
}
rule check_backet_lefecycle_expiration when %s3_bucket_resources !empty {
when %s3_bucket_resources.Properties.LifecycleConfiguration exists {
%s3_bucket_resources.Properties.LifecycleConfiguration.Rules[*].ExpirationInDays == r[30, 365]
}
}
テスト内容は下記の通りです。
- パブリックアクセスブロックが
true - オブジェクトロックが無効
- ライフサイクルルールで有効期限が30日以上365日以下
※パラメータが存在しない場合はSKIPとする
それでは、cfn-guard validateコマンドでテンプレートの検証を行います。
SKIPが含まれる場合も全体としてはPASS(正常)として判断されます。
$ cfn-guard validate --rules s3_rules.guard --data s3.yml -S all
/work/cfn/s3.yml Status = PASS
SKIP rules
s3_rules.guard/check_backet_object_lock_enabled SKIP
PASS rules
s3_rules.guard/check_backet_public_access_block PASS
s3_rules.guard/check_backet_lefecycle_expiration PASS
---
実行結果がFAILの場合は以下のように表示されます。
戻り値も1以上の値が返されたため、CI/CDにも組み込みやすそうです。
$ cfn-guard validate --rules s3_rules.guard --data s3.yml -S all
/work/cfn/s3.yml Status = FAIL
SKIP rules
s3_rules.guard/check_backet_object_lock_enabled SKIP
PASS rules
s3_rules.guard/check_backet_public_access_block PASS
FAILED rules
s3_rules.guard/check_backet_lefecycle_expiration FAIL
---
Evaluating data /home/tatsuya/work/cfn/s3.yml against rules s3_rules.guard
Number of non-compliant resources 1
Resource = GuardBucketSuccess {
Type = AWS::S3::Bucket
Rule = check_backet_lefecycle_expiration {
ALL {
Check = %s3_bucket_resources[*].Properties.LifecycleConfiguration.Rules[*].ExpirationInDays EQUALS [30,364] {
ComparisonError {
Error = Check was not compliant as property value [Path=/Resources/GuardBucketSuccess/Properties/LifecycleConfiguration/Rules/0/ExpirationInDays[L:40,C:30] Value=365] not equal to value [Path=[L:0,C:0] Value=[30,364]].
PropertyPath = /Resources/GuardBucketSuccess/Properties/LifecycleConfiguration/Rules/0/ExpirationInDays[L:40,C:30]
Operator = EQUAL
Value = 365
ComparedWith = [30,364]
Code:
38. - Id: LifeCycleRule
39. Status: Enabled
40. Prefix: tmp/
41. ExpirationInDays: 365
42. Tags:
43. - Key: Env
}
}
}
}
}
まとめ
CloudFormation Guardを使用することで予期せぬリソース作成を抑止できるため、セキュリティ的にもコスト的にも助かりそうです。
CI/CDへの組み込み方なども気になるので、追々学んでいきたいと思います。