GCPでのロールをsuコマンドみたいに切り替える

## TL;DR 

- Cloud Identityを有効にしてgroupで権限管理をする。
- プロジェクトは環境とサービス毎にいっぱい作る。共有VPC統一管理
- adminとviewerで権限を分ける。必要に応じてコマンドで切り替える
- 今回作ったgsuコマンドはこちら => https://github.com/koduki/gsu

## はじめに

最近、GCPというかクラウドに本格的に入門したのですがIAM便利ですね。
きめ細やかにアカウントが管理出来ますし、Cloud Identityのロールと紐付ければ個人では無くロールでアカウント管理が出来て非常に便利です。

ただし、少し不満を言えば権限のエスカレーション、つまりLinuxでいうsuコマンドのように特権アカウントの昇格への方法が無い事です。
大人数で運用するなら人毎にロールを設定して弱い権限の人と強い権限の人を分けれますが、少人数の場合は強い権限を持たざる得ません。というか規模によらず普段からProject Ownerみたいな特権アカウントで作業したくありません。オペミスが怖すぎる。。

というわけで、今回は中規模のシステムを少人数で運用するために考えた事と、それをサポートするために権限を切り替える仕組みを作ったのでそちらを紹介していきます。

## 実現したい事/前提条件

以下が前提条件です。

- 複数プロジェクトの運用
- 少人数での運用
- 共有アカウント、ダメ、絶対

常に全プロジェクトをProject Ownerで操作するという案でもこの課題は解決できますが「さいきょう」の権限での常時運用は怖すぎるので0番目の前提としてそれはしません。
あと特に共有アカウントの日常的な運用は絶対に避けたいところです。

## Cloud Identity

GCPは「プロジェクト」が最小単位なのです。ただ、それを束ねる上位概念に「組織」があります。
VPC Service Controlsとか、AzureADやその他SAMLでのSSOとか便利なセキュリティ周りの仕組みは組織を前提にしていますので、取り敢えず作ったほうが良いです。

これを使うにはG SuiteかCloud Identityを使うことになります。Cloud Identityは無料なのでGMailを独自ドメインで運用したいとかでなければこちらを使えば良いと思うのですが注意点として自分で管理しているドメインが必要です。
HTTPS周りとか含めてなんだかんだで持っておくと便利なので、個人利用だとしても 「[Google Domains](https://domains.google/)」か「[お名前.com](https://www.onamae.com/)」あたりで取っておくのが良いと思います。年間2,000円前後ですし安いのなら。

Google Domainsの場合はいくつかの設定をGCP/Cloud Identityと連携するときには省けるのでおすすめです。

## GCPに置けるプロジェクトの考え方

GCPでのプロジェクトはかなり柔軟なリソース管理の単位です。一見すると１社１プロジェクトとかで良い気がしてしまうのですが、以下の特徴があるので目的別にざくざく作るのがベストプラクティスだと思います。

- Cloud Identityによる統一的なアカウント管理
- プロジェクト間の通信でのパフォーマンスペナルティ無し
- 共有VPCによりプロジェクトを超えた一貫したネットワーク/ACLが構築可能

なので単純に権限や管理を分けたい単位で作れば良さそうです。柔軟過ぎてどう作るかが悩ましいですが、私は「環境(Prod, STG, DEV および個人情報の有無)」と「サービス」でマトリックスを作ってそれ毎にプロジェクトを作っています。
で、環境毎に共有VPCを作って単一のホストプロジェクトで集中管理させています。

- hosted-network-prj
- prd-std-blog-prj
- prd-sec-payment-prj
- ...

こうする事でサービスや環境にアクセスする人およびその権限をシンプルに管理出来ますし、情報漏洩など重大な問題に関わるネットワーク周りの設定を多くのメンバーに解放する必要がありません。

正直、数名での運用になるので少し過剰かとも考えたのですが、Cloud Identityと共有VPCがあればさほど管理が複雑では無さそうだと感じたので、スケール性と小規模時の運用コストのバランスが取れそうだったのでこの形式にしました。後から変えれ良いだけですが後から変えるパワーが要るところですし。

## Google Groupによる権限管理

GCPの権限管理は色々ありますが、オペレータの権限としてCloud IAMでGoogleアカウントに直接権限を付与するのは、管理が煩雑になるのでお勧めできません。

カスタムロールを使うことも出来ますが、Cloud Identity/G Suiteの機能であるGoogle Groupを作ってそのメールアドレスを各プロジェクトのIAMで必要な権限を付与することが出来ます。

こうする事でProject Ownerの権限も含めて、単にGroupへの所属の有無でロール管理ができるので運用がシンプルになります。

Groupは下記のページから　管理できます。
https://admin.google.com/ac/groups

これによってOwner権限が欲しい時はOwnerグループに入れば良いだけなので、共有アカウントを運用する必要は無くなります。

Groupはプロジェクト毎にRead Onlyの`-viewer`とOwner権限の`-admin`を作成して通常には`-viewer`、構築や設定変更を行う時は一時的に例えば`group-prd-std-blog-admin`に所属する、と行った運用ににしています。
これによって「普段は低めに運用して必要な時だけエスカレーション」という運用を実践できます。

現時点では`-admin`はProject Ownerですが、将来的にはIAM管理権限を分離してProject Ownnerをなくす形での運用を考えています。


## gsuによる切り替え

さて、ようやく本題なのですがこのオペレーションを支援するためにgsuコマンドを作成しました。

というのもGCPの標準機能だけでは都度Admin Consoleに入って権限を付与する必要があるからです。これは日常的な作業としては厳しいです。これを解決するためにDirecotry APIを裏で叩いて権限の昇格を行えるスクリプトがgsuです。
https://github.com/koduki/gsu

使い方は簡単です。以下のように対象ユーザとグループを指定して、`attach`で権限の付与を行い不要になれば`detach`でグループから抜けます。
attachのタイミングでは`-admin`のグループからは一旦全部外すようにしてあり、特権アカウントは同時に複数持てないようにしています。

```bash
$ gsu attach {user_name} {group_name}
$ gsu detach {user_name} {group_name}
```

また、以下のように`gsu -l`を使うことで自分に紐づいてる権限を取得できます。

```bash
$ gsu -l {user_name}
```
## gsuの実装方法

ソースコードは[こちら](https://github.com/koduki/gsu/tree/master/api)にあります。
主なロジックとしてはAdminDirectoryをラッピングしたGCPAdminAPIです。

```ruby
class GCPAdminAPI
    def initialize
        scope = [ 
            'https://www.googleapis.com/auth/admin.directory.group.readonly', 
            'https://www.googleapis.com/auth/admin.directory.group.member', 
            'https://www.googleapis.com/auth/admin.directory.user.readonly' 
        ]

        ENV['GOOGLE_APPLICATION_CREDENTIALS'] = ENV['SA_KEY'] if ENV['SA_KEY']

        @domain = ENV['GCP_DOMAIN']
        @service = Google::Apis::AdminDirectoryV1::DirectoryService.new
        authorization = Google::Auth.get_application_default(scope).dup
        authorization.sub = ENV["GCP_ADMIN_USER"]
        @service.authorization = authorization    
    end

    def attach_group(user_name, group_name)
        member = @service.list_users(domain: @domain, query: "email:" + user_name).users.first

        clear_privileges(user_name)
        @service.insert_member(group_name, member)
    end

    def detach_group(user_name, group_name)
        @service.delete_member(group_name, user_name)
    end

    def list_groups_all()
        @service.list_groups(domain: @domain).groups.map{|g| g.email}
    end

    def list_groups(user_name)
        @service.list_groups(domain: @domain, user_key: user_name)
                .groups
                .map{|g| g.email}
    end

    def clear_privileges(user_name)
        admins = @service.list_groups(domain: @domain, user_key: user_name).groups
            .find_all{|x| x.email.include?("-admin@")}
        admins.each{|g| @service.delete_member(g.email, user_name) }
    end
end
```

割と見たままだと思うので中身に関してはあまり説明はしないですが、グループへの追加や削除をラッピングした処理になります。こちらをsinatraでラップしてCloud Runにデプロイします。
外部に公開する意味は無いので、Cloud Runの公開設定は非公開を選びます。そのためトークンを渡してやるなどをして認証する必要があります。

また、コマンドライン側は以下の通りシンプルにcurlでAPI呼び出しを行なっています。プロトタイプなので今はエラー処理等は無い状態です。

```bash
API_URL=$(cat ${HOME}/.gsu_config|awk '{print $2}')
TOKEN="Authorization: Bearer $(gcloud auth print-identity-token)"
CONTENT_TYPE="Content-Type: application/json"

if [ "$1" = "attach" ]; then
    curl -d {} -H "${TOKEN}" -H "${CONTENT_TYPE}" -X POST ${API_URL}/attach/$2/$3
elif [ "$1" = "dettach" ]; then
    curl -d {} -H "${TOKEN}" -H "${CONTENT_TYPE}"-X POST ${API_URL}/detach/$2/$3
elif [ "$1" = "-la" ]; then
    curl -H "${TOKEN}" -H "${CONTENT_TYPE}" -X GET ${API_URL}/groups
elif [ "$1" = "-l" ]; then
    curl -H "${TOKEN}" -H "${CONTENT_TYPE}" -X GET ${API_URL}/groups/$2
else
    echo "gsu: Switch GCP user role."
    echo "usage:"
    echo "    gsu attach {user_name} {group_name}"
    echo "    gsu detach {user_name} {group_name}"
    echo "    gsu -la"
    echo "    gsu -l {user_name}"
fi
```

gcloudコマンドからtokenを取得しているので事前にインストールする必要があります。

## サービスアカウントへの権限委譲

少し面倒なのがサービスアカウントへの権限委譲です。AdminDrectoryを含む[Admin SDK](https://developers.google.com/admin-sdk)はGCPの外側の設定なので、GCP上でサービスアカウントを作成した上で、以下の設定が必要です。

まず[Adomin Console](https://admin.google.com/)にログインします。
続いて、「セキュリティ」 -> 「詳細設定」 -> 「認証」 -> 「API クライアント アクセスを管理する」を選んで「APIスコープ」に`https://www.googleapis.com/auth/admin.directory.group.readonly,https://www.googleapis.com/auth/admin.directory.group.member,https://www.googleapis.com/auth/admin.directory.user.readonly`を追加します。

その後、サービスアカウントの秘密鍵をJSONでダウンロードしGOOGLE_APPLICATION_CREDENTIALSに指定して権限をアプリに付与します。

この辺りは以下を読むと分かりやすいです。

- [Perform G Suite Domain-Wide Delegation of Authority](https://developers.google.com/admin-sdk/directory/v1/guides/delegation)
- [google-api-client Webブラウザをなしで認証](https://qiita.com/eleven_2012/items/6f8a246ad462f960f6ca)

## サービスアカウントとCloud Run

さて上記の設定でローカルでは問題無く動くのですがCloudRunへのデプロイ時に少し罠がありました。
通常、GCPの本番環境で動かすときは`GOOGLE_APPLICATION_CREDENTIALS`に秘密鍵を指定するのでは無く、サービスアカウントをデフォルトの実行ユーザとしておく事で対応するかと思います。
ライブラリ側で`http://metadata.google.internal`にアクセスして情報を取得してごにょごにょしてくれて上手く繋がるアレです。

しかし、厳密にはAdmin SDKはIAM範疇では無いせいなのか`GOOGLE_APPLICATION_CREDENTIALS`を使う分には問題ないのですが、デフォルトサービスアカウントでは適切に動作しませんでした。私の設定ミスなのか仕様なのか分からないのですがとりあえずそうらしい。

なので、ちょっと変則的ですが[Berglas](https://github.com/GoogleCloudPlatform/berglas)を使ってgsu実行用のサービスアカウントの秘密鍵を暗号化してGCSに保存し、自分自身の秘密鍵をberglas使って取り出してGOOGLE_APPLICATION_CREDENTIALSにセットするというハックをしています。
BerglasはGoogle謹製の暗号化ツールでk8sのシークレットやHashiCorpのValtみたいなもんです。バックエンドとしてCloudKMSとGCSを使うのでサーバレスで運用できます。

細かな使い方は以下が詳しいです。

- [Berglas - Cloud Run - Ruby](https://github.com/GoogleCloudPlatform/berglas/tree/master/examples/cloudrun/ruby)
- [[GCP] berglasを使って機密情報を安全にアプリケーションに受け渡す](https://dev.classmethod.jp/cloud/gcp-berglas/)

### 秘密鍵の登録

以下の手順で秘密鍵の登録ができます。なお、Berglas自体もGCSバケット作ったりKMS設定したりとGCPの権限が必要なので、ローカルで作業するときはBerglas向けにサービスアカウント作って権限を付与しておくのが良いと思います。

この例では`sa-key`という名前で秘密鍵を暗号化して保存しています。また、指定したサービスアカウントに読み取り権限を付与しています。

```bash
export PROJECT_ID=${YOUR_PROJECT_ID}
export BUCKET_ID=${YOUR_BUCKET_ID}
export KMS_KEY=projects/${PROJECT_ID}/locations/global/keyRings/berglas/cryptoKeys/berglas-key
export SA=${YOUR_SEARVICE_ACCOUNT}

$ gcloud services enable --project ${PROJECT_ID} \
  cloudkms.googleapis.com \
  storage-api.googleapis.com \
  storage-component.googleapis.com

$ berglas bootstrap --project $PROJECT_ID --bucket $BUCKET_ID
$ berglas create ${BUCKET_ID}/sa-key "$(cat secreat.json)"  --key ${KMS_KEY}
$ berglas grant ${BUCKET_ID}/sa-key --member serviceAccount:${SA}
```

### Cloud Run向けDockerfileの設定

暗号化したファイルをメモリに値として取得する方法と、一時ファイルとして保存してそのパスを取得する方法があるようです。
今回は、`GOOGLE_APPLICATION_CREDENTIALS`に保存したいので一時ファイルにします。

```dockerfile
FROM ruby

RUN gem install google-api-client googleauth sinatra
COPY --from=gcr.io/berglas/berglas:latest /bin/berglas /bin/berglas

ADD ./ /app
WORKDIR /app

CMD ["/bin/berglas", "exec", "ruby", "app.rb"]
```

実行時に`berglas`でフックする事で複合周りのごにょごにょを自動でしてくれるようです。そのトリガーになるのは環境変数です。

```
SA_KEY=berglas://${BUCKET_ID}/sa-key?destination=tempfile
```

`berglas://`で始まる環境変数の値をトリガーにアクションがされます。この場合はsa-keyを複合してtempファイルに保存しSA_KEYという環境変数にその値を渡します。なのでRuby側では単純に以下のように環境変数を読むだけです。

```ruby
ENV['GOOGLE_APPLICATION_CREDENTIALS'] = ENV['SA_KEY'] if ENV['SA_KEY']
```

ライブラリなどが不要なのでPG本体のコードはほぼ書き換えなくて良いのが良いですね！

### Cloud Runへのデプロイ

さて、いよいよCloud Runへのデプロイです。

```bash
export PROJECT_ID=${YOUR_PROJECT_ID}
export GCP_ADMIN_USER=${YOUR_ADMIN_MAIL}
export GCP_DOMAIN=${YOUR_ADMIN_MAIL}
export BUCKET_ID=${YOUR_BUCKET_ID}

$ gcloud builds submit --tag gcr.io/${PROJECT_ID}/gsu .
$ gcloud run deploy \
    --image gcr.io/${PROJECT_ID}/gsu \
    --set-env-vars "GCP_ADMIN_USER=${GCP_ADMIN_USER},GCP_DOMAIN=${GCP_DOMAIN},SA_KEY=berglas://${BUCKET_ID}/sa-key?destination=tempfile" \
    --platform=managed --region us-east1 \
    --service-account gsu-455@${PROJECT_ID}.iam.gserviceaccount.com 
```

`service-account`を指定するのを忘れないでください。あと、GCP_ADMIN_USERとGCP_DOMAINはCloudIdentityの管理者メールアドレスとドメインを指定しておけば一旦大丈夫です。

最後にデプロイされたURLでローカルの`~/.gsu_config`を変更します。

```bash
$ cat ~/.gsu_config
URL: http://localhost:8080
```

こちらを修正すればgsuコマンドの向け先が変わります。


## まとめ

あんまり類似のツールや運用を見つけれなかったのでクラウド流のベストプラクティスは他にあるのかもですが、コンテキストによって権限を変えたいときは多いと思います。
GCPの機能としてもロケーションやアクセス時間帯で権限がコントロールできますが、こういった任意のタイミングで出来るのも利便性は高いかと。

今後はCLI側を真面目に作り込むのと、今は誰でも好きな権限になれてしまうのでその辺の対策や24時間で自動的に一度特権が外される機能とかを実装していきたいと思います。

それにしても個人で使ってた時と違ってお仕事で使う場合はIAM管理の便利さが身に染みます。
オンプレでもこういうの欲しい。。。

それではHappy Hacking!

## 参考

- [中規模プロジェクトでの GCP権限管理(アクセス制御)ベストプラクティス](https://qiita.com/kusuwada/items/f2acb7288070e0c0e724)
- [エンタープライズ企業のベスト プラクティス](https://cloud.google.com/docs/enterprise/best-practices-for-enterprise-organizations?hl=ja)
- [GCP の IAM をおさらいしよう](https://medium.com/google-cloud-jp/gcp-iam-beginner-b2e1ef7ad9c2)
- [Admin SDK - Ruby](https://developers.google.com/admin-sdk/directory/v1/quickstart/ruby)