CloudFormationで認証情報を扱うベストプラクティス

AWS

2020.8.7

Topics

はじめに

こんにちは。データサイエンスチームのmotchieです。

AWS CloudFormationを使うことで、YAMLやJSON形式のテンプレートでAWS上のインフラストラクチャを記述・管理することが出来ます。

インフラをコード化することで、以下のような様々な利点が得られます。

  • どのようなリソースがどのような設定値で構築されているか、コードで素早く把握できる
  • デプロイ/ロールバックの作業を自動化でき、作業コストや人為的ミスを減らせる
  • インフラ構成の変更点をコードレビューでき、変更の履歴を残すことができる

インフラをコード化する際には、認証情報の扱い方に注意が必要です。
コード内に認証情報をハードコーディングしてしまうと、漏洩のリスクがあります。
実際、CloudFormationのベストプラクティスでも、テンプレートに認証情報を埋め込まないことが推奨されています。

上記のドキュメントでは、認証情報を保管するストアとしてAWS Systems Manager パラメータストアAWS Secrets Managerが挙げられています。
しかし、これらのサービスを詳しく見てみると、認証情報の生成/更新に関する機能の有無や、CloudFormationテンプレートで管理できるか否かなど、違いがあります

この記事の内容

そこで本記事では、CloudFormationで認証情報を扱う様々な方法を比較していきたいと思います
RDSのDBインスタンスを作成するCloudFormationのテンプレートを使って、以下の4つのケースをご紹介します。

  1. 認証情報がテンプレート内に含まれてしまっている例 (template-1.yaml)
  2. CloudFormationのパラメータ(NoEcho)で認証情報を設定する例 (template-2.yaml)
  3. AWS Systems Managerパラメータストアに認証情報を格納し、CloudFormationの「動的な参照」でテンプレート内の値を置き換える例 (template-3.yaml)
  4. 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 パラメータストアと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-idversion-stageがあります。
Secrets Managerもパラメータには複数のバージョンが存在し、ステージングラベルを指定し、過去のバージョンの値を取得することが可能です。
versionに関する上記のパラメータを指定しない場合、デフォルトで version-stageAWSCURRENT が指定されたものとして、最新のシークレットの値を取得します。

動的参照でパラメータストアから最新バージョンの値を取得しようとすると、認証情報の値を更新するたびに 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の活用を検討してみてください。

motchie

2017年4月、NHNテコラスに新卒入社。データサイエンスチームに所属し、AWSを活用したデータ分析サービスの設計開発を担当。 注力分野は自然言語処理、好きなAWSサービスはEMR、SageMaker。お洒落な音楽が好き。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら