アプリケーション レベルの暗号化: Memorystore for Redis

このドキュメントでは、Google Cloud の Redis ユーザーに、Redis のスピードと柔軟牲を犠牲にすることなく、悪意のある攻撃を軽減できる暗号化方法の概要について説明します。特に、このドキュメントでは、MemorystoreCloud Key Management Service(Cloud KMS)を使用してエンベロープ暗号化を実装することで、機密データを保護する方法について説明します。Cloud KMS を使用すると、パフォーマンスへの影響を最小限に抑えながら、鍵のローテーションを柔軟に行うことができます。

このドキュメントに関連するチュートリアルでは、Memorystore for Redis ストアに格納されたコンテンツを暗号化するために、Cloud Key Management Service API と通信するアプリケーションを作成します。

このドキュメントはアプリケーション デベロッパーとセキュリティ専門家を対象にしています。また、Java プログラミングとセキュリティのコンセプトに関する基本的な知識があることを前提としています。このドキュメントのコード スニペットはすべて Java で記述されています。

Memorystore for Redis

Memorystore for Redis は Redis インメモリ データストアを利用したフルマネージド サービスで、ミリ秒未満のデータアクセスを実現するアプリケーション キャッシュを構築できます。

Redis のコア セキュリティ モデルでは、信頼できるクライアントにのみ直接アクセスを許可する環境にそのインスタンスを維持しています。このセキュリティ モデルでは、ネットワーク インターフェースへのアクセスの遮断、基本認証の要求、コマンド名の難読化など、基本的なセキュリティ コンセプトのみを実装します。プライベート IP と Identity and Access Management(IAM)のロールベースのアクセス制御により、Redis インスタンスのセキュリティと保護を強化します。標準の高可用性インスタンスが常にゾーン間で複製され、可用性 99.9% のサービスレベル契約(SLA)を実現します。

ミリ秒未満のデータアクセスを実現する Redis のユースケース

Redis には、最新のセキュリティ アーキテクチャに対応するアプリケーションが多数用意されています。多くの企業では、基本的なセキュリティ アーキテクチャでほとんどのセキュリティ要件を満たすことができます。ただし、一部の業界では、より厳しいセキュリティが求められます。

たとえば、金融サービス業界の機械学習アプリケーションについて考えてみましょう。金融トランザクションでは、低レイテンシの推論のため、アカウントの詳細をキャッシュに保存する必要があります。低レイテンシが要求されるため、データをトークン化、マスキング、匿名化することはできません。このため、データを保護し、予期しない攻撃による情報漏洩のリスクを減らすため、セキュリティ階層を追加することが重要になります。

エンベロープ暗号化を使用する場合の考慮事項

一般に、エンベロープ暗号化では暗号化を何層も重ねて行います。このプロセスでは、1 つ以上のデータ暗号鍵(DEK)を使用してデータを暗号化します。次に、鍵暗号鍵(KEK)を使用して DEK を暗号化します(元データではありません)。次の図にこのプロセスを示します。

エンベロープ暗号化のアーキテクチャ。

エンベロープ暗号化により、大規模なデータ暗号化プロセスが簡素化されます。また、KEK のみをローテーションし、DEK のローテーションは行わないため、鍵のローテーションのオーバーヘッドが少なくなります。この方法では、DEK を使用して元データを一度だけ暗号化します。Redis データベース全体を再暗号化するよりも DEK を再暗号化したほうが、負荷がはるかに小さくなります。ローテーション サイクルで DEK 鍵を更新する場合、暗号化されたデータセット全体を再処理する必要があります。エンベロープ暗号化では、暗号化されたデータのコーパス全体を再処理するオーバーヘッドを削減しながら、鍵のローテーションのベスト プラクティスを使用します。

以降のセクションでは、コード スニペットを示しながら、Cloud KMS、Tink ライブラリ、Memorystore を使用してエンベロープ暗号化を実装する方法について説明します。このドキュメントのコードサンプルは、tinkCryptoHelper アプリケーションの Git リポジトリにあります。

Tink と Redis での鍵管理

Google では、アプリケーション レイヤでの暗号化に対応している Cloud Key Management Service(Cloud KMS)を提供しています。このサービスを使用すると、一元化されたクラウドサービスで暗号鍵の作成、インポート、管理、暗号オペレーションを行うことができます。これらの鍵を使用して暗号化オペレーションを実行するには、Cloud KMS、Cloud HSM、または Cloud External Key Manager(Cloud EKM)を直接使用するか、顧客管理の暗号鍵(CMEK)を他の Google Cloud サービス内で使用します。

Cloud KMS は、鍵を保持するキーリングから構成される階層的なレイヤを中心に設計されています。各鍵には複数のバージョンがあり、そのうちの 1 つが primary 属性を持ちます。キーリングはリージョンに割り当てられ、権限の継承元となるプロジェクトの一部を構成します。

鍵は、この階層を反映した URI でアドレス指定されます(たとえば、次のようなパターン)。

gcp-kms://projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING_ID/cryptoKeys/KEY_ID

このパターンの変数の意味は次のとおりです。

これ以外にもコンプライアンス要件がある場合は、Cloud HSM というクラウドホスト型ハードウェア セキュリティ モジュール(HSM)サービスを利用できます。たとえば、FIPS 140-2 Level 3 認定のハードウェア環境内で暗号オペレーションを行う必要がある場合は、この Google Cloud のサービスを使用できます。

Google Cloud では、デフォルトでエンベロープ暗号化と内部鍵管理サービス(KMS)を使用して、保存されているユーザー コンテンツを暗号化します(一部例外があります)。

Tink ライブラリ

このドキュメントで説明する方法では、Tink ライブラリと Cloud KMS を使用して暗号化を管理します。Tink は、Google の暗号化エンジニアとセキュリティ エンジニアが作成した暗号 API を提供する多言語のクロス プラットフォーム ライブラリです。Tink は、容易に使用できることと、暗号の一般的な欠点を減らす安全な API を提供することを目的としています。Tink の設計上の考慮事項の詳細については、Real World Crypto のトークと付属のスライドをご確認ください。

鍵管理のライフサイクル

作業データの暗号化と復号を開始する前に、まず Redis クライアント アプリケーションの鍵管理ライフサイクルのコンポーネントを構成する必要があります。次の図に、鍵とデータの関係の概要を示します。

鍵と IAM のロールを含む KMS ライフサイクルのコンポーネント。

このライフサイクルでは、Redis データベースに保存されるすべてのデータが単一の DEK で暗号化されます。このプロセスの最初のステップは DEK の生成です。次のコードサンプルに示すように、Tink は鍵のコレクションをキーセットに整理し、KeysetHandle オブジェクトでラップします。

KeysetHandle k = KeysetHandle.generateNew
    (AeadKeyTemplates.createAesGcmKeyTemplate(256 / 8));

キーセット内のすべての鍵は 1 つのプリミティブに対応します。ベスト プラクティスとして、このドキュメントとこれに関連するチュートリアルでは、Galois/Counter Mode(GCM)の AES を使用しています。

DEK へのアクセスを有効にするには、DEK を安全に保存する必要があります。このドキュメントでは、Cloud KMS を使用して KEK で DEK を暗号化します。このドキュメントの URI で説明しているように、Tink ライブラリで Cloud KMS が使用する KEK は特定のバージョンの鍵で、特定のキーリングに属しています。この鍵は特定のロケーションに存在し、特定のプロジェクトに関連付けられています。

Cloud KMS KEK は、他の Google Cloud リソースから分離されたプロジェクトに作成することをおすすめします。これにより、そのプロジェクトで IAM のオーナーロールを持つユーザーまたはサービス アカウントだけが Cloud KMS の鍵にアクセスし、データを復号できるようになります。権限とアクセスを管理するベスト プラクティスについては、職掌分散をご覧ください。

Cloud KMS で KEK へのアクセス権を付与するには、KEK で暗号化と復号を行う権限を持つサービス アカウントを使用します。詳細については、適切な IAM ロールの選択をご覧ください。サービス アカウントの認証情報は Google Cloud Console で取得できます。また、Cloud Shell で gcloud iam service-accounts keys create コマンドを使用しても取得できます。Tink を使用して KMS にアクセスする場合、次のコードサンプルに示すように、Cloud KMS クライアントをインスタンス化するためにサービス アカウントの認証情報(kmsCredentialsFilename)が提供されます。

KmsClient kmsClient = new
    GcpKmsClient().withCredentials(kmsCredentialsFilename);

KMS では、KEK によって暗号化された DEK を持つキーセットがファイルに書き込まれます(この場合はローカル ファイル システムの JSON ファイル)。

k.write(JsonKeysetWriter.withFile(new File(keysetFilename)),
    kmsClient.getAead(keyResourceIdUri));

Redis データの暗号化と復号

鍵管理ライフサイクルを開始し、DEK を安全に保存すると、次の図のように、Redis クライアント アプリケーションが DEK を使用して Redis 値の暗号化と復号をできるようになります。

Memorystore for Redis は、DEK を使用してデータの暗号プロセスを実行します。

Tink を使用して、保存された DEK で暗号化と復号を行う場合、次の処理が行われます。

  1. Aead プリミティブが KeysetHandle オブジェクトから取得されます。
  2. encrypt(または decrypt)メソッドが実行され、暗号化されるデータが渡されます。

次のスニペットは、暗号化関数と復号関数を実行するコードを示しています。

Aead aead = k.getPrimitive(Aead.class);
byte[] ciphertext = aead.encrypt(clearText.getBytes("UTF-8"), aa);
String decryptedText = new String(aead.decrypt(ciphertext, aa), "UTF-8");

Aead.encrypt メソッド(関連データによる認証済み暗号化)に渡される関連データ(aa)は、暗号化を任意のコンテキスト(レコードの ID など)にバインドします。関連データは、パスワードの暗号化で使用される salt 引数と同じように扱うことができます。この手法は、データの信頼性と整合性を確保するのに役立ちます。

この Redis クライアント アプリケーションでは、暗号化と復号を行うモジュールで定義された定数が関連データとなる場合があります。Redis クライアント アプリケーションが開始したときに、Cloud KMS から DEK を読み込み、KEK で復号し、Redis に保存されるデータを暗号化する必要があります。

Tink を使用して、初期化プロセス中に作成および保存された DEK キーセットを読み込みます。次のコード スニペットのように、Tink は Cloud KMS の鍵 URI から提供される KEK を使用して復号を行います。

File keyset = new File(keysetFilename);
if (keyset.exists()) {
    KeysetHandle k =
        KeysetHandle.read(JsonKeysetReader.withFile(keyset),
            kmsClient.getAead(keyResourceIdUri));

    Aead aead = k.getPrimitive(Aead.class);
    String decText = new String(aead.decrypt(ciphertext, aa));
}

Redis と暗号化の融合

前のセクションでは、Redis データベースを参照せずに、Tink を使用してデータの暗号化と復号を行う方法について説明しましたが、Redis への暗号化機能の統合は簡単です。このドキュメントに関連するチュートリアルでは、クライアント Java ライブラリとして Jedis を使用します。数行のコードを使用するだけで、Key-Value ペアを渡して暗号化できます。このコードは任意のデータ ストレージに適用できます。次のコードサンプルは、このコードを Redis に適用する方法を示しています。

void set(Iterator<Map.Entry<String, String>> kvs) throws
             NullPointerException, GeneralSecurityException, IOException {
    try (Pipeline p = jedisPool.getResource().pipelined()) {
      while (kvs.hasNext()) {
        Map.Entry<String, String> r = kvs.next();
        p.set(r.getKey(), cryptoHelper.encrypt(r.getValue()));
      }
      p.sync();
    }
}

List<String> get(String... keys) throws UnsupportedEncodingException, NullPointerException,
GeneralSecurityException, IOException {
    Jedis j = jedisPool.getResource();
    List<String> values = j.mget(keys);
    List<String> l = new ArrayList<String>();
    for (String v : values) {
      l.add(cryptoHelper.decrypt(v));
    }
    return l;
}

キーはクリアテキストのままで、値だけが DEK で暗号化されます。したがって、機密情報を Redis キーに埋め込まないように注意してください。値の暗号化に関する欠点の 1 つは、ネストされた値の構造をネイティブの Redis 関数で照会できないことです。データ アクセス レイヤを設計する際は、この特性を考慮する必要があります。

次のステップ