秘密情報を扱うとき、マスキングを最初の防御にしてはいけない

秘密情報を扱うとき、マスキングを最初の防御にしてはいけない

APIキーやDBのパスワードのような秘密情報を扱うとき、多くの現場では「表示するときはマスキング(redact)すればいい」という考え方がまず出てきます。ログに出す前に伏せ字にする、画面に出す前に一部を***に置き換える。一見もっともらしい対策ですが、これを最初の防御ラインにするのは危険です。

複数のAIエージェントが並行してコードやインフラ操作を行う運用を検証していたとき、この順序の取り違えが実際に事故を招く場面を確認しました。今回はそこで整理した原則を、一般化した形で紹介します。

起きた失敗: 決め打ちの正規表現は素通りする

DB接続文字列に含まれるパスワードをマスキングするためのsedが、次のような決め打ちのパターンで書かれていました。

postgres://<user>:<value>@<host>/<db>

このスキーム名(postgres://)を固定した正規表現で認証情報部分を潰す実装になっていたのですが、実際のデータのスキームはpostgresql://でした。1文字違うだけでパターンが一致せず、マスキング処理は何もせず素通りし、認証情報を含む接続文字列がそのまま出力に残ってしまいました。

これは特殊な不運ではなく、決め打ちパターンによるマスキングが構造的に抱える弱点です。スキームの表記ゆれ、キー名の命名規則の違い、想定していなかったフォーマット。マスキング対象の「形」をあらかじめ全て言い当てることは、実質的に不可能です。

第一防御は「そもそも出さないこと」

ここから導かれる結論は単純です。マスキングは最後の保険であって、最初の防御にしてはいけません。優先すべきは、秘密情報を展開・表示するコマンドそのものを実行しないことです。

具体的には、以下のような操作を避けます。

  • 設定ファイルの内容を環境変数展開込みでそのまま表示するコマンド
  • .envのようなシークレットを含むファイルを丸ごとcatlessする操作
  • シークレットを読み込んだシェルでenvprintenvを無条件に実行する操作
  • シークレット変数をそのままechoする操作

こうした操作は、たとえ後段にマスキング処理を挟んでいたとしても、まず候補から外します。「マスキングできるかどうか」を検討する前に、「そもそも実行しない」という判断を先に下すということです。マスキングが必要になっている時点で、すでに秘密情報を出力へ流す設計になっている、と捉えるべきです。

やむを得ず値の一部に触れる作業が必要な場合は、次のような代替手段を優先します。

  • ビルドや構文チェックなど、値を解決・展開しない操作に限定する
  • ダミー値に差し替えた一時的な設定ファイルで動作確認する
  • 出力する情報を件数・成功可否・HTTPステータスコードなど、値そのものを含まないものに絞る

どうしてもマスキングが必要なとき

設定ファイルの一部を見せる必要があるなど、マスキングを避けられない場面もあります。その場合に最低限守るべき条件が二つあります。

一つは、スキームや命名規則を決め打ちしない汎用パターンを使うことです。接続文字列であれば特定のスキーム名に依存せず、スキーム名://<ユーザー名>:<値>@という構造そのものにマッチするパターンを使います。設定ファイルの記法(KEY=VALUE形式か、'key' => 'value'のような別言語の記法か)が複数あるなら、それぞれに対応したパターンを個別に用意する必要があります。一つの記法用のパターンだけを当てて「マスキングした」と思い込むのが、最初の事故の再発パターンです。

もう一つは、マスキング処理を実行した後に、危険なパターンが本当に残っていないかを機械的に検証することです。目視での確認に頼らず、たとえば「ユーザー名:パスワード@という形が出力に残っていないか」をパターンマッチで検査し、残っていれば処理を止めて出力させない、という一段構えを入れます。マスキングは「した」ことより「効いた」ことを確認して初めて対策として成立します。

DB接続情報は引数に載せない

秘密情報を扱う作業でもう一つ有効なのは、そもそも値をコマンドライン引数や標準出力に載せない構成にすることです。

たとえばDBへの接続確認で、接続文字列をコマンドの引数にそのまま渡すと、psコマンドの出力やシェルのデバッグトレース(bash -xなど)にも値が現れてしまいます。これを避けるため、接続情報は環境変数経由で渡し、コマンドライン自体には値を一切含めない形にします。

set -a
. /path/to/.env
set +a

export PGHOST="$DB_HOST"
export PGUSER="$DB_USER"
export PGDATABASE="$DB_NAME"
# 認証情報用の環境変数(クライアントの公式ドキュメントで定義された名前)も同様にexportする

psql -tAc "SELECT count(*) FROM some_table;"

接続文字列を組み立てて引数に渡す方式は避け、値をシェル変数として保持したまま各種の環境変数へ流し込みます。認証情報に対応する変数名は、利用するクライアントの公式ドキュメントで定義されている名前をそのまま使います。使い終えた環境変数はコマンド完了後に速やかに解除し、後続の処理へ持ち越さないようにします。確認のための出力も、値そのものではなく件数や真偽値にとどめます。

設定ファイルの値を書き換える場合も同様に、変更後の値を標準出力に出さない方法(対象行だけを置換するsed -iなど)を使い、コマンド文字列の中に新しい値を平文で残さないようにします。

値を見せずに一致を確認する

秘密情報を扱う作業でしばしば必要になるのが、「二つの値が一致しているか」の確認です。たとえばハードコードされていた値と、移行先の設定ファイルに書いた値が同じかどうか、といった場面です。

このとき、両方の値を画面に表示して見比べるのは避けるべきです。代わりに、それぞれの値のSHA-256ハッシュを計算し、ハッシュ同士が一致するかどうかだけで判定します。値そのものは一度も表示されず、ハッシュという別の値の一致・不一致だけが結果として残ります。

printf '%s' "$value_a" | sha256sum
printf '%s' "$value_b" | sha256sum

出力される二つのハッシュ文字列を比較し、一致していれば値も一致している、という判定に使います。移行作業の検証を「値を見て確認する」から「ハッシュを見て確認する」に置き換えるだけで、確認作業そのものが漏洩経路になるリスクを避けられます。

「確かめるために見る」こと自体がリスクになる場合

もう一つ興味深い論点があります。ある記録が実際に秘密情報を含んでいるのか、それとも似た形式のダミーデータや訓練用のサンプル文言による誤検知なのかを判定したいとき、多くの人はまず中身を見て確認しようとします。しかし、その「確認のための閲覧」自体が、新たな閲覧者を秘密情報にさらす行為になってしまいます。

ここには非対称性があります。誤検知だったものを内容を見ずに一括でマスキングしても、失うものはほとんどありません(せいぜい無害なテキストが伏せ字になるだけです)。一方で、実漏洩かどうかを判定するために中身を確認してしまうと、それが実漏洩だった場合、確認した人自身が新たな露出の当事者になります。

判断コストとリスクがこれほど非対称であれば、答えは明確です。件数が多く、閲覧して個別判定するコストが高い場合には、疑わしいものは全て無閲覧で一括マスキングしてしまう方が安全側に倒れます。「本当に危険なものだけ狙って処理する」という発想自体が、閲覧という追加のリスクを生んでいることに注意が必要です。

まとめ

秘密情報の取り扱いで押さえておきたい順序は次の通りです。

1. **まず、そもそも秘密を展開・表示するコマンドを実行しない設計にする。** マスキングは検討の起点にしない。 2. **マスキングが避けられない場合は、汎用パターンを使い、実行後に残存確認を機械的に行う。** 決め打ちパターンは素通りするリスクを常に抱える。 3. **接続情報などは環境変数経由で渡し、コマンドライン引数や標準出力に値を載せない。** 確認出力は件数・真偽値にとどめる。 4. **値の一致確認はハッシュで行い、値そのものを画面に出さない。** 5. **誤検知かどうかの判定コストが高い場合は、閲覧せず一括で安全側に倒す判断も選択肢に入れる。**

これらは特別なツールを必要としない、手順と習慣の話です。複数の人間やAIエージェントが同じ設定ファイルやDBに触れる運用が増えるほど、「見せてから隠す」のではなく「最初から見せない」設計を基本に据えることの価値は大きくなります。