Secrets Managerで「その他のシークレットのタイプ」でローテーションさせてみた

2022.02.08

いわさです。

先日、以下の記事でSecrets Managerの自動ローテーションに関するアップデートを紹介させて頂きました。

この検証をしている時に気づいたのですが、RDSやDocumentDB、Redshift以外でローテーションさせる場合、ちょっと面倒なんですね。
というのも、上記のサービスはローテーション設定時に必要な関数を自動生成してくれるのですが、例えば「その他のシークレットのタイプ」などではまず自分でローテーション関数を用意する必要があります。

本日は、「その他のシークレットのタイプ」で使える最低限のローテーション関数を作ってみました。

ローテーション関数の設定画面

RDSなど

RDSなどであれば以下のようにローテーション設定画面で関数名を指定すると自動で作成してくれます。
あとはVPC内のLambdaがRDSとSecrets Managerのエンドポイントにアクセス出来る構成になっていればすぐに使い始めることが出来ます。ローテーション関数の中身を見たことの無い人も多いかもしれません。

その他のシークレットのタイプ

その他のシークレットのタイプだと、作成済みの関数を指定するだけです。
そして、作成機能のリンクはありますが、関数の作成画面へ遷移するだけのものです。

ローテーション関数の作成

では、作成してみます。
ローテーション関数は汎用的なテンプレートが用意されていますのでこちらを使うのが良いです。

コンソールでは、Serverless Application Repositoryで「SecretsManagerRotationTemplate」という名前で登録されています。

パラメータとしてはendpointが必須パラメータとなっています。
これは、boto3でSecrets Managerクライアントインスタンスを生成する際のエンドポイントパラメータです。
今回は東京リージョンを使っているため、https://secretsmanager.ap-northeast-1.amazonaws.comを設定します。

ただし、こちらはあくまでもテンプレートなので、このまま実行しても例外をスローするだけで利用出来ないです。
コードを少し修正します。

ローテーション関数の流れなのですが、ローテーション時には以下のステップを必ず実行します。

  • ステップ1: シークレットの新しいバージョンを作成する (createSecret)
  • ステップ2: データベースまたはサービスの認証情報を変更する (setSecret)
  • ステップ3: 新しいシークレットバージョンをテストする (testSecret)
  • ステップ4: ローテーションを完了する (finishSecret)

そして、これを実行するためのメソッドがテンプレートにはすでに用意されています。
ただし、ステップ2とステップ3の処理は何も実装されおらず未実装のエラーがスローされます。

これは、このステップが外部システムのシークレット更新と、外部システムへの新しいシークレットを使った接続テストなどを行う処理だからです。
例えばMySQLが外部システムの場合は、ステップ2でSET PASSWORDし、ステップ3で新しい認証情報でデータベースへの接続を行っています。(Lambdaから)

本日は外部の連携先がないので、この2つのステップでは何もしないようにしておきましょう。

def set_secret(service_client, arn, token):
    #raise NotImplementedError
    return

def test_secret(service_client, arn, token):
    #raise NotImplementedError
    return

ステップ1とステップ4についてはテンプレートのままで利用出来る状態になっています。
ここで一度ためしてみましょう。

ランダムなプレーンテキスト形式ですが、ローテーションされました。

Key-Valueにしたい

次は以下のような形でKey-Value形式でシークレットを作成している場合に、一部のキー情報のみを更新したいと思います。

ステップ1(create_secretメソッド)で新しいシークレットを作成しているのですが、以下のハイライト部分でランダムなパスワードをそのままシークレットとして登録しています。

def create_secret(service_client, arn, token):
    service_client.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT")
    try:
        service_client.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING")
        logger.info("createSecret: Successfully retrieved secret for %s." % arn)
    except service_client.exceptions.ResourceNotFoundException:
        exclude_characters = os.environ['EXCLUDE_CHARACTERS'] if 'EXCLUDE_CHARACTERS' in os.environ else '/@"\'\\'
        passwd = service_client.get_random_password(ExcludeCharacters=exclude_characters)
        service_client.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=passwd['RandomPassword'], VersionStages=['AWSPENDING'])
        logger.info("createSecret: Successfully put secret for ARN %s and version %s." % (arn, token))

これを、事前に取得した変更前シークレットの一部分のを更新してシークレットとして更新させるように変更します。
ちなみに、RDSなどのローテーション関数ではこの実装方法でシークレットのうちのパスワード部分のみを更新しています。

import json
...
def create_secret(service_client, arn, token):
    current_secret = service_client.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT")
    current_dict = json.loads(current_secret['SecretString'])
    try:
        service_client.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING")
        logger.info("createSecret: Successfully retrieved secret for %s." % arn)
    except service_client.exceptions.ResourceNotFoundException:
        exclude_characters = os.environ['EXCLUDE_CHARACTERS'] if 'EXCLUDE_CHARACTERS' in os.environ else '/@"\'\\'
        passwd = service_client.get_random_password(ExcludeCharacters=exclude_characters)
        current_dict['key1'] = passwd['RandomPassword']
        service_client.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=json.dumps(current_dict), VersionStages=['AWSPENDING'])
        logger.info("createSecret: Successfully put secret for ARN %s and version %s." % (arn, token))

ローテーションさせてみましょう。

Key1のみローテーションされました。

さいごに

本日は、「その他のシークレットのタイプ」での最低限のローテーション関数を作成してみました。

Lambda関数としては、シークレットの更新APIや外部サービスのシークレット更新を実装するだけなのですが、ローテーションの操作に伴ってステップ1〜4まで通しで実行されるというのがローテーション機能の実体でした。あとはステージングラベルを使ってローテーションをさせている感じですね。

導入時には、今回実装しなかった、ステップ2が一番大変そうですね。