0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gitleaksは、日本の機密情報96件を1件も止めなかった -- 180件実測

0
Last updated at Posted at 2026-10-27

gitleaks 8.30.1 に陽性180件を流した結果、トークン類7種84件は全部止まりましたが、日本の機密情報(カード・マイナンバー・DB接続文字列・電話・住所・口座・氏名+生年月日・文中のパスワード)計96件は1件も止まりませんでした。gitleaks は「形の決まった国際トークン」を見るスキャナで、日本の機密情報は見ない設計です。この境界を知らないまま CI に挿すと「安心している穴」が出ます。

秘密スキャンというとまず gitleaks を思い浮かべる人は多いはずです。実際、GitHub PAT や AWS キーのような形の決まったトークンは、既定ルールだけで漏れなく拾ってくれます。問題は、カード番号・マイナンバー・DB 接続文字列のように「形はあるが gitleaks の既定ルールに入っていない」ものです。これらは Luhn や検査用数字で機械的に検知できるのに、既定ルールが見ないので全部素通りします。

数字は、私の実験 iris-lab exp036 「機密ゲート: 正規表現とローカルモデルの分担」(私の実験記録。リポジトリは非公開) で測ったものです。ルールの仕様は gitleaks 8.30.1 の既定ルール仕様 を参照しました。評価セットの324件はすべて架空の値 (data/build_gate_set.py が seed 固定で生成) で、実データではありません。

gitleaks は国際トークン84件を全部止めたが、日本の機密情報96件を1件も止めなかった - iris-lab exp036 実測

  • 別軸との棲み分け: 姉妹記事「話題のJevをローカルLLMで再現、機密情報の見逃し20件がしきい値だけで2件に」はローカル LLM 判定のしきい値調整、「あなたの.envはDockerイメージに焼き込まれ、誰でも抜き出せる」は .env の漏洩経路、「.envは.gitignoreに書いても漏れる:私が実演した3経路」は .env 流出の実演、「GitHubから消した秘密は消えていない」は削除漏洩、「あなたのGitHubコミットは誰でも偽装できる」はコミット偽装、「npmサプライチェーン攻撃Shai-Hulud」は npm 攻撃でした。違いは、「gitleaks の既定ルールが日本の機密情報に対して何を見ないか」と、その穴の埋め方を測った点です
  • 数字の出どころ: 本記事の数字はすべて、私が行った iris-lab exp036 の評価セット (陽性180件 + 陰性144件、15種×12、seed 固定の架空値) に gitleaks 8.30.1 を当てた結果です。合わせて gitleaks 公式リポジトリの既定ルール と、個人情報の保護に関する法律 の定義する「個人情報」の範囲を参照しました

「gitleaks を入れたから秘密スキャンは万全」は、半分だけ正しい

gitleaks を CI に挿した話を書いた記事は、Qiita や Zenn でもよく見かけます。多くは「GitHub PAT / AWS キー / Slack token を検出してくれる」という成功報告です。これは正しい結果で、exp036 の実測でも gitleaks は形の決まったトークン類 7種84件を 84/84 で全部拾いました。例示値 (sk_test で始まる Stripe テストキー) にだけ 5件誤反応しましたが、取りこぼしはゼロです。

一方で、同じ exp036 で用意した「日本の機密情報」96件には、既定ルールが 1件も反応しませんでした。内訳は後述しますが、カード番号・マイナンバー・DB 接続文字列・電話番号・住所・口座・氏名+生年月日・文中のパスワードです。「形がない個人情報」ではありません。カード番号は Luhn アルゴリズムで、マイナンバーは検査用数字で、電話は 070/080/090 のパターンで、機械的に拾える対象です。しかし既定ルールはそもそもこれらを見にいっていないので、全部素通りします。

ここでいう「見ない」は、該当するパターンが既定ルールに1つも無い、ということです。gitleaks のリポジトリの config/ 配下を grep すれば分かる通り、既定ルールは海外のサービスのトークン検出が中心で、カード番号ルール (generic-api-key ではない Luhn 検査) は入っていません。マイナンバーはそもそも日本独自の仕様なので入りようがありません。

gitleaks の既定ルールは「見逃したら全世界のサービスで悪用される形の決まったトークン」を優先しています。カード番号やマイナンバーは「所有者が決まる個人情報」なので、スキャナが拾う前に「扱わない設計」で防ぐのが本筋、というのが設計思想です。ただし現実にはログ・DB バックアップ・テストコードに混入する事故が後を絶たず、入口のスキャナで止めたい需要は確実にあります。

陽性180件の内訳と、gitleaks の反応

評価セットの陽性180件は 15種×12 で組んであります。gitleaks 8.30.1 の既定ルールを gitleaks stdin で流した結果は次の通りでした。

種類 件数 gitleaks 検出 残り
GitHub PAT (ghp_ 36桁) 12 12 0
AWS Access Key (AKIA 20桁) 12 12 0
Slack Bot Token (xoxb-) 12 12 0
Stripe Secret Key (sk_live_) 12 12 0
OpenAI API Key (sk-) 12 12 0
RSA 秘密鍵 (BEGIN...END) 12 12 0
JWT (3セクション) 12 12 0
DB 接続文字列 (postgres://user:pass@) 12 0 12
カード番号 (Luhn 有効) 12 0 12
マイナンバー (検査用数字有効) 12 0 12
電話+メール 12 0 12
住所 12 0 12
氏名+生年月日 12 0 12
口座 12 0 12
文中のパスワード (「パスワードは〜」) 12 0 12
合計 180 84 96

トークン類7種は 84/84、残り8種は 0/96 で、2つの集団にきれいに分かれました。「残り」の 96件は、1件も既定ルールに引っかからずに通ります。

誤検知は陰性144件に対して 5件でした。全部 Stripe の sk_test_ で始まる公式ドキュメントの例示値で、本物のトークンではありません。許可リスト (gitleaks allowlist) に sk_test_ パターンを足せば消えますが、既定のままで誤検知率 5/144 (約 3.5%) ならかなり優秀な方です。本物の秘密を拾う点では、既定ルールで十分でした。

96件の「取り逃し」は、こう分かれる

既定ルールが見なかった 96件の内訳をもう少し細かく分けます。

  • カード番号 12件: 16桁 (JCB / Mastercard レンジの BIN を混ぜた架空値) で Luhn 検算が通るもの
  • マイナンバー 12件: 12桁 + 検査用数字計算が合うもの (個人番号法で定義された計算式)
  • DB 接続文字列 12件: postgres://user:password@host:5432/db 形式。パスワード部が平文
  • 電話+メール 12件: 090-xxxx-xxxx と example@example.co.jp が同じ文脈に出現
  • 住所 12件: 「東京都○○区○○1-2-3」のような都道府県+市区町村+番地
  • 氏名+生年月日 12件: 「山田太郎 1985年3月14日」のような組み合わせ
  • 口座 12件: 銀行名 + 支店 + 口座番号7桁
  • 文中のパスワード 12件: 「パスワードは abcd1234 です」のような地の文での露出

このうち、カード番号とマイナンバーは「検査用数字で自力検証可能」な対象です。他は「文脈で判定するしかない」対象です。gitleaks は正規表現ベースなので、原理的には前者を拾う能力があります。後者は正規表現では難しく、別の仕掛けが要ります。

日本の正規表現ルールだけで、どこまで取れるか

exp036 では jp_rules.py という自前の正規表現を書いて、同じ陽性180件に当てました。設計は単純で、カードは Luhn、マイナンバーは検査用数字、電話は 070/080/090、住所は都道府県+市区町村+番地、口座は銀行名+支店+番号、氏名+生年月日は漢字姓+名+和暦/西暦の生年月日、文中パスワードは「パスワードは〜」の構文パターン、です。

結果は次の通りでした。

種類 件数 日本ルール検出 残り
カード・マイナンバー・DB 接続文字列 36 36 0
電話・住所・口座 36 36 0
氏名+生年月日 (ja) 12 6 6
氏名+生年月日 (en) 12 12 0
文中のパスワード (ja) 12 8 4
文中のパスワード (en) 12 12 0

ここで ja/en と分けているのは、同じ種類でも日本語データと英語データで表現が揺れるからです。氏名+生年月日の ja 6/12 は、「渡辺健太 / 2003年12月4日生まれ」のように「生年月日」のキーワードを伴わない「〜生まれ」の言い回しを拾い逃しています (dob ルールは「生年月日」「born」を接頭に要求する設計のため)。文中パスワードの ja 8/12 は「パスワードが abcd1234 なんですが〜」のように「は/is/:/=」ではなく「が」で続く構文が残っています (password-kw ルールは「は/is/:/=」のみ対応)。

日本ルール単独の陰性 144件に対する誤検知は 14件で、6件が「カード例の 4111… の先頭12桁がマイナンバーの検査用数字を通過する」もの、残り8件が「公開住所 (企業の問い合わせ住所など)」の過剰反応でした。例示値の許可リストと、公開情報は検出対象外にする設定を足せば減らせます。

ここまでで、カード・マイナンバー・DB 接続文字列・電話・住所・口座の「形で機械判定できる」6種72件は、日本の正規表現で全部拾えました。gitleaks の既定ルールだけだと 0/72、日本ルールだけだと 72/72 です。この差は、gitleaks の設計の良し悪しより、見ている対象の違いから来ています。両方を同じ CI に挿せば済みます。

補完策: gitleaks + 日本ルール + ローカル qwen3.5:9b の3段構成

正規表現で取れない残りを埋めるため、exp036 では3段目にローカル LLM を置きました。Ollama 経由の qwen3.5:9b (think=false、temperature 0、num_predict=1 で Y/N の logprobs を取るだけ) に「この文字列に機密が含まれるか」を 0.5 固定の閾値で問うだけです。

5条件の見逃し数と誤検知数を並べると、分担の意味が見えます。

構成 ja 見逃し/180 ja 誤検知/144 en 見逃し/180 en 誤検知/144
正規表現だけ (gitleaks+日本ルール) 10 19 0 19
正規表現 + qwen3.5:4b (0.5 固定) 6 19 0 19
正規表現 + qwen3.5:9b (0.5 固定) 5 27 0 23
正規表現 + gemma4:e4b (0.5 固定) 6 28 0 31
モデル単体を見逃し 0 に較正 (4b) 0 137 0 101

正規表現だけだと ja で 10件漏れていたのが、qwen3.5:9b を 2段目に置くと 5件まで減りました。半分です。誤検知が 19→27 件に増えますが、<YOUR_API_KEY> のような伏せ字に反応する分で、許可リストでさらに削れる余地があります。

一番下の「モデル単体を見逃し 0 に較正」行は捨てパターンの参考です。閾値を下げて見逃し 0 を狙うと、誤検知が 137/144 (約 95%) になります。実運用では使えません。モデルを 1段目に置くと「ノイズまみれで誰も見ない」状態になるので、正規表現で取れるものは正規表現で取り切り、残りをモデルに任せる 2段構成が妥当、という結論でした。

モデル単体を見逃し 0 に較正 の列が、ローカル LLM 単体で機密スキャンを置き換える案の破綻を示しています。AUROC は 0.91〜0.98 と悪くない数字ですが、閾値を下げて見逃しをなくそうとすると誤検知が壊滅します。「全部 LLM でレビューさせる」みたいな流儀を、機密スキャンに適用するとノイズに溺れます。

残った 5件は、全部「氏名+生年月日」だった

3段構成でも残った ja の見逃し 5件は、全部「氏名+生年月日」でした。exp036 の results/gate_summary.md の詳細を見ると、プロンプトの機密の定義にそもそも「生年月日」を書いていませんでした。モデルは定義に書かれたものしか機密と判定しないので、書き忘れた分は見逃します。原因は仕様の抜けで、プロンプトに生年月日を足せば消える類の問題です。モデルの性能を上げなくても直せます。

一方で誤検知の主因は正規表現側にありました。gitleaks の Stripe ルールが sk_test_ 例に反応した 5件と、日本ルールの住所が企業の公開住所に反応した 8件が主で、計 13件 (19件中) です。モデル側の誤検知は「<YOUR_API_KEY> を機密と判定した」のような、文字列としては伏せ字でも「機密を書く場所」として反応するタイプで、意味を理解した上での誤検知です。

「書かれているか」の確認 (照合) は、「書く情報の抽出」より間違えやすい、というのが別実験 (exp040) で分かっている傾向です。モデルに「ここに機密があるか」を聞くより、「ここから秘密っぽい部分を抽出せよ」と聞いたほうが、人間の目で確認しやすい形で出ます。機密判定は後段 (閾値カット) に任せる、という設計分担が合いそうです。

CI に挿すときの最短構成

以上の実測を踏まえて、現時点で妥当だと考える構成は次の通りです。

  1. 1段目: gitleaks (既定ルール + 許可リストで sk_test_ などを除外)
    • 役割: 形の決まった国際トークンを漏れなく止める
    • 得意: GitHub PAT / AWS キー / Slack / Stripe / OpenAI / 秘密鍵 / JWT
    • 期待: トークン類は 100% 検出、sk_test_ 等の例示値を許可リストで除外
  2. 2段目: 日本の正規表現ルール (jp_rules.py 相当、公開住所の許可リスト付き)
    • 役割: 形で機械判定できる日本の機密情報を拾う
    • 得意: カード Luhn / マイナンバー検査用数字 / DB 接続 / 電話・住所・口座
    • 期待: この層で 72/96 を補足
  3. 3段目: ローカル qwen3.5:9b の分類器 (0.5 固定の閾値、プロンプトに機密の定義を列挙)
    • 役割: 文脈でしか判定できない「氏名+生年月日」「文中のパスワード」を拾う
    • 得意: 言い回しの揺れ
    • 期待: この層でさらに 4〜5件回収。残りはプロンプト追記で対応

この3段構成で、評価セットの180件に対して ja の見逃しが 5件、誤検知が 27/144 に収まりました。CI に組むなら、見逃しは Telegram 等に警告で流し、誤検知は PR 内のインラインコメントで allowlist キーワード付きで出して、人間が1クリックで却下できる動線にしておくと運用に乗ります。

クラウドの LLM を機密スキャンに使うのは筋が悪い

exp036 では一貫してローカル LLM を使いました。クラウドの API を機密スキャンに使うと、「スキャンのために機密を外部に送る」という矛盾が発生するからです。モデル推論のログに残らない保証がある API もありますが、「ログに残らない」と「スキャンの入力として受け取られていない」は違います。少なくとも事故時の説明責任を考えると、スキャナの入力は社内の推論環境を出ないほうが安全です。

ローカル LLM で機密スキャンを 2段目〜3段目に置く構成なら、クラウドへ秘密を送らずに済みます。qwen3.5:9b はローカルの GPU 1枚で動かせるサイズのモデルです。

まとめ

  • gitleaks 8.30.1 の既定ルールは、トークン類7種84件を 84/84 で止めました。誤検知も sk_test_ 例示値の 5件のみで優秀です
  • 一方、日本の機密情報 96件 (カード・マイナンバー・DB 接続・電話・住所・口座・氏名+生年月日・文中パスワード) は 1件も止めませんでした。gitleaks は見る対象を国際トークンに絞っているので、設計通りです
  • 日本の正規表現ルール (Luhn / 検査用数字 / 都道府県+番地 など) を 2段目に置くと、形で機械判定できる 72件を回収できます
  • 残りの文脈判定が要るものは、ローカル qwen3.5:9b (0.5 固定、温度 0、Think なし) を 3段目に置くと、5件まで減らせます。プロンプトに機密の定義を網羅するのが鍵です
  • モデル単体でカバーしようとすると、誤検知率 95% で破綻します。モデルは「正規表現で漏れた分を拾う 2〜3段目」に置くのが分担の筋です

評価セットは架空値 (seed 固定、各種12件) で組んだ合成データなので、実データでは数字が揺れます。それでも「gitleaks + 日本ルール + ローカル LLM の3段で分担する」という骨組みは、同じ構造で動かせるはずです。

AI エージェントに秘密を渡す側の設計 (MCP サーバーのトークン管理、OAuth、OWASP MCP Top 10) は、拙著『MCP セキュリティ実践ガイド』で扱っています。

実測条件 (再掲)

  • 評価セット: iris-lab exp036 の data/gate_set.jsonl (seed 固定、架空値、陽性180件 15種×12 + 陰性144件 6種×24、日英各半分)
  • gitleaks バージョン: 8.30.1、既定ルール
  • 正規表現: jp_rules.py (日本の機密情報を形と検査用数字で拾うルール)
  • ローカル LLM: Ollama 経由 qwen3.5:9b、think=false、temperature 0、num_predict=1 で Y/N の logprobs を取り softmax で確率化
  • 計測スクリプト: eval_regex.py / analyze_gate.py、全表は results/gate_summary.md
  • 本記事は上記実測結果と、gitleaks 8.30.1 の既定ルール、個人情報の保護に関する法律 をもとにしています。数字は私の実験 (iris-lab) の結果で、この記事のための追加の測定はしていません
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?