[Google Cloud]IAM Deny policiesを試す

# はじめに
* 2022/03にGoogle Cloudで[IAM Denyポリシー](https://cloud.google.com/iam/docs/deny-overview)がPreviewで発表されました
* 本記事では、IAM Denyポリシーの設定検証やユースケース・所感を紹介します
* ここでは、IAM Denyに対してIAM権限付与を(IAM)Allowと記載します

# 機能要点
*Preview時点の内容のため今後変更になる可能性があります*
* IAM Denyは組織・フォルダ・プロジェクト単位で設定可能かつ継承される(Allowと同じ)
* リソース(GCE/GCSなど)には設定できない
* IAM Conditionsは利用可能であるが、リソースタグしか使えない(Allowで利用可能な時間などは使えない)
* 全てのPermissionが利用できるわけではない
    * 利用できるPermissionは[こちら](https://cloud.google.com/iam/docs/deny-permissions-support?hl=ja)
* IAM Role単位(`roles/xxx`)でのDenyはできない
    * Permission(`yyy.googleapis.com/zzz.get`等)単位の指定が必要
* AllowとDenyの両方が設定されている場合、Denyが優先される(継承されているものも含めて)
    * 例 ： フォルダでDeny・プロジェクトでAllowの場合、プロジェクトで該当権限を利用することはできない
* IAM Denyを設定するためには組織に拒否管理者のロール（`roles/iam.denyAdmin`）が付与されている必要がある
* フォルダやプロジェクトレベルでDenyを設定する場合でも作業者は組織に対して権限(`roles/iam.denyAdmin`)を付与しなくてはいけない
    * IAM Denyの設定を見る場合も組織レベルに`roles/iam.denyAdmin`もしくは`roles/iam.denyReviewer`が必要
    * IAM Deny関連の権限は基本ロール(`roles/owner`等)には含まれていない
* グループ・ドメインでDenyし、そのグループに所属している特定のユーザだけDenyから除外することはできる
* コンソール(画面)からは設定できない(確認もできない)
    * [CloudCLI](https://cloud.google.com/sdk/gcloud/reference/beta/iam/policies/create)(or [RESTAPI](https://cloud.google.com/iam/docs/reference/rest/v2beta/policies) or [Terraform](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_deny_policy))で設定する必要がある
* 全てのプリンシパル(User/SA)に対してDenyするためには`allUsers`を利用する
    * betaAPIの場合、`principalSet://goog/public:all`を利用する

# 機能検証
### 事前準備
* 組織に`roles/iam.denyAdmin`を付与する

### 動作検証
ここでは、特定のプロジェクトに対してGCSバケット作成権限(`storage.googleapis.com/buckets.create`)をDenyします

1. policyの作成・反映
preview(betaAPI)の場合、Principalは[こちら](https://cloud.google.com/iam/docs/principal-identifiers?hl=ja#v2)の形式を利用する必要があります
※ユーザを指定する場合、`user:USER_EMAIL_ADDRESS`ではなく`principal://goog/subject/USER_EMAIL_ADDRESS`を指定する
* policyの作成
```shell
$ cat policy.yaml
{
  "displayName": "IAM deny test",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": [
          "principal://goog/subject/USER_EMAIL_ADDRESS"
        ],
        "deniedPermissions": [
          "storage.googleapis.com/buckets.create"
        ]
      }
    }
  ]
}
```
* policyの反映
```shell
$ POLICY_ID="test-iam-deny"
$ PROJECT_ID="PROJECT_ID"
$ RESOURCE_ID="cloudresourcemanager.googleapis.com/projects/${PROJECT_ID}"
$ POLICY_FILE="./policy.yaml"
$ gcloud beta iam policies create ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies --policy-file=${POLICY_FILE}
Create in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(PROJECT_NUMBER)/denypolicies/test-iam-deny/operations/8cff10a7c279ff01].
```
* 設定確認
    * コンソールでは確認できないためgcloudコマンドを利用する
```shell
$ gcloud beta iam policies list --attachment-point=cloudresourcemanager.googleapis.com/projects/PROJECT_NUMBER --kind=denypolicies --format=json
{
  "policies": [
    {
      "createTime": "2022-09-07T09:29:12.263375Z",
      "displayName": "IAM deny test",
      "kind": "DenyPolicy",
      "name": "policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(PROJECT_NUMBER)/denypolicies/test-iam-deny",
      "uid": "f8930905-eb70-5cfa-93e3-a7c7fc721466",
      "updateTime": "2022-09-07T09:29:12.263375Z"
    }
  ]
}
```

* バケットが作成できなくなっていることを確認
```shell
# 該当プロジェクト
gcloud alpha storage buckets create gs://${BUCKET_NAME} --project=${PROJECT_ID}
Creating gs://${BUCKET_NAME}/...
ERROR: (gcloud.alpha.storage.buckets.create) HTTPError 403: USER_EMAIL_ADDRESS does not have storage.buckets.create access to the Google Cloud project.

# 他のプロジェクト(影響なし)
% gcloud alpha storage buckets create gs://${BUCKET_NAME} --project=${OTHER_PROJECT_ID}
Creating gs://${BUCKET_NAME}/...
% gcloud alpha storage ls --project=${OTHER_PROJECT_ID}
gs://${BUCKET_NAME}
```

* 設定を戻す
```shell
$ gcloud beta iam policies delete ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies
Delete in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(PROJECT_NUMBER)/denypolicies/test-iam-deny/operations/82c5ee83c279ff01].
```
* 戻っていることを確認
```shell
# policyが空{}になっていることを確認
$ gcloud beta iam policies list　--attachment-point=cloudresourcemanager.googleapis.com/projects/PROJECT_NUMBER --kind=denypolicies  --format=json
{}

# バケットも作成できるようになった
% gcloud alpha storage buckets create gs://BUCKET_NAME --project=${PROJECT_ID}
Creating gs://BUCKET_NAME/...
% gcloud alpha storage ls --project=${PROJECT_ID}
gs://BUCKET_NAME
```

### AllowとDenyが混在する場合の動作
![iamdeny.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2876448/19beb65f-8d6c-0e96-63dd-af2e735c38d8.png)

##### プロジェクトでDeny・フォルダ(or プロジェクト)でAllowの場合の動作 → Denyが優先される
* 上記[動作検証](#動作検証)に記載した動作となる

##### フォルダでDeny・プロジェクトでAllowの場合 → Denyが優先される
* フォルダにDeny設定を入れる
```shell
$ cat policy.yaml
{
  "displayName": "IAM deny test",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": [
          "principal://goog/subject/USER_EMAIL_ADDRESS"
        ],
        "deniedPermissions": [
          "storage.googleapis.com/buckets.create"
        ]
      }
    }
  ]
}
$ POLICY_ID="test-iam-deny"
$ POLICY_FILE="./policy.yaml"
$ FOLDER_ID="FOLDER_ID"
$ RESOURCE_ID="cloudresourcemanager.googleapis.com/folders/${FOLDER_ID}"                  
$
$ gcloud beta iam policies create ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies --policy-file=${POLICY_FILE}
Create in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Ffolders%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny/operations/819a97b14a79ff01].
$ gcloud beta iam policies list --attachment-point=${RESOURCE_ID} --kind=denypolicies --format=json
{
  "policies": [
    {
      "createTime": "2022-09-08T03:47:39.367551Z",
      "displayName": "IAM deny test",
      "kind": "DenyPolicy",
      "name": "policies/cloudresourcemanager.googleapis.com%2Ffolders%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny",
      "uid": "1fcbd5e0-fa13-a6f4-94c0-7847f4a92373",
      "updateTime": "2022-09-08T03:47:39.367551Z"
    }
  ]
}
```

##### プロジェクトでDenyしてリソースもしくはIAM ConditionsでAllowした場合 → Denyが優先される
* `storage.googleapis.com/buckets.list`をプロジェクトでDenyする
```shell
$ cat policy.yaml
{
  "displayName": "IAM deny test",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": [
          "principal://goog/subject/USER_EMAIL_ADDRESS"
        ],
        "deniedPermissions": [
          "storage.googleapis.com/buckets.list"
        ]
      }
    }
  ]
}
$ gcloud beta iam policies create ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies --policy-file=${POLICY_FILE}
Create in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny/operations/8657ed224a79ff01].
$ gcloud beta iam policies list --attachment-point=${RESOURCE_ID} --kind=denypolicies --format=json
{
  "policies": [
    {
      "createTime": "2022-09-08T04:08:06.362015Z",
      "displayName": "IAM deny test",
      "kind": "DenyPolicy",
      "name": "policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny",
      "uid": "899dfb65-15a1-58ee-1bc9-f8044c6d5d01",
      "updateTime": "2022-09-08T04:08:06.362015Z"
    }
  ]
}
```

* `storage.buckets.list`権限がないためアクセスできない
```shell
$ gcloud alpha storage ls
ERROR: (gcloud.alpha.storage.ls) HTTPError 403: USER_EMAIL_ADDRESS does not have storage.buckets.list access to the Google Cloud project.
```

* バケットにストレージ管理者を付与してもDenyが優先される
    * IAM Conditionsでも同様
```shell
$ gcloud alpha storage ls
ERROR: (gcloud.alpha.storage.ls) HTTPError 403: USER_EMAIL_ADDRESS does not have storage.buckets.list access to the Google Cloud project.
```

##### グループでDenyしてそのグループ内の特定メンバだけAllowする場合の動作 → ドキュメント通り特定のユーザのみ除外できる
* `storage.googleapis.com/.buckets.list`で検証
    * まずはグループ全体をDeny
```shell
$ cat policy.yaml
{
  "displayName": "IAM deny test",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": [
          "principalSet://goog/group/GROUP_EMAIL_ADDRESS
        ],
        "deniedPermissions": [
          "storage.googleapis.com/buckets.list"
        ]
      }
    }
  ]
}
$ gcloud beta iam policies create ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies --policy-file=${POLICY_FILE}
Create in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny/operations/86a6fbe1ca79ff01].
$ gcloud beta iam policies list --attachment-point=${RESOURCE_ID} --kind=denypolicies --format=json
{
  "policies": [
    {
      "createTime": "2022-09-08T05:01:58.562975Z",
      "displayName": "IAM deny test",
      "kind": "DenyPolicy",
      "name": "policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny",
      "uid": "efab961f-b758-c94b-3cad-7e0ae333e7a6",
      "updateTime": "2022-09-08T05:01:58.562975Z"
    }
  ]
}
```
* バケット一覧が見られなくなることを確認した後、グループに所属する特定のユーザだけallowする
```shell
$ cat policy.yaml
{
  "displayName": "IAM deny test",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": [
          "principalSet://goog/group/GROUP_EMAIL_ADDRESS"
        ],
        "deniedPermissions": [
          "storage.googleapis.com/buckets.list"
        ],
        "exceptionPrincipals": [
          "principal://goog/subject/USER_EMAIL_ADDRESS"
        ]
      }
    }
  ]
$ gcloud beta iam policies update ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies --policy-file=${POLICY_FILE}
Update in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(FOLDER_NUMBER)/denypolicies/test-iam-deny/operations/8b8dc80eca79ff01].
```
* バケット一覧が見えるようになることを確認
```shell
% gcloud alpha storage ls --project=${PROJECT_ID}
gs://BUCKET_NAME
```

##### `allUsers`(`principalSet://goog/public:all`)の動作 → サービスアカウント含めて全ユーザに適用されている
* 全ユーザDenyの設定を入れる
```shell
$ cat policy.yaml
{
  "displayName": "IAM deny test",
  "rules": [
    {
      "denyRule": {
        "deniedPrincipals": [
          "principalSet://goog/public:all"
        ],
        "deniedPermissions": [
          "storage.googleapis.com/buckets.list"
        ]
      }
    }
  ]
}
$ gcloud beta iam policies create ${POLICY_ID} --attachment-point=${RESOURCE_ID} --kind=denypolicies --policy-file=${POLICY_FILE}
Create in progress for denyPolicy [policies/cloudresourcemanager.googleapis.com%2Fprojects%2FXXX(PROJECT_NUMBER)/denypolicies/test-iam-deny/operations/81b6c7340a79ff01].
```

##### サービスアカウントもユーザと同様にDenyされることの確認
* 前提：サービスアカウント(`SA_NAME@PROJECT_ID.iam.gserviceaccount.com`)を作成し必要な権限は付与済み
```shell
# user
$ gcloud alpha storage ls
ERROR: (gcloud.alpha.storage.ls) HTTPError 403: USER_EMAIL_ADDRESS does not have storage.buckets.list access to the Google Cloud project.

# SA
$ gcloud alpha storage ls --impersonate-service-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com --project=PROJECT_ID
WARNING: This command is using service account impersonation. All API calls will be executed as [SA_NAME@PROJECT_ID.iam.gserviceaccount.com].
WARNING: This command is using service account impersonation. All API calls will be executed as [SA_NAME@PROJECT_ID.iam.gserviceaccount.com].
ERROR: (gcloud.alpha.storage.ls) HTTPError 403: SA_NAME@PROJECT_ID.iam.gserviceaccount.com does not have storage.buckets.list access to the Google Cloud project.
```

# IAM Deny Policieのユースケース
* [Google Cloud ドキュメント](https://cloud.google.com/iam/docs/deny-overview?hl=ja#use-cases)
* 個人的に利用価値の高いと感じている箇所
    * (VPC-SCは境界のネストができないので、)同一のVPC-SC境界内の特定のプロジェクト/リソースに対して細かい制限を行う場合
    * 特定のリソースへのアクセスを確実に制限したい場合
        * 例：機微情報を含むGCSバケットへのアクセスをDenyする
    * フォルダや組織に権限付与を行っている状態でその配下の特定プロジェクトの権限を剥奪したい場合
    * 基本ロール(もしくは`roles/XXX.admin`といった強いロール)を付与したいが、特定のPermissionだけ除外したい場合(許可したいPermissionの方が多く、カスタムロールの作成が大変な場合)

# 所感
* Allowのみでは煩雑となるケースにおいてはDenyをうまく活用することで権限管理をシンプルかつ安全に行える機能であると感じた
* IAM Denyを多用すると複雑化し、意図せず権限が拒否されてしまう恐れがあるため利用は最小限にとどめたほうがよさそう