AWS Secrets Managerで RDSのパスワードローテーションしてみる in 2022

2022.01.14

こんにちは、AWS事業本部コンサルティング部のたかくにです。

今回はタイトルの通り、AWS Secrets ManagerでRDSのパスワードローテーションをしてみようと思います。

弊社ブログ内でいくつが既に紹介されていますが、Secrets ManagerがVPCエンドポイントをサポートしていたためアップデート記事となります。

年初めに、タイトルに「in 2022」をつけて大丈夫か内心ソワソワしています...

公式ドキュメント

ローテーション関数へのネットワークアクセス

チュートリアル: AWS データベースのシークレットをローテーションする

弊社ブログ

機密管理サービス AWS Secrets Manager で RDS のパスワードローテーションを試す

新サービス「AWS Secrets Manager」をチュートリアル2種(基本設定、RDSローテーション)で基礎から学ぶ

構成図

ポイントは以下の通りです。

  1. セッションマネージャー使用用途でssm、ec2message、ssmmessagesのVPCエンドポイントを作成します。Secrets Managerとは直接的に関係はありません。
  2. LambdaからSecretsの取得用途で、secretsmanagerのVPCエンドポイントを作成します。
  3. 接続テスト用EC2インスタンスには、既にMySQLクライアントがインストールされています。
  4. 今回はテスト用のためLambda関数は、RDSと同一サブネットに配置していますが、同一VPC内であれば別サブネットに配置して問題ありません
  5. ap-northeast-1cに配置したサブネットは、サブネットグループを作成するために使用するサブネットです。
  6. VPCエンドポイントの設定が完了後、チュートリアル: AWS データベースのシークレットをローテーションするの手順を踏んでいきます。(必要に応じて読み替えを行います。)

事前準備 その1:セキュリティグループの作成

使用するセキュリティグループは以下になります。

  • テスト用EC2インスタンスのセキュリティグループ
  • Lambdaのセキュリティグループ
  • RDSのセキュリティグループ
  • VPCエンドポイント(ssm, ssmmessages, ec2messages)のセキュリティグループ
  • VPCエンドポイント(secretsmanager)のセキュリティグループ

セキュリティグループのルール設定

各セキュリティグループの詳細です。

テスト用EC2インスタンスのセキュリティグループ

インバウンドルール無し(セッションマネージャーで接続)

アウトバウンドルールフルオープン

Lambdaのセキュリティグループ

インバウンドルール:無し

アウトバウンドルール:フルオープン

参考:アウトバウンドのセキュアパターン
CIDR/SG ポート 用途
RDSのSG 3306 パスワード変更で接続するため
VPCエンドポイント(secretsmanager)のSG 443 パスワード取得で接続するため

RDSのセキュリティグループ

インバウンドルールは以下の通り

CIDR/SG ポート 用途
LambdaのSG 3306 パスワード変更で接続するため
EC2 3306 テストで接続するため

アウトバウンドルール:フルオープン

VPCエンドポイント(ssm, ssmmessages, ec2messages)のセキュリティグループ

インバウンドルールは以下の通り

CIDR/SG ポート 用途
テスト接続用EC2のSG 443 セッションマネージャーで接続するため

アウトバウンドルール:無し

VPCエンドポイント(secretsmanager)のセキュリティグループ

インバウンドルールは以下の通り

CIDR/SG ポート 用途
LambdaのSG 443 パスワード取得で接続するため
今回の構成では、Lambdaのセキュリティグループのみ許可する設定にしています。

EC2インスタンスからシークレットを取得する際は、443ポートでEC2インスタンスのセキュリティグループを別途許可する必要があります

加えて、EC2の環境変数にSecrets Managerの情報(DBのユーザ・パスワード)を登録する方法は、こちらをご参照ください。

アウトバウンドルール:無し

事前準備 その2:VPCエンドポイントの設定

VPCエンドポイントを4つ(ssm, ssmmessages, ec2messages, secretsmanager)作成します。

ステップ1: テスト用 MySQL データベースの設定

テスト用 MySQL データベースの設定

検証用のRDSインスタンスを作成します。

今回は検証用であるためインスタンスタイプ含め最小構成で作成を行います。

DBエンジンのバージョンもDBインスタンスのクラスも任意で構いません。

RDSの設定値は次の通り設定しました。

設定値
DB インスタンス識別子 MyTestDatabaseInstance
マスターユーザー名 adminuser
マスターパスワード KeyRotatePassword

チュートリアルとの相違点(テスト用 MySQL データベースの設定)

ドキュメントではパブリックアクセスを「Yes」に変更していますが、今回はVPC Endpoint経由でアクセスするためNoのまま設定しました。

セキュリティグループ設定欄で事前準備したRDS用セキュリティグループを設定しました。

ステップ2: シークレットを作成する

ステップ 2: シークレットを作成する

Secrets Managerコンソール画面からシークレットを作成します。

ステップ1で指定したユーザー名、パスワード、ローテーション対象のRDSインスタンスを設定します。

あくまでシークレットの作成でローテーションは別のステップで行われます。

「次へ」をクリックすると、シークレット名と説明の設定画面へ遷移します。

シークレットの名前は、チュートリアルの通り「MyTestDatabaseMasterSecret」と設定しました。

「次へ」を選択すると、ローテーション設定に遷移します。

今回は、事前にRDSで設定したパスワードの接続確認を行うため、チュートリアルに従い「自動ローテーション」は無効に設定します

作成するシークレットの確認画面へ遷移します。

問題なければ、「保存」でシークレットを作成します。

シークレットが作成されました。

ステップ3: 最初のシークレットを検証する

ステップ 3: 最初のシークレットを検証する

接続確認用EC2インスタンスからRDSへ接続を行います。シークレットとして登録した後でも問題なく接続できました。

ステップ4: シークレットのローテーションを設定する

ステップ 4: シークレットのローテーションを設定する

ここからが本題です。

ステップ2にて作成したシークレットにローテーション機能を追加します。

Secrets Managerコンソールからステップ2にて作成したシークレットを選択し、「ローテーション構成」の変更を行いました。

ローテーション間隔は、1から365日までで選択可能でした。

ローテーション設定を保存すると画面上部のポップアップに「ローテーション用の AWS CloudFormation リソースを作成します。」と表示されました。

内部的に、スタックを作成する仕組みのようです。

ローテーションを行いますがLambda関数のVPC設定が未チューニングのためエラーが発生します。

ステップ 4 +α: Lambda関数のVPC設定を行う

VPC EndpointへLambda関数が接続できるように「VPC設定」を変更します。

セキュリティグループを事前準備で作成したLambda用のセキュリティグループに変更します。

ステップ5: 成功したローテーションの確認

ステップ 5: 成功したローテーションの確認

それでは、パスワードを変更していきましょう。

再びSecrets Managerコンソールへ戻り、シークレットを選択。

「ローテーション構成」から「シークレットをローテーションさせる」を選択します。

シークレットの値が更新されていることが確認できました!

ステップ5 +α: 接続確認

新しくローテーションを行ったパスワードでMySQLへ接続を行います。

パスワードの文字列の関係で対話式でログインを行いましたが、問題なくログインできました!

もっと知りたい人向け

CloudFormationで設定されるリソースについて

スタックの中身としては、「AWS::Serverless::Function」、「AWS::IAM::Role」、「AWS::Lambda::Permission」を作成しているみたいです。

ソースコードは以下になります。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
    endpoint:
        Type: String
        Description: The Secrets Manager endpoint to use.
    functionName:
        Type: String
        Description: The name of the Lambda function.
    invokingServicePrincipal:
        Type: String
        Description: The service principal for the invoking service.
        Default: "secretsmanager.amazonaws.com"
    vpcSubnetIds:
        Type: CommaDelimitedList
        Description: A comma-separated list of VPC subnet IDs applied to the database network. 
        Default: ""
    vpcSecurityGroupIds:
        Type: CommaDelimitedList
        Description: A comma-separated list of security group IDs applied to the database.
        Default: ""
    kmsKeyArn:
        Type: String
        Description: The ARN of the KMS key that Secrets Manager uses to encrypt the secret.
        Default: ""
    excludeCharacters:
        Type: String
        Description: A string of the characters that you don't want in the password.
        Default: "/@\"'\\" # MySQL DB設定時はデフォルト値が設定されていました。

Conditions:
    AddVpcConfig:
        !And
            - !Not [!Equals ["", !Join ["", !Ref vpcSubnetIds]]]
            - !Not [!Equals ["", !Join ["", !Ref vpcSecurityGroupIds]]]
    KmsKeyArnExists:
        !Not [!Equals ["", !Ref kmsKeyArn]]

Resources:
    SecretsManagerRDSMySQLRotationSingleUser:
        Type: AWS::Serverless::Function
        Properties:
            FunctionName:
                Ref: functionName
            Description: Rotates a Secrets Manager secret for Amazon RDS MySQL credentials using the single user rotation strategy.
            Handler: lambda_function.lambda_handler
            Runtime: python3.7
            CodeUri: s3://secrets-manager-rotation-apps-7c4fea7a5d7a5497325114af491e6b58/SecretsManagerRDSMySQLRotationSingleUser/SecretsManagerRDSMySQLRotationSingleUser.zip # AWSさんのURL 既存EC2からはアクセスできませんでした。
            Timeout: 30
            Policies: 
            - VPCAccessPolicy: {}
            - AWSSecretsManagerRotationPolicy:
                FunctionName:
                    Ref: functionName
            - !If
                - KmsKeyArnExists
                - Version: '2012-10-17'
                  Statement:
                      - Effect: Allow
                        Action:
                            - kms:Decrypt
                            - kms:DescribeKey
                            - kms:GenerateDataKey
                        Resource: !Ref kmsKeyArn
                - !Ref "AWS::NoValue"
            Environment:
                Variables:
                    SECRETS_MANAGER_ENDPOINT:
                        Ref: endpoint
                    EXCLUDE_CHARACTERS:
                        Ref: excludeCharacters
            VpcConfig:
                !If
                    - AddVpcConfig
                    -
                        SubnetIds: !Ref vpcSubnetIds
                        SecurityGroupIds: !Ref vpcSecurityGroupIds
                    - !Ref "AWS::NoValue"
            Tags:
                SecretsManagerLambda: Rotation

    LambdaPermission:
        Type: 'AWS::Lambda::Permission'
        Properties:
            Action: 'lambda:InvokeFunction'
            FunctionName: !GetAtt SecretsManagerRDSMySQLRotationSingleUser.Arn
            Principal: !Ref invokingServicePrincipal

Outputs:
    RotationLambdaARN:
        Description: The ARN of the rotation lambda
        Value: !GetAtt SecretsManagerRDSMySQLRotationSingleUser.Arn

自動作成されるLambdaに関して

IAMロール

Lambdaに付与されるIAMロールは、AWS管理ポリシー「AWSLambdaBasicExecutionRole」、「AWSLambdaVPCAccessExecutionRole」に加えて以下のポリシーがインラインポリシーで定義されていました。

また、LambdaのリソースベースポリシーではSecrets Managerからのみ実行できるように制限されていました。

Secrets ManagerでIAMロールを設定していない(できない)ため、Lambda側から許可する用途で使用されているのではないかと思います。

VPC設定

ステップ4で自動生成されたLambdaの「VPC設定」の初期状態は以下になります。

ほとんど、マスキングでわかりませんがVPC設定をまとめると以下になります。

  • マルチAZでデプロイされる(RDS本体のマルチAZ設定の有無は関係ない)
  • サブネットはRDSのサブネットグループで選択したサブネットで作成される
  • セキュリティグループは、RDSにアタッチされたセキュリティグループと同じセキュリティグループがアタッチされている

まとめ

個人的に、簡単にローテーションを組むことができて画期的な機能だと思いました。

今回、記事としてはネットワーク経路をよりセキュアに変更した記事でしたが、「運用面も考慮して実装したい!」という方は、新サービス「AWS Secrets Manager」をチュートリアル2種(基本設定、RDSローテーション)で基礎から学ぶ 【運用上を気をつけておくべきポイント3点】も合わせてご覧いただけますと幸いです。

AWS事業本部コンサルティング部のたかくにでした!