CloudFormation Hookを試してみる

[CloudFormation Hook](https://aws.amazon.com/jp/about-aws/whats-new/2022/02/aws-announces-general-availability-aws-cloudformation-hooks/)が利用可能になったので、試してみる。

## やりたいこと

**IaCに予防的統制を組み込み**たい。
これまで、例えばS3で暗号化を強制したい場合、以下のような手段はあった。

- SCPでS3暗号化設定の変更を禁止する。
- Config RulesでS3暗号化が設定されていないバケットを検知する。
- CFn GuardでS3暗号化設定が入っているかどうかを事前にチェックする。

いずれも有益な統制ではあるが、より直截的に「S3暗号化設定が入ってなければスタックの作成を禁止」したい場合、これらの方法では難しかった。
フックならそれが実現できそうだ。

## 準備
CFnテンプレートを二つ用意する。

一つ目は何も設定されていないテンプレート。
```yaml:hook-test-ng.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Hook Test. Encryption settings is not present."
Resources:
  S3HookTestBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub "hooktest-ng-${AWS::AccountId}-${AWS::Region}-bucket"
```

二つ目は、暗号化が設定されているテンプレート。
```yaml:hook-test-ok.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Hook Test. Encrypytion is enabled."
Resources:
  S3HookTestBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub "hooktest-ok-${AWS::AccountId}-${AWS::Region}-bucket"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'aws:kms'
              KMSMasterKeyID: '12345678-abcd-efgh-1234-1234567890ab'
            BucketKeyEnabled: true
```

## ステップバイステップ

### 1. サンプルHookを検索する

フックは**CloudFormation→レジストリ→パブリック拡張機能**から検索できる。
`AWSSamples`という名前でいくつか登録されているので、ここから検索する。以下二点に注意。

- パブリッシャーはAWSではなくサードパーティー。
- 前方一致。`AWSSamples::S3`はマッチするが、`S3`はマッチしない。
![スクリーンショット 2022-03-10 午前11.54.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/b86cae8f-1a25-7ea4-afb0-21c837943810.png)
今回は上から二つ目の`AWSSamples::S3BucketEncrypt::Hook`を有効化する。

### 2. サンプルHookを有効化する
フックを選択すると詳細が表示されるので、右上の「アクティブ化」ボタンをクリックする。
![スクリーンショット 2022-03-10 午後8.18.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/6ed8d4bf-4c6d-83cd-7e33-5d1a2136024d.png)
アクティブ化の詳細設定画面に遷移するが、どうやらフックを登録するためのIAMロールのARNが必要らしい。
![スクリーンショット 2022-03-10 午後2.19.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/8b8021ac-cfc4-e656-1ce6-8a0da11bf67c.png)

### 3. IAMロールを準備する
[マニュアル](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry-public.html?icmpid=docs_cfn_console#registry-public-enable-execution-role)を参考に、信頼関係を以下の通り設定したロールを作成する（ちなみに`resources.cloudformation.amazonaws.com`はフックではなく拡張リソースタイプを登録するための設定）。
ロール名は`CFnHookTestRole`とし、権限そのものはいったん'AdministratorAccess'とした。

```json:CFnHookTestRole（信頼関係）
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "hooks.cloudformation.amazonaws.com",
                    "resources.cloudformation.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "123456789012"
                }
            }
        }
    ]
}
```

### 4. アクティベート
IAMロールとログ設定を入れ、他の設定はブランクのまま、「拡張機能のアクティブ化」をクリックする。
![スクリーンショット 2022-03-10 午後8.20.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/e14d63a9-26c7-286a-0337-2c227c73cea6.png)

エイリアスと設定JSONの設定入力画面が出るので、それぞれ入力する。
エイリアスはリージョンごと・アカウントごとに一意なものを設定する必要がある模様。
別リージョン・別アカウントならかぶってもよいということになるが、マニュアルの記載を見る限り、なるべく一意にしておくことが望ましいようだ。ただ、その割にハイフンやアンスコが使えないのは不便としか言えない。
...と、色々調べてはみたものの、試した限りでは、このフックは`default`というエイリアスしか受け付けないらしい。何なんだ。。。

```
Invalid configuration alias S3BucketEncrypt12345678901tokyo for HOOK type AWSSamples::S3BucketEncrypt::Hook. Only 'default' alias is supported for HOOK types
```
仕方ないのでエイリアスを`default`として作成する。
![スクリーンショット 2022-03-10 午後8.35.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/2ff3e2a3-fc33-aecf-7a7e-c9d36684dc76.png)

設定JSON（HookConfiguration）は以下のような感じになる。詳細は[こちら](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/hooks-structure.html#hook-configuration-schema)に記載がある。

```json:設定JSON
{
  "CloudFormationConfiguration": {
    "HookConfiguration": {
      "TargetStacks": "ALL",
      "FailureMode": "FAIL",
      "Properties": {
        "minBuckets":"1",
        "encryptionAlgorithm": "aws:kms"
      }
    }
  }
}
```

|項目|内容|取りうる値|
|:--|:--|:--|
|TargetStacks|対象スタックぽく見えるが、実質、有効/無効の設定。|ALL, NONE|
|FailureMode|フックのルールに違反した際の挙動。FAILにするとスタック作成が中止される。WARNだと警告止まり。|FAIL, WARN|
|Properties|各フックの設定スキーマごとに定義された独自設定。|フックによる|

S3暗号化のフックの場合、PropertiesはminBuckets（準拠している最小バケット数。これは意図がよくわからない）とencryptionAlgorithm（暗号化アルゴリズム）の二つ。
これはフックの設定スキーマに定義されているので、少し長いが引用しておく。

```json:S3暗号化フックの設定スキーマ
{
    "typeName": "AWSSamples::S3BucketEncrypt::Hook",
    "description": "Example resource hook that check Amazon S3 bucket encryption properties",
    "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-samples/tree/main/hooks/python-hooks/s3-bucket-encryption",
    "documentationUrl": "https://github.com/aws-cloudformation/aws-cloudformation-samples/blob/main/hooks/python-hooks/s3-bucket-encryption/README.md",
    "typeConfiguration": {
        "properties": {
            "minBuckets": {
                "description": "Minimum number of compliant buckets",
                "type": "string"
                
            },
            "encryptionAlgorithm": {
                "description": "Encryption algorithm for SSE",
                "default": "AES256",
                "type": "string"
            }
        },
        "required": [],
        "additionalProperties": false
    },
    "required": [],
    "handlers": {
        "preCreate": {
            "targetNames": [
                "AWS::S3::Bucket"
            ],
            "permissions": []
        },
        "preUpdate": {
            "targetNames": [
                "AWS::S3::Bucket"
            ],
            "permissions": []
        }
    },
    "additionalProperties": false
}
```

### 5. 設定確認
アクティブ化が完了したら、**レジストリ→アクティブ化済みの拡張機能**から設定を確認してみる。
デフォルトでは一つも出てこないのでアレ？となるが、これはフィルターが「非公開登録」になっているためで、「アクティブ化済みのサードパーティー」に変更すると表示される。
![スクリーンショット 2022-03-10 午後8.36.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/5a4f4601-d3e9-5be9-298b-a8f11103b1ba.png)

アクティブ化され、失敗モード（FailureMode）がちゃんと「失敗（Fail）」になっていることがわかる。

### 6. 挙動確認
用意したCFnテンプレートを使ってテストする。

まずは、暗号化設定のない`hooktest-ng.yml`でスタックを作成してみる。
![スクリーンショット 2022-03-10 午後8.38.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/6e499942-f700-9f3f-f137-60f23bbd2558.png)
S3暗号化のフックで評価に失敗した旨を表示して、CREATE_FAILEDとなった。
CloudWatch Logs（`log-group-hook-test`）を見ると、以下のように詳細が記録されている。
![スクリーンショット 2022-03-10 午後8.46.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/d29aeaed-f9b1-bdea-6911-92f7829baf8e.png)

次に暗号化を有効化した`hook-test-ok.yml`でテスト。
![スクリーンショット 2022-03-10 午後9.23.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/1384c9ef-81eb-3bb3-266d-d2d84fd24379.png)
無事成功した。
CloudWatch Logsの記録は以下の通り。
![スクリーンショット 2022-03-10 午後9.29.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/94de89d0-ec97-5eb6-1851-9a2adfea7dc9.png)
余談だが、AES256では`Bucket key not enabled for bucket`とのエラーが出て上手くいかなかった。
サンプルフックのコードに何か問題があるのかも知れないが、本題から逸れるのでここではスルー。

## まとめ
フックによって、従来より強い水準での予防的統制が実現出来るようになった。
ただ、他の大多数のセキュリティ施策と同じく、やり過ぎれば開発自由度を阻害し、運用を煩雑化することに繋がりかねない点は注意が必要かも知れない。
諸刃の剣ではあるが、うまく使えばIaCにもいい感じのガードレール（自動ブレーキと言った方が近いかも）を敷くことができそうな気はする。

## おまけ
フックの開発について詳細は[こちら](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/hooks.html)。
こっちもいずれトライしてみたい。