🍱

Secrets Managerのシークレットはどんな粒度で分けるべき?

に公開2

はじめに

Secrets Managerでは、1つのシークレットの中でJSON形式を用いて複数のKey-Valueが登録できます。
どんな粒度で分けるべきか疑問に思い調べたところ、意外と情報が見つからなかったので書いてみました。

公式ドキュメントを探索してみる

ベストプラクティスは?

ベストプラクティスのページを見ても、分け方の粒度について明示的な案内はありません。
ただし、このページではいくつかのヒントを得ることができます。

https://docs.aws.amazon.com/secretsmanager/latest/userguide/best-practices.html

「Limit access to secrets」には、最小権限の原則に従うよう書かれています。
また、「Rotate your secrets」では定期的なローテーションを推奨するよう書かれています。

つまり、アクセス制限やローテーションを行うことを考慮すると、 用途とライフサイクルが異なる秘密情報を1シークレットにまとめるべきではない と考えられます。

具体例

あわせて、公式ドキュメントの他のページを見ると、いくつかの例を見つけられます。

APIキー

こちらの例では、外部サービスのAPI利用に必要な情報が1シークレットにまとめられています。

https://docs.aws.amazon.com/secretsmanager/latest/userguide/api-keys-security-sensitive.html

{
  "apiKey": "your-api-key-value",
  "apiKeyId": "key-identifier",
  "endpoint": "https://api.example.com/v1",
  "provider": "example-service"
}

DB接続情報

こちらの例でも同様に、DB接続に必要な情報がまとめられています。

https://docs.aws.amazon.com/secretsmanager/latest/userguide/whats-in-a-secret.html

{
  "host"       : "ProdServer-01.databases.example.com",
  "port"       : "8888",
  "username"   : "administrator",
  "password"   : "EXAMPLE-PASSWORD",
  "dbname"     : "MyDatabase",
  "engine"     : "mysql"
}

適切に分けないとどんな不都合があるのか

もし、役割もライフサイクルも異なる秘密情報を1つのシークレットにひとまとめにしてしまうと、どのような問題が起こりうるでしょうか。

最小権限にならない

例えば、1つのシークレット all_secrets に「DBの接続情報」と「外部サービスのAPIキー」を含めたとします。
この環境で、DB接続を行うLambda関数を作りたいと思って、Lambda関数のRoleに all_secrets の読み取り権限を付与すると、DBの接続情報だけでなくAPIキーも読み取れるようになってしまいます。

このように、本来なら不要な情報まで読み取れることになり、過剰な権限付与となります。

IaCで差分が確認しづらい場合もある

Secrets Managerのバージョン管理はJSON全体がひとまとまりの単位です。
そのため、JSON内の1アイテムだけを編集した場合、利用するIaCツールによっては差分の内訳が見えないことがあります。

実際に、CloudFormationとTerraformで試してみたところ、 Terraformのみ差分の内訳が見えない という結果になりました。

CloudFormation

検証のための簡単なテンプレートを用意し、デモを行います。

デモ手順

手元にテンプレートを作成します。
DbPassword はパラメータ一覧で内容が見えないよう、 NoEcho: true とします。

cfn-secret-demo.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Secrets Manager demo

Parameters:
  DbHost:
    Type: String
    Default: db.example.com
    Description: db hostname
  DbUser:
    Type: String
    Default: admin
    Description: db user
  DbPassword:
    Type: String
    NoEcho: true
    Description: db password

Resources:
  DemoSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: cfn-changeset-demo-secret
      Description: demo secret
      SecretString: !Sub |
        {
          "host": "${DbHost}",
          "username": "${DbUser}",
          "password": "${DbPassword}"
        }
      Tags:
        - Key: Purpose
          Value: changeset-demo

スタックを作成します。
DbPassword の初期値は my-secret-pass とします。

aws cloudformation create-stack \
    --stack-name cfn-secret-demo \
    --template-body file://cfn-secret-demo.yaml \
    --parameters ParameterKey=DbPassword,ParameterValue=my-secret-pass \
    --profile sandbox-profile

DbPassword の値を new-secret-pass に変更するよう、変更セットを作成します。

aws cloudformation create-change-set \
    --stack-name cfn-secret-demo \
    --change-set-name update-password-only \
    --template-body file://cfn-secret-demo.yaml \
    --parameters ParameterKey=DbPassword,ParameterValue=new-secret-pass \
    --profile sandbox-profile

作成された変更セットをコンソールから見ると、行単位で差分が確認できます。

JSONの変更行がハイライトされている
JSONの変更行がハイライトされている

DbPasswordNoEcho: true ですが、あくまでパラメータ一覧でマスクされるというだけで、変更セットでは平文で内容が見えます。
ですが、これは「あくまで現時点での挙動」と考えるべきでしょう。シークレットの読み取り権限の有無にかかわらず、変更セットの読み取り権限があるだけでシークレットの内容を読めてしまうことになるので、セキュリティ的に将来ここも隠蔽される可能性はあると思います。
CloudFormationを利用する場合も、適切な粒度で分けておいて損はないはずです。

Terraform

同様にデモを行います。

デモ手順

tfファイルを作成します。

terraform/main.tf
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region  = "ap-northeast-1"
  profile = "sandbox-profile"
}

variable "db_host" {
  type        = string
  default     = "db.example.com"
  description = "データベースホスト名"
}

variable "db_user" {
  type        = string
  default     = "admin"
  description = "データベースユーザー名"
}

variable "db_password" {
  type        = string
  sensitive   = true
  description = "データベースパスワード"
}

resource "aws_secretsmanager_secret" "demo" {
  name        = "tf-plan-demo-secret"
  description = "Terraform plan確認用デモシークレット"

  tags = {
    Purpose = "plan-demo"
  }
}

resource "aws_secretsmanager_secret_version" "demo" {
  secret_id = aws_secretsmanager_secret.demo.id
  secret_string = jsonencode({
    host     = var.db_host
    username = var.db_user
    password = var.db_password
  })
}

db_password に値を付与するための tfvars を作成します。

terraform/terraform.tfvars
db_password = "my-secret-pass"

初期化と初回applyを実行します。

cd terraform
terraform init
terraform apply

passwordだけ変更してplanを確認します。
今回は確認手順を簡略化するため terraform.tfvars は編集せず、引数で上書きしました。

terraform plan -var='db_password=new-secret-pass'

plan結果を見ると、 secret_string に差分があることだけは分かりますが、内容は (sensitive value) とマスクされています。

Terraform will perform the following actions:

  # aws_secretsmanager_secret_version.demo must be replaced
-/+ resource "aws_secretsmanager_secret_version" "demo" {
      ~ arn                  = "arn:aws:secretsmanager:ap-northeast-1:000000000000:secret:tf-plan-demo-secret-XXXXXX" -> (known after apply)
      + has_secret_string_wo = (known after apply)
      ~ id                   = "arn:aws:secretsmanager:ap-northeast-1:000000000000:secret:tf-plan-demo-secret-XXXXXX|terraform-20260825140021818400000002" -> (known after apply)
      ~ secret_string        = (sensitive value) # forces replacement
      ~ version_id           = "terraform-20260825140021818400000002" -> (known after apply)
      ~ version_stages       = [
          - "AWSCURRENT",
        ] -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

内訳が分からない以上、そのplan結果が妥当かの判断は「今回編集したシークレットか?」という粒度でしか量れません。あらゆる秘密情報が1シークレットにまとまっていると、もしドリフトがあっても気付きにくくなります。
つまり、Terraformでは適切な粒度で分けることがより重要であるといえます。

まとめ

セキュリティやコード管理の観点から、Secrets Managerのシークレットは「同じ役割・同じライフサイクル」の粒度で分けることをおすすめします。

GENDA

Discussion

Y.AY.A

CloudFormationとTerraformで実際に検証してTerraformだけJSON差分の内訳が見えないという結果を出しているのが実用的でした。粒度を分ける理由が「最小権限」だけでなく「IaCでの差分の見やすさ」にも及んでいる視点、参考になります。