複雑なS3BucketPolicyの設計

# 概要
ポリシー厳しめのS3バケットを作成したいときに、S3BucketPolicyを設定すると思います。
今回はS3BucketPolicyを設定時に、`DenyPolicy`かつ`Not〜`を利用すると理解に苦しむほど複雑になるのでまとめました。
また、CloudFormationを利用してBucketPolicyを作成した事例になります。

# 背景
上記ではポリシー厳しめと書きましたが、具体的にはいくつかのポリシーが必要になりました。

- セキュアな情報を格納するため、許可ではなく明示的に拒否しセキュリティレベルを高めたい
- ある特定の操作（Action_B）のみ許可するようにしたい
- あるユーザー（User_C）のみ操作を許可するようにしたい
- あるAssumeRole（RoleID_D）にスイッチした場合のみ許可するようにしたい

参考ですが、想像しやすいようにより具体的に例をあげると

- 個人情報を含むデータを格納するため、セキュリティレベルは高い必要がある
- 改ざん、閲覧は禁止のなので、PutObjectのみ許可したい、Get、List、Deleteなどは拒否したい
- 個人情報を配置する専用のユーザー（アクセスキー発行）からのみPutObjectを許可したい
- メンテンスやテストをするために、例外としてはAssumeRoleにスイッチした場合のみPutObjectを許可したい


今回は概念を説明するために、汎用的な場合で説明します。
ここで要件を満たすためにサンプルテンプレートを作成しました。

```yaml:s3_bucket_policy.yml

  S3BucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: 'Bucket_A'
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Deny'
            NotAction: 'Action_B'
            NotPrincipal: 'User_C'
            Resource: 
              - 'BucketA/'
              - 'BucketA/*'
            Condition:
              StringNotLike: 'RoleID_D'
```

# 説明
上記テンプレートを反映させた場合の挙動を説明します。
`Bucket_Aに対して、Action_B以外の操作を、User_C以外のユーザーかつ、RoleID_D以外の場合は拒否する`
ちょっと良くわからないですね。図にします。

![S3BucketPolicy_05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/1783b33d-e3bd-2f94-597d-9892a43939ed.jpeg)


Action_B以外の操作を、User_Cと、RoleID_Dは許可され（厳密には未定義）、それ以外は拒否されているのがわかると思います。
一つ一つ説明していきます。

## 初期状態
![S3BucketPolicy_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/7b6a4249-e835-3497-01d9-da6c3b873862.jpeg)
初期状態（未定義)ではBucket_Aに対してすべての操作が可能になっています
例：GetObject,ListObject,DeleteObject　等

## Effect: 'Deny'
![S3BucketPolicy_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/f92fe25b-65b9-bafc-ad46-02cdc6fb8a85.jpeg)
DenyPolicyにすることですべての操作が拒否されるようになります。
※便宜上、現段階ではAction,Principalは * として説明しています。

## NotAction: 'Action_B'
![S3BucketPolicy_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/bbc14884-24bf-78b5-0f0c-ea8fa3e2815f.jpeg)
NotAction: 'Action_B'によって、Action_B以外の操作を拒否するようになりました。
Action_Bについては未定義のため許可となっています。
例えば以下の様に書くと、更新系の拒否（参照系以外の拒否）が可能になります。

```yaml
NotAction:
 - 's3:Get*'
 - 's3:List*'
```

## NotPrincipal: 'User_C'
![S3BucketPolicy_04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/0eec4779-290f-0c95-d4ec-bfa810824b1d.jpeg)
NotPrincipal: 'User_C'によって、Action_B以外かつ、User_C以外の操作は拒否する様になりました。
User_Cについては未定義のため許可となっています。
例えば以下の様に書くと、IAMUserHoge以外の操作は拒否するようになります。（IAMUserHogeは同じテンプレート内で作成）

```yaml
NotPrincipal:
  "AWS":
    - !GetAtt IAMUserHoge.Arn
```

## Condition: StringNotLike: 'RoleID_D'
![S3BucketPolicy_05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/6222c7b9-8389-0b4e-976c-912dc9dd2ef6.jpeg)

Condition: StringNotLike: 'RoleID_D'によって、RoleID_D以外を利用した操作は拒否する様になりました。
Conditionを利用することで先程まで拒否になっていた操作に例外を作り出すことができます。
例えば以下の様に書くと、IAMRoleFuga以外の操作は拒否するようになります。

```yaml
Condition:
  StringNotLike:
    aws:userid:
      - !Sub '${IAMRoleFuga.RoleId}:*'
```


以上で当初の目的であった`Bucket_Aに対して、Action_B以外の操作を、User_C以外のユーザーかつ、RoleID_D以外の場合は拒否する`が実装できました。


## 追加作業
ここまでだと、Action_Bは誰でも操作できてしまうため、Action_Bも拒否するようにポリシーを追加します。

```yaml
          - Sid 'DenyAction_B'
          - Effect: 'Deny'
            Action: 'Action_B'
            Principal: '*'
            Resource: 
              - 'BucketA/'
              - 'BucketA/*'

```

![S3BucketPolicy_06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/2a884e66-b7a5-f7ce-9ac5-f5d91ee08360.jpeg)
より実践的なPolicyになったのではないでしょうか。

# まとめ
以上の様にして、複雑なS3BucketPolicyを設定することができます。

# 参考
参考程度ですが、今回僕が実装した最終版のテンプレートを配置しておきます。
要件がより詳細になったことで最終的にはDenyポリシーを５つ使用した大変メンテンスしづらいテンプレートができてしまいました。

- PutObjectはアクセスキーを発行した、Hogeサーバーのみ許可。社内ネットワークIP制限も実施
- PutObject以外の更新系は基本拒否
- 更新系を上記だけに絞るとCFnができなくなるので、例外としてCFｎ専用のRoleを作成し更新系（Put、Delete、、etc)を許可
- 参照は専用に作成したAssumeRoleにスイッチできるユーザーのみ許可

```yaml
  S3BucketPolicy:
    Condition: InDev
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        # 更新系に関するDeny：特定のRoleをAssumeしたユーザとHogeサーバ以外の更新系禁止
          - Effect: 'Deny'
            NotAction:
              - 's3:Get*'
              - 's3:List*'
            NotPrincipal:
              "AWS":
                - !GetAtt IAMUserHogeServer.Arn # Hogeサーバに置くトークン発行ユーザ
            Resource:
              - !Sub '${S3Bucket.Arn}'
              - !Sub '${S3Bucket.Arn}/*'
            Condition:
              StringNotLike:
                aws:userid:
                  - !Sub '${IAMRoleUser.RoleId}:*'
                  - !Sub '${IAMRoleCFn.RoleId}:*'　# CFnで更新ができるように指定
        # 更新系に関するDeny：Hogeサーバ以外のPutObjectを全て禁止
          - Sid: 'DenyPutExceptHogeServer'
            Effect: 'Deny'
            Action:
              - 's3:PutObject'
            NotPrincipal:
              "AWS":
                - !GetAtt IAMUserHogeServer.Arn #Hogeサーバに置くトークン発行ユーザ
            Resource:
              - !Sub '${S3Bucket.Arn}/*'
              - !Sub '${S3Bucket.Arn}'
        # 更新系に関するDeny：HogeサーバのPutObject以外を全て禁止
          - Sid: 'DenyExceptPut'
            Effect: 'Deny'
            NotAction:
              - 's3:PutObject'
            Principal:
              "AWS":
                - !GetAtt IAMUserHogeServer.Arn # Hogeサーバに置くトークン発行ユーザ
            Resource:
              - !Sub '${S3Bucket.Arn}/*'
              - !Sub '${S3Bucketw.Arn}'
        # 更新系に関するDeny：HogeサーバのPutObjectを社内ネットワークに限定
          - Sid: 'DenyPutOutOfServerNetwork'
            Effect: 'Deny'
            Action: 's3:PutObject'
            Principal:
              "AWS":
                - !GetAtt IAMUserHogeServer.Arn # Hogeサーバに置くトークン発行ユーザ
            Resource:
              - !Sub '${S3Bucket.Arn}'
              - !Sub '${S3Bucket.Arn}/*'
            Condition:
              NotIpAddress:
                aws:SourceIp:
                  - 'x.x.x.x' # HogeサーバのIP
        # 参照系に関するDeny：SecureRole以外からの参照系禁止
          - Sid: 'DenyGetListExceptIAMRole'
            Effect: 'Deny'
            Principal: '*'
            Action:
              - "s3:Get*"
              - "s3:List*"
            Resource:
              - !Sub '${S3Bucket.Arn}'
              - !Sub '${S3Bucket.Arn}/*'
            Condition:
              StringNotLike:
                aws:userid:
                - !Sub '${IAMRole.RoleId}:*'

```