[Functions] Key Managementを使った設定変数の暗号化と復号/Using Key Management To Encrypt And Decrypt Configuration Variables

原文はこちら

最近このブログではOracle Functionsに関連する様々なテーマについてご紹介してきましたが、今回ご紹介するのはもしかするとこのブログシリーズで最初に取り上げるべきだったかもしれないテーマです。以前のポストでアプリケーションとファンクションの設定変数の設定方法をご説明したんですが、こうした設定変数をセキュアに保持しておく方法はそのときにはご説明していませんでした。このポストでは、Oracle CloudテナンシーのKey Managementを使って設定を暗号化、復号することによってこのセキュアな保持を実現する方法をご説明します。

これをやるにはいくつかのステップを進んでいくことになるので、ここでアウトラインを示しておきますね:
  • KMS Vaultの作成
  • Master Encryption Keyの作成
  • Data Encryption Key (DEK) をMaster Encryption Keyから生成
  • DEK plaintext から返却された値を使って sensitive value を暗号化(オフライン)
  • 暗号化された sensitive value をサーバレスアプリケーションの設定変数として保存
  •  sensitive value の暗号化に使ったDEK ciphertext と initVector をファンクションの設定変数として保存
  • ファンクションの中でOCIDとCryptographic Endpointを使ってOCI KMS SDKを呼び出し、DEK ciphertext を復号して plaintext に戻す
  • 復号したDEK plaintext と initVectorを使ってsensitive value を復号する
ここで sensitive value と記載しているものは、暗号化が必要なものであればなんでも有り得ます。データベースパスワードだったり、APIキーだったり、などなど。Oracle Functionsの設定変数の中に平文ではなく暗号化して保存したいものを示すプレースホルダーであり、実態がなんであれ構いません。
なんかステップ多いな、って感じですよね。でも実のところ書いてあるとおり進めていけばそんなに大変じゃありません。それに、本当のところ、アプリケーションとファンクションにとってセキュリティというのは一番大事なことですからね。

始める前に

ここではOracle Functionsの中でリソースプリンシパルを活用することになります。これはすなわち、OCI SDKを使う際にOCI設定ファイルを含める必要は生じないということですが、一方で、KMS APIとやり取りするうえでの適切な許可ポリシーを備えたダイナミックグループを作成しておく必要があるということになります。
まず、'Dynamic Group'を作成して'rules'を以下のようにセットしてください(ファンクションをデプロイするコンパートメントのOCIDを使ってください):
ダイナミックグループとリソースプリンシパルをOracle Functionsと組み合わせて使う方法について詳しく知りたければドキュメントを参照ください。次のステップはダイナミックグループ用にポリシーを作り、 keysvaults と key-delegate の管理権限を与えることです。こちらについても、ポリシーのドキュメントを読んでどのようなポリシーを適用としているのか理解してください。

アプリケーションとファンクションの作成

では、サーバレスアプリケーションを作成しましょう(適切なサブネットOCISで置き換えてください):
fn create app --annotation oracle.com/oci/subnetIds='["ocid1.subnet.oc1.phx..."]' fn-kms
そしてファンクションを作成します:
fn init --runtime java fn-kms-demo

依存対象

KMS SDKのための依存関係を pom.xml に追加しておく必要があります:
<dependency>
    <groupId>com.oracle.oci.sdk</groupId>
    <artifactId>oci-java-sdk-keymanagement</artifactId>
    <version>1.6.0</version>
</dependency>
view rawpom.xml hosted with ❤ by GitHub
Java 11をお使いの場合は、 javax.activation-apiもマニュアルで追記しておきます:
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>javax.activation-api</artifactId>
    <version>1.2.0</version>
</dependency>
view rawpom.xml hosted with ❤ by GitHub

Dockerfile

いくつかの環境変数に依存することになるので、お手製の Dockerfile を作成する必要があります。デフォルトで実行される Dockerfile との違いはファンクションをデプロイする際のDockerビルドでのテスト実行をスキップするところだけです。なぜこれをスキップするかというと、現状ではでファンクションをデプロイする際に環境変数をDockerビルドコンテキストに渡す術がないからです。以下のお手本を使ってください:
FROM fnproject/fn-java-fdk-build:jdk11-1.0.98 as build-stage
WORKDIR /function
ENV MAVEN_OPTS -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Dhttp.nonProxyHosts= -Dmaven.repo.local=/usr/share/maven/ref/repository
ADD pom.xml /function/pom.xml
RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=true", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target", "--fail-never"]
ADD src /function/src
RUN ["mvn", "package", "-DskipTests=true"]
FROM fnproject/fn-java-fdk:jre11-1.0.98
WORKDIR /function
COPY --from=build-stage /function/target/*.jar /function/app/
CMD ["codes.recursive.KmsDemoFunction::handleRequest"]
view rawDockerfile hosted with ❤ by GitHub
ファンクションをデプロイする前にマニュアルでテストを実行することをお忘れなく!

VaultとMaster Encryption Keyの作成

OCIコンソールのサイドバーから、'Governance and Administration'の配下の'Security'→'Key Management'を選択してください:
'Create Vault'をクリックしてVaultの詳細を入力します:
Vaultが作成できたら、Vaultの名前をクリックするとVaultの詳細が表示されます。詳細画面で'Create Key'をクリックして新しいMaster Encryption Keyを作成し、ダイアログの入力欄を埋めていきます:
Master Encryption KeyのOCIDとVaultの'Cryptographic Endpoint'をコピーしておいてください。これはあとでDBパスワード用のData Encrption Key(DEK)を作成する際に使います。

Data Encryption Key (DEK)の作成

以下のようにData Encrption Key(DEK)をOCI CLIから作成します:
oci kms crypto generate-data-encryption-key \
--key-id ocid1.key.oc1.phx.... \
--include-plaintext-key true \
--key-shape "{\"algorithm\": \"AES\", \"length\": 16}" \
--endpoint [Cryptographic Endpoint]
view rawgenerate-dek.sh hosted with ❤ by GitHub
 generate-data-encryption-key から返ってくる ciphertext と plaintext の値を手元に持っておいてください。あとですぐに必要になります。
DEK ciphertextの例
I...[random chars]...​AAAAAA==
ciphertextをアプリケーションの設定変数として保持させます:
fn config app fn-kms DEK_CIPHERTEXT I...[random chars]...​AAAAAA==
DEK plaintextの例
0…​[random chars]...=

パスワードの暗号化

このステップではパスワードをファンクションの中ではなく、オフラインで暗号化します。あとでファンクションは稼働時に復号を行うことになります。スタンドアロンのJavaプログラムの中で先程のDEKを使ってパスワードを暗号化します。以下のお手本を使ってもらってもよいです。
注意:Plug in your DEK plaintext の値をプラグインして initVectorにランダムな16byteのStringを与えてください。 initVector はあとで復号の際に使えるように設定変数として保持します。
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
class Main {
    private static String key = "0...=="; //DEK plaintext value
    private static String initVector = "abcdefghijklmnop"; //must be 16 bytes
    public static void main(String[] args) {
        System.out.println(encrypt("hunter2"));
    }
    public static String encrypt(String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            return Base64.getEncoder().encodeToString(encrypted);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}
view rawEncrypt.java hosted with ❤ by GitHub
ランダムな16 byteの initVector Stringをアプリケーションの設定変数として保持させます:
fn config app fn-kms INIT_VECTOR_STRING [Random 16 byte string]
上述のプログラムの出力した値をコピーしてください。これが暗号化されたパスワードです。これもアプリケーションの設定変数として保持させます:
fn config app fn-kms ENCRYPTED_PASSWORD N...==
最後に、Master Encryption KeyのOCIDとCryptographic Endpointをアプリケーションの設定変数として格納します:
fn config app fn-kms KEY_OCID ocid1.key.oc1.phx...
fn config app fn-kms ENDPOINT https://...-crypto.kms.us-phoenix-1.oraclecloud.com

サーバレスファンクション

これでサーバレスファンクションに暗号化されたパスワードを復号するよう修正を加えることができるようになりました。以下のようになります:
package codes.recursive;
import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider;
import com.oracle.bmc.keymanagement.KmsCryptoClient;
import com.oracle.bmc.keymanagement.model.DecryptDataDetails;
import com.oracle.bmc.keymanagement.requests.DecryptRequest;
import com.oracle.bmc.keymanagement.responses.DecryptResponse;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.util.Base64;
import java.util.Map;
public class KmsDemoFunction {
    private final String initVector;
    public KmsDemoFunction() {
        this.initVector = System.getenv().get("INIT_VECTOR_STRING");
    }
    public Map<String, String> decryptSensitiveValue() throws IOException {
        Boolean useResourcePrincipal = Boolean.valueOf(System.getenv().getOrDefault("USE_RESOURCE_PRINCIPAL", "true"));
        String encryptedPassword = System.getenv().get("ENCRYPTED_PASSWORD");
        String cipherTextDEK = System.getenv().get("DEK_CIPHERTEXT");
        String endpoint = System.getenv().get("ENDPOINT");
        String keyOcid = System.getenv().get("KEY_OCID");
        /*
        * when deployed, we can use a ResourcePrincipalAuthenticationDetailsProvider
        * for our the auth provider.
        * locally, we'll use a ConfigFileAuthenticationDetailsProvider
        */
        AbstractAuthenticationDetailsProvider provider = null;
        if( useResourcePrincipal ) {
            provider = ResourcePrincipalAuthenticationDetailsProvider.builder().build();
        }
        else {
            provider = new ConfigFileAuthenticationDetailsProvider("/.oci/config", "DEFAULT");
        }
        KmsCryptoClient cryptoClient = KmsCryptoClient.builder().endpoint(endpoint).build(provider);
        DecryptDataDetails decryptDataDetails = DecryptDataDetails.builder().keyId(keyOcid).ciphertext(cipherTextDEK).build();
        DecryptRequest decryptRequest = DecryptRequest.builder().decryptDataDetails(decryptDataDetails).build();
        DecryptResponse decryptResponse = cryptoClient.decrypt(decryptRequest);
        String decryptedDEK = decryptResponse.getDecryptedData().getPlaintext();
        String decryptedPassword = decrypt(encryptedPassword, decryptedDEK);
        /*
        * returning the decrypted password for demo
        * purposes only. in your production function,
        * obviously you should not do this.
        */
        return Map.of(
                "decryptedPassword",
                decryptedPassword
        );
    }
    private String decrypt(String encrypted, String key) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encrypted));
            return new String(original);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}
view rawKmsDemoFunction.java hosted with ❤ by GitHub

テスト

前述のように、ファンクションのテストをマニュアルで行う必要があります。
ローカルでファンクションをテストできるようにするには、いくつかの環境変数をセットしておく必要があります。GitHubプロジェクトのルートにある env.sh を見るとどの変数をセットする必要があるかわかります(あるいは↓からコピーしてください)。これらの値は全て上述のステップから取得できます(格納しておいたアプリケーションの設定変数と対応していることに留意ください)。
#!/usr/bin/env bash
export ENCRYPTED_PASSWORD=
export INIT_VECTOR_STRING=
export KEY_OCID=
export DEK_CIPHERTEXT=
export ENDPOINT=
export USE_RESOURCE_PRINCIPAL=false
view rawenv.sh hosted with ❤ by GitHub
必要な環境変数をセットしたら、ユニットテストを書きましょう:
package codes.recursive;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fnproject.fn.testing.*;
import org.junit.*;
import java.io.IOException;
import java.util.Map;
import static org.junit.Assert.*;
public class KmsDemoFunctionTest {
    @Rule
    public final FnTestingRule testing = FnTestingRule.createDefault();
    @Test
    public void shouldDecryptPassword() throws IOException {
        testing.givenEvent().enqueue();
        testing.thenRun(KmsDemoFunction.class, "decryptSensitiveValue");
        FnResult result = testing.getOnlyResult();
        System.out.println(result.getBodyAsString());
        Map<String, String> resultMap = new ObjectMapper().readValue(result.getBodyAsString(), Map.class);
        assertEquals("hunter2", resultMap.get("decryptedPassword"));
    }
}

デプロイ

以下でファンクションをデプロイします:
fn deploy --app fn-kms
呼び出しは以下:
fn invoke fn-kms fn-kms-demo
復号されたパスワードが返却されるでしょう:
{"decryptedPassword":"hunter2"}

まとめ

このポストではOCI KMSを使ってVaultとMaster Encryption Keyを作成しました。そしてそのMaster Encryption Keyを使ってData Encrption Key(DEK)を作成し、DEKを使って機密情報を暗号化し、その暗号化された機密情報をサーバレスファンクションの設定変数に保存しました。そしてこの暗号化された機密情報にデプロイしたファンクションからアクセスし、ファンクションで使えるように復号しました。

参考情報

Oracle Functions関連のわたしの他のポストもチェックしてみてください:
ご参考までに、デモの中で使ったコードはGitHubで利用可能です。https://github.com/recursivecodes/fn-kms-demo

0 件のコメント:

コメントを投稿