deepseek.ac日本語で学ぶ、実践する

security

DeepSeek APIキーの漏洩を前提にした設計 — 失効とローテーションの実装

· 約10分

リード

APIキーの漏洩は「起きるかどうか」ではなく「いつ起きるか」の問題です。 この記事では、DeepSeek APIキーが漏れた場合に被害を最小化する設計 — キーの分離、失効手順、ローテーションの自動化 — を実装目線で整理します。

前提

注: DeepSeek の API キー管理画面の具体的なUIやローテーション機能の有無は変更されやすいため、実装前に公式ドキュメントで最新仕様を確認してください。本記事は「キー文字列を安全に差し替えられる」ことを前提にした一般設計です。

本文

なぜ「漏洩前提」で設計するのか

「コミットしない」「ログに出さない」といった予防策は必要ですが、それだけでは不十分です。実際の漏洩経路は多様です。

予防と同時に、漏れても短時間で無効化できる状態を作っておくことが重要です。

原則1: キーを用途別に分離する

1つのキーを全環境・全用途で使い回すと、1箇所の漏洩が全体に波及します。

単位 例 目的
環境 dev / staging / prod 開発環境の漏洩が本番に影響しない
用途 対話的利用 / バッチ / 外部公開API 影響範囲の限定
主体 開発者ごと / サービスごと 漏洩元の特定と個別失効

DeepSeek 側で複数キーを発行できる場合、この粒度で分けておくとローテーションも局所化できます。

原則2: キーはコード・環境変数だけで持たない

.env はローカル開発には便利ですが、本番ではシークレットマネージャから動的に取得します。

# 例: AWS Secrets Manager から取得(概念)
import json
import boto3
from functools import lru_cache

@lru_cache(maxsize=1)
def get_deepseek_key() -> str:
    client = boto3.client("secretsmanager")
    secret = client.get_secret_value(SecretId="deepseek/api-key")
    return json.loads(secret["SecretString"])["api_key"]

キャッシュを短命にしておくと、ローテーション後も再起動なしで新キーに追従できます。可能なら TTL 付きキャッシュにします。

原則3: ローテーションを手順化する(二重キー期間を設ける)

キーを差し替える瞬間に「旧キー無効化 → 新キー配布」を同時に行うと、ダウンタイムが発生します。新キーを先に配布し、両方が有効な期間を設けてから旧キーを失効させます。

1. 新キー発行(旧キーはまだ有効)
2. シークレットマネージャの値を新キーに更新
3. 各サービスが新キーを取得したことを確認(TTL経過 or 再デプロイ)
4. 旧キーの使用状況を監視(呼び出しがゼロになったか)
5. 旧キーを失効

各ステップの間に「切り戻せる」余地を残すのがポイントです。

原則4: 失効を「すぐ実行できる」状態にする

漏洩に気づいたとき、失効作業が属人化していると初動が遅れます。

原則5: 漏洩検知と併せて設計する

失効はあくまで「気づいた後」の対応です。気づく仕組みも合わせて用意します。

実装例: ローテーション対応のクライアント

import os
import time
import httpx

class DeepSeekClient:
    def __init__(self, key_provider, ttl_seconds=300):
        self._key_provider = key_provider
        self._ttl = ttl_seconds
        self._cached_key = None
        self._expires_at = 0.0

    def _key(self) -> str:
        now = time.monotonic()
        if self._cached_key is None or now >= self._expires_at:
            self._cached_key = self._key_provider()
            self._expires_at = now + self._ttl
        return self._cached_key

    def chat(self, messages, model="deepseek-chat"):
        # 401/403 を検知したら即座にキャッシュを破棄して再取得
        for attempt in range(2):
            resp = httpx.post(
                "https://api.deepseek.com/chat/completions",
                headers={"Authorization": f"Bearer {self._key()}"},
                json={"model": model, "messages": messages},
                timeout=30,
            )
            if resp.status_code in (401, 403) and attempt == 0:
                self._cached_key = None  # 失効・ローテーションを検知
                continue
            resp.raise_for_status()
            return resp.json()

注: エンドポイントや認証ヘッダの仕様は変更されうるため、実装時は公式ドキュメントで確認してください。上記は「キー失効時に再取得する」パターンの例示です。

つまずきポイント

症状 対処
ローテーション後に全リクエストが 401 になる 旧キー無効化が早すぎる。二重キー期間を設ける
新キーに切り替わらないサービスがある キャッシュ TTL が長すぎる/再デプロイ漏れ。TTL 短縮と台帳で確認
失効したはずのキーが使われ続ける キーが複数箇所にコピーされている。台帳と利用ログで発見
漏洩に気づくのが遅れた シークレット検知・利用量監視を未整備。原則5を参照

まとめ

チェックリスト