目次
はじめに
システムで使用するユーザ名やパスワードのシークレット情報について、Systems ManagerのParameterStoreやSecrets Managerを利用している方が多いと思います。
今回、そんなSecrets Managerでシークレットの自動ローテーション機能がアップデートされ、時間指定でのローテーションが可能となったので試してみます。
Secrets Managerについておさらい
Secrets Managerについては、過去に当ブログであまり扱っていなかったため、改めておさらいします。
Secrets Managerとは
ユーザ名やパスワードなどのシークレット情報(秘密情報)を安全に格納できるAWSサービスのひとつです。
従来型アプリケーションでは、シークレット情報をアプリケーションコード内、または設定ファイル内に記載していました。
しかしSecrets Managerを使うことで、コンテナやサーバにシークレットを安全に動的挿入可能です。
The Twelve-Factor Appでも推奨されており、実践しているシステムも多いのではないでしょうか。
PrameterStoreとの違い
同様サービスとして、Systems ManagerのParameterStoreがあります。
ParameterStoreについては、利用可能なAWSサービスが多岐に渡っており、Secrets Managerはデータベースに特化しています。
どちらを使うかは予算やシステム要件次第かと思います。
なお、パスワードローテーションについてはSecrets Managerの自動ローテーションが推奨されています。
自動ローテーション対象のサービス
Amazon RDS、Amazon DocumentDB、Amazon Redshift
アップデート概要
Secrets Managerのアップデートの概要は下記です。
これまではローテーション期間(日数)の指定のみで、実際にローテーションされる時間を指定できませんでした。
今回のアップデートにより、新たに希望する時間にローテートを実施できます。
AWS Secrets Manager は特定の時間ウィンドウ内でシークレットローテーションをスケジューリングする機能をサポートします。この機能により、シークレットローテーションを特定日の特定時間に限定できます。以前は、Secrets Manager は指定されたローテーションインターバルの最後の24時間内のシークレットの自動ローテーションをサポートしました。本日の新機能の提供開始により、マネージドローテーションの利便性か、メンテナンスウィンドウの安全性のどちらかを選択する必要がなくなります。
AWS Secrets Manager がローテーションウィンドウをサポート
ローテーションの仕組み
ローテーションを設定するとローテーション用lambda関数が自動生成されます。
lambda関数が指定時間に呼び出されることによりシークレットのローテーションが実現されます。
ローテーションの実践
新たにSecrets ManagerにRDSのシークレットを保存し、指定時刻にローテーションされるか試してみたいと思います。
手順
Secrets Managerにシークレットを保存
マネージメントコンソールより新規にシークレットを作成し、対象のRDSを指定し、「ユーザ名」と「パスワード」を保存します。
RDSの「ユーザ名」と「パスワード」を入力します。
対象のRDSインスタンスを選択します。
今回は、わかりやすいように、まず「自動ローテーション無効」で作成します。
自動ローテーションを有効にする
作成したシークレットの「ローテーションの編集」から、自動ローテーションを有効にします。
ローテーションスケジュールの設定
ローテーションウィンドウでの指定には、cronおよびrate式をサポートしています。
今回は下記の内容で設定し、「毎日7:00:00 UTC」にローテーションされるように設定します。
項目 | 設定値 |
---|---|
スケジュール式 | cron(0 7 * * ? *) |
ウィンドウ期間 | 1h |
すぐにローテーション | OFF |
<注意点>
- Secrets ManagerのタイムゾーンはUTCです。
- cron式のminute、およびyearは0しか設定できません。
- これはローテーションウィンドウが正時に開始されるため、および1年を超える設定ができないためです。
- すぐにローテーションはネットワーク次第で失敗してしまいますので[OFF]にしておくのがオススメです。
ローテーション関数の設定
ローテーション関数は、あらかじめ用意されているローテーション関数のテンプレートに基づいたローテーション関数を自動作成するか、作成済みの自作のローテーション関数が利用可能です。
項目 | 内容 |
---|---|
ローテーション関数を作成 | ローテーション関数を自動作成 |
アカウントからローテーション関数を使用 | 作成済みローテーション関数を利用 |
「個別の認証情報を使用してこのシークレットをローテーション」は「いいえ」を選択します。
こちらは後述の「ローテーション戦略」の指定です。今回は一般的な「シングルユーザーのローテーション戦略」をとります。
項目 | 内容 |
---|---|
いいえ | シングルユーザーのローテーション戦略 |
はい | 交代ユーザーのローテーション戦略 |
設定が完了するとあらかじめ用意されているCloudFormationテンプレートからLambdaにローテーション関数が自動生成されます。
ここで作成されるlambda関数は、「VPC、サブネット、セキュリティグループ」がシークレット作成時に指定したRDSから引き継がれます。
<注意点>
- lambda関数のSGからRDSのSGにアクセス許可されていること。
- プライベートサブネットであればPrivateLinkやNATを経由してSecrets Managerにアクセス可能であること。
- RDSと同じ場所に作成されることに抵抗がある場合は、作成後に移動しましょう。
- NATがない環境では、意外とハマりポイントなのでご注意です。
これでローテーションの設定は完了です。
自動ローテーション結果の確認
設定した時間に自動ローテーションされるか確認してみます。
- ローテーション前
1 2 3 4 5 6 7 8 9 |
[ssm-user@ip-10-0-xxx-xx ~]$ <b>aws secretsmanager get-secret-value --secret-id rotation/mysql | jq .SecretString | jq fromjson</b> { username: "sm_rotation", password: ".3V~QGPp1bzi?c5u:}xp.Y)Mc?kvx&h]", engine: "mysql", host: "sm-rotation.xxxxxx.ap-northeast-1.rds.amazonaws.com", port: 3306, dbInstanceIdentifier: "rotation" } |
- ローテーション後
1 2 3 4 5 6 7 8 9 |
[ssm-user@ip-10-0-xxx-xx ~]$ aws secretsmanager get-secret-value --secret-id rotation/mysql | jq .SecretString | jq fromjson { username: "sm_rotation", password: "YG`3$<2wf6gw&OJy$g8M|nz%V)Uie;2E", engine: "mysql", host: "sm-rotation.xxxxxx.ap-northeast-1.rds.amazonaws.com", port: 3306, dbInstanceIdentifier: "rotation" } |
無事にパスワードがローテーションされています。
RDSにログインしてみると問題なく接続できます。
1 2 3 4 5 6 7 8 9 |
[ssm-user@ip-10-0-xxx-xx ~]$ password='YG`3$<2wf6gw&OJy$g8M|nz%V)Uie;2E' [ssm-user@ip-10-0-xxx-xx ~]$ mysql -usm_rotation -p${password} -h sm-rotation.cltlmqvebtjp.ap-northeast-1.rds.amazonaws.com Welcome to the MariaDB monitor. Commands end with ; or \g. Your MySQL connection id is 170 Server version: 8.0.27 Source distribution Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. |
次にどのタイミングでローテートされているのか確認してみます。
10秒単位でシークレットの状態をダンプし続けてみると、指定時刻である「03:08:00 UTC」に変更されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[ssm-user@ip-10-0-101-57 ~]$ while true; do date;aws secretsmanager get-secret-value --secret-id rotation/mysql |jq .SecretString |jq fromjson; sleep 10; done :(省略) Thu Mar 30 03:08:03 UTC 2022 { username: "sm_rotation", password: ".3V~QGPp1bzi?c5u:}xp.Y)Mc?kvx&h]", engine: "mysql", host: "sm-rotation.xxxxxx.ap-northeast-1.rds.amazonaws.com", port: 3306, dbInstanceIdentifier: "rotation" } Thu Mar 30 03:08:13 UTC 2022 { username: "sm_rotation", password: "YG`3$<2wf6gw&OJy$g8M|nz%V)Uie;2E", ←変更された engine: "mysql", host: "sm-rotation.xxxxxx.ap-northeast-1.rds.amazonaws.com", port: 3306, dbInstanceIdentifier: "rotation" } :(省略) |
Secrets ManagerのLastRotatedDateを数日確認したところ、指定時刻の0分〜10分でローテートが実行されているようです。
1 2 3 4 |
[ssm-user@ip-10-0-xxx-xx ~]$ aws secretsmanager describe-secret --secret-id rotation/mysql | jq .LastRotatedDate | awk '{print strftime("%c",$1)}' Thu 30 Mar 2022 03:08:13 AM UTC ssm-user@ip-10-0-xxx-xx ~]$ aws secretsmanager describe-secret --secret-id rotation/mysql | jq .LastRotatedDate | awk '{print strftime("%c",$1)}' Thu 31 Mar 2022 03:10:02 AM UTC |
アプリケーションからの接続確認
アプリケーションがローテーション後に正しいシークレット値を取得できるのかという点も気になります。
シークレットを作成すると、サンプルコードが表示されますので、
そのサンプルコードをベースに、取得したシークレットでRDSにアクセスするlambda関数を作成しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# Use this code snippet in your app. # If you need more information about configurations or implementing the sample code, visit the AWS docs: # https://aws.amazon.com/developers/getting-started/python/ import boto3 import base64 import ast import pymysql import logging from botocore.exceptions import ClientError def get_secret(event, context): logger = logging.getLogger() logger.setLevel(logging.INFO) secret_name = "arn:aws:secretsmanager:ap-northeast-1:[アカウントID]:secret:[シークレット名]" region_name = "ap-northeast-1" # Create a Secrets Manager client session = boto3.session.Session() client = session.client( service_name='secretsmanager', region_name=region_name ) try: get_secret_value_response = client.get_secret_value( SecretId=secret_name ) except ClientError as e: if e.response['Error']['Code'] == 'DecryptionFailureException': raise e elif e.response['Error']['Code'] == 'InternalServiceErrorException': raise e elif e.response['Error']['Code'] == 'InvalidParameterException': raise e elif e.response['Error']['Code'] == 'InvalidRequestException': raise e elif e.response['Error']['Code'] == 'ResourceNotFoundException': raise e else: if 'SecretString' in get_secret_value_response: secret = get_secret_value_response['SecretString'] params = ast.literal_eval(secret) host = params["host"] username = params["username"] password = params["password"] dbname = params["dbInstanceIdentifier"] try: conn = pymysql.connect(host=host, user=username, passwd=password, db=dbname, connect_timeout=5) except pymysql.MySQLError as e: print("ERROR: Unexpected error: Could not connect to MySQL instance.") logger.info("SUCCESS: Connection to RDS MySQL instance succeeded") |
おまけとして、この関数をローテーション期間中に1mim毎に実行してみました。
実際にローテーションされた時刻を挟んでもRDS接続に失敗しませんでした。
ローテーション時刻
1 2 |
ssm-user@ip-10-0-xxx-xx ~]$ aws secretsmanager describe-secret --secret-id rotation/mysql | jq .LastRotatedDate | awk '{print strftime("%c",$1)}' Thu 31 Mar 2022 08:10:02 AM UTC |
ただし、実際には微小な時間でデータベース接続のダウンタイムが発生する可能性があります。
これについては適切なローテーション戦略を取ることより、ある程度回避可能です。
(長くなってしまうため、機会があったら次のブログで)
おわりに
ローテーションの時間指定が可能になったことで、より厳しいセキュリティ要件にも対応できます。
ローテーション時間まで指定する要件は少ないかもしれませんが、いつローテーションされるのかを把握し、エンジニアが把握できる範囲を広げることが可能です。
参考一覧
AWS Secrets Manager がローテーションウィンドウをサポート
投稿者プロフィール
-
システムアーキテクト部 Develop課
中途入社でスカイアーチネットワークスにJoin。
猫好き。
アプリケーションレイヤ〜インフラレイヤを担当しています。
これまでは、SSO統合認証システムの立ち上げや、高負荷な広告配信システムの構築を経験してきました。
よりAWSに近い位置で業務がしたいと思いスカイアーチに入社しました。
今後もDevOps推進やサーバレス化の知識を深めていきたいです!