Forgejoの全リポジトリを
月1回シークレットスキャン
するようにした話
push時のチェックだけでは拾えない「すでに入ってしまったシークレット」を見つけるため、Forgejo上の全リポジトリを月1回まとめてスキャンする仕組みを作りました。
push時のチェックだけでは足りなかった
前回の記事では、Forgejoのpre-receive hookで、シークレットのpushをブロックする仕組みを紹介しました。ただ、pre-receive hookが見ているのはそのpushで追加された差分だけです。そのため、次のようなシークレットは検知できません。
導入前から残っているもの
フックを入れる前にpushされたコードは、一度もチェックされていません。
チェックを飛ばしたもの
誤検知を回避する仕組みで、本物のシークレットが一緒に入る可能性があります。
入れてすぐ消したもの
最新のファイルから消えていても、コミット履歴には残っています。
そこで、「これから入ってくるもの」はpush時に止め、「すでに入っているもの」は定期的に棚卸しする2段構えにしました。
検知エンジンをGitleaksに移行
月次スキャンの話に入る前に、前回の記事から大きく変わった点を紹介します。前回は自作の正規表現で検知していましたが、OSSのシークレットスキャナー Gitleaks に置き換えました。
- 標準で100以上の検知ルールがあり、自前でパターンを増やし続ける必要がなくなりました
- 誤検知の除外を設定ファイルに書けるので、正規表現の本体をいじらずに済みます
- 標準ルールでカバーしきれないパターンは、独自ルールとして追加できます
後継のbetterleaksへの移行も検討中
Gitleaksの作者らが中心になって、後継となる betterleaks の開発が進んでいます。今後のメンテナンスを考えると、いずれはこちらに移行したいと考えています。
月次スキャンの仕組み
何をスキャンするか
Forgejo上の全リポジトリを対象に、3種類のスキャンを用意しました。検知ルールはpush時と共通にしていて、片方だけ除外し忘れる、といったことが起きないようにしています。
①で「今残っているもの」を、②で「チェックを飛ばして入ったものや、入れてすぐ消したもの」を拾います。③は量が多いため、毎月は回していません。
結果は毎月1つのIssueにまとめる
最初はリポジトリごとにIssueを立てる設計でしたが、リポジトリの数が多いとIssueも増えて追いきれなくなるため、月ごとに1つのIssueに集約しました。
前月比
リポジトリ別の件数を前月比付きで載せ、減っているかがひと目で分かります。
解消したリポジトリ
消しても履歴には残るため、キーの無効化まで済んでいるかを確認します。
該当行へのリンク
詳細からForgejoの該当ファイル・該当行をそのまま開けます。
スキャンが終わると、チャットに要約とIssueへのリンクを通知します。レポートには全リポジトリの検知箇所が集まるため、見られる人は必要な人だけに絞っています。
実際に動かしてみて
過去の誤検知もまとめて出てくる
push時のチェックは「新しく入ってくるもの」だけを見るので、誤検知が出てもその場で対処できていました。全リポジトリを一度に見ると、過去の誤検知もまとめて出てきます。
ダミー値には目印を付ける
ダミー値は、毎月のスキャンで同じものが出続けてしまいます。かといって、push全体のチェックを飛ばす方法では、本物のシークレットが一緒に入っても気付けません。そこで、同じ行に [Dummy] という目印が書かれていたら、その行だけ検知しないようにしました。
password: testpass # [Dummy]
- 外れるのはその行だけです。同じファイルの別の行は、これまで通りチェックされます
- コードを読んだ人にも「これはダミー値」と分かるので、目印としても役に立ちます
結果を担当者に届ける
スキャンして終わりではなく、各リポジトリの担当者に確認してもらう必要があります。ただ、全リポジトリの結果が1つにまとまっていると、自分の担当分を探すのが大変です。そこで、結果をプロジェクトごとに分けて、それぞれの担当者に直接渡しました。誤検知だったものは除外設定に追加して、次の月から出ないようにしています。
誤検知との格闘
運用を始めてからも、誤検知の相談がいくつか届きました。前回の記事でも誤検知には苦労しましたが、今回は「なぜ引っかかったのか」が一目では分からないものが多く、原因を突き止めるまでに時間がかかりました。
ハッシュ値が、クレジットカード番号に見えた
Rustのプロジェクトで、Cargo.lock をpushしたらクレジットカード番号として検知された、という相談がありました。Cargo.lock は、使っているライブラリのバージョンとチェックサム(ハッシュ値)を記録する、ツールが自動生成するファイルです。
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea2..."
ハッシュ値の途中に、たまたま数字だけが15桁並んだ部分があり、それがクレジットカード番号の形(Amexの15桁)になっていました。さらに運の悪いことに、Luhnのチェックも通ってしまっていました。
実は、APIキーなどのチェック(Gitleaks)では、ロックファイルはもともと除外していました。ところが、クレジットカード番号のチェックだけは別の仕組みで動いていて、この除外を通っていなかったのです。原因はここにありました。
直し方はいくつか考えられ、検知漏れとのバランスで悩みました。
除外するファイルは、脆弱性スキャナー OSV-Scanner の対応ロックファイル一覧 を参考に決めました。ただし一覧には requirements.txt や pom.xml のような人が手で書くファイルも含まれるため、それらは外していません。手で書くファイルには、トークン入りのURLなどが書かれることがあるためです。
ライブラリの説明文にある「設定例」
月次スキャンでは、リポジトリに同梱しているライブラリの中身も検知されました。たとえば、HTTPライブラリの説明文に書かれた、プロキシの設定例です。
proxy_url="socks5h://<username>:<password>@proxy-host"
「URLの中にユーザー名とパスワードが入っている」という形なので、認証情報入りURLのルールに引っかかっていました。ライブラリのファイルを書き換えると、バージョンを上げたときに元に戻ってしまいます。そこで、「このファイルの、この書き方」の両方が一致したときだけ除外するようにしました。同じファイルに本物のパスワードが書かれた場合や、別のファイルに同じ書き方がある場合は、これまで通り検知されます。
セルフホストだからできること
私たちのForgejoは、AWS上のサーバーに自分たちでインストールして運用している(セルフホスト)ので、サーバーの中で何をするかを自由に決められます。今回の仕組みも、セルフホストだからこそ作れたものです。
受け取る側で止められる
サーバーで動くpre-receive hookに、好きなチェックを入れられます。
コードを外に出さない
スキャンは自分たちのサーバーの中で完結し、外部サービスにコードを送りません。
全リポジトリを直接読める
APIの回数制限やcloneを気にせず、全リポジトリを一気にスキャンできます。
運用に合わせて作り込める
検知ルール、ダミー値の目印、通知やレポートの形式まで自由に決められます。
まとめ
今回までに整えた仕組みの全体像です。
push時のチェックだけでは、「すでに入ってしまったもの」は見えないままでした。月次スキャンを入れたことで、リポジトリ全体の状態を定期的に把握できるようになりました。

コメント