こんにちは、技術本部 Eight Engineering Unit でエンジニアをやっている茂木(@shinnopo_)です。
今回は前回のブログで執筆した、ローカル環境で Lint をかける機構を利用して、pre-commit 時にシークレットスキャンを行うようにした話をしていきます。
前回のブログはこちら↓
目次
背景
Eight では、コミットしたファイルに秘密鍵が含まれていた場合に該当箇所を Pull Request 上でコメントしてれる action-detect-secrets という GitHub Actions を導入しています。
しかし、これでは秘密鍵を GitHub にプッシュしてから気づくことになります。
プロダクトのコードはプライベートリポジトリにホスティングされているため、すぐに大きな問題になるわけではありませんが、できれば GitHub にプッシュする前に気づきたいです。
そこで、この問題を解決するために、pre-commit の仕組みを利用し、ローカル環境でシークレットスキャンを行うようにします。
方針
以前 Lint を行う pre-commit 機構を導入していたため、そこに乗っかる形にします。
今回はローカル環境のため、コメントを行う必要はありません。そのため、Reviewdog は利用せず、detect-secrets のみ利用します。
実装
既存の Git フックの流れは以下の通りです。
git commit ↓ [確認] front/ 以下に変更はあるか? │ ├─ はい → [実行] frontend hooks を呼ぶ │ └─ いいえ → [確認] ruby関連ファイルに差分はあるか? │ ├─ はい → [実行] backend hooks を呼ぶ │ └─ いいえ → コミットする
新しい Git フックは、Lint を行う前にシークレットスキャンを行います。
git commit ↓ [確認] 変更に秘密鍵が含まれているか? │ ├─ はい → [実行] secret scan hooks を呼ぶ │ └─ いいえ → [確認] front/以下に変更はあるか? │ ├─ はい → [実行] frontend hooks を呼ぶ │ └─ いいえ → [確認] ruby関連ファイルに差分はあるか? │ ├─ はい → [実行] backend hooks を呼ぶ │ └─ いいえ → コミットする
このフックを定義したシェルスクリプトのファイルは以下の通りです。
#!/bin/sh set -eo pipefail frontend_hook_run=false backend_hook_run=false secret_scan_hook_run=false function call_frontend_hooks { if [ "$frontend_hook_run" = false ] ; then echo "Running frontend hooks..." front/script/hooks/pre-commit frontend_hook_run=true fi } function call_backend_hooks { if [ "$backend_hook_run" = false ] ; then echo "Running backend hooks..." .gitconfig/serverside_hooks/pre-commit backend_hook_run=true fi } function call_secret_scan_hooks { if [ "$secret_scan_hook_run" = false ] ; then echo "Running secret scan hooks..." ./.gitconfig/secret-scan secret_scan_hook_run=true fi } call_secret_scan_hooks git diff --cached --quiet -- front || call_frontend_hooks git diff --cached --quiet -- '*.rb' '*.rake' '*.ru' '*.jbuilder' Gemfile Guardfile Capfile .irbrc || call_backend_hooks
上記のシェルスクリプトでは、シークレットスキャンのフックを コミットした際に複数ファイルあった場合は、そのファイル数分実行されてしまうため、実行後 hoge_hook_run フラグを true にすることで重複実行を防いでいます。
次にフックの内部の挙動を紹介しますが、フロントエンド/バックエンドのフックの内容は割愛します。
気になる方は前回記事をご覧ください。
シークレットスキャンのフックは以下のように設定をしています。
#!/bin/sh set -eo pipefail # localにdetect-secretsが存在しない場合, secret scanを実行せずに終了する if ! which detect-secrets &> /dev/null then echo "detect-secrets could not be found. Please install it first by running: brew install detect-secrets" exit 1 fi tmpfile=$(mktemp) git diff --cached --name-only | xargs detect-secrets scan | jq .results > "$tmpfile" line=$(cat "$tmpfile" | wc -w) if [ $line -gt 2 ]; then echo "Private key detected. Please delete it." trap 'rm -f "$tmpfile"' EXIT INT TERM HUP exit 1; fi trap 'rm -f "$tmpfile"' EXIT INT TERM HUP
簡単にスクリプトの説明をします。
まず、ローカル環境に detect-secrets
があるかを確認し、なければインストールするよう警告します。
detect-secrets
が既に存在すれば、detect-secrets scan
を実行し、実行結果を tmp ファイルに保存します。
tmp ファイルの単語数が 2 より大きい場合、つまり秘密鍵が検出された場合、警告をしてコミットをブロックします。
稀に誤検知されるケースもありますが、その場合は --exclude-lines
を行うことで、その行をスキップすることが可能です。
導入後の効果
前提として、各々が秘密鍵をコミットしないよう気をつけていたため、目にみえる効果はありません。 しかし、万が一コミットしてしまいそうになった時に、未然に防ぐことができるようになったのは、大きい成果だと思います。
また、この仕組みによって CI からシークレットスキャンを削除することが可能になり、コスト削減や CI の高速化に期待できます。
まとめ
導入のインパクトはそこまで多くないものの、確実にセキュアになったため、やる意義は大きかったと思います。 また、自作の pre-commit 機構を導入していたことで、カスタマイズが容易なのも良かったです。
また、Eight ではエンジニアを募集中です。 少しでもご興味があれば、エントリー/DM お待ちしております!