Forgejoの全リポジトリを月1回シークレットスキャンするようにした話

Security Forgejo Gitleaks

Forgejoの全リポジトリを
月1回シークレットスキャン
するようにした話

push時のチェックだけでは拾えない「すでに入ってしまったシークレット」を見つけるため、Forgejo上の全リポジトリを月1回まとめてスキャンする仕組みを作りました。

push時のチェックだけでは足りなかった

前回の記事では、Forgejoのpre-receive hookで、シークレットのpushをブロックする仕組みを紹介しました。ただ、pre-receive hookが見ているのはそのpushで追加された差分だけです。そのため、次のようなシークレットは検知できません。

🕰️

導入前から残っているもの

フックを入れる前にpushされたコードは、一度もチェックされていません。

⏭️

チェックを飛ばしたもの

誤検知を回避する仕組みで、本物のシークレットが一緒に入る可能性があります。

🗑️

入れてすぐ消したもの

最新のファイルから消えていても、コミット履歴には残っています。

そこで、「これから入ってくるもの」はpush時に止め、「すでに入っているもの」は定期的に棚卸しする2段構えにしました。

git push
→
pre-receive hook
→
検知 → ブロック(前回)
毎月
→
全リポジトリをスキャン
→
Issueにまとめる
→
チャットに通知(今回)

検知エンジンをGitleaksに移行

月次スキャンの話に入る前に、前回の記事から大きく変わった点を紹介します。前回は自作の正規表現で検知していましたが、OSSのシークレットスキャナー Gitleaks に置き換えました。

  • 標準で100以上の検知ルールがあり、自前でパターンを増やし続ける必要がなくなりました
  • 誤検知の除外を設定ファイルに書けるので、正規表現の本体をいじらずに済みます
  • 標準ルールでカバーしきれないパターンは、独自ルールとして追加できます
✎ クレジットカード番号は、前回作ったLuhnアルゴリズムによるチェックを引き続き併用しています。

後継のbetterleaksへの移行も検討中

Gitleaksの作者らが中心になって、後継となる betterleaks の開発が進んでいます。今後のメンテナンスを考えると、いずれはこちらに移行したいと考えています。

! 大きく作り直されたv2は、執筆時点ではまだ正式リリース前です。設定ファイルの書き方や検知結果の形式もGitleaksから変わっているため、正式版が出てから、今の設定と検知結果を比べたうえで移行する予定です。

月次スキャンの仕組み

何をスキャンするか

Forgejo上の全リポジトリを対象に、3種類のスキャンを用意しました。検知ルールはpush時と共通にしていて、片方だけ除外し忘れる、といったことが起きないようにしています。

スキャン実行対象
① 現在の状態毎月各リポジトリのデフォルトブランチの、最新のファイル
② 直近1か月毎月全ブランチの、前月に入ったコミット
③ 全履歴初回のみ全コミット(棚卸し用)

①で「今残っているもの」を、②で「チェックを飛ばして入ったものや、入れてすぐ消したもの」を拾います。③は量が多いため、毎月は回していません。

i ポイント:リポジトリを毎回cloneせず、直接読むようにしています。検知結果は伏せ字にして、レポートにシークレットの値そのものは残りません。

結果は毎月1つのIssueにまとめる

最初はリポジトリごとにIssueを立てる設計でしたが、リポジトリの数が多いとIssueも増えて追いきれなくなるため、月ごとに1つのIssueに集約しました。

前月比

リポジトリ別の件数を前月比付きで載せ、減っているかがひと目で分かります。

解消したリポジトリ

消しても履歴には残るため、キーの無効化まで済んでいるかを確認します。

該当行へのリンク

詳細からForgejoの該当ファイル・該当行をそのまま開けます。

スキャンが終わると、チャットに要約とIssueへのリンクを通知します。レポートには全リポジトリの検知箇所が集まるため、見られる人は必要な人だけに絞っています。

実際に動かしてみて

過去の誤検知もまとめて出てくる

push時のチェックは「新しく入ってくるもの」だけを見るので、誤検知が出てもその場で対処できていました。全リポジトリを一度に見ると、過去の誤検知もまとめて出てきます。

誤検知の種類例
テスト用・説明用のダミー値テストコードやREADMEに書かれたサンプルのキー
ライブラリに含まれるサンプルライブラリの説明文やサンプルデータに書かれた、例としての値

ダミー値には目印を付ける

ダミー値は、毎月のスキャンで同じものが出続けてしまいます。かといって、push全体のチェックを飛ばす方法では、本物のシークレットが一緒に入っても気付けません。そこで、同じ行に [Dummy] という目印が書かれていたら、その行だけ検知しないようにしました。

password: testpass  # [Dummy]
  • 外れるのはその行だけです。同じファイルの別の行は、これまで通りチェックされます
  • コードを読んだ人にも「これはダミー値」と分かるので、目印としても役に立ちます
✎ Gitleaksでは、設定ファイルに「この文字列を含む行は除外する」という設定を足すだけで実現できます。

結果を担当者に届ける

スキャンして終わりではなく、各リポジトリの担当者に確認してもらう必要があります。ただ、全リポジトリの結果が1つにまとまっていると、自分の担当分を探すのが大変です。そこで、結果をプロジェクトごとに分けて、それぞれの担当者に直接渡しました。誤検知だったものは除外設定に追加して、次の月から出ないようにしています。

誤検知との格闘

運用を始めてからも、誤検知の相談がいくつか届きました。前回の記事でも誤検知には苦労しましたが、今回は「なぜ引っかかったのか」が一目では分からないものが多く、原因を突き止めるまでに時間がかかりました。

ハッシュ値が、クレジットカード番号に見えた

Rustのプロジェクトで、Cargo.lock をpushしたらクレジットカード番号として検知された、という相談がありました。Cargo.lock は、使っているライブラリのバージョンとチェックサム(ハッシュ値)を記録する、ツールが自動生成するファイルです。

checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea2..."

ハッシュ値の途中に、たまたま数字だけが15桁並んだ部分があり、それがクレジットカード番号の形(Amexの15桁)になっていました。さらに運の悪いことに、Luhnのチェックも通ってしまっていました。

i どれくらい起きるのか:ランダムなハッシュ値(SHA-256)を10万個作って試したところ、約1,200個に1個が引っかかりました。依存ライブラリが数百個あるプロジェクトなら、ファイルを追加したときに引っかかるのは珍しくありません。

実は、APIキーなどのチェック(Gitleaks)では、ロックファイルはもともと除外していました。ところが、クレジットカード番号のチェックだけは別の仕組みで動いていて、この除外を通っていなかったのです。原因はここにありました。

直し方はいくつか考えられ、検知漏れとのバランスで悩みました。

案判断理由
英数字に挟まれた数字は無視する見送りハッシュ値には効くが、英字とくっつけて書かれた本物のカード番号を見逃す
連絡のあったファイルだけ除外見送り安全だが、Rustのプロジェクトが増えるたびに同じ相談が来る
自動生成のロックファイルを名前で除外採用人が手で書かないファイルなので、カード番号が入ることはまずない

除外するファイルは、脆弱性スキャナー OSV-Scanner の対応ロックファイル一覧 を参考に決めました。ただし一覧には requirements.txt や pom.xml のような人が手で書くファイルも含まれるため、それらは外していません。手で書くファイルには、トークン入りのURLなどが書かれることがあるためです。

✎ 除外するのはクレジットカード番号のチェックだけです。APIキーやパスワードなどのチェックは、ロックファイルにもこれまで通りかかります。

ライブラリの説明文にある「設定例」

月次スキャンでは、リポジトリに同梱しているライブラリの中身も検知されました。たとえば、HTTPライブラリの説明文に書かれた、プロキシの設定例です。

proxy_url="socks5h://<username>:<password>@proxy-host"

「URLの中にユーザー名とパスワードが入っている」という形なので、認証情報入りURLのルールに引っかかっていました。ライブラリのファイルを書き換えると、バージョンを上げたときに元に戻ってしまいます。そこで、「このファイルの、この書き方」の両方が一致したときだけ除外するようにしました。同じファイルに本物のパスワードが書かれた場合や、別のファイルに同じ書き方がある場合は、これまで通り検知されます。

→ 学んだこと:除外を広げるほど誤検知は減りますが、そのぶん見逃しのリスクが増えます。「どこまで外してよいか」を毎回考え、除外はできるだけ狭くすることを心がけています。

セルフホストだからできること

私たちのForgejoは、AWS上のサーバーに自分たちでインストールして運用している(セルフホスト)ので、サーバーの中で何をするかを自由に決められます。今回の仕組みも、セルフホストだからこそ作れたものです。

🛑

受け取る側で止められる

サーバーで動くpre-receive hookに、好きなチェックを入れられます。

🔒

コードを外に出さない

スキャンは自分たちのサーバーの中で完結し、外部サービスにコードを送りません。

📂

全リポジトリを直接読める

APIの回数制限やcloneを気にせず、全リポジトリを一気にスキャンできます。

🛠️

運用に合わせて作り込める

検知ルール、ダミー値の目印、通知やレポートの形式まで自由に決められます。

✎ その分、新しいリポジトリへのフックの配布や検知エンジンの更新など、仕組みの面倒は自分たちで見る必要があります。それでも、「自分たちのコードを、自分たちの基準で守れる」のはセルフホストの大きな強みだと感じています。

まとめ

今回までに整えた仕組みの全体像です。

✓
pushブロックpre-receive hook(前回)
✓
月次スキャン全リポジトリを月1回棚卸し
✓
検知エンジンGitleaks + Luhn
✓
検知ルールの共通化push時と月次で同じルール
✓
月1つのIssue前月比で推移を追える
✓
ダミー値の目印その行だけ検知から外す

push時のチェックだけでは、「すでに入ってしまったもの」は見えないままでした。月次スキャンを入れたことで、リポジトリ全体の状態を定期的に把握できるようになりました。

→ 今後の展望:毎月の結果を見ながら、誤検知の除外を育てていく予定です。また、betterleaksの正式版が出たら、移行を検討します。

Recruit

ディーメイクでは各ポジションで一緒に働く仲間を募集中! エンジニア、デザイナー、ディレクターなど、多彩な職種があります。
一緒に成長していきましょう!

  • URLをコピーしました!

コメント

コメントする