VPC Service Controls保護下でCloud Functionsのデプロイをするためのルール設定

# はじめに
Cloud Functionsは、VPC Service Controlsによる保護が可能です。
ただし、デプロイに際してはやや癖があり難儀するところがありましたので、その検証を記事として残すことにしました。

# 困ったこと
Cloud Functionsのデプロイの際に許可すべきVPC Service Controlsのルールが記載されていない。

公式のドキュメントには、[Cloud Build サービス アカウントに VPC Service Controls の境界へのアクセスを許可する](https://cloud.google.com/functions/docs/securing/using-vpc-service-controls?hl=ja#grant-build-access)があります。ただこれは、AccessContextManagerによるアクセスレベルでまるごと、このサービスアカウントを許可することになり、保護してイマイチなのではと思っています。


## 環境/事前設定など
今回の検証は、以下の中で実施しました。

### リリース元
- リリース元用のGCPプロジェクト(src-prj)
     - GCPプロジェクトに対しては、デプロイ実行ユーザーがオーナー権限あり
     - GCEインスタンス
         - gcloud SDK 405.0.0


### リリース先環境
- リリース元GCEインスタンスと別境界にあるGCPプロジェクト(dst-prj)
    - GCPプロジェクトに対しては、デプロイ実行ユーザーがオーナー権限あり

### VPC Service Controls
 - リリース先GCPプロジェクト(dst-prj)のみを保護
 - すべてのAPI(2023/3/16現在139)を保護
 - アクセスレベルで、デプロイ実行ユーザーを許可
 - それ以外はデフォルト


### デプロイするアプリケーション
- go.119を利用
<details><summary>サンプルコード</summary><div>

```function.go
// Package p contains an HTTP Cloud Function.
package p

import (
        "encoding/json"
        "fmt"
        "html"
        "io"
        "log"
        "net/http"
)

// HelloWorld prints the JSON encoded "message" field in the body
// of the request or "Hello, World!" if there isn't one.
func HelloWorld(w http.ResponseWriter, r *http.Request) {
        var d struct {
                Message string `json:"message"`
        }

        if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
                switch err {
                case io.EOF:
                        fmt.Fprint(w, "Hello World!")
                        return
                default:
                        log.Printf("json.NewDecoder: %v", err)
                        http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
                        return
                }
        }

        if d.Message == "" {
                fmt.Fprint(w, "Hello World!")
                return
        }
        fmt.Fprint(w, html.EscapeString(d.Message))
}

```

```go.mod
module example.com/cloudfunction
```
</div></details>

- 以下コマンドでデプロイ
> $  gcloud functions deploy sample-app --runtime go119 --trigger-http --allow-unauthenticated --region=asia-northeast1 --vpc-connector=projects/[dst-prj]/locations/asia-northeast1/connectors/sample-vpc-conn --entry-point=HelloWorld --timeout=540s --egress-settings=all --ingress-settings=internal-only --project=[dst-prj] --build-worker-pool=projects/[dst-prjのプロジェクト番号]/locations/asia-northeast1/workerPools/sample-worker-pool

※ VPC Service Controls下でのCloud Functionsデプロイにおけるegress,ingress,プライベートプール利用などの要件があるため設定している項目などがありますが、ここでは割愛します。


## 検証

#### 検証1
まずはルール等変更せずに実行して失敗を確認
これについては、以下のようにエラーメッセージも出てきますし、冒頭の通りアクセスレベルで許可しろという話なので、納得ではあります。

> Build failed: Unable to build your function due to VPC Service Controls. The Cloud Build service account associated with this function needs an appropriate access level on the service perimeter. Please grant access to the Cloud Build service account: '[dst-prjのプロジェクト番号]@cloudbuild.gserviceaccount.com' by following the instructions at https://cloud.google.com/functions/docs/securing/using-vpc-service-controls#grant-build-access.

ただ今回は、アクセスレベルでの許可はしたくないという意図のため、個別に対応していきたいので、VPC Service Controlsで引っかかった情報を確認します。
```
{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "[dst-prjのプロジェクト番号]@cloudbuild.gserviceaccount.com",
    },
    "serviceName": "storage.googleapis.com",
    "methodName": "google.storage.objects.get",
    "resourceName": "projects/[dst-prjのプロジェクト番号]",
    "metadata": {
      "ingressViolations": [
        {
          "targetResourcePermissions": [
            "NO_PERMISSIONS"
          ],
          "targetResource": "projects/[dst-prjのプロジェクト番号]"
        }
      ],
      "violationReason": "NO_MATCHING_ACCESS_LEVEL",
    }
  },
  "severity": "ERROR",
}
```


#### 検証2 
1に基づいて、`service-[dst-prjのプロジェクト番号]@cloudbuild.gserviceaccount.com`が`google.storage.objects.gets`メソッドを呼び出すのを上り（内向き）ルールで許可

引き続き失敗
```
{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "service-[dst-prjのプロジェクト番号]@gcf-admin-robot.iam.gserviceaccount.com"
    },
    "serviceName": "storage.googleapis.com",
    "methodName": "google.storage.buckets.testIamPermissions",
    "resourceName": "projects/[dst-prjのプロジェクト番号]",
    "metadata": {
      "violationReason": "NETWORK_NOT_IN_SAME_SERVICE_PERIMETER",
      "resourceNames": [
        "projects/[dst-prjのプロジェクト番号]/[cloudfunctions.googleapis.com]"
      ],
      "ingressViolations": [
        {
          "source": "projects/[src-prjのプロジェクト番号]",
          "targetResource": "projects/[dst-prjのプロジェクト番号]",
          "targetResourcePermissions": [
            "NO_PERMISSIONS"
          ]
        }
      ],
    }
  },
  "severity": "ERROR",
}
```
`gcf-admin-robot.iam.gserviceaccount.com`については、[権限についてドキュメント](https://cloud.google.com/functions/docs/concepts/iam?hl=ja#access_control_for_service_accounts)はあるものの、今回のVPC Service Controls制限に関する記載は見当たりませんでした。ちょっと引っかかりポイントだと思います。



#### 検証3
2に基づいて、`service-[dst-prjのプロジェクト番号]@gcf-admin-robot.iam.gserviceaccount.com`が`google.storage.buckets.testIamPermissions`メソッドを呼び出すのを上り（内向き）ルールで許可

引き続き失敗  
```
{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "service-[dst-prjのプロジェクト番号]@gcf-admin-robot.iam.gserviceaccount.com"
    },
    "serviceName": "storage.googleapis.com",
    "methodName": "google.storage.objects.create",
    "resourceName": "projects/[dst-prjのプロジェクト番号]",
    "metadata": {
      "violationReason": "NETWORK_NOT_IN_SAME_SERVICE_PERIMETER",
      "resourceNames": [
        "projects/[dst-prjのプロジェクト番号]/[cloudfunctions.googleapis.com]"
      ],
      "ingressViolations": [
        {
          "targetResource": "projects/[dst-prjのプロジェクト番号]",
          "targetResourcePermissions": [
            "NO_PERMISSIONS"
          ],
          "source": "projects/[dst-prjのプロジェクト番号]"
        }
      ],
    }
  },
  "severity": "ERROR",

}
```

#### 検証4
3に基づいて、`service-[dst-prjのプロジェクト番号]@gcf-admin-robot.iam.gserviceaccount.com`が`google.storage.objects.create`メソッドを呼び出すのを上り（内向き）ルールで許可

```
{
  "protoPayload": {
    "@type": "type.googleapis.com/google.cloud.audit.AuditLog",
    "authenticationInfo": {
      "principalEmail": "service-[dst-prjのプロジェクト番号]@gcf-admin-robot.iam.gserviceaccount.com"
    },
    "serviceName": "storage.googleapis.com",
    "methodName": "google.storage.objects.create",
    "resourceName": "projects/[dst-prjのプロジェクト番号]",
    "metadata": {
      "ingressViolations": [
        {
          "source": "projects/[src-prjのプロジェクト番号]",
          "targetResourcePermissions": [
            "NO_PERMISSIONS"
          ],
          "targetResource": "projects/[dst-prjのプロジェクト番号]",
        }
      ],
      "violationReason": "NETWORK_NOT_IN_SAME_SERVICE_PERIMETER",
      "resourceNames": [
        "projects/[dst-prjのプロジェクト番号]/[cloudfunctions.googleapis.com]"
      ],
    }
  },
  "severity": "ERROR",
}
```

上記でデプロイに成功

## 結果
デプロイが出来たときのルール指定をしてみました。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2345258/d734481d-9efd-67b5-e739-1000bbb545b5.png)


上り（内向き）ルール 1
>開始:
ID:
[dst-prjのプロジェクト番号]@cloudbuild.gserviceaccount.com
ソース > すべてのソースを許可
終了:
プロジェクト =
すべてのプロジェクト
サービス =
サービス名: storage.googleapis.com
サービス メソッド:
> google.storage.objects.get

OR

上り（内向き）ルール 2
>開始:
ID:
service-[dst-prjのプロジェクト番号]@gcf-admin-robot.iam.gserviceaccount.com
ソース > すべてのソースを許可
終了:
プロジェクト =
すべてのプロジェクト
サービス =
サービス名: storage.googleapis.com
サービス メソッド:
google.storage.buckets.testIamPermissions
>google.storage.objects.create

# まとめ
VPC Service Controlsによる挙動の制限は何かと複雑なところが多く、手順やドキュメント通り進めても失敗することも少なくありません。
今回の検証結果に関しても、Cloud SDKの更新によって変わってくる可能性は十分にあります。
面倒ではありますが、定期的に見直していくことが必要かもしれません。

# 参考

[VPC Service Controls を使用する | Cloud Functions](https://cloud.google.com/functions/docs/securing/using-vpc-service-controls#grant-build-access)