😽

LambdaでKMSを使用して環境変数の暗号化・復号化を行う

2024/04/01に公開

はじめに

LambdaでKMSを使用して環境変数の暗号化・復号化を行うことで、セキュリティが向上します。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/security-dataprotection.html

LambdaでKMSを使用するメリット

データの保護

環境変数に機密情報(APIキー、データベースのパスワードなど)を保存する場合、その情報を暗号化することで、不正アクセスによる情報漏洩のリスクを軽減できます。KMSを使用して環境変数を暗号化すると、その情報はAWSのセキュリティ基盤によって保護されます。

アクセス制御

KMSでは、暗号化キーへのアクセスを細かく制御できます。IAMポリシーを使用して、特定のユーザーやサービスだけがキーを使用できるように設定することで、権限のないアクセスからキーを保護します。

LambdaでKMSを使用するデメリット

料金

KMSは使用に応じて料金が発生します。キーの作成、保管料、および暗号化・復号化操作の実行によって料金がかかります。特に、大量の暗号化・復号化操作を行う場合、コストが顕著になる可能性があります。
https://aws.amazon.com/jp/kms/pricing/

パフォーマンス

KMSを使用して暗号化・復号化を実行すると、これらの操作に時間がかかるため、アプリケーションのレスポンス時間が影響を受ける可能性があります。特に、Lambda関数のようなサーバーレス環境では、関数の起動時間(コールドスタート)に暗号化・復号化の時間が加わるため、レスポンスの遅延が発生することがあります。

KMS キーの作成

キーの作成を選択

下記内容を選択し、次へを選択

エイリアスを入力して次へを選択

キー管理者は指定せず、次へを選択

内容を確認して完了を選択してください。

環境変数の設定

Lambda > 関数 > 関数を選択 > 設定> 環境変数を選択

編集を選択

キーを入力し、暗号化の設定を選択し、

転送時の暗号化に使用するヘルパーの有効化をチェックします。チェックを入れると、

環境変数キー/値の右側に暗号化と表示されるので選択してください。

転送時の暗号化を行うための AWS KMS キーで先ほど作成したキーを選択し、暗号化を選択します。

値が暗号化されたことを確認し、保存を選択してください。

KMSキーに対するデータの復号化を許可するポリシーをアタッチする

IAM > ロール Lambda関数のIAMロール(設定 > アクセス権限 > 実行ロールからロール名は確認できます)を選択、
許可 > 許可ポリシー > 表示されている許可ポリシーを選択、

JSONを選択し、編集を選択してください
下記を追記して次へを選択、変更を保存を選択してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": "<先ほど作成したKMSキーのARN>"
        }
    ]
}

kms:Decrypt操作は、AWS KMSに保存されている特定のキーを使用して、暗号化されたデータ(Ciphertext)を元の平文データ(Plaintext)に復号化するプロセスを指します。

LambdaでKMS復号化を行う

下記を参考にさせていただきました🙇
https://gist.github.com/smd877/cacf0859b223fd334dabaa3d074ceb2f

環境変数を指定して取得する場合

Python

SECRETという環境変数を取得しています。

lambda_function.py
import boto3
import os

def lambda_handler(event, context):

    kms_client = boto3.client("kms")

ENCRYPTED = os.environ['SECRET']
    DECRYPTED = boto3.client('kms').decrypt(
        CiphertextBlob=b64decode(ENCRYPTED),
        EncryptionContext={'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']}
    )['Plaintext'].decode('utf-8')

return {
        'statusCode': 200,
        'body': DECRYPTED
    }

JavaScript

同じくSECRETという環境変数を取得しています。

index.mjs
import { DecryptCommand, KMSClient } from '@aws-sdk/client-kms';

export const handler = async (event) => {
  const kmsClient = new KMSClient({ region: '<リージョン名>' }); 

  try {
    // 'SECRET'環境変数の値を取得
    const encryptedSecret = process.env.SECRET;
    if (!encryptedSecret) {
      console.error('SECRET environment variable is not defined');
      return;
    }

    // Base64デコードされた値をKMSで復号化
    const { Plaintext } = await kmsClient.send(new DecryptCommand({
      CiphertextBlob: Buffer.from(encryptedSecret, 'base64'),
      EncryptionContext: { LambdaFunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME },
    }));

    // 復号化された値をUTF-8文字列として取得
    const decryptedSecret = new TextDecoder().decode(Plaintext);

    // 復号化された値の使用
    console.log(`Decrypted SECRET: ${decryptedSecret}`);
  } catch (error) {
    console.error(`Error decrypting SECRET: ${error}`);
  }
};

すべての環境変数を取得する場合

Python

lambda_function.py
import boto3
import os

def lambda_handler(event, context):
    environment_variables = os.environ
    decrypted_environment = {}
    kms_client = boto3.client("kms")

 for key, value in environment_variables.items():
        # 取得した環境変数のキーが 'aws:' または 'AWS:' で始まる場合、そのまま decrypted_environmentに追加
        if key.startswith("aws:") or key.startswith("AWS:"):
            decrypted_environment[key] = value
        else:
            try:
                # KMSで暗号化されたデータの復号化
                # kms_client.decrypt()を使用して、KMSによって暗号化されたデータを復号化
                # b64decode(value)を使用して、Base64でエンコードされた暗号化されたデータをバイナリ形式にデコード
                # CiphertextBlobには復号化するデータが含まれている
                # EncryptionContextには、復号化のためのコンテキスト情報が含まれています。ここではLambda関数の名前が含まれています。
                # Plaintextからは、復号化されたテキストが得られる
                decrypted_value = kms_client.decrypt(
                    CiphertextBlob=b64decode(value),
                    EncryptionContext={
                        "LambdaFunctionName": os.environ["AWS_LAMBDA_FUNCTION_NAME"]
                    },
                )["Plaintext"].decode("utf-8")
                decrypted_environment[key] = decrypted_value
            except Exception as e:
                print(f"Error decrypting value for key {key}: {e}")
                decrypted_environment[key] = value

# 含まれているか確認するコード
if secret in decrypted_environment.values():
    print('環境変数にsecretという値が存在します。')

JavaScript

index.mjs
import { DecryptCommand, KMSClient } from '@aws-sdk/client-kms';
import CryptoJS from 'crypto-js';

export const handler = async (event) => {
  const kmsClient = new KMSClient({ region: '<リージョン名>' });

  try {
    const environmentVariables = process.env;
    let decryptedEnvironment = {};

    for (const [key, value] of Object.entries(environmentVariables)) {
      if (!key.startsWith('aws:') && !key.startsWith('AWS:')) {
        try {
          const { Plaintext } = await kmsClient.send(
            new DecryptCommand({
              CiphertextBlob: Buffer.from(value, 'base64'),
              EncryptionContext: {
                LambdaFunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
              },
            })
          );

          const decodedValue = Plaintext.reduce(
            (acc, byte) => acc + String.fromCharCode(byte),
            ''
          );

          decryptedEnvironment[key] = decodedValue;
        } catch (error) {
          console.error(`Error decrypting value for key ${key}: ${error}`);
          decryptedEnvironment[key] = value; 
        }
      } else {
        decryptedEnvironment[key] = value;
      }
    }

// 含まれているか確認するコード
if (Object.values(decryptedEnvironment).includes(secret)) {
  console.log('環境変数にsecretという値が存在します。');
}

参考にさせていただきました

https://smdbanana.hatenablog.com/entry/2021/07/05/212837

終わりに

何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉

Discussion