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


この記事は Firebase Advent Calendar 2020 10日目の記事です。

先日 Cloud Functions for Firebase + Cloud KMS でシークレット情報を扱う という記事を書きました。
今回は、今年 GA になった Secret Manager を使ってみたいと思います。

はじめに

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

また、先日記載した Cloud KMS を使う方法には、いくつか難点がありました。

  • Cloud KMS は鍵の管理のため、暗号化/復号化処理を自分で実装しないといけないこと。
  • 鍵のローテーションに応じて古いバージョンを廃止する場合、再度暗号化が必要なこと。

Secret Manager では、この辺を含めて自動でやってくれる範囲が広く、エンジニアからするとより楽にシークレット情報を取り扱うことができます。

この記事では、Secret Manger を使ってシークレット情報を Cloud Functions for Firebase から取り扱う方法を記載します。

Secret Manager について

Secret Manager とは、GCP で提供されているサービスの一つで、シークレット情報を管理するための安全で便利なストレージシステムです。
Cloud Functionsのドキュメント によると、シークレット情報の取り扱いのベストプラクティスとして Secret Manager が紹介されています。

対象読者

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

Secret Manager の設定

お試しで実践される方は、新規GCPプロジェクトを作成して頂ければと思います。

APIの有効化

Secret Manager を使うためには、API を有効化する必要があります。
GCP コンソール → APIとサービス より、Secret Manager API を有効化します。

シークレットの作成

実際にシークレットを作成してみましょう。
GCP コンソール → セキュリティ → シークレットマネージャー よりシークレットを作成します。

以下を入力して、シークレットを作成 しましょう。
- シークレットの名前
- シークレットの値 (ファイルを直接インポートすることも可能です。)
- リージョン (必要に応じてリージョンを設定してください。)

ステータスが 有効 になっていれば完了です。

Cloud Functions for Firebase で復号化処理の作成

必要なモジュールのインストール

Functions 用の環境がない方は、firebase init 等で事前に環境を生成してください。
Secret Manager を扱うために必要なモジュールをインストールします。

npm i @google-cloud/secret-manager

Secret Manager の情報を環境変数に設定します。

// プロジェクトIDはシークレットを作成したGCPのプロジェクトIDを設定します。
$ firebase functions:config:set project.id="YOUR_PROJECT_ID"

// シークレットの作成の際に入力した名前を設定します。
$ firebase functions:config:set secret.name="secret-sample"

// 作成したシークレットのバージョンを設定します。
$ firebase functions:config:set secret.version="1"

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

$ firebase functions:config:get
{
  "secret": {
    "name": "secret-sample",
    "version": "1"
  },
  "project": {
    "id": "YOUR_PROJECT_ID"
  }
}

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

Node.js
import * as functions from 'firebase-functions';
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

export const helloWorld = functions
    .region("asia-northeast1")
    .https
    .onRequest(async (request, response) => {

        // 環境変数から設定した情報を取得します。
        const projectId = functions.config().project.id;
        const secretName = functions.config().secret.name;
        const secretVersion = functions.config().secret.version;

        // 復号化します。
        const client = new SecretManagerServiceClient();
        const name = client.secretVersionPath(projectId, secretName, secretVersion);
        const [version] = await client.accessSecretVersion({
            name: name,
        });
        const secretValue = version.payload?.data?.toString();

        response.send(`Hello World : ${secretValue}`);
    });

デプロイして動作を確認してみましょう。

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

// 動作確認
$ curl https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/helloWorld
Error: could not handle the request

Functions のログをみてみると以下のようにエラーが出ているのが確認できます。
Cloud KMS の時と同様に、IAM で権限が必要なようです。

Error: 7 PERMISSION_DENIED: Permission 'secretmanager.versions.access' denied for resource 'projects/YOUR_PROJECT_ID/secrets/secret-sample/versions/1'

権限の確認と設定

Functions を実行しているサービスアカウントの確認

GCPコンソール → Cloud Functions → helloWorld → 詳細タブ を開きます。
サービスアカウントが記載されています。
[email protected] というサービスアカウントで実行されていることがわかります。

権限の設定

上記で確認したサービスアカウントに、権限を設定します。
GCP コンソール → セキュリティ → シークレットマネージャー より、作成したシークレットにチェックを入れ、メンバーを追加します。

以下を設定し、保存します。
- 新しいメンバーに Functions を実行しているサービスアカウント を入力する
- ロールに Secret Manager のシークレットアクセサー を設定する

シークレット情報へのアクセスの確認

権限設定ができたので、再度確認してみましょう。

// 動作確認
$ curl https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/helloWorld
Hello World : some secret value

できました!

まとめ

Secret Manager を利用して、Cloud Functions for Firebase からシークレット情報にアクセスする一例を書きました。
実際に運用する際には、まだまだ留意する点があるとは思いますが、Cloud KMS よりも簡単に扱えるのが感じられるかと思います。