最近GitHubの認証情報の取り扱いで悩んでいる GHE管理者の石川です。
GitHub ActionsのOIDCを使った各種クラウドとの認証便利ですよね!
OIDC利用も増えてきたしPAT周りをもっと綺麗にできないだろうかと考えています。
classic PAT廃止してFine-grained PATとGitHub Appにしたい
→ いまどのくらい使われているのだろう
→ GitHub上に認証情報記載されてない?
といった具合に、整理よりも先に掃除の必要性を感じてきました。
弊社のGitHub組織には現状Publicリポジトリはないため、即座に危うい状態というわけではありませんが、連携先やデプロイ先で漏洩する可能性はあるので、昨年何度が記事を目にした Trufflehog を導入することにしました。
サンプルリポジトリでお試し
都合よく認証情報の入ったリポジトリは記憶にないので、公式が用意してくれている認証情報が入ったリポジトリをので体験してみます。
1 |
trufflehog github --repo=https://github.com/trufflesecurity/test_keys --issue-comments --pr-comments |

いい感じですね、コマンド一発でやりたいことができるのは素晴らしい!
組織内全リポジトリに対してスキャン
組織内のすべてのリポジトリに対して実行すると、かなりの時間がかかるのでローカルではなく適当なサーバー上から初回スキャンをしてみます。詳しくは後述してますがGitHub API制限にひっかかりそうなのでスリープをいれて実行しました。
--no-verification
で有効有無を問わず存在を確認して、どのようなDetectorが検証されたのか、検証ありで実行しても大丈夫そうか確認して、本番スキャンでは --results=verified,unknown
にして実行しました。
結果、思ったよりたくさん検出されました!え、こんなにあるの!?と思った大部分はSlack Webhook。これは最悪漏洩しても影響は微量なので一旦放置。
ほかにも見過ごせない認証情報もありましたので、検知結果をIssue化する仕組みを作っていきます。
定期実行用GitHub Actionsを作成
事前準備
GitHub AppとPATを用意します。
- 更新リポジトリチェック&Issue作成用GitHub App
- Read access to metadata
- Read and write access to issues and organization projects
- Trufflehogスキャン用PAT
- Read access to code, issues, metadata, and pull requests
WF1: 更新のあったリポジトリを取得してTruffehogを実行
日次で更新のあったリポジトリのみをスキャン対象としています。
GCSに入れた後BigQueryで集計したいので使いたいので結果は --json
で出力しています。
- cronで日次実行
- 更新のあったリポジトリ一覧を取得(GitHub App Tokenを使用)
- Trufflehogでスキャン(PATを使用)
- スキャン結果をGCSに保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
- name: Run get recent update repos script env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: python get_updated_repos.py > /tmp/recent_update_repos.txt ... - name: Install Trufflehog run: | curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/refs/tags/v3.88.2/scripts/install.sh | sh -s -- -b /usr/local/bin # Trufflehog用のGitHub Actionsはあるが、全スキャン時に使ってたShellを使い回して実行している - name: Secret Scanning env: TRUFFLEHOG_SCAN_PAT: ${{ secrets.TRUFFLEHOG_SCAN_PAT }} run: | wc -l /tmp/recent_update_repos.txt /bin/bash trufflehog_scan.sh /tmp/recent_update_repos.txt - name: Upload scan results id: upload-files uses: google-github-actions/upload-cloud-storage@v2 with: path: scan_logs destination: <YOUR BUCKET> gzip: false |
個々のリポジトリでCI時にスキャンしたい場合
git-secrets等でコミット前に防ぐのが最適ではありますが一応TIPSとして。
--since-commit
で特定のコミット以降、 --max-depth
で対象とするコミット数、 --branch
で対象ブランチなどを指定すればスキャン時間を短縮できます。--github-actions
でGitHub Actions用の出力結果にすることもできます。
WF2: 検知結果を対象リポジトリのIssueへ登録
デバッグしやすいのでスキャンのワークフローとは分けてます。
- WF1が成功したら実行
- スキャン結果とIssue登録済みmd5をGCSから取得
- スキャン結果のmd5を作成してIssue作成済みならスキップ or 特定のDetectorならスキップ
- 作成済みではない検知結果を リポジトリ・パス・Detector で group by
(一つ一つIssueを作ったら邪魔なのである程度まとめる) - 対象リポジトリにIssueを作成し、指定Projectに追加(GitHub App Tokenを使用)
- Issue登録済みmd5をGCSへ保存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
- name: Download scanned results run: | gcloud storage cp gs://<YOUR BUCKET>/scan_result_issue_md5_hashes.txt scan_result_issue_md5_hashes.txt mkdir -p scan_logs gcloud storage rsync gs://<YOUR BUCKET>/scan_logs/date=$(date +%Y-%m-%d)/org=<YOUR ORG>/ scan_logs/ cat scan_logs/*.json > scan_results.json ... # Issueを作る権限のあるTokenを得るために作成対象リポジトリを取得 - name: Get scan result repos matrix id: repo-matrix run: | python get_scanned_results.py scan_results.json >> $GITHUB_OUTPUT - name: Create GitHub App Token uses: actions/create-github-app-token@v1 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: ${{ join(fromJson(steps.repo-matrix.outputs.matrix).repos) }} # PythonからIssue作成とProjectへの追加を実行、例外処理もここで対応 - name: Run create_issue script env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: python create_issue.py scan_results.json scan_result_issue_md5_hashes.txt continue-on-error: true - name: Upload md5 hash id: upload-files uses: google-github-actions/upload-cloud-storage@v2 with: path: scan_result_issue_md5_hashes.txt destination: <YOUR BUCKET> gzip: false |
GitHub Projectに追加することで、どのリポジトリにどのくらい認証情報が残っているか、一覧で確認することができます。
コード側は変更して対処したり、認証情報を無効化した場合、次回スキャン結果から消えます。それをトリガーに自動でIssueも閉じたら綺麗ですがそこまではやっていません。
ちなみに初回スキャンで見つけた検知結果はGCSに上げて、事前にActionsを手動実行することでIssue登録を済ませています。
気を付ける点
TrufflehogはGitHub App Tokenに未対応
Support scanning GitHub Orgs with GitHub App Token · Issue #1513 · trufflesecurity/trufflehog
App対応していないので、個人のPATを使いましょう。
GitHub App Tokenで動かそうとすると、Resource not accessible by integration
エラーが起きます。
GitHubのAPI制限
Trufflehogがどの程度APIを叩いたかは、InsightsのREST APIのグラフからスキャンに使ったPATの所持ユーザーを選択することで確認可能です。

日次で更新されるリポジトリ数にもよりますが、いまのところ5,000req/hour の制限を超えることはまずなさそうです。
初回全リポジトリスキャン、1日に大量のリポジトリ更新、IssueやPRやコメントが大量にある場合はAPI制限を超えやすくなりますので、その場合は時間を分けてスキャンしたり、スリープ入れて1時間内スキャン数を抑えたりして回避しましょう。
参考:Rate limits for the REST API – GitHub Docs
認証情報側が更新されたらスキャンした結果も変わる
リポジトリ上からは消えていないが認証情報は無効にした場合、 VerificationFromCache
や ExtraData
に変化がありました。検知対象の同一性を担保するユニークIDを作る時は、こういう要素を含まないように作成しましょう。これを理解していなくて、最初重複したIssueを作ってしまいました。。
Macから実行したとき error trufflehog failed to parse commit date
エラーが出る
trufflehog fails to parse localized timestamp · Issue #3338 · trufflesecurity/trufflehog
検出結果のTimestampが Timestamp: 0001-01-01 00:00:00 +0000
で表示されます。
Ubuntuではエラーは起きなかったので気になる方は、サーバーなりコンテナなりで実行しましょう。
今後の予定
Trufflehog初めて使いましたが、サクッといい感じにやってくれるし、痒いところにも手が届くツールでとても便利ですね。GitHub Advanced SecurityならSecret scanningも対応していますが、組織内全リポジトリを対象にしたいとなると金銭的に厳しいものがありますし、OSSでカバーできるのはありがたいです。
実は検出まではよくても、本気でGitHub上から跡形もなく消し去るのはかなり大変な作業だったりします。
参考:GitHub上のsensitive dataを削除するための手順と道のり | メルカリエンジニアリング
そのため一旦のゴールとして以下が適当かなと思っています。
- 最新のコードに認証情報が含まれていないこと
- 一度でもGitHub上にコミットしてしまった認証情報は再発行すること
- 各人のgit-secrets等の設定厳密化
まだまだ先は長そうですが、少しずつでも今日よりも明日をセキュアにしていきたいと思います。