この記事はソニックガーデン プログラマ アドベントカレンダーの4日目の記事です。
ソニックガーデンといえば、RailsですがFirebaseでのウェブアプリケーション開発も積極的に採用しています。
お手軽にインフラ構築ができることに定評があるFirebaseですが、お客様の要望によってはデフォルトの構成では足りず、よりセキュアな環境を提供する必要が出てくる時もあります。
今回はその中でもCloud FunctionsにWAFやSSLポリシーを設定するところを説明します。
Cloud Functions for Firebaseをおさらい
個人開発で人気の高いFirebaseですが、その理由はシンプルにさまざまな機能開発を行えるところにあると思います。
Cloud Functions for Firebaseもその一つで、サーバー上に構築するべき処理をシンプルな関数の形で構築することができます。
Cloud Functions for Firebaseでは、用途に合わせていくつかのパターンで作成が可能です。
その中でもHttpsのリクエストを受け付ける場合は、2種類用意されています。
1つ目は、onRequestというもので、これは一般的なHttpsを受け取る関数を構築するもので、Node.jsのHttpサーバーであるExpressを使った開発が行えます。
2つ目は、onCallというもので、Firebaseで構築されたアプリケーションはこちらを使う場合が多いでしょう。
onRequestとは違い、一定のルールを設けて仕様を一般化し、Firebaseのアプリケーションから呼び出しやすい形でサーバーサイドの処理をかける仕組みになります。
Cloud Functions for Firebaseだけではできないこと
onReqeust/onCallともに、すぐにHttpsで受け取るサーバー処理を構築できますが、タイトルの通り、WAFを設定したり、TLS1.2以上の通信に絞るなどの対応ができません。
個人開発でここまでしないということもあるかと思いますが、企業向けに作るサービスでは、セキュリティ要件としてかなりよく出る項目ではないでしょうか?
現状ではFirebaseだけの設定では、どうすることもできないので、Google Cloudの機能も使ってこの要件を答えられるようにしましょう!
Cloud Load Balancingを使って解決
実はこれだけです。
Cloud Load BalancingはGoogle Cloudの負荷分散を行うサービスです。
クライアントとCloud Functionsの間に配置します。
この構成にすることでCloud Load Balancingの設定で、Google CloudのWAFサービスにあたるCloud Armorを設定したり、SSLポリシーと呼ばれるSSL/TLSのバージョンの有効設定や暗号化ルールを設定する仕組みを導入することができます。
Firebaseだけを使ってGoogle Cloudはよくわからない!って人も少なくないと思いますので、この設定を詳しく説明していきます。
サンプル用のCloud Functionsを構築
設定を行うためにまずはCloud Functionsを構築しましょう。
この記事に辿り着いた人は、だいたいわかっていると思いますし、公式を見ればすぐにわかると思いますので、ここでは本当にシンプルな例でやります。
export const convertToUpperCase = onCall(
{
region: 'asia-northeast1',
},
(request) => {
const { message } = request.data;
return { message: message.toUpperCase() };
},
);
ここでは、onCallを利用します。
理由はのちほどでてきますが、onCallの場合はLoad Balancing経由のアクセスするための設定の方法が変わっているので、そこの説明のためにもonCallを選択しています。
この後のCloud Load Balancingの構築に繋がるので、先にデプロイまで済ませておいてください。
Cloud Load Balancingを構築
通常Firebaseだけを利用する場合はFirebaseコンソールのみで設定等は完結すると思いますが、今回はGoogle Cloudコンソールも利用します。以下のURLから飛べます。
開くとヘッダー部分は以下のようになっていると思います。
灰色で隠している部分がプロジェクト選択になるので、対象のプロジェクトを選択します。
その横にあるテキストボックスにLoad Balancing
と書くと、以下のような選択肢が出るので、そこを選んで移動します。
移動すると、エラーメッセージが出ることがありますが、気にせず進むと、Compute Engine API
の画面がでて有効化するように促されますので、そのまま有効化してください。
それが終わると以下のような画面が出るので、ロードバランサの追加から追加していきます。
そのあとは、以下のように選択して進めてください。
- "ロードバランサのタイプ"を
アプリケーション ロードバランサ(HTTP / HTTPS)
- "インターネット接続または内部"を
インターネット接続(外部)
- "グローバルまたはシングル リージョンのデプロイ"を
グローバル ワークロードに最適
- "ロードバランサの世代"を
グローバル外部アプリケーション ロードバランサ
- "構成"を押して完了
ここでは、グローバルなロードバランサを指定していますが、のちに出てくるSSL証明書をマネージドロードバランサを使うための選択になります。
ここまでくると以下のような画面に飛んでると思います。
続けて設定していきます。
- 左上のロードバランサの名前を決めます、仮に"fuctions"とします
- フロントエンドの構成で、新しいフロントエンドのIPとポートを書いていきます
- 名前 functions
- プロトコル HTTPS
- 新しい証明書を作成
- 名前 functions-ssl
- 作成モード Google マネージドの証明書を作成する
- ドメイン 自分でAレコードを設定できるドメイン
- 完了
- バックエンドの構成を選択
- バックエンドサービスの作成
- 名前 functions
- バックエンドタイプ
サーバーレス ネットワーク エンドポイント グループ
- サーバーレス ネットワーク エンドポイント グループの作成
- 名前 functions
- リージョン asia-northeast1(東京)
- サーバーレス ネットワーク エンドポイント グループの種類 Cloud Run
- URL マスクのみを使用する
- URL mask 先ほどのドメイン/
- "Cloud CDN を有効にする"のチェックを外す
- Cloud ArmorバックエンドセキュリティポリシーをそのままにしておくとWAFの設定が有効かされる
- 作成を押す
- 左下の作成を押す
- バックエンドサービスの作成
これでロードバランサ自体の作成は終わりです。
証明書とともにドメインを経由でアクセスできるように自身のDNSの設定で作成された固定IPをAレコードで割り当ててください。
設定の中で、URLマスクというところが少し特殊なので説明します。
サーバーレス ネットワーク エンドポイント グループを利用すると、Cloud RunやCloud Functionsなどをバックエンドとして指定できるのですが、URLマスクを使わない場合、すべてのFunctionsをそれぞれ1つずつ設定していく必要があります。
Functionsはいくつもの関数を作成することが多いと思いますので、それだと設定変更にコストがかかり過ぎてしまいます。
ここで役立つのがURLマスクで、これを使うとCloud Runの名前等を使ってルーティングルールを作ってくれます。
今回の設定では、ドメインの後ろにCloud Runの名前がくる設定になっているので、onCallの呼び出し方にあったURLが作成できます!
あとタイトルにも書いているSSLポリシーは、ロードバランサの設定画面の中にSSLポリシーというメニューがあるので、そこで設定ができます。
今であれば、TLSのバージョンはTLS1.2以上、プロフィールはモダンを設定すると良いでしょう。
アプリ側の設定変更
今回はWebアプリケーションで説明します。
onCallでの呼び出しを記述したことがある人はわかると思いますが、WebアプリのSDKではonCallのときに特段URLを書くことはありません。
以下の通り、書くのはFunctionsの名前のみです。
import { httpsCallable, getFunctions } from 'firebase/functions';
export const convertToUpperCase = httpsCallable(
getFunctions(getApp(), 'asia-northeast1'),
'convertToUpperCase'
);
もちろんこのままでは、先ほど作成したロードバランサ経由のアクセスにはなりません。
ここで変更するのは、実はregionの部分です。
regionを指定しているgetFunctionsの第二引数は名称が"regionOrCustomDomain"になっており、regionだけはなくカスタムドメインを指定できるようになっています。
なのでたとえば今回のドメインが"functions.example.com"だとしたら、以下のようにしてすればいいです。
import { httpsCallable, getFunctions } from 'firebase/functions';
export const convertToUpperCase = httpsCallable(
getFunctions(getApp(), 'https://functions.example.com'),
'convertToUpperCase'.toLowerCase()
);
ここで気をつけてほしいのは、"https://"を入れることです。これを入れないとregionだと認識してしまって思ってもないところにアクセスしようとしてしまいます。
あと、toLowerCaseを呼び出しています。これはCloud FunctionsのURLは大文字小文字が混ざったものになるのですが、Cloud Runのサービス名はすべて小文字になるため、その名前に合うようやっているものです。
関数は普通複数あると思うので、httpsCallable
を切り出して以下のようにしておくと便利です。
import { httpsCallable as _httpsCallable, getFunctions } from 'firebase/functions';
const httpsCallable = <Request, Response>(name: string) =>
_httpsCallable<Request, Response>(getFunctions(getApp(), 'https://functions.example.com'), name.toLowerCase());
export const testRequest = () => httpsCallable<{ message: string }, { message: string }>('convertToUpperCase');
あとは、Cloud Functions側のURLにアクセスできるとせっかくのWAFとSSLポリシーの設定の意味がなくなりますので、閉じる設定もしておきます。
export const convertToUpperCase = onCall(
{
region: 'asia-northeast1',
ingressSettings: 'ALLOW_INTERNAL_AND_GCLB',
},
(request) => {
const { message } = request.data;
return { message: message.toUpperCase() };
},
);
これで、プロジェクトの内部アクセス、もしくはロードバランサ経由以外のアクセスを拒否するようになります。
これで安心したアクセスの完成です!
まとめ
なかなかマイナーな設定の話でしたが、セキュリティに対する対策はやれるならやっておくべきことですし、公式に直接は書いてくれていない設定なので記事にしました。
Firebaseはお手軽に開発できることがメリットの一つですが、今回のようにちょっとした設定変更でよりセキュアなインフラに拡張できるようにできているので、Firebaseを使いこなしたいときは、Google Cloudのことも知っておくといろいろやる幅が広がって良いと思います!
今回はWebアプリケーションの場合のみを書きましたが、どこかでFlutterでも試して設定方法を追記したいと思います!