APIキーをバックエンドでセキュアに保持する

概要
外部APIのAPIキーを保持する、マルチテナント型SaaSを開発されている方も多数いらっしゃるかと思います。
バックエンドとしてAPIキーをどのように保持すべきか?考えてみました。
設計方針
設計方針としては、以下になるかと思います。
- シークレットは「読み取り専用」に絞る
- APIキーは書き換えや参照が頻繁に行われるものではありません。アプリケーションから必要に応じて「読む」だけでよいので、データベースやファイルに平文で置かず、読み取り専用の仕組みを用意します。
- 平文配置した瞬間に、鍵は暴かれるリスクが高まります。どれほどセキュアなサーバーでも、不慮のミスや権限の漏れはゼロにはできません。だからこそ、「読むためには真正性を示し、限定的にしか触れられない場所に格納する」ことが大前提です。
- テナントごとに「分離」を徹底する
- マルチテナント環境では、A社のAPIキーをB社から参照されてはいけません。
- テナントIDをベースに「名前空間」を分ける、あるいはテナントごとに別のシークレットストアを使うなど、キーの保存先を論理的・物理的に分離しましょう。
- 暗号化キー(KMS)は別管理
- シークレットそのものを暗号化して保存する場合、暗号化/復号の鍵(KMSキー)はアプリケーションサーバーとは別の場所で管理します。
- クラウド利用なら「AWS KMS」「Azure Key Vault」「GCP Cloud KMS」といったマネージドKMSを活用し、何があっても平文の鍵が流出しない構造を作ります。
- アクセス権限は「最小権限」に絞る
- アプリケーション本体(バックエンド)がシークレットを取りに行く際は、「読む権限」しか与えないIAMロール/サービスプリンシパルを用意します。
- 本番とは別の環境(ステージング・開発)を用意するなら、それぞれに異なるキーを発行し、本番のキーには一切触れられないようにします。
- 監査ログを有効化する
- 誰がいつ、どのテナントのどのシークレットにアクセスしたのかを必ずログに残す。
- もし不正にアクセスを試みる攻撃者がいても、ログをたどることで侵入経路や責任所在を明確にできます。
- 可能であれば、定期ローテーションを組み込む
- 「一度発行したAPIキーはずっと使い続ける」運用は危険です。
- 毎月または四半期に一度、(あるいはテナントの要望に応じて)自動的に新鍵に切り替える仕組みを用意しましょう。
- ローテーションといっても面倒ではありません。秘密情報の扱いを自動化し、心にゆとりを持った運用を実現しましょう。
2. 具体的な実装パターン例
ここでは代表的な3つのアプローチを示します。どれもメリット・デメリットがありますので、自社の運用規模やクラウドサービスの採用状況に合わせて選択してください。
2.1. クラウドシークレットマネージャ(AWS Secrets Managerなど)の活用
構成イメージ
┌───────────────────┐
│ クラウドKMS/KV │ ← APIキーを暗号化して保管。例: AWS Secrets Manager 、
│(AWS Secrets │ Azure Key Vault、GCP Secret Manager 等
│ Manager など) │
└───────────────────┘
↑
│(暗号化/復号リクエスト)
│
┌───────────────────┐
│ バックエンド │ ← シークレットを読む専用のIAMロールを付与
│ (APIサーバー等) │
│ │
└───────────────────┘
- テナントごとにシークレットを作成する
- 例えば AWS Secrets Manager なら、シークレット名を
/saas/myapp/tenant-{tenant_id}/external-api-key
のように命名します。 - Azure Key Vault も同様に「VaultName/tenant-{tenant_id}-api-key」という形で分ければよいでしょう。
- 例えば AWS Secrets Manager なら、シークレット名を
- 読み取りはSDK経由で行う
- バックエンドからはAWS SDK(Java/Python/Node.js など)や Azure SDK を通して「SecretId(名前)」を指定して暗号化済みのAPIキーを取り出す。
- SDK呼び出しの際、実行環境のInstance Profile や Service Principal に「読み取り権限」だけを与えることで、不正な書き換えや作成を防ぎます。
- ローテーション機能を自動化
- AWS Secrets Manager には「自動ローテーション機能」があり、Lambda関数を組み合わせれば数分で新鍵発行→置き換え→デプロイ完了までを自動化可能です。
- ローテーションの度にテナントオーナーにアラートを送りたい場合は、SNS やメール連携を活用するとよいでしょう。
- メリット
- マネージドサービスなのでオンプレでVaultを立てる手間が不要
- KMSキーやアクセス権周りをクラウドベンダーが管理してくれる安心感
- 監査ログやローテーション機能がデフォルトで使える
- デメリット・懸念点
- クラウドベンダーのサービスロックインを招きやすい
- コストがテナント数×シークレット数でかさむ可能性がある
- クラウド側の障害が直撃すると、一時的にAPIキー取得自体ができなくなるリスクがある
実装ポイント(AWS例)
1. IAMロール作成
- 名前: MyApp-SecretReaderRole
- ポリシー例 (JSON抜粋):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:/saas/myapp/tenant-*/external-api-key-*"
]
}
]
}
2. シークレット作成
AWS CLI:
$ aws secretsmanager create-secret \
--name "/saas/myapp/tenant-1001/external-api-key" \
--description "テナントID=1001向けの外部APIキー" \
--secret-string '{"api_key":"YOUR_API_KEY_HERE"}'
3. バックエンドでの取得 (Node.js例)
import AWS from 'aws-sdk';
const secretsManager = new AWS.SecretsManager({ region: 'ap-northeast-1' });
async function fetchApiKey(tenantId) {
const secretId = `/saas/myapp/tenant-${tenantId}/external-api-key`;
const data = await secretsManager.getSecretValue({ SecretId: secretId }).promise();
const secretObj = JSON.parse(data.SecretString);
return secretObj.api_key;
}
2.2. HashiCorp Vault のオンプレ/Self-Hosted活用
構成イメージ
┌───────────────────┐
│ HashiCorp Vault │ ← 自社管理のVaultクラスター。TLS必須、複数台クラスタで高可用化
│ (Vault サーバー) │
└───────────────────┘
↑
│(HTTPS経由でVault APIを呼び出し)
│
┌───────────────────┐
│ バックエンド │ ← VaultトークンはVault上でPolicyを設定し、「読むだけ」のRole
│ (APIサーバー等) │
│ │
└───────────────────┘
- 各テナントごとに「パス」を分離
- Vaultのkv(Key-Valueストア)を使う場合、
secret/data/tenant/{tenant_id}/api-key
のようにパスを切る。 - Policyでは、
path "secret/data/tenant/${tenant_id}/*"
に対して「read」権限のみを与えたトークンを発行する。
- Vaultのkv(Key-Valueストア)を使う場合、
- 動的シークレット(Dynamic Secrets)も活用可能
- たとえばデータベース認証情報をVaultで管理している場合、テナントごとに専用のデータベースユーザーを自動発行し、期限付きで使わせることも可能。
- 外部APIキーに対しては動的発行は難しいが、DB周りをVaultに任せることでより堅牢な設計ができます。
- 監査ログ・KMS連携
- VaultはAudit Deviceを有効化すれば、誰がいつどのエンドポイントに何をしたかをすべて記録します。
- バックエンドサーバー上でVault Agentを動かし、ローカルキャッシュを使ってVault呼び出しを高速化しつつ、マスターキーはHSMやクラウドKMSと連携できます。
- メリット
- 完全に自社管理なのでクラウドロックインがない
- Policyでのアクセス制御が細かく、必要に応じて階層的に権限を設計可能
- オンプレ・クラウド問わずデプロイできる
- デメリット・懸念点
- Vaultクラスタを立てて運用するための導入コストと運用負荷が高い
- TLS証明書管理やHA構成、バックアップ戦略を自前で整備しなければならない
- スケールを考えた際のノード追加やバージョンアップ時に手間がかかる
実装ポイント
1. Vault初期設定 (CLI例)
$ vault server -config=/etc/vault/config.hcl ← Vaultサーバーを起動
2. テナントごとのパスにAPIキーを書き込む
$ vault kv put secret/tenant/1001/api-key value="YOUR_API_KEY_FOR_1001"
3. テナント専用のPolicy作成 (policy.hcl例)
path "secret/data/tenant/1001/*" {
capabilities = ["read"]
}
$ vault policy write tenant1001 policy.hcl
4. テナントトークン発行
$ vault token create -policy="tenant1001" -ttl="72h"
→ ここで発行されたトークンをバックエンドに設定。
以降、バックエンドはこのトークンを使って `GET /v1/secret/data/tenant/1001/api-key` を呼び出し、
返ってきた暗号文をVault内で復号して「YOUR_API_KEY_FOR_1001」を取得。
5. バックエンド側コード例 (Go言語例)
import "github.com/hashicorp/vault/api"
func fetchApiKey(tenantID string) (string, error) {
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
return "", err
}
client.SetToken(os.Getenv("VAULT_TOKEN")) // テナント用トークンを環境変数で渡す
secretPath := fmt.Sprintf("secret/data/tenant/%s/api-key", tenantID)
secret, err := client.Logical().Read(secretPath)
if err != nil {
return "", err
}
data := secret.Data["data"].(map[string]interface{})
return data["value"].(string), nil
}
2.3. 暗号化済みDBカラム or KMSで自前実装
クラウドのマネージドサービスを使わず、自前で暗号化ロジックを組み込むパターンです。
「簡易構築」として考えるなら選択肢になりますが、長期的には保守が大変なので注意してください。
構成イメージ
┌───────────────────┐
│ KMS (自社 or │ ← 暗号化キーのみKMS/HSMに保管。アプリからはKMS APIで復号を依頼
│ クラウドKMS) │
└───────────────────┘
↑
│(暗号化/復号リクエスト)
│
┌───────────────────┐
│ バックエンド │ ← テナントごとの暗号文をDBカラムに保存 (encrypted_api_key)
│ (APIサーバー等) │
└───────────────────┘
↓
┌───────────────────┐
│ RDB (MySQL等) │ ← 暗号化済みの文字列だけを持つ
│ encrypted_api_key│
└───────────────────┘
- 保存時のフロー
- テナント登録またはAPIキー登録のエンドポイントで、まずKMSに「暗号化リクエスト」を送り、平文のAPIキーをKMSが暗号化してくれる。
- 暗号化された文字列(encrypted_blob)をDBの
encrypted_api_key
カラムにINSERT。
- 読み取り時のフロー
- DBから
encrypted_api_key
カラムをSELECT して取得。 - KMSに「復号リクエスト」を送り、平文のAPIキーを取得。
- 平文APIキーはメモリ上にのみ存在し、実行が終わったらすぐ破棄する。
- DBから
- メリット
- クラウドマネージドのシークレットマネージャを使わずとも、低コストで仕組みを構築できる
- RDBだけで管理できるので運用がシンプル
- テナントごとのアクセスポリシーはアプリケーションコードで柔軟に設計可能
- デメリット・懸念点
- 自前で暗号化ロジックを回すため、KMSのAPI設計変更や鍵管理を自社でフォローアップする負担が大きい
- 暗号化キーをどう安全に管理するかが別途の課題になる
- KMS→アプリ→DB の往復が発生するため、レスポンスが若干遅くなりやすい
実装ポイント(AWS KMS+MySQL例)
1. KMSキー作成
$ aws kms create-key \
--description "マルチテナントSaaS用外部APIキー暗号化キー" \
--key-usage ENCRYPT_DECRYPT \
--origin AWS_KMS
→ 応答に得られる KeyId (例: “1234abcd-12ab-34cd-56ef-1234567890ab”)
2. テナントAPIキー保存時のフロー (Node.js例)
import AWS from 'aws-sdk';
import mysql from 'mysql2/promise';
const kms = new AWS.KMS({ region: 'ap-northeast-1' });
const connection = await mysql.createConnection({ /* DB config */ });
async function storeApiKey(tenantId, plaintextApiKey) {
// 1) KMSで暗号化
const enc = await kms.encrypt({
KeyId: "1234abcd-12ab-34cd-56ef-1234567890ab",
Plaintext: plaintextApiKey
}).promise();
const encryptedBlob = enc.CiphertextBlob.toString('base64');
// 2) DBに保存
await connection.execute(
'INSERT INTO tenant_secrets (tenant_id, encrypted_api_key) VALUES (?, ?) ' +
'ON DUPLICATE KEY UPDATE encrypted_api_key = VALUES(encrypted_api_key)',
[tenantId, encryptedBlob]
);
}
3. テナントAPIキー読み取り時のフロー
async function fetchApiKey(tenantId) {
// 1) DBから暗号文を取得
const [rows] = await connection.query(
'SELECT encrypted_api_key FROM tenant_secrets WHERE tenant_id = ?',
[tenantId]
);
if (rows.length === 0) throw new Error('APIキー未設定');
const encryptedBlob = Buffer.from(rows[0].encrypted_api_key, 'base64');
// 2) KMSで復号
const dec = await kms.decrypt({
CiphertextBlob: encryptedBlob
}).promise();
return dec.Plaintext.toString('utf-8');
}
3. よくある落とし穴と回避策
- ソースコードにハードコーディングしてしまう
- 「テストだから」「一時的だから」といってコードに
const API_KEY = "hogehoge";
と書いてしまうと、GitHubのリポジトリを誤って公開してしまった、といった際に流出してしまいます。 - Git履歴に残ったままになると、消したつもりでも第三者に参照されるリスクがあります。
- → 絶対にやめましょう。 シークレット情報は1秒たりともコードに“べた書き”すべきではありません。
- 「テストだから」「一時的だから」といってコードに
- 暗号化キーも一緒にコミットする
- 自前暗号化パターンを使う場合、「鍵ファイル」や「秘密鍵」をGitに上げるとアウト。
- → 鍵情報は専用のKMS/HSMに預け、バックエンドからのみAPI経由で利用する設計としてください。
- テナント間アクセスコントロールを後回しにする
- 「とりあえず同一テーブルに鍵を入れておけばいいか」と実装してしまうと、気づかぬうちにA社がB社の鍵を参照できる状態になります。
- → 早い段階で「テナントIDをベースにロジック上もDB上も分離」しておき、開発初期から「このクエリだと他社の鍵を読む恐れがある」というレビューを徹底しましょう。
- ローテーションを考慮せずに運用を開始する
- しばらく運用しているうちに「どの鍵がいつ更新されたか」が分からなくなり、最悪「APIキーが漏洩してから初めて更新する」という事態に陥ります。
- → ローテーション(更新)の仕組みを設計・実装してからリリースしましょう。少なくともマニュアルでもよいので、定期的に鍵を差し替える運用フローを決めておくべきです。
- 監査ログを軽視する
- 「うちの会社は小規模だからログなんていらない」と思っていても、実際に問題が起きた際に「誰がいつ鍵を取りにいったか」が分からなければ、対策の穴を見つけられません。
- → シークレットマネージャやVaultが提供する監査機能をオンにし、最低でも「取り出し」「更新」「削除」の全操作ログを保持しましょう。
結論
クラウドネイティブなシークレットマネージャを使うにせよ、Vaultを立てるにせよ、「鍵は鍵、データはデータ」 と変更頻度が異なるものを別で管理すれば、シンプルで安全な構成になります。