Cloud Functions for Firebase + Cloud KMS でシークレット情報を扱う

開発時に後回しになりがちなシークレット情報を Cloud Functions for Firebase + Cloud KMS を利用して取り扱う方法を記載します。

--
(2020/12/10追記)
別途 [Cloud Functions for Firebase + Secret Manager でシークレット情報を扱う](https://qiita.com/hirok115/items/3366b2a60ff14f81ac3c) という記事を書きました！
現在は Secret Manager を使った方が主流だと思いますので、合わせてご覧ください。


## はじめに
サービスを開発し始めると、外部サービスのシークレットキーや認証用のID/PASSWORDなど、秘匿しなければいけない情報を扱いたいケースが多々あります。
これらの情報はソース管理するべきではない情報であるため、環境変数等を利用して対応するケースも多いかと思います。
Cloud Functions for Firebase にも [環境変数](https://firebase.google.com/docs/functions/config-env?hl=ja) はありますが、シークレット情報を直接扱うことには適していません。
[Cloud Functionsのドキュメント](https://cloud.google.com/functions/docs/env-var?hl=ja) にも以下のように記載があります。

> シークレットの管理
環境変数は関数の構成に使用できますが、データベースの認証情報や API キーなどの機密情報の格納には適しません。 このような機密性の高い値は、ソースコードや外部の環境変数以外の場所に保存する必要があります。一部の実行環境やフレームワークでは、環境変数の内容がログに送信されることがあります。YAML ファイル、デプロイ スクリプト、ソース管理に重要な認証情報は保存しないでください。

>シークレットを保存する場合は、シークレット管理のベスト プラクティスを確認することをおすすめします。Cloud KMS と Cloud Functions の固有の統合はありません。

この記事では、Cloud KMS を使ってシークレット情報の暗号化/復号化を行う方法を記載します。

### Cloud Key Management Service (Cloud KMS) について
[Cloud KMS](https://cloud.google.com/security-key-management) とは、GCPの暗号鍵管理サービスです。
詳しく知りたい方は詳細は[公式ドキュメント](https://cloud.google.com/kms/docs)をご覧ください。


## 対象読者
- Cloud Functions for Firebase を利用したことがある方
- GCP の扱いに少しでも慣れている方  
GCP コンソール の細かい操作方法は省略している箇所がありますので、GCPを触ったことない方は分かりにくいかもしれません。

## 大まかなステップ
1. Cloud KMS の設定
2. Cloud KMS で暗号化する
3. 暗号化した情報を Cloud Functions for Firebase の環境変数に登録する
4. Functions で暗号化された環境変数を復号化する


## Cloud KMS の設定
実践してみたい方はGCPの新規プロジェクトを作成して試してみることをお勧めします。

### API の有効化
GCP コンソール → APIとサービス より、**Cloud Key Management Service (KMS) API** を有効化します。
![APIの有効化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/71a05f25-bca8-1b81-54e2-755f40dcdcce.png)

### キーリングの作成
GCP コンソール → セキュリティ → 暗号鍵 より、キーリングを作成します。
![セキュリティ - 暗号鍵](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/4b6ecc87-081a-9676-ee64-3c0be35d0300.png)


「キーリングの名前」を入力し、作成します。
![キーリングの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/63146663-7c79-9c5a-57ed-a6dc00bb5134.jpeg)

「鍵名」を入力し、作成します。
![鍵を作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/d15c0c35-413a-674b-2f35-0017fd2de6db.jpeg)

「ステータス」が **利用可能** になっていれば作成終了です。
![ステータス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/6c726da0-0060-fdfd-9f1f-236e3288e334.jpeg)


## Cloud KMS で暗号化する
gcloud sdkを使って暗号化します。
事前にgcloudの対象プロジェクトを切り替えておいてください。
先ほど作成した キーリング/鍵 を使って暗号化します。

```
// 暗号化したいワード → some secret key とします。
$ echo -n "some secret key" | gcloud kms encrypt \
   --location=asia-northeast1  \
   --keyring=hoge-ring \
   --key=hoge-key \
   --plaintext-file=- \
   --ciphertext-file=- | base64

CiQAkW+QvrLG2eA8WdcjdnA5LgND0wAdh3YDwnN4jkNaRjDz3RkSNwDx02zmdnf9DDGODE9tC4bkUg0M28wkMFY6LSru/Z+uMi/tavKCii1dzmbPKQV5ZsEIdaG8hSQ=
```

## Cloud Functions for Firebase で復号化処理の作成
Functions 用の環境がない方は、`firebase init` 等で事前に環境を生成してください。
Cloud KMS を使うのに必要なモジュールをインポートします。

```
npm install @google-cloud/kms
```

Cloud KMSの情報と先ほど暗号化したものを環境変数に設定します。

```
$ firebase functions:config:set project.id="YOUR_PROJECT_ID"
$ firebase functions:config:set kms.keyring="hoge-ring"
$ firebase functions:config:set kms.key="hoge-key"
$ firebase functions:config:set kms.location="asia-northeast1"
$ firebase functions:config:set secret.encryptedData="CiQAkW+QvrLG2eA8WdcjdnA5LgND0wAdh3YDwnN4jkNaRjDz3RkSNwDx02zmdnf9DDGODE9tC4bkUg0M28wkMFY6LSru/Z+uMi/tavKCii1dzmbPKQV5ZsEIdaG8hSQ="
```

念のため設定した内容を確認します。

```
$ firebase functions:config:get
{
  "kms": {
    "keyring": "hoge-ring",
    "key": "hoge-key",
    "location": "asia-northeast1"
  },
  "secret": {
    "encryptedData": "CiQAkW+QvrLG2eA8WdcjdnA5LgND0wAdh3YDwnN4jkNaRjDz3RkSNwDx02zmdnf9DDGODE9tC4bkUg0M28wkMFY6LSru/Z+uMi/tavKCii1dzmbPKQV5ZsEIdaG8hSQ="
  },
  "project": {
    "id": "YOUR_PROJECT_ID"
  }
```

実際の処理を書いていきましょう。
今回は HTTP トリガーとします。

```TypeScript
import * as functions from 'firebase-functions';
import { KeyManagementServiceClient } from '@google-cloud/kms';

export const helloWorld = functions
    .region("asia-northeast1")
    .https
    .onRequest(async (request, response) => {
        // プロジェクトの情報を環境変数から取得します。
        const projectId = functions.config().project.id;

        // 暗号化されたデータを環境変数から取得します。
        const encryptedData = functions.config().secret.encryptedData;

        // KMS の情報を環境変数から取得します。
        const keyring = functions.config().kms.keyring;
        const key = functions.config().kms.key;
        const location = functions.config().kms.location;

        // 復号化します。
        const client = new KeyManagementServiceClient();
        const keyName = client.cryptoKeyPath(projectId, location, keyring, key);
        const [result] = await client.decrypt({name: keyName, ciphertext: encryptedData,});
        const decryptedData = result.plaintext?.toString();

        // decryptedData を出力するのはセキュアではないですが、今回は検証のため出力しています。
        response.send(`Hello World : ${decryptedData}`);
    });
```

ではデプロイして動作を見てみましょう。

```
// デプロイ
$ firebase deploy --only functions
...

// 動作確認
// ******* はプロジェクトID
$ curl https://asia-northeast1-*******.cloudfunctions.net/helloWorld
Error: could not handle the request
```

何やらエラーがでました。
Functions のログをみてみると以下のようにエラーが出ているのが確認できます。

```
// ******* はプロジェクトID
PERMISSION_DENIED: Permission 'cloudkms.cryptoKeyVersions.useToDecrypt' denied on resource 'projects/*******/locations/asia-northeast1/keyRings/hoge-ring/cryptoKeys/hoge-key'
```

Functions から KMS にアクセスするには、権限が必要なようです。
Functions を動かしているサービスアカウントに復号化の権限を設定します。

## 権限の確認と設定

### Functions を実行しているサービスアカウントの確認
GCPコンソール → Cloud Functions → helloWorld → 詳細タブ を開きます。
サービスアカウントが記載されています。
`YOUR_PROJECT_ID@appspot.gserviceaccount.com` というサービスアカウントで実行されていることがわかります。
![起動サービスアカウントの確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/d4c585bd-a0d4-53dd-5ee0-ce11c342e582.jpeg)

### 権限の設定
GCP コンソール → IAMと管理 を開きます。
先ほどの `YOUR_PROJECT_ID@appspot.gserviceaccount.com` というメンバーが存在するので編集ボタンを押します。
新しいロールとして、Cloud KMS → クラウド KMS 暗号鍵の復号化 を追加します。
![ロールの追加](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/a392ee1a-a37b-aaef-bb88-05c5d8122040.png)


## 復号化の確認
権限の設定ができたので、復号化の確認をします。

```
// ******* はプロジェクトID
$ curl https://asia-northeast1-*******.cloudfunctions.net/helloWorld
Hello World : some secret key
```

できました！

## まとめ
- シークレット情報を利用する際は Cloud KMS を利用して暗号化/復号化を行う。
- Functions から Cloud KMS にアクセスする際は権限をつける。  
今回は Functions 内で復号化しかしていませんが、暗号化する際は暗号化の権限が必要です。
- シークレットでない環境情報は `firebase functions:config:set` で設定する。

## 補足
Cloud Functions を使えば特定の関数のみ復号化の権限をつけることもできます。
Cloud Functions にはサービスアカウントの設定がありますので、復号化権限を持つサービスアカウントを別途作成し、該当の関数にサービスアカウントを指定することで特定の関数のみ Cloud KMS にアクセスできるようになります。