ForgeVision Engineer Blog

フォージビジョン エンジニア ブログ

AWS CloudFormation Guard の使い方を大解剖!

はじめに

こんにちは、クラウドインテグレーション事業部の遅れてきたルーキー八木です。
突然ですが、みなさんは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ですが、ざっくりとした使い方は以下の通りです。

  1. CloudFormation Guard のインストール
  2. DSLを用いてテストファイルを記述
  3. 記述したテストファイルを用いてCloudFormation テンプレートをチェック

リファレンスとしては、公式ドキュメント と githubリポジトリの二つがあります。好みもありますが、どちらかというと自分は公式ドキュメントの方がわかりやすかったです。

github.com

docs.aws.amazon.com

CloudFormation Guardを使ってみる

まずはインストール

Mac OS の方はhomebrewを使うのが簡単です。brew は最新版にアップデートしておいてください。(公式ドキュメントではhomebrewでのインストールの方法は記載されてないのですが、GithubのREADMEには記述されています)

$ brew install cloudformation-guard

Windowsの方は以下のような段階を踏む必要があります。詳しくは公式ドキュメントをご参照ください。

  1. Microsoft Visual C++ Build Toolsのインストール
  2. Rust package managerのインストール
  3. CargoからCloudFormation Guardをインストール

正しくインストールされているか確認しましょう。

$ cfn-guard -V
cfn-guard 2.1.3

テストファイルの生成

インストールできたら、CloudFormationテンプレートをチェックするルールを作っていきます。作成の仕方は以下の2つ。

  1. DSLの記述ルールに従ってひとつひとつルールを定義していく方法
  2. 既存の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 <生成したルールを書き込むファイル>

rulegen command
rulegen コマンド

下の例では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 の書き方の基本

公式ドキュメントによると基本の書き方は以下の通りですが、これを見てもピンと来ないと思いますので一つずつ解説していきます。

Guard Rule Syntax
Guard Rule Syntax

  • query

    • 評価したいプロパティ(設定項目)を記述する箇所です。記述の仕方はCloudFormationのYAMLテンプレートの上位レイヤーから下位レイヤーに向かってdot(.)で繋ぎながらターゲットとする設定項目まで掘り下げていくかたちで記述します。以下のような感じです。
    • 書式: Resource.<論理名>..<個々のProperty名>. もしくは {<下位の設定項目>: <設定値>}
    先ほどのS3のテンプレートでバケット名を指定する場合は以下のようになります。
    query example
    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 <適用するルール(ファイル名)>

validate command
validateコマンド

s3.yaml のVersioningConfigurationEnabledから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 としてルールのテンプレートがあるので、それをベースに改良していくのも良いかもしれません。 この記事が皆様のご理解の助けになれば嬉しいです。