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 固定で生成) で、実データではありません。
- 別軸との棲み分け: 姉妹記事「話題の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段目: gitleaks (既定ルール + 許可リストで
sk_test_などを除外)- 役割: 形の決まった国際トークンを漏れなく止める
- 得意: GitHub PAT / AWS キー / Slack / Stripe / OpenAI / 秘密鍵 / JWT
- 期待: トークン類は 100% 検出、
sk_test_等の例示値を許可リストで除外
-
2段目: 日本の正規表現ルール (
jp_rules.py相当、公開住所の許可リスト付き)- 役割: 形で機械判定できる日本の機密情報を拾う
- 得意: カード Luhn / マイナンバー検査用数字 / DB 接続 / 電話・住所・口座
- 期待: この層で 72/96 を補足
-
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) の結果で、この記事のための追加の測定はしていません
