本記事では”Cloud Functionsで認証情報はどう扱えばよいの?”というところの一案について、4月にGAとなったCloud Functions(Python 3.7)からCloud KMSの鍵で暗号化された情報を扱う方法を紹介します。Cloud Functionsで外部サービスのIDやパスワードを扱う際の参考になればと思います。
目次
Cloud Functionsの現状
2019年4月1日にCloud FunctionsでPython3.7がGAになり、併せてCloud SchedulerもGAとなりました。これまでnode.jsしかサポートがなかったり、スケジュール実行させるのが面倒だったCloud Functionsでしたが、使い勝手がとても良くなりました。特にAWSのLambdaでPythonのコードを動かした経験がある方への間口が広がったように思いますが、
Lambdaと同じ感覚で使おうとすると詰まってしまうポイントがまだまだあります。
詰まるポイントの一つとして「認証情報」をどうやって扱えばよいの?というところがあります。対象がGCP内のサービスであれば、Cloud Functionsのサービスアカウントに役割をつけて利用するのがベストプラクティスとなっています。しかしながら、MailgunなどGCP外のサービスを利用する際にはコード内でID/passwordやシークレットなどの認証情報を使わざるを得ない状況が発生します。
このような場合、Lambdaでは下記どちらかの方法をとるのが一般的かと思います。
AWS KMSの鍵情報を登録して環境変数に設定した認証情報を暗号化し、
関数内では透過的に復号された環境変数を利用する
AWS Secrets Manager 経由で暗号化された認証情報を利用する。
※追記:2020年4月にGCPでもSecret ManagerというサービスがGAになりました!
残念ながら、2019年7月時点でCloud Functionsでは同様の機能が用意されておらず、同じことをやろうとすると利用者側での実装が必要となります。Cloud Functionsで認証情報をどう扱えばよいの?というところの一案について、本記事では下記フローでの処理例を紹介します。
・Cloud KMSで認証情報(文字列)を暗号化する
・暗号化した認証情報をCloud Functionsの環境変数に登録する
・Cloud Functionsの関数内で暗号化された環境変数を復号化する
Cloud KMSとは
Cloud KMSはGCPの暗号鍵管理サービスです。本記事ではCloud KMSについては詳しくは扱いませんが、弊社のブログ記事でも紹介していますので「KMSって何よ?」と思われる方はまずご一読いただけますと幸いです。
Cloud KMSで暗号化された情報を使ってみる!
構成説明
では、実際にやってみましょう。構成と処理の流れは下記図をご確認ください。
注意点として、Cloud KMSのKey ringは削除することができません。Cloud KMSを試される場合はテスト用のプロジェクトを作成して使用することをおすすめします。
Cloud KMSで暗号化した平文をCloud Functionsの環境変数に設定し、関数(Python)のコードから下記処理を行います。
・環境変数の暗号化文字列を取得
・Cloud KMSのAPI経由で復号化
・復号化文字列をoutput
Cloud KMSでKey ringとKeyを作成
GCPコンソールからKey ringとKey(暗号鍵)を作成します。
「ステータス」に緑のチェックマークが入って使用可能になっていれば作成終了です。
Cloud Functions用サービスアカウント作成
Cloud Functionsの関数から、先ほど作成したKeyを使用するためのサービスアカウントを作成します。
役割は下記2つを設定してください。
・Cloud Functions – Cloud Functions 閲覧者
・Cloud KMS – クラウドKMS暗号鍵の復号化
Cloud KMSで平文を暗号化
Cloud KMSを使用して暗号化を行う方法として、下記2つを紹介いたします。
・gcloudコマンドを使用する
・Cloud KMSのクライアント(Python)を使用する
Cloud KMSを使う上で非常に面倒なところなのですが、Cloud KMSで平文(テキスト)を暗号化するとバイナリに変換されます。バイナリファイルを直接扱える環境であれば大した問題ではないのですが、このままではCloud Functionsの環境変数として設定ができないため、HEX(16進数)もしくはbase64でエンコードされたテキストとして出力します。
一般的にはbase64でエンコードする方法が使われると思いますが、16進数に変換するやり方も参考として紹介します。以下、Cloud Shellから hogehoge という文字列を暗号化する例です。
gcloud コマンドで暗号化する
HEXで出力
$ echo -n "hogehoge" | gcloud kms encrypt \
--location=asia-northeast1 \
--keyring=sample-ring \
--key=sample-key \
--plaintext-file=- \
--ciphertext-file=- | xxd -p
0a2400509697f88546f2fca06d5d690faed36c6aee06d435e36510f3ceab
3b7fd9911157000412310024df65e4ebe0ccefe0d3709a9dd1abe464e533
6d41560ce59ca0c0beb7c700ea2b44ec6a165440c2e2f5c5d2ae0fc172
base64で出力
$ echo -n "hogehoge" | gcloud kms encrypt \
--location=asia-northeast1 \
--keyring=sample-ring \
--key=sample-key \
--plaintext-file=- \
--ciphertext-file=- | base64
CiQAUJaX+DxU2Ez4LojK3A3U6e83GgX/FEMo2YZ8CS9u+bsBCQQSMQAk32Xkld7ApqSQui+hp0lW
+7wwWMLdbMTsu4bSQtK13tRI4bce/nyKQPKUvtEBSLc=
Cloud KMSのクライアント(Python)を使用する
事前準備としてCloud KMSのモジュールを導入してください。
$ pip3 install google-cloud-kms
Pythonのサンプルコードは以下になります。
Cloud KMSの鍵情報と暗号化したい文字列を設定してください。
encryption.py
'''
Cloud KMSのAPI経由で暗号化文字列を出力するコード。
'''
import base64
from google.cloud import kms_v1
# Cloud KMSの情報を設定
PROJECT_ID = '<KeyのあるプロジェクトID>' # プロジェクトID
LOCATION_ID = 'asia-northeast1' # キーリングの場所(global、エリア、リージョン)
KEY_RING_ID = "sample-ring" # キーリング名
CRYPTO_KEY_ID = "sample-key" # キー名
# KMSの接続情報を作成
client = kms_v1.KeyManagementServiceClient()
name = client.crypto_key_path_path(
PROJECT_ID,
LOCATION_ID,
KEY_RING_ID,
CRYPTO_KEY_ID
)
# 暗号化したい文字列
plain_str = "hogehoge"
print("▼暗号対象文字列(plain):")
print(plain_str)
# byte列に変換した文字列を暗号化。
byte_plain_str = plain_str.encode('ascii')
byte_encbinary = client.encrypt(name, byte_plain_str).ciphertext
# 暗号化したbyte列を16進数(HEX)に変換
hex_encbinary = byte_encbinary.hex()
print("▼暗号化文字列(暗号化byte列→hex):")
print(hex_encbinary)
# 暗号化したbyte列をbase64でエンコード
base64_encbinary = base64.b64encode(byte_encbinary).decode('ascii')
print("▼暗号化文字列(暗号化byte列→base64):")
print(base64_encbinary)
# 暗号化文字列(暗号化byte列→hex)を復号化
raw_plain_str001 = client.decrypt(name, bytes.fromhex(hex_encbinary)).plaintext.decode('ascii')
print("▼暗号化文字列(暗号化byte列→hex)→暗号対象文字列(plain):")
print(raw_plain_str001)
# 暗号化文字列(暗号化byte列→base64)を復号化
raw_plain_str002 = client.decrypt(name, base64.b64decode(base64_encbinary.encode('ascii'))).plaintext.decode('ascii')
print("▼暗号化文字列(暗号化byte列→base64)→暗号対象文字列(plain):")
print(raw_plain_str002)
実行すると下記のように出力されます。
$ python3 encryption.py
▼暗号対象文字列(plain):
hogehoge
▼暗号化文字列(暗号化byte列→hex):
0a2400509697f8740ab3330f3ac7a024a328549ee083e048782a45d99b63bdd2af4d515d112112310024df65e42eacd094339a5b3031737eb69c1b67073c17747283f
364a4745c1a58401baf61aab67869e6a139661874413d
▼暗号化文字列(暗号化byte列→base64):
CiQAUJaX+HQKszMPOsegJKMoVJ7gg+BIeCpF2ZtjvdKvTVFdESESMQAk32XkLqzQlDOaWzAxc362nBtnBzwXdHKD82SkdFwaWEAbr2Gqtnhp5qE5Zhh0QT0=
▼暗号化文字列(暗号化byte列→hex)→暗号対象文字列(plain):
hogehoge
▼暗号化文字列(暗号化byte列→base64)→暗号対象文字列(plain):
hogehoge
Cloud KMSのクライアントは暗号化したい文字列をbytes型で渡す必要がある等々、ちょっとした癖がありますので使い方については下記リンクをご一読ください。
Client for Cloud Key Management Service (KMS) API
Cloud Functionsで関数作成
では、実際にCloud Functionsでテストしてみましょう。下記内容で関数を作成します。リージョンは Key ring の場所と同じにしています。サービスアカウントはCloud Functions用に作成したものを指定し、環境変数にはCloud KMSで暗号化した文字列とKey ring、Keyの情報を設定してください。
構成
構成 | パラメータ |
---|---|
名前 | function-kms |
割り当てられるメモリ | 128MB |
トリガー | HTTP |
ランタイム | Python 3.7 |
実行する関数 | main |
リージョン | asia-northeast1 |
サービスアカウント | cloud-functions-kms |
環境変数
名前 | 値 |
---|---|
HEX | ※ HEXで出力された暗号化文字列 |
BASE64 | ※ base64で出力された暗号化文字列 |
PROJECT_ID | Cloud KMSのKey ring と KeyがあるプロジェクトID |
LOCATION_ID | Cloud KMSのKey ring があるロケーション |
KEY_RING_ID | Cloud KMSのKey ring 名 |
CRYPTO_KEY_ID | Cloud KMSのKey 名 |
main.py
'''
Cloud Functions からCloud KMSを利用する方法のサンプル
環境変数からHEXとbase64の暗号化文字列を読み込み、
plain textに変換して出力する。
'''
import os
import base64
from google.cloud import kms_v1
# 環境変数読み込み
enc_hex = os.environ.get('HEX')
enc_base64 = os.environ.get('BASE64')
PROJECT_ID = os.environ.get('PROJECT_ID')
LOCATION_ID = os.environ.get('LOCATION_ID')
KEY_RING_ID = os.environ.get('KEY_RING_ID')
CRYPTO_KEY_ID = os.environ.get('CRYPTO_KEY_ID')
# KMSの接続情報を作成
client = kms_v1.KeyManagementServiceClient()
name = client.crypto_key_path_path(
PROJECT_ID,
LOCATION_ID,
KEY_RING_ID,
CRYPTO_KEY_ID
)
def dec_hexenc(encstr_hex):
"""
"HEX(string)→暗号化文字列(byte)" → "暗号化(byte) →復号化(byte)" → "復号化(byte)→平文(string)"
"""
raw_plain = client.decrypt(name, bytes.fromhex(encstr_hex)).plaintext.decode('ascii')
return(raw_plain)
def dec_base64enc(encstr_base64):
"""
"base64(string)→暗号化文字列(byte)" → "暗号化(byte)→復号化(byte)" → "復号化(byte)→平文(string)"
"""
raw_plain = client.decrypt(name, base64.b64decode(encstr_base64.encode('ascii'))).plaintext.decode('ascii')
return(raw_plain)
def main(event):
# HEX復号化
print("HEX:" + dec_hexenc(enc_hex))
# base64復号化
print("base64:" + dec_base64enc(enc_base64))
requirements.txt
# Function dependencies, for example:
# package>=version
google-cloud-kms
テスト
Cloud Functionsでテスト実行してみましょう。暗号化する前の文字列が表示されていたら成功です。
「テスト」の項目から「関数をテスト」のボタンをクリック。
アウトプットに「OK」が表示されて、ログに復号化された文字列(hogehoge)が表示されれば成功です。
まとめ
Cloud Functionsで認証情報はどう扱えばよいの?というところの一案について、Cloud KMSで暗号化した情報をCloud Functionsの環境変数にセットして利用する方法を紹介いたしました。本番環境で使用するには、鍵のローテーションや接続情報が更新された際の運用などいろいろな考慮が必要になるかと思いますが、Cloudのよいところは気軽に始められるところです。使ってみないとわからないことも多々ございますので、まずは最小構成で試してみていただき、ご利用の環境にあった構成を見つけていただければと思います。
個人的には、このようなセキュリティに関する機能はサービス側で実装してほしいという思いがあります。GCPには新しいサービスばかりに目を向けるのではなく、既存サービスの機能向上やサービス間の連携をもっと頑張って欲しいところです。
クラウドエースでは、Google Cloud Platformのご利用について様々な技術支援を行っております。何かお困りのことがございましたらぜひご相談ください!