原文はこちら
最近このブログでは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と組み合わせて使う方法について詳しく知りたければ
ドキュメントを参照ください。次のステップはダイナミックグループ用にポリシーを作り、
keys
,
vaults
と
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> |
Java 11をお使いの場合は、 javax.activation-api
もマニュアルで追記しておきます:
| <dependency> |
| <groupId>javax.activation</groupId> |
| <artifactId>javax.activation-api</artifactId> |
| <version>1.2.0</version> |
| </dependency> |
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"] |
ファンクションをデプロイする前にマニュアルでテストを実行することをお忘れなく!
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] |
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; |
| } |
| } |
ランダムな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; |
| } |
| } |
テスト
前述のように、ファンクションのテストをマニュアルで行う必要があります。
ローカルでファンクションをテストできるようにするには、いくつかの環境変数をセットしておく必要があります。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 |
必要な環境変数をセットしたら、ユニットテストを書きましょう:
| 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関連のわたしの他のポストもチェックしてみてください: