0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFormation Guardことはじめ

0
Posted at

CloudFormation Guardとは

オープンソースのPolicy as Codeを表現し、JSONおよびYAML形式のデータを検証するためのドメイン固有言語です。
上記の説明だといまいち頭に入ってきませんが、簡単に書くとCloudFormationのテンプレートが組織や自身で定義したルールに則って作られていますよ ということを確認します。

また、以下の流れでテストを行います。

  1. CloudFormationのテンプレートを作成
  2. Guard ルールファイルを作成
  3. Guard test コマンドを使用して、ルールが意図したとおりに動作することを確認
  4. 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

作成したユニットテストファイルは下記の通りです。

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のテンプレートを用意しました。

s3.yml
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ルールファイルは以下のように作成しました。

s3_rules.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への組み込み方なども気になるので、追々学んでいきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?