[アップデート]Amazon Data Firehoseが認証情報をAWS Secrets Managerより取得できるようになりました

2024.06.18

初めに

先日Amazon Data FirehoseとAWS Secrets Manager統合のリリースがありました。

これまでAmazon Data FirehoseではNew RelicのAPIキー等認証情報を入力するサービスを利用する際その値をFirehose側で直接指定する形となっておりました。

CloudFormation等を利用して構築する際は動的参照を利用することでSecrets Managerの値を参照してデプロイ時に差し替え、Secrets Manager側からのローテーション時のLambda関数処理による差し替えといったことは可能でしたがあくまで外側の仕組みとしてFirehose側の設定値を書き換えるものとなっておりました。
(シークレットの値の変更+αのαの部分がうまくやればできたけど機能としてのサポートではなかった)

今回のアップデートで認証情報の値直接ではなくシークレットのARNを指定できるようになりFirehose側が必要に応じてSecrets Managerより値を取得するようになったため、+αの処理なくシークレットの値の変更のみで対応できるようになり秘密情報のローテーションをよりシンプルに実現することができるようになります。

またシークレットの値を直接取り扱わないため設定の変更処理をより安全に実現することも可能となります。

試してみる

今回はCloudFormationを利用して設定してみます。今回はHTTPエンドポイントを利用しますがSecretsManagerConfigrationというパラメータが追加されておりこちらに対して有無効、シークレットのARN、取得に利用するIAMロールの指定をすればよさそうです。

またこの値は従来のアクセスキーの指定同様、リクエスト上のX-Amz-Firehose-Access-Keyヘッダに反映されます。

どのヘッダに利用されるか等は送信先のサービスにより異なるため各種サービスの設定値のドキュメントをご参照ください。

実装

以下のテンプレートで環境を構築します。

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  PostUrl:
    Type: String
Resources:
  DataDeliver:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties: 
      DeliveryStreamName: secrets-integration-test
      DeliveryStreamType: DirectPut
      HttpEndpointDestinationConfiguration:
        EndpointConfiguration:
          Url: !Ref PostUrl
        S3BackupMode: FailedDataOnly
        RoleARN: !GetAtt FirehoseRole.Arn
        S3Configuration:
         BucketARN: !GetAtt DeliveryFailedDataBucket.Arn
         RoleARN: !GetAtt FirehoseRole.Arn
        SecretsManagerConfiguration:
          Enabled: True
          SecretARN: !Ref Secrets
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: secrets-integration-firehose-role
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
      Policies:
        - PolicyName: secrets-integration-deliver-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:PutObject'
                Resource: !Sub ${DeliveryFailedDataBucket.Arn}/*
              - Effect: Allow
                Action:
                  - 'secretsmanager:GetSecretValue'
                Resource: !Ref Secrets
  Secrets:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: firehose-integration
      GenerateSecretString:
        SecretStringTemplate: '{}'
        GenerateStringKey: "api_key"
        PasswordLength: 16
  DeliveryFailedDataBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub secrets-integration-fail-log-bucket-${AWS::AccountId}

シークレットに指定すべき値についてはサービスにより異なりますので利用するサービスに合わせて設定しましょう。今回HTTPエンドポイントの場合はapi_keyがキーとなるJSON値を設定します。

今回は以下のような値となっております。

{"api_key":"tOTxb=#9Ek$si^GA"}

HTTPエンドポイントの先ではX-Amz-Firehose-Access-Keyヘッダの値をログに出力するようにしておきます。通信はPOSTで来るため受信側でメソッドの判定がある場合は注意しましょう。

<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;

Route::post('/integration/debug', function (Request $request) {
    Log::debug($request->header('x-Amz-Firehose-Access-Key'));
    return response('Hello World');
});

動作確認

ストリームにデータを設置するとシークレット上のapi_keyの値が出力されていることが確認できます。

% aws firehose put-record --delivery-stream-name secrets-integration-test  --record Data="test"
{
    "RecordId": "xxxxxxxxxxxx",
    "Encrypted": false
}
% tail -f storage/logs/laravel.log
...
[2024-06-18 04:26:52] local.DEBUG: tOTxb=#9Ek$si^GA
[2024-06-18 04:26:54] local.DEBUG: tOTxb=#9Ek$si^GA
[2024-06-18 04:26:59] local.DEBUG: tOTxb=#9Ek$si^GA
[2024-06-18 04:27:07] local.DEBUG: tOTxb=#9Ek$si^GA
[2024-06-18 04:27:21] local.DEBUG: tOTxb=#9Ek$si^GA
[2024-06-18 04:27:55] local.DEBUG: tOTxb=#9Ek$si^GA
[2024-06-18 04:28:53] local.DEBUG: tOTxb=#9Ek$si^GA

Firehose側に変更が入らないように個別にシークレットの値を送信して再度データし変更されていることを確認します。

% tail -f storage/logs/laravel.log
...

[2024-06-18 04:40:31] local.DEBUG: foobar
[2024-06-18 04:40:40] local.DEBUG: foobar
[2024-06-18 04:40:56] local.DEBUG: foobar
[2024-06-18 04:41:25] local.DEBUG: foobar
[2024-06-18 04:42:26] local.DEBUG: foobar
[2024-06-18 04:44:33] local.DEBUG: foobar

https://docs.aws.amazon.com/ja_jp/firehose/latest/dev/using-secrets-manager.html
Firehose caches the secrets with an encryption and uses them for every connection to destinations. It refreshes the cache every 10 minutes to ensure that the latest credentials are used.

なおFirehoseではシークレットの値を約10分程度キャッシュするため切り替えタイミングと配信タイミング次第で意図せず旧値が混ざってしまう可能性があります。

実際に04:44(UTC)ごろに変更を行いデータの送信を行いましたが04:58頃もキャッシュが残っているためか一発目は古いシークレットが利用されており、その直後から値が切り替わっていることが確認できました。

% tail -f storage/logs/laravel.log
...
[2024-06-18 04:44:33] local.DEBUG: foobar
...
[2024-06-18 04:58:04] local.DEBUG: foobar
[2024-06-18 04:58:06] local.DEBUG: foobar1
[2024-06-18 04:58:10] local.DEBUG: foobar1
[2024-06-18 04:58:18] local.DEBUG: foobar1
[2024-06-18 04:58:34] local.DEBUG: foobar1
[2024-06-18 04:59:06] local.DEBUG: foobar1

キャッシュ時間や削除については画面を見る限りでも本日入れたAWS CLIのv1側でもそれっぽい機能は見当たらないためこの辺りの制御は現状できなさそうです。

% sam-cli % bin/aws --version
aws-cli/1.33.10 Python/3.12.3 Darwin/22.6.0 botocore/1.34.128
% bin/aws firehose aaa
...
create-delivery-stream                   | delete-delivery-stream
describe-delivery-stream                 | list-delivery-streams
list-tags-for-delivery-stream            | put-record
put-record-batch                         | start-delivery-stream-encryption
stop-delivery-stream-encryption          | tag-delivery-stream
untag-delivery-stream                    | update-destination
help

終わりに

シークレットの値を直接参照してくれるようになったことで、必要以上に秘密情報を外側で取り合わずより安全にシンプルに対応できるものとなるアップデートでした。

Firehose側で直接値を持つわけではなくSecrets Manager側を動的に参照するためがシークレット参照による料金発生がある点は注意しましょう。

10分程度キャッシュは行われるため毎回というほどではありませんが、逆にキャッシュしてしまう関係で変更反映にラグができてしまうため送信先で認証情報が一つしか持てない場合は一時的に送信に失敗してしまう可能性があります。
追記: ただ試してる限り10分経過せずとも再試行中に新しいシークレットに変わっているような気がするのであまり気にしなくても良いかも?(要確認)

現状この辺りのコントロールができるわけではなさそうなのでローテーションが必要でも場合により別の方法でローテーションした値を直接Firehose側に持たせた方が良い場合もありそうです。
あくまで選択肢が一つ増えたほどで見たいただくのが良いのではないかなと個人的には考えております。