なぜSecretsManagerを使うのか?ハードコーディングが招く本当のリスク

「リポジトリはプライベートだからDBパスワードをコードに書いても大丈夫」——この判断が、本番インシデントの引き金になるケースを何度も見てきた。AWS Secrets Managerを使った自動ローテーションの仕組みを理解すれば、なぜハードコーディングが構造的なリスクなのかが明確になる。

TL;DR:Secrets Managerとハードコーディングの比較

観点ハードコーディングSecrets Manager
認証情報の漏洩経路Gitログ、CI/CDログ、コアダンプIAMポリシーで制御、監査ログあり
パスワードローテーション手動。デプロイが必要自動。アプリ再起動不要
アクセス制御コードを読める人全員IAMロール単位で最小権限
監査証跡なしCloudTrailに全API呼び出しを記録
複数環境管理環境ごとに別ファイル管理が必要シークレット名のパスで分離可能

なぜ「プライベートリポジトリだから安全」は成立しないのか

プライベートリポジトリはアクセス制御の一層に過ぎない。問題はそこではなく、認証情報がコードに埋め込まれた瞬間に複数の経路で漏洩リスクが生まれる点にある。

  • Gitの履歴は削除できない:一度コミットされたパスワードは、後からファイルを修正してもgit log -pで復元できる。git filter-repoで履歴を書き換えても、フォークやローカルクローンには残る。
  • CI/CDパイプラインのログ:環境変数として渡した場合でも、デバッグログやエラートレースに平文で出力されることがある。
  • コアダンプとヒープダンプ:アプリケーションがクラッシュした際、メモリ上の文字列がダンプファイルに含まれる。
  • 内部脅威:リポジトリへのアクセス権を持つ開発者が退職した後も、その認証情報は有効なまま残る。
ハードコーディングされたパスワードは、金庫の鍵を金庫の扉に貼り付けているようなものだ。扉が閉まっていても、鍵が見えていれば意味がない。

Secrets Managerはこれらの問題を、認証情報をコードから完全に分離することで解決する。アプリケーションはパスワードそのものを知らず、実行時にAPIを呼び出して取得する。

Secrets Managerの動作メカニズム

Secrets Managerを使うアプリケーションの動作を理解しておくと、ローテーション設計の判断がしやすくなる。

sequenceDiagram participant App as アプリケーション participant IAM as IAM(ロール検証) participant SM as Secrets Manager participant DB as RDS Database App->>IAM: AssumeRole(EC2/Lambda実行ロール) IAM-->>App: 一時認証情報 App->>SM: GetSecretValue('prod/myapp/db-password') SM-->>App: {username, password}(AWSCURRENT) App->>DB: DB接続(取得したパスワードを使用) DB-->>App: 接続成功 Note over App,SM: キャッシュTTL経過後、次回アクセス時に再取得
  1. アプリ起動時の取得:アプリケーションはIAMロールの権限でSecrets Manager APIを呼び出し、シークレット値を取得する。コード内にパスワードは存在しない。
  2. SDKキャッシュ:AWS SDK(特にJava/Python向けのSecrets Manager Caching Client)はシークレットをメモリにキャッシュし、TTL経過後に再取得する。不必要なAPI呼び出しを減らせる。
  3. 自動ローテーション:設定されたスケジュールでLambda関数がトリガーされ、DBパスワードを変更してシークレット値を更新する。アプリ側はキャッシュTTLが切れた後、新しいパスワードを自動的に取得する。
  4. CloudTrail記録:すべてのGetSecretValue呼び出しがCloudTrailに記録される。誰がいつシークレットにアクセスしたかを監査できる。

自動ローテーションの仕組みを具体的に理解する

Secrets Managerの自動ローテーションは、内部的に4フェーズのLambda実行で動作する。このフェーズを知らずに設計すると、ローテーション中に接続エラーが発生する原因がわからなくなる。

graph TD A["ローテーション開始
(スケジュール or 手動)"] --> B["createSecret
新パスワード生成 → AWSPENDING"] B --> C["setSecret
DBに新パスワードを設定"] C --> D["testSecret
AWSPENDING で接続検証"] D -->|成功| E["finishSecret
AWSPENDING → AWSCURRENT に昇格"] D -->|失敗| F["ローテーション中断
AWSCURRENT は変更されない"] E --> G["旧パスワードは AWSPREVIOUS として保持"]
  1. createSecret:新しいパスワード候補を生成し、AWSPENDINGステージとしてシークレットに追加する。この時点でDBはまだ古いパスワードを使用している。
  2. setSecretAWSPENDINGのパスワードをDBに実際に設定する。この瞬間、DBは新旧両方のパスワードを一時的に受け付ける(RDSの場合)。
  3. testSecretAWSPENDINGのパスワードでDB接続を検証する。接続失敗時はローテーションが中断される。
  4. finishSecretAWSPENDINGAWSCURRENTに昇格させ、古いパスワードをAWSPREVIOUSに降格させる。アプリがキャッシュを更新するまでの猶予期間としてAWSPREVIOUSも一定期間有効に保たれる。

ローテーション中の接続断が怖い、という声をよく聞く。だが実際にはAWSPREVIOUSが残るため、キャッシュTTLを適切に設定すれば無停止でローテーションできる。

実装:Secrets Managerからシークレットを取得する

まずシークレットを作成し、アプリケーションから取得する基本的な流れを確認する。

シークレットの作成

# DBパスワードをSecrets Managerに登録する
aws secretsmanager create-secret \
  --name 'prod/myapp/db-password' \
  --description 'Production RDS password for myapp' \
  --secret-string '{"username":"dbadmin","password":"initial-password-here"}' \
  --region us-east-1

シークレット値の取得(CLI確認用)

# シークレット値を取得して確認する
aws secretsmanager get-secret-value \
  --secret-id 'prod/myapp/db-password' \
  --region us-east-1 \
  --query 'SecretString' \
  --output text

Pythonアプリケーションからの取得例

🔽 Pythonコード例(クリックで展開)
import boto3
import json
from botocore.exceptions import ClientError

def get_db_credentials(secret_name: str, region_name: str) -> dict:
    client = boto3.client(
        service_name='secretsmanager',
        region_name=region_name
    )
    try:
        response = client.get_secret_value(SecretId=secret_name)
    except ClientError as e:
        # 呼び出し元でハンドリングさせる
        raise e

    secret = json.loads(response['SecretString'])
    return secret

# 使用例
credentials = get_db_credentials('prod/myapp/db-password', 'us-east-1')
db_user = credentials['username']
db_pass = credentials['password']
# db_passをコードに書かず、ここで初めて取得する

必要なIAMポリシー:最小権限の設計

アプリケーションのIAMロールには、特定のシークレットへのGetSecretValueのみを許可する。ListSecretsDescribeSecretは通常のアプリ実行には不要だ。

🔽 IAMポリシー例(クリックで展開)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGetSpecificSecret",
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/db-password-*"
    }
  ]
}

ARNの末尾に-*を付けているのは、Secrets Managerがシークレット作成時にランダムなサフィックスを付加するためだ。完全一致で指定するとリソースが見つからないエラーになる。

自動ローテーションの設定(RDS対応)

RDS向けのマネージドローテーションを有効化する。Secrets ManagerはRDS用のLambdaローテーション関数を自動でプロビジョニングする。

# RDSインスタンスのシークレットに自動ローテーションを設定する
# rotation-lambda-arnはSecrets Managerがマネージドローテーションで自動作成するため、
# コンソールまたはCloudFormation経由での設定を推奨する

# 既存シークレットのローテーション設定を確認する
aws secretsmanager describe-secret \
  --secret-id 'prod/myapp/db-password' \
  --region us-east-1 \
  --query '{RotationEnabled:RotationEnabled,RotationRules:RotationRules}'

RDS for MySQLやPostgreSQLに対しては、Secrets Managerコンソールから『Rotate immediately』を選択することでマネージドローテーションを有効化できる。この場合、Lambda関数はAWSが管理するため、自前でローテーション関数を実装する必要はない。

実際に踏んだ落とし穴:ローテーション後の接続エラー

症状:ローテーション設定後、翌朝になるとアプリケーションのDB接続が断続的に失敗し始めた。ログにはAccess denied for user 'dbadmin'が散発していた。

最初の誤診:ローテーション自体が失敗していると思い、CloudWatch LogsのLambda実行ログを確認した。ローテーションは正常完了していた。

実際の原因:アプリケーションがDB接続プールを起動時に一度だけ初期化し、Secrets Managerから取得したパスワードをメモリに保持し続けていた。ローテーション後にAWSPREVIOUSの猶予期間(デフォルトでは次のローテーションまで保持)が切れると、古いパスワードは無効になる。しかしアプリは再起動しない限り新しいパスワードを取得しない設計だった。

修正:接続エラー発生時にSecrets Managerから再取得してリトライするロジックを追加した。またはAWS提供のSecrets Manager Caching Clientを使い、TTLベースで定期的にシークレットを更新する設計に変更した。

ローテーションが動いているのに接続が切れる場合、問題はほぼ確実にアプリ側のキャッシュ戦略にある。シークレットの取得タイミングを設計に組み込まないと、ローテーションは機能しているのに障害が起きるという奇妙な状態になる。

シークレットの存在確認とローテーション状態の監視

本番運用では、ローテーションが正常に完了しているかを定期的に確認する必要がある。

# ローテーション履歴と最終ローテーション日時を確認する
aws secretsmanager describe-secret \
  --secret-id 'prod/myapp/db-password' \
  --region us-east-1 \
  --query '{
    RotationEnabled: RotationEnabled,
    LastRotatedDate: LastRotatedDate,
    LastChangedDate: LastChangedDate,
    NextRotationDate: NextRotationDate
  }'
# 全シークレットのローテーション状態を一覧確認する
aws secretsmanager list-secrets \
  --region us-east-1 \
  --query 'SecretList[*].{Name:Name,RotationEnabled:RotationEnabled,LastRotatedDate:LastRotatedDate}' \
  --output table

Secrets Managerを使うべき場面の判断基準

すべての設定値をSecrets Managerに入れる必要はない。判断の基準は明確だ。

graph TD Start["この設定値は機密情報か?"] -->|Yes| Rotate["定期ローテーションが必要か?"] Start -->|No| Audit["アクセス監査が必要か?"] Rotate -->|Yes| SM1["Secrets Manager
自動ローテーション有効化"] Rotate -->|No| SM2["Secrets Manager
ローテーションなし"] Audit -->|Yes| SM3["Secrets Manager
CloudTrail連携"] Audit -->|No| PS["SSM Parameter Store
標準パラメータで十分"]
  1. 機密性:漏洩した場合にシステムへの不正アクセスや法的リスクが生じる値はSecrets Managerへ。
  2. ローテーション要件:定期的な変更が必要な認証情報はSecrets Managerの自動ローテーションが有効。
  3. 監査要件:誰がいつアクセスしたかの証跡が必要な場合はSecrets Manager経由でCloudTrailに記録する。
  4. 非機密の設定値:APIエンドポイントURLやタイムアウト値などはSystems Manager Parameter Storeの標準パラメータで十分なケースが多い。

まとめとネクストステップ:Secrets Managerで認証情報管理を構造化する

ハードコーディングのリスクは「リポジトリが公開されているか」ではなく、「認証情報がコードのライフサイクルに縛られているか」にある。Secrets Managerを使うことで、パスワードの変更がデプロイと切り離され、アクセス制御がIAMで一元管理され、監査証跡がCloudTrailに残る。

次のステップとして以下を検討してほしい:

  • 既存のコードベースで環境変数やコード内に認証情報が含まれていないかgit log -pgrepで確認する
  • 新規アプリケーションではIAMロールベースのアクセスとSecrets Manager取得をデフォルト設計にする
  • RDSを使用している場合はマネージドローテーションを有効化し、アプリ側のリトライロジックを実装する
  • 公式ドキュメント:AWS Secrets Manager ユーザーガイド

用語集

用語説明
AWSCURRENT現在有効なシークレット値のステージラベル。アプリが通常取得する値。
AWSPENDINGローテーション中に生成された新しいパスワード候補のステージラベル。
AWSPREVIOUS直前まで有効だったパスワードのステージラベル。ローテーション後の猶予期間に使用される。
マネージドローテーションAWSがLambda関数を管理し、RDS等のサポート対象サービスのパスワードを自動更新する機能。
リソースベースポリシーシークレット自体にアタッチするポリシー。クロスアカウントアクセス制御に使用する。

Related Posts

コメント

このブログの人気の投稿

EC2 SSH接続タイムアウトの原因と修正方法 — セキュリティグループのインバウンドルール完全ガイド

S3パブリックアクセス拒否の原因と解決策:バケットレベルの「Block Public Access」が優先される仕組み

EC2インスタンスIDをメタデータから取得する方法 — IMDSv2が安全な理由