1.この記事を書こうと思った背景
昨今のニュースを眺めていると、クレデンシャル情報(シークレット情報/機密情報)漏洩対策の一環として、ガードレール的なツールを使いたい、またそういったツールを継続的に利用したいというお気持ちが一層強くなる。
そういうわけで Secretlint をはじめとするガードレール的なツールを使おうと思うのだが、SecretlintをCIパイプラインでお手軽に使う方法をひらめいたのでここに書き残しておく。なお、今回の導入対象のCIパイプラインは、GitHub Actionsとしている。というのも https://github.com/secretlint にSecretlintをNode.jsのライブラリとしてGitHub Actions上で扱うサンプルコードが公開されていることから検証のハードルが低いと感じたためである。
secretlint/secretlint-github-actions-example
さて、この記事では、SecretlintをCIパイプラインでお手軽に使う方法の他に、誤検知対策としてカンタンにルールを追加する方法についても触れたい。この手のツールは誤検知が大量に作動してしまえば、そのツールはオオカミ少年と認識されてしまいかねない。それだけにSecretlintでは簡易的とはいえ誤検知対策ができるという点は、かゆいところに手が届いているといえるのではないだろうか。
2.解決したい課題とその解決策
まず、具体的な方法論について話す前に前提となる解決したい課題と、ここで提案する解決策について述べておきたい。
2-1.解決したい課題
- SecretlintをGitHub Actionsのワークフローでお手軽に使いたい
- Secretlintを使うためにはDockerかNode.jsが必要である戦う
- つまり、GitHub ActionsのワークフローのなかでDockerかNode.jsのいずれかを使えるようにセットアップするJobが必要
- Node.jsでSecretlintを使う場合、Secretlintのインストールが必要とやや手間
$ npm install secretlint @secretlint/secretlint-rule-preset-recommend --save-dev
- ルールの追加もお手軽にしたい
- Secretlintではビルトインのルールが提供されていないが、専用の設定ファイルの
.secretlintrc.{yml,yaml,js} (以下、.secretlintrc.json)
でルールを利用者側で導入する必要がある - Dockerコンテナイメージの場合、推奨ルールセットである、
@secretlint/-rule-preset-recommend
が同梱されており、イメージをbuildするだけでok - Node.jsの場合、上述した
@secretlint/-rule-preset-recommend
などを.secretlintrc.json で指定し、secretlintコマンドを実行するディレクトリに配置しておかなければならない
- Secretlintではビルトインのルールが提供されていないが、専用の設定ファイルの
# Dockerコンテナイメージを使う場合、Secretlintのインストールは不要
# `.secretlintrc.json もイメージに同梱されている
# ルールを追加したい場合、自分で用意する必要があり、その方法は後述
$ docker run -v `pwd`:`pwd` -w `pwd` --rm -it secretlint/secretlint secretlint "**/*"
# Node.jsの場合、以下のコマンドで .secretlintrc.json を生成する必要がある
$ npx secretlint --init
# .secretlintrc.json はsecretlintコマンドを実行するディレクトリに配置する必要がある
$ cat <<EOF > .secretlintrc.json
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend"
}
]
}
EOF
ところで、SecretlintをGitHub Actionsのワークフローで使うメリットはなんだろうか?
- メリットのひとつは、Secretlintの開発者であるazuさんの、 SecretlintでAPIトークンや秘密鍵などのコミットを防止する | Web Scratch で記載されている、
一度きりのチェックだと継続的なセキュリティは担保できない
という点であろう一度きりのチェックだと継続的なセキュリティは担保できないので、CIやGitコミットフックなどでプロジェクトに導入する方法を紹介しています。 また、個人環境のグローバルなGitコミットフックに常にSecretlintのチェックを入れることもできます。
- SecretlintをCIパイプラインで使う副次的効果として、Secretlintを通過したという証跡を残すことも期待できる
- 検証している当初は、Secretlintをローカルで使えなくてもCIパイプラインで組み込むことで代用できると思っていた。しかし、いくらリポジトリをプライベート運用していたところでActions上で動かす以上、CIパイプラインでツールを使うことで安心してはいけないなと考え直した。CIパイプラインで使うツールの立ち位置はあくまでもガードレール的な扱いとし、ローカルでもなにかしらの対策を導入しておいたほうがいいだろう。
継続的なセキュリティに関連する考え方として多層防御というものがある
多層防御という考え方の根底にあるものは、個々の防御策には抜け道がどうしてもできてしまうという問題意識である。この「抜け道を塞いでいく」ために、異なる検知/防御ロジックの防御策をパイ生地のように幾重にも重ね合わせようというアプローチが多層防御である。
この多層防御という文脈で改めてSecretlintを考えると、Secretlintの得意/不得意を理解し、Secretlintを補完するツールの調査というトピックが浮上してくるが、この記事では取り扱わないこととする笑。
2-2.SecretlintをGitHub Actionsのワークフローでお手軽に使う解決策
- addnab/docker-run-action を使ってSecretlintのDockerコンテナを使う
- ルールの追加はDockerホストとDockerコンテナ間でボリュームを共有すればok
- 具体的には追加したいルールが書かれた、.secretlintrc.jsonをDockerホストからDockerコンテナへ渡す
3.[結論]GitHub ActionsでSecretlintのDockerコンテナを実行するワークフローの書き方
addnab/docker-run-action に基本的な書き方は載っているのでSecretlintをDockerコンテナ上で実行する場合にかぎった注意点について、とりわけwith directiveについて書いていく。
# 略
steps:
- uses: actions/checkout@v3
# 変更が入ったファイルのみをsecretlintコマンドの引数に渡すために使う
- uses: technote-space/get-diff-action@v6
- name: Run secretlint command on docker
id: run-secretlint-command
# GitHub Actions上でdocker runコマンドを実行するために使う
uses: addnab/docker-run-action@v3
# with directiveで書くことはdocker runコマンドに渡す引数とほとんど同じ
with:
# https://hub.docker.com/r/secretlint/secretlint/tags を参考にタグを指定
image: secretlint/secretlint:v5.1.1
options: >
-v ${{ github.workspace }}:${{ github.workspace }}
-w=${{ github.workspace }}
--rm
# 変更が入ったファイル名。technote-space/get-diff-action で取得。
run: secretlint ${{ env.GIT_DIFF_FILTERED }}
# docker run コマンドで実行する際のカレントディレクトリ配下全てのファイルを対象とする場合
#run: secretlint "**/*"
- with directiveで何をどう書くか?の基本的な考え方は、docker runコマンドに渡すオプションをimage directive, options directive, run directiveそれぞれにあてはめていくということ!
$ docker run \
-v `pwd`:`pwd` \ # with directive
-w `pwd` --rm -it \ # 同上
secretlint/secretlint \ # image directive
secretlint ${{ env.GIT_DIFF_FILTERED }} # run directive
- カレントディレクトリ配下全てのファイルが Docker コンテナ内にある理由は、options directive で-v オプションを使い、DockerホストのリポジトリのルートディレクトリをDockerコンテナ間とボリュームを共有しているため
- また -w オプションで docker run コマンドの実行ディレクトリを指定している(ここではリポジトリのルートディレクトリ)
-it
オプションだけoptions directiveに書かない理由(仮説)
おそらく、docker runコマンドを介してDockerコンテナ内で実行するコマンドの標準出力をDockerホスト側へ出力させる必要がないからだと思われる。手元で -it
オプション無しでもdocker runコマンドを実行できることを確認できた。もし、ご存知の方がいらっしゃいましたら、教えていただけるとうれしいです :pray:
4.ルールを追加する方法
- Secretlintではビルトインのルールはなく、.secretlintrc.json に書かれたルールを上から下に評価される
- デフォルトでは全てがallow
- したがって、まずルールセットの “@secretlint/secretlint-rule-preset-recommend” で評価してから個別に評価したい値や、後述する “allowMessageIds” を書くのがセオリー
- 以下の .secretlintrc.json は secretlint/configuration.md at master · secretlint/secretlint を参考に作成
- RexExp(正規表現)については以下を参考にした
- 以下のAWSのクレデンシャル情報のサンプルはこちらから拝借した
{
"rules": [
{
// ↓ 追加したいルールを指定
"id": "@secretlint/secretlint-rule-preset-recommend",
"rules": [
{
// ↓ 個別に追加したいルールを指定
"id": "@secretlint/secretlint-rule-aws",
"options": {
"allows": [
// 以下の値はallow(陰性)とする
//"wJalrXUtnFEMI/K7MDENG/bPxRfiCYSECRETSKEY" // allowする値を固定値としたい場合
"/wJalrXUtnFEMI/ig" // RexExp(正規表現)を使った場合
]
}
}
]
}
]
}
4-1.誤検知対策
[ケース]検知したものは誤って陽性となってしまった!
まず、.secretlintrc.json では @secretlint/secretlint-rule-preset-recommend
をルールとして追加している状況下で、以下のファイルをコミットしようとしたとする。なお、このファイルはSecretlintの テストのインプットファイル から拝借した。
{
"type": "service_account",
"project_id": "xxxxxxxx",
"private_key_id": "98ssssssssssssssssssssssssssssssssssss676",
"private_key": "-----BEGIN PRIVATE 略
}
すると、以下のように検知する(エラーとなる)。
error [PrivateKeyJSON] found GCP Service Account's private key(json): gcp.ng.privatekey.json
@secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-gcp
error [PrivateKey] found private key: -----BEGIN PRIVATE 略
略
9+x877\n1BdcHCjWoYHxsnXIEao=\n----- @secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-privatekey
これはツールとしては正しい挙動だ。しかし、このリポジトリ特有の事情というような、Secretlintの範疇外の理由で陽性は誤検知だった(偽陽性/false-positive)!陽性ではなく陰性と判定したい!というケースがあったとする。
このようなケースに対してSecretlintではルールを追加するというアプローチが用意されているので、そちらについて書いていきたい。
ルールを追加して偽陽性とされたものを陰性とする!
やることは.secretlintrc.jsonに、以下のように allows
で陰性としたい値を書いたり、allowMessageIds
を指定するだけである。
{
"rules": [
{
"id": "@secretlint/secretlint-rule-preset-recommend",
"rules": [
{
"id": "@secretlint/secretlint-rule-aws",
"options": {
"allows": [
// 以下の値はallow(陰性)とする
//"wJalrXUtnFEMI/K7MDENG/bPxRfiCYSECRETSKEY" // allowする値を固定値としたい場合
"/wJalrXUtnFEMI/ig" // RexExp(正規表現)を使った場合
]
}
},
{
// ↓ 追加
"id": "@secretlint/secretlint-rule-gcp",
// ↓ @secretlint/secretlint-rule-gcp が吐いた `allowMessageId`
"allowMessageIds": ["PrivateKeyJSON"]
},
{
// ↓ 追加
"id": "@secretlint/secretlint-rule-privatekey",
// ↓ @secretlint/secretlint-rule-gcp が吐いた `allowMessageId`
"allowMessageIds": ["PrivateKey"]
}
]
}
]
}
allows
で陰性としたい値は分かるが、allowMessageIds
はどうやって調べるのだろうか? allowMessageIds
は実は先ほどのエラーメッセージでいうところの error
の横に書かれている。サンプルとコメントを追記したのでもう一度見てみよう。
error [<allowMessageIds>]
<検知したルール/プリセット>
# "@secretlint/secretlint-rule-gcp"で指定する "allowMessageIds" は PrivateKeyJSON
error [PrivateKeyJSON] found GCP Service Account's private key(json): gcp.ng.privatekey.json
@secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-gcp
# "@secretlint/secretlint-rule-privatekey"で指定する "allowMessageIds" は PrivateKey
error [PrivateKey] found private key: -----BEGIN PRIVATE 略
略
9+x877\n1BdcHCjWoYHxsnXIEao=\n----- @secretlint/secretlint-rule-preset-recommend > @secretlint/secretlint-rule-privatekey
上述した allows
は陰性としたい値をベタ書きで指定するだけではなく、正規表現を駆使することもできる。allowMessageIds
では、誤検知ごとに確実に判定を制御できるという印象を受けた。
4-2.false-positive(偽陽性)とfalse-negative(偽陰性)
allows
や allowMessageIds
で偽陽性判定を陰性へ反転させる方法を書いたが、allowとするルールを追加し続けるということは潜在的に偽陰性(false-negative)を増やしてしまう危険を孕んでいる。そこで、偽陽性を少なくするためのアプローチとして、allowとするルールを追加するだけではなく、検知した該当箇所のリファクタリングなど別のアプローチも選択肢として持っておくといいはずである。(それができないからルールの追加という選択を取ることになることが往々にあるのだが。)
false-positive(偽陽性)とfalse-negative(偽陰性)。検知の精度を引き上げるためにどちらの極小化を目指せばよいのか?難しい問題だ。この問題について明確な答えを提示できないので、考えを巡らす上で参考になりそうな記事だけでも貼らせていただきたい。
False PositiveとFalse Negative - Qiita
さて、これまでルールの追加とその是非について扱ってきたが、検知対象を絞り込むこともできるのでそちらについても書いていきたい。
5.検知対象の絞り方
Secretlintでは検知対象を絞ることもできる。secretlintコマンドの実行時のカレントディレクトリ配下すべてのファイルを対象とする場合と差分が入ったファイルのみを対象としたい場合の2つについて、ワークフローから抜粋する。
# 上述したワークフロー抜粋
with:
# https://hub.docker.com/r/secretlint/secretlint/tags
image: secretlint/secretlint:v5.1.1
# https://github.com/secretlint/secretlint#using-docker
options: >
-v ${{ github.workspace }}:${{ github.workspace }}
-w=${{ github.workspace }}
--rm
# 変更が入ったファイル名。technote-space/get-diff-action で取得。
run: secretlint ${{ env.GIT_DIFF_FILTERED }}
# docker run コマンドで実行する際のカレントディレクトリ配下全てのファイルを対象とする場合
# カレントディレクトリ配下全てのファイルが Docker コンテナ内にある理由は、 options directive でDockerホストのリポジトリのルートディレクトリをDockerコンテナ間とボリュームを共有することで
#run: secretlint "**/*"
6.GitHub ActionsでSecretlintのDockerコンテナを実行するワークフロー全体
最後にワークフロー全体を貼っておく。
name: Secretlint
# https://github.com/secretlint/secretlint/issues/166
on: [pull_request]
# secretlint + git diff on Pull Request
# https://github.com/secretlint/secretlint
jobs:
test:
name: "Secretlint"
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
# 変更が入ったのみをsecretlintコマンドの引数に渡すために git diff をする
- uses: technote-space/get-diff-action@v6
- name: Run secretlint command on docker
id: run-secretlint-command
# GitHub Actionsで docker run コマンドを実効するために使う
uses: addnab/docker-run-action@v3
# with directive で書くことは docker run コマンドに渡す引数にあたる
with:
# https://hub.docker.com/r/secretlint/secretlint/tags
image: secretlint/secretlint:v5.1.1
# https://github.com/secretlint/secretlint#using-docker
options: >
-v ${{ github.workspace }}:${{ github.workspace }}
-w=${{ github.workspace }}
--rm
# 変更が入ったファイル名。technote-space/get-diff-action で取得。
run: secretlint ${{ env.GIT_DIFF_FILTERED }}
# docker run コマンドで実行する際のカレントディレクトリ配下全てのファイルを対象とする場合
# カレントディレクトリ配下全てのファイルが Docker コンテナ内にある理由は、 options directive でDockerホストのリポジトリのルートディレクトリをDockerコンテナ間とボリュームを共有することで
#run: secretlint "**/*"
2022/03/27更新
- この記事で紹介した、GitHub ActionsでSecretlintのDockerコンテナを実行する方法とNode.jsを使ってSecretlintを実行する方法を使ったデモPRを作った
- Demo: Run Secretlint only on changed files with Node.js or Docker by gkzz · Pull Request #7 · secretlint/secretlint-github-actions-example
- このデモでは、変更が入ったファイルに対してのみSecretlintを実行するやりかたを紹介している
- デモでは、Secretlintを実行する前に technote-space/get-diff-action: GitHub Actions to get git diff を使って変更が入ったファイルを
GIT_DIFF_FILTERED
として保持し、secretlintコマンドに渡している
secretlint ${{ env.GIT_DIFF_FILTERED }}
7.参考
- Secretlint
- Secretlintの開発者であるazuさんの紹介記事
allowMessageIds
を指定してallowとする例allows
でRexExp(正規表現)を使ってallowとする例- False PositiveとFalse Negative - Qiita
- Demo: Run Secretlint only on changed files with Node.js or Docker by gkzz · Pull Request #7 · secretlint/secretlint-github-actions-example