ISID テックブログ

ISIDが運営する技術ブログ

AuroraクラスタのパスワードローテーションをCDKで実装する

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。
CDKでAmazon Auroraデータベースクラスタを作成し、Secrets Managerで管理しているパスワードをローテーションしてみました。
ローテーションはSecrets Managerのマネジメントコンソールからでも設定できますが、CDKでも非常に簡単に書けました。やり方は公式ドキュメントには書かれているものの、日本語の情報があまり見当たらなかったため書き残しておきます。

※この記事のサンプルコードではAurora Serverlessを作成していますが、プロビジョンド版でも同じ方法でパスワードローテーションを実現できます。

公式ドキュメント

CDKの aws_rds モジュールの Rotating credentials セクションにクレデンシャルのローテーションに関する記載があり、これを参考にしました。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds-readme.html#rotating-credentials

マスターユーザーのローテーション(シングルユーザーローテーション)

以下のCDKコードでリソースを作成します。

import { Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";
import { Construct } from "constructs";

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // プライベートサブネットを持つVPC
    const vpc = new ec2.Vpc(this, "MyVpc", {
      cidr: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          name: "myPrivateSubnet",
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 20,
        },
      ],
    });

    // VPCエンドポイント用セキュリティグループ
    const VpceSG = new ec2.SecurityGroup(this, "MyVpceSg", {
      vpc: vpc,
      allowAllOutbound: true,
    });

    // Secrets ManagerへのVPCエンドポイント
    vpc.addInterfaceEndpoint("SecretsManagerEndpoint", {
      service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
      subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      securityGroups: [VpceSG],
      privateDnsEnabled: true,
    });

    // Aurora Serverlessクラスタ
    const auroraCluster = new rds.ServerlessCluster(this, "MyAuroraCluster", {
      engine: rds.DatabaseClusterEngine.AURORA_MYSQL,
      vpc: vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
    });
    // パスワードのローテーションを設定
    auroraCluster.addRotationSingleUser();
  }
}

Aurora Serverlessクラスタを以上のサンプルコードで作成すると、マスターユーザーの情報はSecrets Managerに保存されます。
マスターユーザーのパスワードをローテーション設定しているのは次の一行のみです。これだけでローテーションが有効化され、ローテーションを実行するLambda関数などのリソースが作成されます。非常に簡単ですね。

auroraCluster.addRotationSingleUser();

マネジメントコンソールでSecrets Managerのシークレットを確認すると、確かにローテーションが有効になっていることがわかります。 ローテーション有効

addRotationSingleUser() 関数で有効になるローテーションは「シングルユーザーローテーション」と呼ばれ、ユーザーのパスワードをそのまま更新するだけの単純なローテーションです。

Secrets Managerにアクセスできない場合のエラー

デフォルトでは、ローテーションを実行するLambda関数はデータベースクラスタと同じサブネットにデプロイされます。Lambda関数がSecrets Managerにアクセスできるようにする必要があり、今回はそのためのVPCエンドポイントを作成しています。

Lambda関数がSecrets Managerにアクセスできない場合、マネジメントコンソールから手動でローテーションを実行すると以下のエラーが表示されます。 ローテーションエラー

シークレット「MyAuroraClusterSecretD92700-ozDfy6jZIGiv」をローテーションできませんでした。 A previous rotation isn't complete. That rotation will be reattempted.

また、Lambda関数のCloudWatch Logsロググループには次のようにタイムアウトが記録されます。

START RequestId: 3031c3a5-9624-49ed-994e-db8f0cb14d63 Version: $LATEST
END RequestId: 3031c3a5-9624-49ed-994e-db8f0cb14d63
REPORT RequestId: 3031c3a5-9624-49ed-994e-db8f0cb14d63  Duration: 30035.14 ms   Billed Duration: 30000 ms   Memory Size: 128 MB Max Memory Used: 70 MB  
2022-05-24T04:28:30.600Z 3031c3a5-9624-49ed-994e-db8f0cb14d63 Task timed out after 30.04 seconds

シングルユーザーローテーションで作成されたリソースを見てみる

auroraCluster.addRotationSingleUser();

この一行でどのようなリソースが作成されているのか見てみました。

ローテーションを実行するLambda関数

まず、 MyStackMyAuroraClusterRotationSingleUser~ という名前でLambda関数が作成されていました。
このLambda関数はデータベースクラスタと同じサブネットに配置されています。
ローテーションを実行するLambda関数

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

Lambda関数のセキュリティグループも新規に作成されていました。インバウンドルールはなく、アウトバウンドルールは全ての通信を許可しています。
Lambda関数のセキュリティグループ

また、データベースクラスタのセキュリティグループは、Lambda関数のセキュリティグループから3306ポートのインバウンド通信が許可されていました。 データベースクラスタのセキュリティグループ

Lambda関数の実行ロール

Lambda関数の実行ロールが作成され、4つのポリシーが付けられていました。

  • AWSLambdaBasicExecutionRole(AWS管理)
  • AWSLambdaVPCAccessExecutionRole(AWS管理)
  • SecretsManagerRDSMySQLRotationSingleUserRolePolicy0(カスタマーインライン)
  • SecretsManagerRDSMySQLRotationSingleUserRolePolicy1(カスタマーインライン)

インラインポリシー SecretsManagerRDSMySQLRotationSingleUserRolePolicy0 は次のようになっていました。(ネットワークインターフェースの操作を許可しているのですが、なぜここで必要なのか分かりません)

{
    "Statement": [
        {
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DeleteNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DetachNetworkInterface"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

インラインポリシー SecretsManagerRDSMySQLRotationSingleUserRolePolicy1 は次のようになっていました。Lambda関数からSecrets Managerへのアクセスを許可しています。
Resourceは該当リージョンの全てのSecrets Managerシークレットを指しており、広めの許可です。

{
    "Statement": [
        {
            "Condition": {
                "StringEquals": {
                    "secretsmanager:resource/AllowRotationLambdaArn": "arn:aws:lambda:ap-northeast-1:<アカウントID>:function:MyStackMyAuroraClusterRotationSingleUser4A86DF55"
                }
            },
            "Action": [
                "secretsmanager:DescribeSecret",
                "secretsmanager:GetSecretValue",
                "secretsmanager:PutSecretValue",
                "secretsmanager:UpdateSecretVersionStage"
            ],
            "Resource": "arn:aws:secretsmanager:ap-northeast-1:<アカウントID>:secret:*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "secretsmanager:GetRandomPassword"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

全体の構成は次の図のようになっています。
構成図

シングルユーザーローテーションのオプション

auroraCluster.addRotationSingleUser();

シングルユーザーローテーションはこの一行で書けますが、いくつかオプションを渡すこともできます。

import { Duration } from "aws-cdk-lib";

auroraCluster.addRotationSingleUser({
  automaticallyAfter: Duration.days(30),
  excludeCharacters: " %+~`#$&*()|[]{}:;<>?!'/@\"\\",
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
  endpoint: endpoint,
});
  • automaticallyAfter: ローテーション間隔(デフォルトは30日)
  • excludeCharacters: パスワードから除外する文字。デフォルトは「 %+~`#$&*()|[]{}:;<>?!'/@\"\」
  • vpcSubnets: ローテーション用Lambda関数を配置するVPCサブネット(デフォルトはデータベースクラスタと同じサブネット)
  • endpoint: ローテーション用Lambda関数がSecrets Managerにアクセスするために使うVPCエンドポイント。プライベートDNSVPCで有効なら特に指定不要

マスターユーザー以外のユーザーのパスワードローテーション(マルチユーザーローテーション/交代ユーザーローテーション)

アプリケーションからデータベースにアクセスするときはマスターユーザーではなく、権限を制限したユーザーを使うのが望ましいです。
addRotationSingleUser() 関数はマスターユーザーのローテーションのみを行うため、マスターユーザー以外のユーザーのパスワードをローテーションする場合、少し書き方が異なります。

// 「user」というユーザー名でパスワードを自動生成する
const userSecret = new rds.DatabaseSecret(this, "MyUserSecret", {
  username: "user",
  secretName: "MyAuroraClusterUserSecret",
  masterSecret: auroraCluster.secret,
});
// データベースの接続情報を追加する
const secretAttached = userSecret.attach(auroraCluster);

// ローテーションを設定
auroraCluster.addRotationMultiUser("MyUserRotation", {
  secret: secretAttached,
});

ローテーションには addRotationMultiUser() 関数を使います。これは「マルチユーザーローテーション」もしくは「交代ユーザーローテーション」と呼ばれるローテーション方法です。
ローテーション実行時はユーザーのパスワードをすぐに上書きするのではなく、元のユーザーと同じ権限を持つユーザーを新たにデータベースに作成します。同時に2つのユーザーが有効になるため、データベースにアクセスするアプリケーションがクレデンシャル情報をキャッシュしている場合でも、ローテーションによって急に接続できなくなる事態を回避できます。そして2回目以降のローテーションでは新規にユーザーは作成せず、2つ前のユーザーのパスワード情報を上書きすることで、古いパスワードを利用できなくします。
マルチユーザーローテーションを行うには、ユーザーをクローンする権限が必要であるため、マスターユーザーのシークレットを渡しています。

masterSecret: auroraCluster.secret,

以上はあくまでもシークレットの作成とローテーションの設定であり、別途データベースに接続し、同じユーザー名でユーザーを作成する必要があります。作成するユーザーにはSecrets Managerに登録された自動生成パスワードを設定します。

1度ローテーションを実行した後のデータベースユーザー一覧を見ると、user という名前のユーザーに加え、 user_clone という名前のユーザーも存在することがわかります。これ以降、 useruser_clone のパスワードが交互に変更されていきます。

データベースユーザー一覧

マルチユーザーローテーションで作成されたリソース

マルチユーザーローテーションを設定すると、シングルユーザーローテーションと同じく以下のリソースが作成されます

  • ローテーション用Lambda関数
  • Lambda関数のセキュリティグループ
  • Lambda関数の実行ロール

Lambda関数の実行ロールは、シングルユーザーローテーションの時と若干異なり、以下のポリシーが付いていました。

  • AmazonRDSReadOnlyAccessAWS管理)
  • AWSLambdaBasicExecutionRole(AWS管理)
  • AWSLambdaVPCAccessExecutionRole(AWS管理)
  • SecretsManagerRDSMySQLRotationMultiUserRolePolicy1(カスタマーインライン)
  • SecretsManagerRDSMySQLRotationMultiUserRolePolicy2(カスタマーインライン)
  • SecretsManagerRDSMySQLRotationMultiUserRolePolicy3(カスタマーインライン)

インラインポリシー SecretsManagerRDSMySQLRotationMultiUserRolePolicy1SecretsManagerRDSMySQLRotationMultiUserRolePolicy2 は、シングルユーザーローテーションの時のインラインポリシーと同じ内容でした。

インラインポリシー SecretsManagerRDSMySQLRotationMultiUserRolePolicy3 はシングルユーザーローテーションにはなかったポリシーで、マスターユーザーのシークレットへのアクセスを許可しています。(~UserRolePolicy2 で該当リージョンの全シークレットへの GetSecretValue を既に許可しているため、冗長であるように思えます)

{
    "Statement": [
        {
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": "arn:aws:secretsmanager:ap-northeast-1:<アカウントID>:secret:MyAuroraClusterSecretD92700-ozDfy6jZIGiv-iVeG0u",
            "Effect": "Allow"
        }
    ]
}

マルチユーザーローテーションのオプション

マルチユーザーローテーションではパラメーター secret にローテーション対象のアタッチ済みシークレットを指定する必要があります。

auroraCluster.addRotationMultiUser("MyUserRotation", {
  secret: secretAttached,
});

その他のオプションはシングルユーザーローテーションと同じものを指定できます。

  • automaticallyAfter
  • excludeCharacters
  • vpcSubnets
  • endpoint

まとめ

CDKでAuroraクラスタのパスワードをローテーションする設定を非常に簡単に書けました。マスターユーザーはシングルユーザーローテーション、それ以外のユーザーはマルチユーザーローテーションとなるのが個人的に面白かったです。
今回は試していませんが、公式ドキュメントによるとAurora以外のRDSデータベースでも、同じような方法でパスワードのローテーションを実現できそうです。

執筆:@kou.kinyo2、レビュー:@sato.taichiShodoで執筆されました