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コマンドを実行するディレクトリに配置しておかなければならない
# 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.ルールを追加する方法

{
  "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(偽陰性)

allowsallowMessageIds で偽陽性判定を陰性へ反転させる方法を書いたが、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更新

secretlint ${{ env.GIT_DIFF_FILTERED }}

7.参考