Secrets Managerのシークレットはどんな粒度で分けるべき?
はじめに
Secrets Managerでは、1つのシークレットの中でJSON形式を用いて複数のKey-Valueが登録できます。
どんな粒度で分けるべきか疑問に思い調べたところ、意外と情報が見つからなかったので書いてみました。
公式ドキュメントを探索してみる
ベストプラクティスは?
ベストプラクティスのページを見ても、分け方の粒度について明示的な案内はありません。
ただし、このページではいくつかのヒントを得ることができます。
「Limit access to secrets」には、最小権限の原則に従うよう書かれています。
また、「Rotate your secrets」では定期的なローテーションを推奨するよう書かれています。
つまり、アクセス制限やローテーションを行うことを考慮すると、 用途とライフサイクルが異なる秘密情報を1シークレットにまとめるべきではない と考えられます。
具体例
あわせて、公式ドキュメントの他のページを見ると、いくつかの例を見つけられます。
APIキー
こちらの例では、外部サービスのAPI利用に必要な情報が1シークレットにまとめられています。
{
"apiKey": "your-api-key-value",
"apiKeyId": "key-identifier",
"endpoint": "https://api.example.com/v1",
"provider": "example-service"
}
DB接続情報
こちらの例でも同様に、DB接続に必要な情報がまとめられています。
{
"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 とします。
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の変更行がハイライトされている
DbPassword は NoEcho: true ですが、あくまでパラメータ一覧でマスクされるというだけで、変更セットでは平文で内容が見えます。
ですが、これは「あくまで現時点での挙動」と考えるべきでしょう。シークレットの読み取り権限の有無にかかわらず、変更セットの読み取り権限があるだけでシークレットの内容を読めてしまうことになるので、セキュリティ的に将来ここも隠蔽される可能性はあると思います。
CloudFormationを利用する場合も、適切な粒度で分けておいて損はないはずです。
Terraform
同様にデモを行います。
デモ手順
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 を作成します。
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のシークレットは「同じ役割・同じライフサイクル」の粒度で分けることをおすすめします。
Discussion
CloudFormationとTerraformで実際に検証してTerraformだけJSON差分の内訳が見えないという結果を出しているのが実用的でした。粒度を分ける理由が「最小権限」だけでなく「IaCでの差分の見やすさ」にも及んでいる視点、参考になります。