CloudFormationで認証情報を扱うベストプラクティス
はじめに
こんにちは。データサイエンスチームのmotchieです。
AWS CloudFormationを使うことで、YAMLやJSON形式のテンプレートでAWS上のインフラストラクチャを記述・管理することが出来ます。
インフラをコード化することで、以下のような様々な利点が得られます。
- どのようなリソースがどのような設定値で構築されているか、コードで素早く把握できる
- デプロイ/ロールバックの作業を自動化でき、作業コストや人為的ミスを減らせる
- インフラ構成の変更点をコードレビューでき、変更の履歴を残すことができる
インフラをコード化する際には、認証情報の扱い方に注意が必要です。
コード内に認証情報をハードコーディングしてしまうと、漏洩のリスクがあります。
実際、CloudFormationのベストプラクティスでも、テンプレートに認証情報を埋め込まないことが推奨されています。
上記のドキュメントでは、認証情報を保管するストアとしてAWS Systems Manager パラメータストアとAWS Secrets Managerが挙げられています。
しかし、これらのサービスを詳しく見てみると、認証情報の生成/更新に関する機能の有無や、CloudFormationテンプレートで管理できるか否かなど、違いがあります。
この記事の内容
そこで本記事では、CloudFormationで認証情報を扱う様々な方法を比較していきたいと思います。
RDSのDBインスタンスを作成するCloudFormationのテンプレートを使って、以下の4つのケースをご紹介します。
- 認証情報がテンプレート内に含まれてしまっている例 (template-1.yaml)
- CloudFormationのパラメータ(NoEcho)で認証情報を設定する例 (template-2.yaml)
- AWS Systems Managerパラメータストアに認証情報を格納し、CloudFormationの「動的な参照」でテンプレート内の値を置き換える例 (template-3.yaml)
- AWS Secrets Managerを使い、テンプレート内で認証情報の生成・自動ローテーションの設定を行い、「動的な参照」で値を置き換える例 (template-4.yaml)
各テンプレートによって作成されるリソースを概観すると、以下のようになります。
なお、以降の手順では、下記のAWSCLIのバージョンで実行確認を行いました。
$ aws --version aws-cli/1.18.108 Python/3.7.7 Darwin/19.6.0 botocore/1.17.31
認証情報がテンプレート内に含まれている例
まず初めに、認証情報がテンプレートに埋め込まれている例を見てみます。
構成図
テンプレートとデプロイ用コマンド
AWSTemplateFormatVersion: 2010-09-09 Description: "Sample template that contains password in template file" Resources: MyRDSInstance: Type: AWS::RDS::DBInstance DeletionPolicy: Delete Properties: AllocatedStorage: 20 DBInstanceClass: db.t2.micro Engine: mysql MasterUsername: "admin" MasterUserPassword: "password1122" BackupRetentionPeriod: 0 DBInstanceIdentifier: !Sub '${AWS::StackName}-db-instance' DBSecurityGroups: - Ref: MyDbSecurityByCIDRIPGroup MyDbSecurityByCIDRIPGroup: Type: AWS::RDS::DBSecurityGroup Properties: GroupDescription: Ingress for CIDRIP # WARNING: this sg allows all inbound traffic to DB instance DBSecurityGroupIngress: - CIDRIP: "0.0.0.0/0"
$ aws cloudformation create-stack \ > --stack-name techblog-stack-1 \ > --template-body file://template-1.yaml
テンプレートの解説と課題点
上記では、DBのパスワードの値がテンプレートにハードコーディングされてしまっています(12行目)。
テンプレートに認証情報をハードコーディングしないことが重要です。パスワードをコードに埋め込んだままGitレポジトリにアップロードして認証情報が漏洩、などといった事態は避けなくてはなりません。
そのための最も簡単な方法として、CloudFormationのパラメータ機能を使って、デプロイ時の引数で認証情報を与える方法が考えられます。
CloudFormationのパラメータ(NoEcho)で認証情報を設定する例
構成図
テンプレートとデプロイ用コマンド
AWSTemplateFormatVersion: 2010-09-09 Description: "Sample template with password parameter with noecho" Parameters: MasterUserPassword: Type: String Description: Password string for master user of DB Instance NoEcho: true Resources: MyRDSInstance: Type: AWS::RDS::DBInstance DeletionPolicy: Delete Properties: AllocatedStorage: 20 DBInstanceClass: db.t2.micro Engine: mysql MasterUsername: admin MasterUserPassword: !Ref MasterUserPassword BackupRetentionPeriod: 0 DBInstanceIdentifier: !Sub '${AWS::StackName}-db-instance' DBSecurityGroups: - Ref: MyDbSecurityByCIDRIPGroup MyDbSecurityByCIDRIPGroup: Type: AWS::RDS::DBSecurityGroup Properties: GroupDescription: Ingress for CIDRIP # WARNING: this sg allows all inbound traffic to DB instance DBSecurityGroupIngress: - CIDRIP: "0.0.0.0/0"
別途、一時的なテキストファイルを作成し、パスワードの値を保存しておきます(詳しくは後述)。
password1122
$ aws cloudformation create-stack \ > --stack-name techblog-stack-2 \ > --template-body file://template-2.yaml \ > --parameters ParameterKey=MasterUserPassword,ParameterValue=$(cat tmp-secrets.txt) $ # 一時的なテキストファイルの削除 $ shred -u tmp-secrets.txt
テンプレートの解説
CloudFormationのパラメータを使うことで、値をデプロイ時の引数で与えられるようになり、テンプレートに認証情報を埋め込まなくて済みます。
認証情報を直接コマンドで入力してしまうと、認証情報の値がコマンド履歴に残ってしまい、漏洩の可能性があります。
認証情報を一時的にテキストファイル(tmp-secrets.txt
)に保存し、コマンド実行時にファイルから値を読み込んで指定することで、コマンド履歴から認証情報が流出する可能性に対処できます。
AWS CLI を使用してシークレットを保存するリスクの軽減
また、パラメータのプロパティにおいて、NoEcho
の値をtrue
に設定することで、パラメータの値がコンソール、コマンドラインツール、APIなどに表示されないようになります(7行目)。
課題点
しかし、この方法にも以下のような課題があります。
- アプリケーションからデータベースに接続する際に、認証情報を管理するストアが必要になる(あるいはプログラム内に認証情報をハードコーディングする必要がある(非推奨))
解決策として、AWS Systems Manager パラメータストアやAWS Secrets Managerを利用し、AWS上で認証情報を管理する方法があります。
CloudFormationでは、動的な参照を使うことで、上記のサービスに格納された認証情報を簡単かつ安全にCloudFormationのテンプレート内で扱うことができます。
動的な参照
AWS上で認証情報を一元管理する場合、AWS Systems Manager パラメータストアまたはAWS Secrets Managerが利用できます。
さらに、CloudFormationの動的な参照を使うことで、デプロイ時にCloudFormation側で上記のサービスから認証情報の値を取得し、テンプレートの値を置換してくれるため、テンプレートに認証情報の値を埋め込まなくて済みます。
執筆現在(2020年8月)では、暗号化された値の動的参照に関しては、CloudFormationでは以下のリソースをサポートしています。
- AWS Systems ManagerのSecureStringタイプのパラメータ
- AWS Secrets Managerのシークレット
AWS Systems Manager パラメータストアとAWS Secrets Manager。名前も用途も似ていますが、認証情報の生成/更新に関する機能の有無や、CloudFormationテンプレートで管理できるか否かなど、違いがあります。
まず、パラメータストアからサービスの特徴と「動的な参照」の記述方法を見ていきたいと思います。
AWS Systems Managerパラメータストアを使用する例
AWS Systems Manager パラメータストアを使うことで、設定値や認証情報を安全な階層型ストレージで管理することができます。値はプレーンテキストまたは暗号化されたデータとして保存できます。
パラメータストアに格納された値は、CloudFormationの動的な参照によって、テンプレート内で簡単かつ安全に扱うことができます。
構成図
事前準備
今回、認証情報の値をあらかじめパラメータストアに格納しておく必要があります。
前の手順と同様、一時的なテキストファイルを利用することで、パスワードの値がコマンド履歴に残らないようにします。
以下がテキストファイルとコマンドの例になります。
password1122
$ aws ssm put-parameter \ > --type SecureString \ > --name /Techblog/MasterUserPassword \ > --value $(cat tmp-secrets.txt) $ shred -u tmp-secrets.txt
{
“Version”: 1,
“Tier”: “Standard”
}
認証情報は、SecureStringパラメータとして格納することで、値が暗号化されます(2行目)。
パラメータストアでは、パラメータの値はバージョンで管理されています。
新規で作成時はバージョン1から始まり、値を更新するたびにバージョンの値も1ずつ増え、特定のパラメータバージョンを指定して値を取得できます(指定しない場合は最新のバージョンが取得されます)。
AWS Systems Manager ユーザーガイド: パラメータバージョンの使用
テンプレートとデプロイ用コマンド
AWSTemplateFormatVersion: 2010-09-09 Description: "Sample template with dynamic references by AWS Systems Manager" Parameters: MasterUserPasswordSSMParam: Type: String Description: colons-separated parameter name and version for master user's password for DB instance. e.x. parameter-name:1 Resources: MyRDSInstance: Type: AWS::RDS::DBInstance DeletionPolicy: Delete Properties: AllocatedStorage: 20 DBInstanceClass: db.t2.micro Engine: mysql MasterUsername: admin MasterUserPassword: !Sub '{{resolve:ssm-secure:${MasterUserPasswordSSMParam}}}' BackupRetentionPeriod: 0 DBInstanceIdentifier: !Sub '${AWS::StackName}-db-instance' DBSecurityGroups: - Ref: MyDbSecurityByCIDRIPGroup MyDbSecurityByCIDRIPGroup: Type: AWS::RDS::DBSecurityGroup Properties: GroupDescription: Ingress for CIDRIP # WARNING: this sg allows all inbound traffic to DB instance DBSecurityGroupIngress: - CIDRIP: "0.0.0.0/0"
$ aws cloudformation create-stack \ > --stack-name techblog-stack-3 \ > --template-body file://template-3.yaml \ > --parameters \ > ParameterKey=MasterUserPasswordSSMParam,ParameterValue=/Techblog/MasterUserPassword:1
テンプレートの解説
CloudFormationの動的参照を使い、デプロイ時にパラメータストアから認証情報の値を取得、テンプレートの値を置換しています(17行目)
Systems Manager Secure String パラメータの動的参照は、以下のフォーマットで取得対象のリソースを記述します。
'{{resolve:ssm-secure:parameter-name:version}}'
AWS CloudFormation ドキュメント: 動的な参照を使用してテンプレート値を指定する: SSM Secure String パラメータ
version
に関して、現在、CloudFormation側で最新バージョンのパラメータを使用するように指定することはできません。
すなわち、正確なバージョンの値を指定する必要があります。そのため、parameter-name:version
の部分をCloudFormationのパラメータで与える形にしておくと便利です(4-6行目)。
今回のテンプレートでは、17行目の値は、CloudFormationの関数Fn::Subによって{{resolve:ssm-secure:/Techblog/MasterUserPassword:1}}に置換されたのち、CloudFormationの動的参照によって、パラメータストアに格納されている値 password1122 に置換されます。
動的参照による値の置換はスタックのデプロイ中に行われ、パスワードの値がマネジメントコンソールやコマンドラインツール、APIなどに表示されることはありません
課題点
ただ、パラメータストアを使う方法にもいくつか課題があります。
まず、現在、CloudFormation は SecureString パラメータタイプの作成をサポートしていません。
そのため、手動でパラメータストアの認証情報を作成/削除する作業コストが発生し、CloudFormationテンプレートを見てもパラメータストアのリソースを把握できないなど、インフラをコード化するメリットが損なわれてしまいます。
また、SecureStringタイプのパラメータの動的な参照をサポートするリソース/プロパティは限られています。
例えば、RDSのDBインスタンスを作成する場合、SecureStringタイプのパラメータはパスワード(MasterUserPassword)のプロパティの動的参照しかサポートしていないため、ユーザー名(MasterUsername)など、他のプロパティでも動的参照を行いたいケースには対応できません。
AWS CloudFormation ユーザーガイド: Secure String のための動的なパラメータパターンをサポートするリソース
これらの課題点を解決するには、AWS Secrets Managerが使用できます。
AWS Secrets Managerを使用する例
AWS Secrets Managerは、前述のパラメータストアと同様、AWS上で認証情報を暗号化して一元管理でき、APIを使ってアプリケーション内で認証情報を取得できる機能を提供します。
しかし、Secrets Managerはパラメータストアに比べて、以下のように認証情報の扱いにより特化した機能を持っています。
- CloudFormationのテンプレートでSecrets Managerのリソースを記述できるため、認証情報のリソースもインフラコード化の対象に含められる
- CloudFormationのテンプレートで認証情報の自動生成・自動ローテーションの設定を記述できる
構成図
テンプレートとデプロイ用コマンド
AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: "Sample template with dynamic references by AWS Secrets Manager" Resources: MyRDSSecret: Type: "AWS::SecretsManager::Secret" Properties: Description: "This is a Secrets Manager secret for an RDS DB instance" GenerateSecretString: SecretStringTemplate: '{"username": "admin"}' GenerateStringKey: "password" PasswordLength: 16 ExcludeCharacters: '"@/\' MyRDSInstance: Type: AWS::RDS::DBInstance Properties: AllocatedStorage: "20" DBInstanceClass: db.t2.micro Engine: mysql MasterUsername: !Sub '{{resolve:secretsmanager:${MyRDSSecret}:SecretString:username}}' MasterUserPassword: !Sub '{{resolve:secretsmanager:${MyRDSSecret}:SecretString:password}}' BackupRetentionPeriod: 0 DBInstanceIdentifier: !Sub '${AWS::StackName}-db-instance' DBSecurityGroups: - Ref: MyDbSecurityByCIDRIPGroup MyDbSecurityByCIDRIPGroup: Type: AWS::RDS::DBSecurityGroup Properties: GroupDescription: Ingress for CIDRIP # WARNING: this sg allows all inbound traffic to DB instance DBSecurityGroupIngress: - CIDRIP: "0.0.0.0/0" SecretRDSInstanceAttachment: Type: "AWS::SecretsManager::SecretTargetAttachment" Properties: SecretId: !Ref MyRDSSecret TargetId: !Ref MyRDSInstance TargetType: AWS::RDS::DBInstance SecretRDSRotationSchedule: Type: AWS::SecretsManager::RotationSchedule DependsOn: SecretRDSInstanceAttachment Properties: RotationLambdaARN: !GetAtt SecretsManagerRDSMySQLRotationSingleUser.Outputs.RotationLambdaARN RotationRules: AutomaticallyAfterDays: 1 SecretId: !Ref MyRDSSecret SecretsManagerRDSMySQLRotationSingleUser: Type: AWS::Serverless::Application Properties: Location: ApplicationId: arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser SemanticVersion: 1.1.58 Parameters: endpoint: !Sub "https://secretsmanager.${AWS::Region}.amazonaws.com" functionName: MyLambdaRotaionFunction
$ aws cloudformation create-stack \ > --stack-name techblog-stack-4 \ > --template-body file://template-4.yaml \ > --capabilities CAPABILITY_AUTO_EXPAND CAPABILITY_IAM
テンプレートの解説
CloudFormationでの扱いやすさ
上のように、Secrets Managerのリソースは、CloudFormationテンプレートで作成・管理することができます。
さらに、認証情報の自動生成もサポートしており、GenerateSecretString
でパスワードの生成方法を細かく指定することが可能です(9-13行目)。
AWS CloudFormation ユーザーガイド: AWS::SecretsManager::Secret
生成された認証情報は動的参照によってテンプレート内で取得することができます(20-21行目)。
動的参照の記述方法はパラメータストアの場合と似ています。
{{resolve:secretsmanager:secret-id:secret-string:json-key:version-stage:version-id}}
AWS CloudFormation ユーザーガイド: 動的な参照を使用してテンプレート値を指定する: Secrets Manager のシークレット
バージョンに関するパラメータとしてversion-id
とversion-stage
があります。
Secrets Managerもパラメータには複数のバージョンが存在し、ステージングラベルを指定し、過去のバージョンの値を取得することが可能です。
version
に関する上記のパラメータを指定しない場合、デフォルトで version-stage
に AWSCURRENT
が指定されたものとして、最新のシークレットの値を取得します。
動的参照でパラメータストアから最新バージョンの値を取得しようとすると、認証情報の値を更新するたびに version
の値を1ずつ増やしていく必要がありましたが、Secrets Managerではその必要がありません。
後述の認証情報の自動ローテーションも考慮すると、基本的に最新のシークレット(AWSCURRENT
)を指定する方法で良いかと思います。
MasterUsername: !Sub '{{resolve:secretsmanager:${MyRDSSecret}:SecretString:username}}' MasterUserPassword: !Sub '{{resolve:secretsmanager:${MyRDSSecret}:SecretString:password}}'
また、パラメータストア(ssm-secure)の動的参照では、サポートしているリソース/プロパティが限られていました。
一方、Secrets Managerでは、全てのリソースのプロパティで動的参照が使えます。
今回のテンプレートでも、DBインスタンスのパスワード(MasterPassword)に加えて、ユーザー名(MasterUsername)のプロパティでも動的参照で値を設定しています(20行目)。
使える幅が広がる一方で、プロパティによっては、設定された値がユーザーに見える場合あるため、シークレットの値が誤って露出しないように、設定値の可視性に注意する必要があります。
AWS CloudFormation ユーザーガイド: Secrets Managerのシークレットに動的パラメータを使用する際の重要な考慮事項
認証情報の自動ローテーション
さらに、Secrets Managerを使うことで、認証情報の自動ローテーションが設定できます。
認証情報はSecrets Managerで一元管理し、プログラムからは実行時にAPIで認証情報を取得する形に統一することで、認証情報の定期的な自動更新と、それによる漏洩時の侵害リスクの低下が実現できます。
AWS Secrets Manager > ユーザーガイド: AWS Secrets Manager シークレットの更新
今回は、AWS Serverless Application Repositoryで公開されているSecretsManagerRDSMySQLRotationSingleUserを活用しました。
上記のアプリケーションをAWS::Serverless::ApplicationのリソースとしてCloudFormationテンプレートに組み込むことで、認証情報の自動ローテーションの設定もCloudFormationテンプレート内で完結させることができます。
SecretRDSRotationSchedule: Type: AWS::SecretsManager::RotationSchedule DependsOn: SecretRDSInstanceAttachment Properties: RotationLambdaARN: !GetAtt SecretsManagerRDSMySQLRotationSingleUser.Outputs.RotationLambdaARN RotationRules: AutomaticallyAfterDays: 1 SecretId: !Ref MyRDSSecret SecretsManagerRDSMySQLRotationSingleUser: Type: AWS::Serverless::Application Properties: Location: ApplicationId: arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser SemanticVersion: 1.1.58 Parameters: endpoint: !Sub "https://secretsmanager.${AWS::Region}.amazonaws.com" functionName: MyLambdaRotaionFunction
指定した頻度(AutomaticallyAfterDays)でLambda関数がトリガーされ、データベースの認証情報を更新し、更新前後の値をSecrets Managerに格納します。
Secrets Managerでは、DBの種類ごとにローテーション用のLambda関数が事前に用意されています。
AWS Secrets Manager ユーザーガイド: Lambda ローテーション関数の作成に使用できる AWS テンプレート
注意点としては、認証情報の更新時には、ホスト名やポート番号など、DBエンジンごとに指定の情報があらかじめSecrets Managerに格納されている必要があります。
各DBエンジンごとにどのような設定情報が必要とされるかについては、上記のローテーション関数のドキュメントがわかりやすいです。
例えば、RDS MySQL シングルユーザのシークレットでは、ローテーション時に以下の値がシークレットに保存されている必要があります。
{ "engine": "mysql", "host": "<required: instance host name/resolvable DNS name>", "username": "<required: username>", "password": "<required: password>", "dbname": "<optional: database name. If not specified, defaults to None>", "port": "<optional: TCP port number. If not specified, defaults to 3306>" }
そのためには、CloudFormationのSecretTargetAttachment のリソースを作成し、DBインスタンスのホスト名やポート番号の情報がSecrets Managerのプロパティに追加されるように設定できます。
SecretRDSInstanceAttachment: Type: "AWS::SecretsManager::SecretTargetAttachment" Properties: SecretId: !Ref MyRDSSecret TargetId: !Ref MyRDSInstance TargetType: AWS::RDS::DBInstance
これにより、CloudFormationのスタックのデプロイ時は、まず、認証情報のシークレットが生成され(Secret)、次にその認証情報でDBが作成され(DBInstance)、作成されたDBの情報がシークレットに追加され(SecretTargetAttachment)、ローテーションのLambda関数が作成され(ServerlessApplication)、パスワードの初回ローテーションが行われるという流れで処理が進みます。
実際に作成されるシークレットの例です。
このように、Secrets Managerを活用することで、CloudFormationによる管理と認証情報のさらなる保護が可能になります。
課題点
ただし、一点考慮が必要なポイントがあります。
前述のパラメータストアにはパラメータ層という概念があり、高機能なアドバンストパラメータではなく、スタンダードパラメータとして値を保存する場合、認証情報の格納と取得に料金がかかりません。
AWS Systems Manager の料金
一方、Secrets Managerはデータの格納と取得に料金が発生し、シークレットあたり 0.40USD/月
、10,000 件の API コールあたり0.05USD
の料金が発生します。 (執筆時点)
AWS Secrets Manager の料金
ただし、Secrets Managerのデータ取得コストを削減するために、AWS が開発したオープンソースのクライアント側キャッシングコンポーネントを使用する方法もあります。
上記のキャッシュの利用も含め、パラメータストアとSecrets Managerの比較では、機能面に加えてコスト面の観点も必要になります。
リソースの後始末
コスト削減のため、検証用に作成したリソースを削除しておきます。
$ aws cloudformation delete-stack --stack-name techblog-stack-1 $ aws cloudformation delete-stack --stack-name techblog-stack-2 $ aws cloudformation delete-stack --stack-name techblog-stack-3 $ aws cloudformation delete-stack --stack-name techblog-stack-4
まとめ
この記事では、RDSのDBインスタンスを作成するテンプレートを使い、CloudFormationで認証情報を扱う方法を比較しました。
- 認証情報はテンプレート内にハードコーディングしない
- 動的な参照を使うことで、デプロイ時にCloudFormation側で認証情報を取得し、テンプレートの値が置換されるため、認証情報をコードに埋め込まずに済む
- AWS Systems Manager パラメータストアを使えば、認証情報の一元管理や動的な参照が可能だが、CloudFormationのテンプレート内で管理できず、動的参照が使えるプロパティも限られている
- AWS Secrets Managerを使うことで、認証情報の一元管理と動的な参照が可能な上、認証情報の自動生成と自動ローテーションもCloudFormationテンプレートで記述できるため、セキュリティ対策とインフラのコード化をさらに押し進められる
- AWS Systems Manager パラメータストアとAWS Secrets Managerの比較では、機能面に加えてコスト面の観点も必要になる
インフラのコード化をセキュアに進める際は、ぜひAWS CloudFormationとAWS Secrets Managerの活用を検討してみてください。
テックブログ新着情報のほか、AWSやGoogle Cloudに関するお役立ち情報を配信中!
Follow @twitter2017年4月、NHNテコラスに新卒入社。データサイエンスチームに所属し、AWSを活用したデータ分析サービスの設計開発を担当。
Recommends
こちらもおすすめ
Special Topics
注目記事はこちら
データ分析入門
これから始めるBigQuery基礎知識
2024.02.28
AWSの料金が 10 %割引になる!
『AWSの請求代行リセールサービス』
2024.07.16