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


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

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

はじめに

サービスを開発し始めると、外部サービスのシークレットキーや認証用のID/PASSWORDなど、秘匿しなければいけない情報を扱いたいケースが多々あります。
これらの情報はソース管理するべきではない情報であるため、環境変数等を利用して対応するケースも多いかと思います。
Cloud Functions for Firebase にも 環境変数 はありますが、シークレット情報を直接扱うことには適していません。
Cloud Functionsのドキュメント にも以下のように記載があります。

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

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

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

Cloud Key Management Service (Cloud KMS) について

Cloud KMS とは、GCPの暗号鍵管理サービスです。
詳しく知りたい方は詳細は公式ドキュメントをご覧ください。

対象読者

  • 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 を有効化します。

キーリングの作成

GCP コンソール → セキュリティ → 暗号鍵 より、キーリングを作成します。

「キーリングの名前」を入力し、作成します。

「鍵名」を入力し、作成します。

「ステータス」が 利用可能 になっていれば作成終了です。

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 トリガーとします。

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 → 詳細タブ を開きます。
サービスアカウントが記載されています。
[email protected] というサービスアカウントで実行されていることがわかります。

権限の設定

GCP コンソール → IAMと管理 を開きます。
先ほどの [email protected] というメンバーが存在するので編集ボタンを押します。
新しいロールとして、Cloud KMS → クラウド KMS 暗号鍵の復号化 を追加します。

復号化の確認

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

// ******* はプロジェクト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 にアクセスできるようになります。