はじめに
この記事ではBFGというツールを使ってGitHub上のコミット履歴からパスワードを削除する手順を説明します。
想定するユースケース
- パスワードを平文でファイルに保存し、それをGitHubにpushした。
- しばらくしてから平文はまずいということに気づき、パスワードは環境変数から取得するようにコードを書き換えた。
- しかし、過去のコミット履歴をさかのぼればパスワードが見えてしまう。なのでコミット履歴からもパスワードを削除したい。
制限事項
この方法ではコミット履歴に残っているパスワードは削除できますが、pull requestの履歴(diff)にはパスワードが残ったままになっています。
pull requestからもパスワードを削除したい場合はGitHub上のリポジトリを一度削除するか、GitHubのサポートに依頼してpull requestを削除してもらう必要があります。
参考: Git push has some rejections · Issue #36 · rtyley/bfg-repo-cleaner
サンプルプロジェクトの構成
本記事では以下のような単純なリポジトリをサンプルとして想定します。
.
├── sample.txt
└── secrets.yml
secrets.ymlにはもともと次のようにパスワードが平文で書かれていました。
password: foobar
secret: barbuz
これを次のように修正したのが現在の状況です。
password: "<%= ENV['PASSWORD'] %>"
secret: "<%= ENV['SECRET'] %>"
また、GitHub上に実際のリポジトリ(パスワードはコミット履歴から削除済み)も用意しています。
手順
BFGのインストール
BFGをインストールしていない場合はBFGをインストールします。
以下はHomebrewを使ってインストールする手順です。
$ brew update
$ brew install bfg
$ bfg --version
bfg 1.12.15
なお、gitのバージョンは2.12.2で動作確認しています。
$ git --version
git version 2.12.2
ローカルマシン上の開発中のコードを全部GitHubにpushする
ローカルマシン上にまだpushしてない開発中のコードがあればGitHubにpushしてください。
ローカルマシンに置いたままでも構いませんが、コミット履歴が変わってしまうため、あとからローカルマシンにpull&mergeしようとすると少し手間がかかります。
開発者が複数いる場合は、他の開発者にもその旨声をかけましょう。
GitHub上のpull requestをmergeまたはcloseする
無用なトラブルを避けるため、GitHub上にOpenなpull requestが残っている場合はマージするかcloseしてください。
全ブランチのHEADで平文のパスワードが残っていないことを確認する
GitHub上に複数のブランチがある場合は、全ブランチのHEAD(つまり最新のコード)に平文のパスワードが残っていないこと(今回であれば環境変数から取得するようになっていること)を確認します。
パスワードが残ったままになっていると、BFGの実行時に以下のようなメッセージが表示され、目的のファイルのコミット履歴が変更されないためです。
Protected commits
-----------------
These are your protected commits, and so their contents will NOT be altered:
* commit 922243a1 (protected by 'HEAD') - contains 1 dirty file :
- secrets.yml (22 B)
WARNING: The dirty content above may be removed from other commits, but as
the *protected* commits still use it, it will STILL exist in your repository.
パスワードが残っている場合はブランチを削除したり、最新版のコード(平文パスワードを使用しないコード)をマージするなどして、HEADにパスワードが残らないようにしてください。
--mirror
オプションを付けて新たにリポジトリをcloneする
ローカルマシンで現在開発しているディレクトリとは別の場所に、新たにリポジトリをcloneします。
このとき、--mirror
オプションを付けます。
以下はworkという作業用のディレクトリを用意して、そこへcloneする例です。
$ mkdir work
$ cd work
$ git clone --mirror git@github.com:JunichiIto/bfg-sandbox.git
passwords.txtに削除したいパスワードを保存する
passwords.txtというファイルを作成し、そこへ削除したいパスワードを記述します。
foobar
barbuz
workディレクトリの構成はこうなります。
work/
├── bfg-sandbox.git/
└── passwords.txt
なお、passwords.txtは他にもいくつかの指定方法があります。
参考: BFG Repo-Cleaner --replace-text example
PASSWORD1 # Replace literal string 'PASSWORD1' with '***REMOVED***' (default)
PASSWORD2==>examplePass # replace with 'examplePass' instead
PASSWORD3==> # replace with the empty string
regex:password=\w+==>password= # Replace, using a regex
regex:\r(\n)==>$1 # Replace Windows newlines with Unix newlines
BFGを実行する
次のコマンドでBFGを実行します。
$ bfg --replace-text passwords.txt bfg-sandbox.git
このコマンドを実行すると、ターミナルに以下のような表示が出ます。
Using repo : /Users/jit/dev/sandbox/work/bfg-sandbox.git
Found 3 objects to protect
Found 6 commit-pointing refs : HEAD, refs/heads/bob-feature, refs/heads/develop, ...
Protected commits
-----------------
These are your protected commits, and so their contents will NOT be altered:
* commit fd0f3ec7 (protected by 'HEAD')
Cleaning
--------
Found 9 commits
Cleaning commits: 100% (9/9)
Cleaning commits completed in 55 ms.
Updating 5 Refs
---------------
Ref Before After
--------------------------------------------
refs/heads/bob-feature | ef2cd1af | 9664245c
refs/heads/develop | 1eb3d80b | ba3c5ab8
refs/heads/master | fd0f3ec7 | 147f4942
refs/pull/1/head | 6b269cae | 7a38c8e2
refs/pull/2/head | 1eb3d80b | ba3c5ab8
Updating references: 100% (5/5)
...Ref update completed in 19 ms.
Commit Tree-Dirt History
------------------------
Earliest Latest
| |
D D D D D m m m m
D = dirty commits (file tree fixed)
m = modified commits (commit message or parents changed)
. = clean commits (no changes to file tree)
Before After
-------------------------------------------
First modified commit | e9df6ca3 | 2cebd96f
Last dirty commit | a47e91e4 | 15ab3de5
Changed files
-------------
Filename Before & After
------------------------------------------------------
secrets.yml | 0d217b89 ⇒ 9460a9cb, 6cb378ce ⇒ 2a248e70
In total, 13 object ids were changed. Full details are logged here:
/Users/jit/dev/sandbox/work/bfg-sandbox.git.bfg-report/2017-04-27/05-36-48
BFG run is complete! When ready, run: git reflog expire --expire=now --all && git gc --prune=now --aggressive
--
You can rewrite history in Git - don't let Trump do it for real!
Trump's administration has lied consistently, to make people give up on ever
being told the truth. Don't give up: https://www.theguardian.com/us-news/trump-administration
--
ターミナルに出力された"Protected commits"の欄に注目してください。
Protected commits
-----------------
These are your protected commits, and so their contents will NOT be altered:
* commit fd0f3ec7 (protected by 'HEAD')
ここに"secrets.yml"が載っていなければ、コミット履歴からパスワードを削除できたことになります。
GitHubにpushする
次の手順で変更した履歴をGitHubにpushします。
$ cd bfg-sandbox.git
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
Counting objects: 21, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (16/16), done.
Writing objects: 100% (21/21), done.
Total 21 (delta 2), reused 6 (delta 0)
$ git push
Counting objects: 21, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (21/21), 2.19 KiB | 0 bytes/s, done.
Total 21 (delta 2), reused 21 (delta 2)
remote: Resolving deltas: 100% (2/2), done.
To github.com:JunichiIto/bfg-sandbox.git
+ ef2cd1a...9664245 bob-feature -> bob-feature (forced update)
+ 1eb3d80...ba3c5ab develop -> develop (forced update)
+ fd0f3ec...147f494 master -> master (forced update)
! [remote rejected] refs/pull/1/head -> refs/pull/1/head (deny updating a hidden ref)
! [remote rejected] refs/pull/2/head -> refs/pull/2/head (deny updating a hidden ref)
error: failed to push some refs to 'git@github.com:JunichiIto/bfg-sandbox.git'
git push
したあとに! [remote rejected]
やerror: failed to push some refs
という文言が見えますが、これは想定通りです。
制限事項の項で説明したとおり、pull request上の履歴は変更できないため、このようなメッセージが表示されます。
GitHub上のコミット履歴からパスワードが消えたことを確認する
GitHubにアクセスしてコミット履歴を確認してください。
パスワードが***REMOVED***
のようになっていれば成功です。
https://github.com/JunichiIto/bfg-sandbox/commit/7a38c8e217e30b718804b1a8bfa92cfa72f26200
ただし、制限事項の項で述べたように、pull requestの履歴ではパスワードが残ったままになります。
https://github.com/JunichiIto/bfg-sandbox/pull/2/files
開発用リポジトリを改めてgit cloneする
コミット履歴が書き換えられているため、もともと開発用で使っていたリポジトリに戻ってgit pull
すると、fatal: refusing to merge unrelated histories
というメッセージが出て正常にpullできないはずです。
$ git pull
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 20 (delta 2), reused 19 (delta 2), pack-reused 0
Unpacking objects: 100% (20/20), done.
From github.com:JunichiIto/bfg-sandbox
* branch develop -> FETCH_HEAD
+ 1eb3d80...6da4d6f develop -> origin/develop (forced update)
fatal: refusing to merge unrelated histories
git merge --allow-unrelated-histories origin/develop
のようにすれば、強制的にマージすることもできますが、新しくマージコミットができたりするので、改めてcloneし直した方が良いと思います。
# 旧開発用リポジトリをリネーム
$ mv bfg-sandbox bfg-sandbox.old
# 改めてclone
$ git clone git@github.com:JunichiIto/bfg-sandbox.git
開発者が複数いる場合は、他の開発者にも改めてgit cloneするように伝えてください。
手順は以上です。
参考資料
公開したくないデータをリポジトリから削除する方法を説明しているGitHubの公式ページです。
BFGを使わずに手作業でデータを削除する方法も載っています。
Removing sensitive data from a repository - User Documentation
BFGの公式ページです。
作業前にはこちらもひととおり目を通しておくと良いでしょう。