はじめに
こんにちは、クラウドインテグレーション事業部の遅れてきたルーキー八木です。突然ですが、みなさんはCloudFormation のテンプレートを誤った設定のままデプロイしてしまった経験ってありますでしょうか?
設定が単に間違っているというだけなら設定変更してデプロイすればよいだけなのですが、セキュリティリスクのある設定のまま気付かずデプロイしてしまうのは避けたいところですよね。
そんなときに役に立つのが、今回ご紹介する
AWS CloudFormation Guard
というツールです。
これを使うと、CloudFormation のYAMLテンプレートにコンプライアンス違反やセキュリティ上の設定ミスがないかデプロイ前にチェックできる、というツールです。
本ブログでわかること
- ざっくりAWS CloudFormation Guardとはどんなものか
- AWS CloudFormation Guardの使い方(Guardルールの書き方)
AWS CloudFormation Guard とは
AWS CloudFormation Guardは自社のコンプライアンスやセキュリティガイドラインをチェックするルールをDSL(Domain Specific Language - ドメイン固有言語)の記法に則って記述し、そのルールにCloudFormation テンプレートに準拠しているかチェックできるCLIベースのツールです。ポリシーをコードとして記述することから、policy as code とも呼ばれるみたいです。
AWS CloudFormation Guardは2020年10月にVer. 1.0がGA(一般提供)されていて、2023年2月現在の最新バージョンは2022年6月にリリースされたAWS CloudFormation Guard 2.1です。本記事で扱うのはVer. 2.1であり、Ver. 1.0とは互換性がないのでご注意ください。
CloudFormation Guard 使用の流れ
さて、このCloudFormation Guardですが、ざっくりとした使い方は以下の通りです。
- CloudFormation Guard のインストール
- DSLを用いてテストファイルを記述
- 記述したテストファイルを用いてCloudFormation テンプレートをチェック
リファレンスとしては、公式ドキュメント と githubリポジトリの二つがあります。好みもありますが、どちらかというと自分は公式ドキュメントの方がわかりやすかったです。
CloudFormation Guardを使ってみる
まずはインストール
Mac OS の方はhomebrewを使うのが簡単です。brew は最新版にアップデートしておいてください。(公式ドキュメントではhomebrewでのインストールの方法は記載されてないのですが、GithubのREADMEには記述されています)
$ brew install cloudformation-guard
Windowsの方は以下のような段階を踏む必要があります。詳しくは公式ドキュメントをご参照ください。
- Microsoft Visual C++ Build Toolsのインストール
- Rust package managerのインストール
- CargoからCloudFormation Guardをインストール
正しくインストールされているか確認しましょう。
$ cfn-guard -V cfn-guard 2.1.3
テストファイルの生成
インストールできたら、CloudFormationテンプレートをチェックするルールを作っていきます。作成の仕方は以下の2つ。
- DSLの記述ルールに従ってひとつひとつルールを定義していく方法
- 既存のCloudFormationテンプレートからルールを生成する方法
まずは簡単な後者の方法を試してみましょう。
テンプレートファイルからルールを生成
はじめにCloudFormation Guard でチェックしたいCloudFormationのテンプレートを用意します。
今回は「バージョニングの有効化」、「パブリックアクセスをすべてブロック」が ON になっているS3バケットのYAMLファイルを使います。
AWSTemplateFormatVersion: 2010-09-09 Resources: SampleBucket: Type: AWS::S3::Bucket Properties: BucketName: "forgevision-test-bucket-2023" VersioningConfiguration: Status: Enabled #バージョニング有効化 PublicAccessBlockConfiguration: BlockPublicAcls: true #パブリックアクセスをすべてブロック BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true
rulegen
このテンプレートからガードルールを生成するには、サブコマンドrulegen
を使います。
書式:
cfn-guard rulegen --template <ルール生成元になるYAML> --output <生成したルールを書き込むファイル>
下の例ではs3.yaml を参照して生成したルールをs3.guardというファイルに書き込んでます。
$ cfn-guard rulegen --template s3.yaml --output s3.guard $ cat s3.guard let aws_s3_bucket_resources = Resources.*[ Type == 'AWS::S3::Bucket' ] rule aws_s3_bucket when %aws_s3_bucket_resources !empty { %aws_s3_bucket_resources.Properties.BucketEncryption == {"ServerSideEncryptionConfiguration":[{"ServerSideEncryptionByDefault":null,"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"arn:aws:kms:us-east-1:123456789:key/056ea50b-1013-3907-8617-c93e474e400"}]} %aws_s3_bucket_resources.Properties.BucketName == "forgevision-test-bucket_2023" %aws_s3_bucket_resources.Properties.VersioningConfiguration == {"Status":"Enabled"} %aws_s3_bucket_resources.Properties.PublicAccessBlockConfiguration == {"BlockPublicAcls":true,"BlockPublicPolicy":true,"IgnorePublicAcls":true,"RestrictPublicBuckets":true} }
ここで生成されたs3.guardの内容が DSLに則って生成されたGuard rule です。これを見てなるほどこう書くのか!となった方はこの後を読む必要ありません。あとは公式リファレンスを片手にガンガンCloudFormation Guard を使いこなしてください。
わたしは初見では意味がわからず、公式ドキュメントと格闘することになりました。ぱっと見て意味わからん!となった方、このGuard Ruleの書き方を一緒に紐解いていきましょう。
Guard Rule の書き方の基本
公式ドキュメントによると基本の書き方は以下の通りですが、これを見てもピンと来ないと思いますので一つずつ解説していきます。
query
- 評価したいプロパティ(設定項目)を記述する箇所です。記述の仕方はCloudFormationのYAMLテンプレートの上位レイヤーから下位レイヤーに向かってdot(.)で繋ぎながらターゲットとする設定項目まで掘り下げていくかたちで記述します。以下のような感じです。
- 書式:
Resource.<論理名>.
.<個々のProperty名>. もしくは {<下位の設定項目>: <設定値>}
queyの記述 operator
exists
やempty
といったかたちでquery
の箇所で指定したプロパティが存在するかしないかなどのチェック==(等しい)
や!=(等しくない)
、>(〜より大きい)
、>=(〜以上)
などの演算子を入れて、value literal
で設定した値と比較し、ルールの適否を判断します。
query | value literal
- ここにプロパティのあるべき値を記述します。値のデータ型を指定する場合は
string
やinteger(64)
といった基本のデータ型の他にlower_limit <= k <= upper_limit
というかたちで範囲内におさまっているかなどの記述も可能です。 - 値の範囲を指定する場合
書式:Properties.<何らかのProperty> IN r[下限値,上限値]
例:ボリュームのサイズは50以上200以下Resources.NewVolume.Properties.Size IN r[50,200]
- 特定の値のみ許可する場合
書式:Properties.<何らかのProperty> IN ['option1','option2','option3' ]
例:ストレージタイプはio1, io2, gp2のみ許可するResources.NewVolume.Properties.NewVolume.VolumeType IN [ 'io1','io2','gp3' ]
- ここにプロパティのあるべき値を記述します。値のデータ型を指定する場合は
custom massage
validate
やtest
といったサブコマンドを実行したときに判定結果とともに表示されるコメントを自分で作成することができます。エラーが出たときにどのように修正すればよいかのガイダンスなどをいれておけば便利かもしれません。-
書式:
<< custom message you want to mention>>
上記を踏まえて先ほどS3のYAMLをチェックするルールをつくってみます。ここではバケットのバージョニングが有効になっていることを確認する簡単なルールにしましょう。有効になっていない場合に"バージョニングは有効になっていること(=VersioningConfiguration must be enalbed)"というメッセージを表示します。
Resources.SampleBucket.Properties.VersioningConfiguration=={"Status":"Enabled"} <<VersioningConfiguration must be enabled>>プロパティ以降の書き方に注意してください。
{"Key":"Value"}
の形式であるべき値を定義します。
テンプレートをルールで評価する
validate
作ったルールでテンプレートを評価するにはサブコマンドvalidate
を使います。
書式:
$ cfn-guard validate --data <評価するYAMLテンプレート> --rules <適用するルール(ファイル名)>
s3.yaml のVersioningConfiguration
をEnabled
からSuspended
に書き換えてチェックに引っかかるか確認してみましょう。
s3.yaml の書き換え
AWSTemplateFormatVersion: 2010-09-09 Resources: SampleBucket: Type: AWS::S3::Bucket Properties: BucketName: "forgevision-test-bucket_2023" VersioningConfiguration: Status: Suspended 以下省略
s3.guardは前項で作ったバージョニングが有効化されていることをチェックするルールで上書き
$ cat s3.guard Resources.SampleBucket.Properties.VersioningConfiguration=={"Status":"Enabled"} <<VersioningConfiguration must be enabled>>
validate
コマンドによるチェック
$ cfn-guard validate --data s3.yaml --rules s3.guard s3.yaml Status = FAIL FAILED rules s3.guard/default FAIL --- Evaluating data s3.yaml against rules s3.guard Number of non-compliant resources 1 Resource = SampleBucket { Type = AWS::S3::Bucket Rule = default { ALL { Check = Resources.SampleBucket.Properties.VersioningConfiguration EQUALS {"Status":"Enabled"} { ComparisonError { Message = VersioningConfiguration must be enalbed Error = Check was not compliant as property value [Path=/Resources/SampleBucket/Properties/VersioningConfiguration[L:8,C:8] Value={"Status":"Suspended"}] not equal to value [Path=[L:0,C:0] Value={"Status":"Enabled"}]. PropertyPath = /Resources/SampleBucket/Properties/VersioningConfiguration[L:8,C:8] Operator = EQUAL Value = {"Status":"Suspended"} ComparedWith = {"Status":"Enabled"} Code: 6. Properties: 7. BucketName: "forgevision-test-bucket_2023" 8. VersioningConfiguration: 9. Status: Suspended 10. PublicAccessBlockConfiguration: 11. BlockPublicAcls: true } } } } }
s3.yaml Status = FAIL
と表示され、Message
には先ほど設定したcustom message
がちゃんと出力されて、Error`
以下にどこのプロパティが準拠してないか確認できますね。
ちなみに、ルールに準拠している場合は何も表示されません。OKの場合も何かしらの出力が必要な場合は、--show-summary all
というオプションをつけるとPASS、FAILどちらの場合も結果が出力されます。各種サブコマンドとオプションはこちらの公式ドキュメントに詳しく記載がありますので、ご参照ください。
$ cfn-guard validate --data s3.yaml --rules s3.guard --show-summary all s3.yaml Status = PASS PASS rules s3.guard/default PASS
より実用的なルールの書き方
ここまで読んでいただきCloudFormation Guardのルールの基本的な書き方がある程度お分かりいただけたかと思います。ここからはルールをもう少し実用的な書き方にしていきます。
同一タイプのResourceへのルール適用
さて、お気付きかもしれませんが、guard ruleの記述には CloudFormation の論理名が混ざってます。論理名はテンプレート内で一意(重複してはいけない)なので、このルールで評価できるリソースはテンプレート内で論理名が一致する一つだけになってしまいます。例えば同じテンプレート内で複数のS3バケットが定義されている場合に、この書き方だとバケット(論理名)の数だけルールを書く必要がでてしまうというわけです。
テンプレート内の同じタイプのリソースに対しては共通のルールを適用する場合は、論理名を記述する代わりにワイルドカードとリソースを記述することで汎用的なルールになります。
書式:*[ Type == 'AWS::リソース名' ]
例:全てのS3バケット
Resources.*[ Type == 'AWS::S3::Bucket' ]
同一Resourceに対して複数のGuard Ruleを適用
同一のResourceタイプの複数のプロパティを評価したい場合、基本構文に従うと以下のように書く必要がありますが、全てS3バケット向けのルールであることから共通部分を抜き出して書き換えることができます。
書き換え前
Resources.S3Bucket.Properties.BucketName is_string Resources.S3Bucket.Properties.BucketName != /(?i)enypt/ Resources.S3Bucket.Properties.BucketEncryption exists
書き換え後
Resources.S3Bucket.Properties { BucketName is_string BucketName != /(?i)encrypt/ BucketEncryption exists }
条件文 when
さらにある条件のときは、このルールが適用されていること、といった記述も可能です。例えばS3バケットがテンプレート内にあった場合は必ず暗号化すること、といったルールを書いておけばS3がなければルールは無視されて、S3がテンプレート内に存在するときだけ該当ルールが評価されるといった使い方が考えられます。
書式は次の通りで、条件文もguard rule の記法に則って記述します。
when [条件文] { guard rule 1 guard rule 2 }
例
when Resources.*[ Type == 'AWS::S3::Bucket' ] exists { Resources.*[ Type == 'AWS::S3::Bucket' ].Properties.PublicAccessBlockConfiguration == {"BlockPublicAcls":true,"BlockPublicPolicy":true,"IgnorePublicAcls":true,"RestrictPublicBuckets":true} }
また、複数のルール(rule set) に名前をつけて、一つのルールとし、それを他のルール内で使い回すことも可能です。rule set内のルールはAND条件やOR条件を適用できます。
書式は以下の通りです。
rule rule_name_A { Guard_rule_1 OR Guard_rule_2 ... } rule rule_name_B { Guard_rule_3 Guard_rule_4 ... } rule rule_name_C { rule_name_A OR rule_name_B }
[公式ドキュメント](https://docs.aws.amazon.com/cfn-guard/latest/ug/cfn-guard-command-reference.html)より
自動生成ルールを解読
あれこれとGuard Ruleの書き方を学んできましたが、これらを踏まえて改めて初めに自動生成してみたルールを見てみましょう。
$ cfn-guard rulegen --template s3.yaml --output s3.guard $ cat s3.guard let aws_s3_bucket_resources = Resources.*[ Type == 'AWS::S3::Bucket' ] rule aws_s3_bucket when %aws_s3_bucket_resources !empty { %aws_s3_bucket_resources.Properties.BucketEncryption == {"ServerSideEncryptionConfiguration":[{"ServerSideEncryptionByDefault":null,"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"arn:aws:kms:us-east-1:123456789:key/056ea50b-1013-3907-8617-c93e474e400"}]} %aws_s3_bucket_resources.Properties.BucketName == "forgevision-test-bucket_2023" %aws_s3_bucket_resources.Properties.VersioningConfiguration == {"Status":"Enabled"} %aws_s3_bucket_resources.Properties.PublicAccessBlockConfiguration == {"BlockPublicAcls":true,"BlockPublicPolicy":true,"IgnorePublicAcls":true,"RestrictPublicBuckets":true} }
だいぶ理解ができるようになったのではないでしょうか。
1行目ですが、これは変数 aws_s3_bucket_resources
にすべてのS3バケットを代入してます。
2行目はrule aws_s3_bucket
を定義して、条件 when %aws_s3_bucket_resources !empty
S3バケットが存在している(emptyでない)場合に{ }内のガードルールを適用ということになります。
要約すると、このルールでチェックしているのは以下の内容です。
- テンプレート内の記述にS3バケットがある場合(
aws_s3_bucket
というルールが適用される場合) に{}内のルールを適用すること - サーバーサイドの暗号化がされていること
- バケット名が"forgevision-test-bucket_2023"であること
- バージョニングが有効化されていること
- ブロックパブリックアクセスの4項目が有効になっていること
最後に
ここまでブログをお読み頂きありがとうございました。CloudFormation Guardは公式リファレンスの和訳がまだなく、少しとっつきにくい印象でしたが、一度ルールの書き方を覚えればCloudFormationのテンプレートに従って様々なパターンのGuard ruleを作っていけそうに感じました。github には、guard-example としてルールのテンプレートがあるので、それをベースに改良していくのも良いかもしれません。 この記事が皆様のご理解の助けになれば嬉しいです。