GitHub上のsensitive dataを削除するための手順と道のり

Advent Calendar day 7 担当の vvakame です。 予告では Apollo Federation Gateway Node.js実装についてポイント解説 としていましたが、社内各所のご協力によりAdvent Calendarの私の担当日に間に合う形で公開できる運びとなりました。そのため告知とは異なりますが GitHub上のsensitive data削除の手順と道のり をお届けしていきたいと思います。

メルペイVPoE hidekによるday 1の記事で振り返りがあったように、今年、弊社ではCodecovのBash Uploaderに係る情報流出という事案が発生しました。当該インシデント対応において、プレスリリースにも記載のある通り、ソースコード上に混入してしまった認証情報や一部個人情報などの機密性の高い情報(sensitive data)について調査を実施し、対応を行いました。

本記事では、その対応の一環として実施した、GitHub上にある様々なデータ、リポジトリに含まれるsensitive data(各種tokenや個人情報など)の削除作業を行ったときの手順などをご紹介します。 こういったインシデントは発生するべきではないもので、ポリシーや仕組みによって防ぐべきものですが、機密性の高い情報がソースコードに混入してしまった際の対処方法として、参考にしていただけると幸いです。

GitHub上に存在する情報

メルカリでは、ソースコード管理のために、GitHub Enterprise Cloudを利用しています。 GitHub上で我々が行っているのはプロダクトの開発・運用(の一部)です。中心にソースコードがあり、その周辺にPull RequestやIssueなどの形で開発者がコミュニケーションを行っています。 何かしらの情報を削除したい場合、我々開発者だけで対処可能な範囲と、GitHub社にオペレーションを頼まないとどうしようもない範囲があります。

筆者の役割

まず、筆者の役割について簡単に触れておきます。 筆者は当初本タスクのメンバーではありませんでした。一方、OSSに関わっている期間が長いため、gitやGitHubについての理解度は会社の中で比較的高い方です。そのため、弊社で契約しているGitHubのPremium Support PlusでTicketを切るアカウントを持っています。

当該インシデントの対応の際、当初はSupportにTicketを切ってほしいという依頼を社内でもらいました。何を質問したいのか聞き取り調査を行っていくうちに、GitHub特有のエッジケースを思いついたので、直接対応チームに参加し、詳細を詰めることにしました。 このような経緯でチームに参加し技術検証+GitHub社とのコミュニケーションを担当しました。 社内コミュニケーションや組織的ワークフローの整備や監督は別の方々が行っています。

削除作業の全体とその手順

gitリポジトリへの作業とGitHub上のデータへの作業について、まずはGitHub社が提供するドキュメントを参照します。 Removing sensitive data from a repository というそのものズバリのものがあります。

これらのデータは、開発ブランチのHEADから消えればそれで消えたことにはなりません。gitの仕組み上、変更履歴はすべてツリー中に含まれるからです。 また、sensitive dataを含むcommitをpushしてしまったら、それを消してforce pushしたとしてもremoteリポジトリ上に残っていないことは保証できません。

対処のための必要な手順は大きく分けると4ステップです。

  1. 削除するべきsensitive dataの特定 (混入したcommit SHAとfile name)
  2. gitリポジトリの全ヒストリからのsensitive dataの削除
  3. 意図せぬ再混入の防止措置
  4. GitHubのWebのview cacheの削除やPull Requestの削除/de-reference、リポジトリでのgc

これら4つのステップを全リポジトリを対象に組織全体で実行していく必要があります。

削除するべきsensitive dataの特定

まずは、削除するべきsensitive dataがどこにあるか特定する必要があります。 たとえばハードコードされてしまっていたcredentialの場合もありますし、個人情報の場合もあります。

前者については、当該インシデント対応の際、早急に社内でソースコード検索のために使用しているSourcegraphや、インシデント対応として新規開発されたツールにより早急に全ヒストリを対象に検出/Revokeされ、入れ替えられハードコードしないように修正されていきました。

問題は後者の場合で、RevokeもRotateもできません。つまり、万が一リポジトリが再度historyごと流出した場合に備え、これらの情報が含まれないようにするしかありません。 当該インシデントにおいては、外部機関による協力のもと、セキュリティチームによる全リポジトリの全量調査が行われ、リストが作成されました。

作成されたリストは厳重に管理されていて、各リポジトリオーナー(実際の削除作業をハンドルする人)にのみ共有されました。 なので、技術的な方針を策定した筆者も対象リポジトリまでは把握していますが実際の個人情報の内容については基本的に把握していません。

gitリポジトリの全ヒストリからのsensitive dataの削除検証

前述のGitHub社のドキュメントを参考に、削除手順の検証を行いました。

当然ながらこの作業は歴史の変更です。gitの仕組み上、該当の変更が含まれるcommitはもちろん、それ以降のすべてのcommitやtag(やrelease)が影響を受けてしまいcommit SHAが変わります。 開発を行う上で、historyは資産でありドキュメントでもあります。wikiなどの社内ドキュメントからのリンクもこれらSHAをベースで行われている場合がほとんどです。 これらの資産を毀損しないためにもsensitive dataが混入しないよう、そしてそれが歴史の奥深くに埋まらないよう普段から心がけて良いエンジニアとして過ごしましょう。

まずは、リポジトリの歴史を改変するための下準備をします。 改変は特定のcommitに含まれるコンテンツをリポジトリのいかなるbranchやtagからも辿れないようにすることです。 そのためには開発者がそのcommitへのポインタを持っていない状態にする必要があります。もし改変前のbranchをbaseにした変更をlocalに持っていたら、PRを作るときに意図せぬ再混入が発生してしまうかもしれません。

確実を期すため、今回の作業では代表作業者がremoteリポジトリをcloneし、改変作業をしてからforce pushし、作業者含む全員がlocalリポジトリを破棄してcloneしなおすこととしました。 そのために、事前にローカルにある作業途中のファイルはすべてcommitしremoteにpushします。 また、作業負荷を減らすためにmergeできるPRはmergeし、いらないremote branchのお掃除もしてしまってもよいかもしれません。

紹介されているBFGツールを利用し、gitリポジトリのhistoryから特定の文字列やファイルを削除します。 この作業はローカルにcloneしたリポジトリで何度でも練習できます。 作業後、BFGツールの出力したログは変更前後のcommit SHAの対応関係などを含むため、保管しておきます。

リポジトリに改変後の履歴をforce pushする前に、branchやtagのpushに反応するCI/CDなどが誤って反応し、意図しないrevisionが提供されてしまわないよう確認しておきます。

改変後のsensitive dataを含まないhistoryをforce pushできたらそれを共有し、全員でlocal repopositoryをcloneしなおします。 これでgitリポジトリに対する作業は終了です。一旦お疲れさまでした!

GitHub上に存在するデータを削除する

GitHub上ではgitリポジトリそのものの他に、Pull RequestやIssueに貼られたEmbed commentなど、gitリポジトリ内のコンテンツが表示されうる機会が存在します。 これらの情報を完全に削除するにはGitHubのサポートに連絡して削除してもらうしかありません。

まずは削除手順全体の流れを掴むため、sensitive dataが混入したことにした実験用リポジトリを作成してからサポートに連絡し、検証に協力してもらいました。 質問や検証の結果、わかったことや教えてもらったことを次にまとめます(記事執筆時点)。

Q. サポートチームに削除依頼を出すとなにをしてもらえるの?
A. 色々ある! 消すべきcommit SHAがリポジトリ内のどこからも参照(branch, tag, etc… includes forked repo)されなくなっている必要がある(ユーザ側が実施)。 また、それを参照するPull Requestも完全に削除するか、de-reference(コミットへの参照を取り外す)する必要がある(サポートチームが実施)。 消えていたら、garbage collectionをサポートチームが走らせます。するとcommit SHAへの直接リンクもpruneされて参照できなくなります。

リポジトリ内のどこからも参照されないcommit SHAでもURLに直接アクセスすると参照できてしまう。黄色いラベルがでる。GCすると404になる。
リポジトリ内のどこからも参照されないcommit SHAでもURLに直接アクセスすると参照できてしまう。黄色いラベルがでる。GCすると404になる。

Q. PRの削除とde-referenceってどう違うの?
A. PRの削除は完全に消えるよ。de-referenceはcommitとPRの紐づきを取り外して CommitsFiles Changed が空欄になるよ。

de-referenceされたPR
de-referenceされたPull Requestの様子 Commits, Files Changed が ∞ になっている

Q. 削除処理をしてもらうにはどういった情報が必要なの?
A. owner/repository 名と該当のcommit SHAだよ。リクエストをあげるのは、リポジトリのAdminかOrganizationのAdminである必要があるよ。

Q. 2021年1月1日に作ってmergeしたPRでsensitive dataが混入したとして、それより後の2021年2月1日に作ってmergeしたPRは影響を受けるの?
A. 1月1日のmerge commitを含む、すべてのPRは削除かde-referenceされる必要があるよ。また、それ以降のcommit SHAも変わったら、当然GitHub上のURLも変更になるので既存のcommitをポイントしているLinkは壊れちゃうよ。

Q. かなりの量のProductionで使っているリポジトリがあるんだけど、実際に作業する前に実験用リポジトリでどういうサポートが受けられるか、実験に協力してくれますか?
A. いいよ!ただし、PRの削除ならできるけど、de-referenceは別のチームにエスカレーションしなきゃいけなくて、そのチームは “live” の問題を扱うチームなので、そっちは難しいかも。

…と、初手でこれだけの情報が返ってくるので、GitHubのSupportは非常に優秀だなと感心しました。 また、オペレーションの都合上、大量のリポジトリに対して作業をリクエストする場合、1チケット 1リポジトリでリクエストをあげる必要があるようです。 これはユーザ側の環境に対してなんらかの操作を行う以上、証跡を残す社内規定などがある様子でした。

我々としては、PRに残された議論というのは開発上重要な資産の一部であるため、削除よりde-referenceが好ましい、という判断になりました。 このためのやり取り、操作のGitHub側の窓口を一本化してもらうのが効率的でしょう。

そこでおすすめされたのが GitHub Professional Services で、GitHubのプロフェッショナルエキスパートが専属でこちらについてくれて、様々な相談や依頼を捌いてくれるというものだそうです。 有料サービスですが、社内で大量のチケット捌きに従事するエンジニアのコストを考えると十分ペイすると考え、このサービスを利用し窓口になってもらうことにしました。

本来であれば、実現方法もわからないような挑戦の水先案内人として活躍する人々だと思うのですが、今回の例ではGitHubサポートとの専用窓口になって活躍していただきました。ありがとうございます!

Professional Servicesとの協業

というわけでTicketごしではない、生のエンジニアにSlackやビデオチャット経由で対応してもらえることになりました。 対応済リポジトリと対処したcommit SHAを記載したGoogle Sheetsを共有しての作業となりました。

GitHub側の人たちはこちらのリポジトリやIssueやPull Requestの内容など、ユーザの資産に直接関連するものは(こちらから一般的な方法で権限を付与しない限り)直接参照できない体制になっているようで、Professional Servicesでもそれは例外ではないようです。 なので、コミュニケーションをする際、問題が発生している箇所のURLをお出しするやり方では問題が共有できないので、そこは少し注意が必要です。

Spreadsheetで管理した項目はおおよそ次の通り

  • repository name
    • 作業対象となるリポジトリ (Organizationは固定)
  • commit SHAs
    • 消し去りたいcommitのSHAのリスト
    • de-referenceするべきPRはGitHub側でリストアップできる
  • GitHub side
    • Status
      • None, In Progress, Done, Error
      • Error Fixed もあったけど使い分けが微妙だった記憶
    • Additional reference that needs cleanup
      • PRのde-referenceなどを行う前段階に不備があった場合の欄
    • Comment
      • コメント。書いたら書いた日付を併記する運用に途中からなった
  • Mercari side
    • Status
      • None, In Progress, Done, Have Feedback
      • Error もあったけど Have Feedback と使い分けが微妙だった記憶
    • Comment
      • コメント。これも書いたら書いた日付を併記する運用に途中からなった
  • その他作業に便利なカラム
    • commit SHAへのリンクやJIRAチケットへのリンクなど

途中の軌道修正が何回かありましたが、最終的にはこのような情報をやりとりすることになりました。 GitHub side と Mercari side の Status が微妙に同期しきれていなくて、パット見、それぞれのリポジトリについてどちらがボールを持っているのかわかりにくかったのは反省点です。

作業を進めていく上で発見・発生したエッジケースやトラブルをいくつか紹介したいと思います。

リンクをコメントしたときのファイル埋め込み

これは削除作業の検証中に見つかったものです。実際のsensitive dataに絡んだ例は発見されていません。

GitHubの便利な機能として、commit SHA、ファイルパス、行番号などを指定したURLを作成しそれをコメントすると、該当箇所のコンテンツがembedされます。 もしこれがsensitive dataに対して発生した場合どうなるのか?という疑問があります。

コンテンツの埋め込み
commit SHA, ファイル, 行番号を指定したURLをコメントするとコンテンツが埋め込まれる。

検証した結果、該当のcommit SHAを削除し、GCしてもらいリンク先自体が404になった場合でもコンテンツは埋め込まれたままです。 埋め込まれたコンテンツ自体はAPI経由ではアクセスできないそうで、完全にWeb UI上の問題です。 このキャッシュ自体はGitHub側でも消す操作は記事執筆時点では存在していないそうで、即座に消すには該当コメントを1回Editする必要があります。 また、時間経過でもキャッシュが破棄されるようで、筆者の観測範囲では長くても2週間くらいで埋め込まれたコンテンツは消え、単なるリンクに戻っていました。

PR中のConversationとコード断片

これは削除作業の検証中に見つかったものです。実際のsensitive dataに絡んだ例は発見されていません。

上の例に似ていますが、PRのConversationsタブ中のソースコードに対するインラインコメントを行っていた場合、該当部分のコンテンツはde-reference後も保持されます。 このコンテンツはAPI経由ではアクセスできないそうなので、Web UI上の露出にとどまります。 ここを完全にケアするのであればde-referenceではなく、削除を選んだほうがよさそうです。

また、保持されているコンテンツを消したい場合、単に該当のコメントを削除すればよさそうです。

PRを経由せずにcommitされたsensitive data

こちら側での削除処理が完了したのでPRのde-reference作業を行ってもらったところ、該当commitをparentに持つPRが見つからなかったパターン。 おそらくPRを経由せず入り込み、またその後PRが生じなかったと考えられる。 de-reference後のcache cleanの操作を依頼して終わりとしました。

GitHubのWeb UI上で確認できない参照

branchでもtagでもない、謎のreferenceがremoteリポジトリ上に存在して作業が継続できない、という事象がありました。

refs/heads/xxx であればlocal branchでしょうし、refs/tags/xxx であればタグです。 不思議なことに、remote branchをtrackしているはずの refs/remotes/xxx がremoteリポジトリ上に生えていてこいつが問題になることがありました。 これらの参照を見つけた場合、消す必要がありました。

GitHubのremoteリポジトリ上にこのタイプの参照が発生する理由がイマイチわかっていないのでご存知の方がいたら筆者に教えてください…。

wikiリポジトリの掃除

GitHubにはリポジトリとともにwikiが存在していて、そのwikiもgitリポジトリとしてcloneできます。 wikiリポジトリに対しても全量調査を行い、viewなどの削除を行ってもらう必要があります。 リポジトリのcommitはcommit SHAさえ分かればそれに含まれるファイルがわかるUIが存在しています。 wikiは過去のリビジョンを見ることはできますが、そこに含まれていたファイル(ページ)が何かはわかりません。しかし、ファイル名が予めわかっているのであれば、削除されたページにもアクセス可能です。 この問題に対処するために、wikiリポジトリに対してもviewの削除などをサポートチームに行ってもらう必要があります。

まとめ

以上が筆者が担当した範囲で得られた知見です。 作業対象のリポジトリが広い範囲のチームで共同で作っている場合、再混入を検知するために既知のcommit SHAがpushされたら自動的にIssueを建てるGitHub Actionsを作成したりもしました。 GitHubが提供する機能と柔軟性の素晴らしさや、Supportの優秀さなど、GitHubのことが更に好きになりました。

ここまで読んでいただきありがとうございました。

明日のMerpay Advent Calendar 2021は、Frontend Engineerの @tanawa さんです。お楽しみに!