手を動かして学ぶOWASP DevSecOps Guideline - 第2回:シークレットスキャニング編

手を動かして学ぶOWASP DevSecOps Guideline - 第2回:シークレットスキャニング編

本記事は、手を動かして学ぶOWASP DevSecOps Guideline - 第1回:CI/CD基盤構築編の続編です。

目次

    はじめに

    こんにちは!クラウドインテグレーション部の野村です。

    この記事は、OWASP DevSecOps Guidelineを参考に、 セキュアなCI/CDパイプラインの構築を目指すシリーズの第2回です!

    前回の記事では、OWASP DevSecOps Guidelineの紹介と、今後セキュアなパイプライン構築のハンズオンを実施するための、ベースとなるCI/CDパイプラインの作成を行いました。

    今回は、「コミット時」と「PR作成時」の2つのフェーズで自動検知し、誤った秘密情報の漏洩を未然に防ぐため、パスワード・APIキーなどのリポジトリへの混入を検知・防止するシークレットスキャニング機能を組み込んでいきます。

    フローの以下の部分が対象です。20260710_e921e3a7_1 出典:OWASP Foundation, ‘OWASP DevSecOps Guideline - v-0.2’, Licensed under CC BY-SA 4.0

    秘密情報(パスワード・APIキー)の取り扱いについて

    APIキーやパスワード、アクセストークン、秘密鍵などの秘密情報は、アプリケーションや外部サービスにアクセスするための「鍵」となる情報です。

    これらの情報がソースコードやGitリポジトリに含まれてしまうと、第三者に不正利用される可能性があります。たとえば、クラウドサービスのアクセスキーが漏えいした場合、不正なリソース作成やデータ参照、想定外の課金につながるおそれがあります。

    特にGitHubなどのパブリックリポジトリは、公開されたコードに含まれるクレデンシャルを探索するツール等によって監視されている可能性があります。そのため、誤って秘密情報をpushしてしまうと、短時間で発見・悪用されるリスクがあります。

    また、Privateリポジトリであっても安全とは言い切れません。コードホスティングサービスのアカウントが侵害された場合、リポジトリ内に保存されていた別サービスの認証情報が芋づる式に奪取される可能性があります。そのため、公開・非公開にかかわらず、リポジトリに秘密情報を保存しないことが重要です。

    さらに、Gitは変更履歴を保持する仕組みであるため、一度コミットされた秘密情報は、該当ファイルを削除しただけでは十分に消えない場合があります。

    ガイドラインのSecret Managementの章 では、以下のような内容が解説されています。

    • パスワード、秘密鍵、APIキー、秘密情報などをソースコードやGitリポジトリに保存してはいけない。

    • 誤って認証情報をコミットすると、削除してもGit履歴やコードホスティングサービス上に残る可能性がある。

    • そのため、リポジトリに入った後で検出するよりも、コミット前に検出して防ぐことが理想。

    • もしシークレットが漏えいした場合、その認証情報はすでに侵害されたものとして扱い、無効化・再発行する必要がある。

    上述の内容を仕組みで担保するために、CI/CDパイプラインにはシークレットスキャンの機能を実装することが重要になってきます。


    秘密情報の混入をどこで検知する?

    ガイドラインでは、秘密情報の混入を以下の2か所で検知することを推奨しています。

    1つ目は、開発者のローカル端末です。Pre-commit hooksを利用し、コミット時にシークレットスキャニングを実行することで、コードベースに秘密情報が含まれていることを検知します。 検知した場合は、コミットを中断し、開発者に修正を促します。

    出典:OWASP Foundation, ‘OWASP DevSecOps Guideline - v-0.2’, Licensed under CC BY-SA 4.0

    2つ目は、ビルドサーバやビルドプロセスです。GitHub ActionsやJenkinsなどのパイプライン上でシークレットスキャニングを実行し、リポジトリ内に秘密情報が含まれていないかを継続的に確認します。

    ローカルでの対策のみだと、開発者がシークレットスキャニングを無効化することも考えられるため、ローカルとリモート双方での確認が有効と考えられます。


    補足

    ガイドラインでは言及がありませんが、コードホスティングサービスではリモートへのpush時にシークレットの混入を検知する機能を持つものがあります。
    実運用においては、そのような機能の利用も検討すべきかと考えられます。

    代表例:
    GitHubのPush protection (パブリックリポジトリでは標準で無料利用可能です)

    シークレットスキャニングに利用できるツール

    リポジトリ内の秘密情報を検知するツールとして、以下のようなツールが紹介されています。

    gittyleaks - Gitリポジトリの秘密情報を見つける
    git-secrets - Gitリポジトリへのシークレットや認証情報のコミットを防止する
    truffleHog - Gitリポジトリを検索し、コミット履歴を深く掘り下げてエントロピーの高い文字列や秘密情報を探索する

    実践

    ここからは、実際に前回記事で作成したCI/CDパイプラインに、シークレットスキャニングを実装していきます。

    前提条件

    本記事の内容を実践するにあたって、以下の環境が準備されていることを前提とします。

    • 前回の記事で作成したGitHubリポジトリが存在すること
    • Gitがインストール済みであること
    • GitHub Actionsを利用できること
    • Pythonまたはpipxを利用できること
    • pre-commitをインストールできること

    今回使用する主なツールは以下の通りです。

    名称 用途
    gitleaks リポジトリ内のシークレット検知
    pre-commit コミット前にgitleaksを実行するためのフック管理
    GitHub Actions CI/CDパイプライン上でのシークレットスキャン実行

    また、以降のハンズオンを実施した執筆者のローカル環境は、WSL2/Ubuntu環境です。

    手順1. ブランチ戦略の見直し

    前回記事で構築したパイプラインでは、mainブランチへの直接Pushが可能な状態となっていました。

    チーム開発やCI/CDパイプラインを運用する場合には、Pull Requestを起点とした開発フローを採用することが一般的です。

    Pull Requestを経由することで、以下のようなメリットがあります。

    • コードレビューを実施できる
    • CI/CDによる自動テストや各種チェックを実行できる
    • 変更履歴を追跡しやすくなる

    そこで本記事では、mainブランチへの直接Pushを禁止し、Pull Request経由でのみ変更を取り込めるように設定します。

    今回採用するブランチフローは以下の通りです。

    feature/*
        ↓
    Pull Request
        ↓
    main
    

    このPull Requestをトリガーとして、後続の手順で実装するシークレットスキャンや脆弱性スキャンなどのセキュリティチェックを実行できるようになります。


    1.1 mainブランチを保護する

    まず、mainブランチへの直接のpushを禁止し、Pull Request経由でのみ変更を取り込めるように設定していきます。

    1. リポジトリのSettingsを開く

    2. 左メニューからBranchesを選択

    3. Branch protection rulesでAdd classic branch protection ruleをクリック

    1. Branch name patternにmainと入力する

    1. Require a pull request before mergingを有効化する
      ※Require approvalsは、mergeする際に承認者の承認を必要とする設定です。実運用では有効化が推奨されますが、今回は検証のため無効にします。

    1. Createをクリックする

    この設定により、mainブランチへ直接変更を反映するのではなく、Pull Requestを経由して変更を取り込むフローになります。


    1.2 featureブランチを作成する

    手順 1.1でmainブランチへの直接変更を禁止したため、以降の手順ではfeatureブランチで作業を行い、Pull Requestでmainブランチへ取り込む流れになります。

    作業用のfeatureブランチを作成しましょう。

    ローカルリポジトリでmainブランチに移動し、最新の状態を取得します。

    git checkout main 
    git pull origin main
    

    作業用のfeatureブランチを作成します。

    git checkout -b feature/secret-scanning
    

    現在のブランチが切り替わっていることを確認します。

    git branch
    

    手順2. gitleaksをローカルで実行する

    ここからは、シークレットスキャニングツールである gitleaksを利用して、ローカル端末上でリポジトリ内の秘密情報を検知してみます。

    まずはPre-commit hooksやGitHub Actionsに組み込む前に、gitleaks単体でどのようにシークレットを検知できるのかを確認します。


    2.1 gitleaksをインストールする

    gitleaksをローカル端末にインストールします。

    macOSの場合は、Homebrewでインストールできます。

    brew install gitleaks
    

    Windowsの場合は、Scoopでインストールできます。

    scoop install gitleaks
    

    Linuxの場合は、GitHub Releasesからバイナリをダウンロードして利用できます。

    curl -LO https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz
    tar -xzf gitleaks_8.30.1_linux_x64.tar.gz
    sudo mv gitleaks /usr/local/bin/
    sudo chmod +x /usr/local/bin/gitleaks
    

    インストール後、以下のコマンドでgitleaksが実行できることを確認します。

    gitleaks version
    

    バージョン情報が表示されればOKです。


    2.2 テスト用のシークレットを作成する

    検知動作を確認するため、テスト用のファイルを作成します。
    ※手順1.2で用意したリポジトリにファイルを作成してください。

    注意:ここでは検証用のダミー文字列を使用します。実在する認証情報やAPIキーは絶対に使用しないでください。

    mkdir -p test
    
    cat << 'EOF' > test/dummy-secret.txt
    slack_token = "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx"
    github_token = "ghp_abcdefghijklmnopqrstuvwxyz0123456789ABCD"
    api_key = "1234567890abcdef1234567890abcdef12345678"
    EOF
    

    作成したファイルを確認します。

    cat test/dummy-secret.txt
    

    2.3 gitleaksでリポジトリをスキャンする

    次に、gitleaksでリポジトリをスキャンします。

    gitleaks dir . --verbose
    

    シークレットが検知されると、以下のように検知内容が表示されます。

    Finding:     slack_token = "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx"
    Secret:      xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx
    RuleID:      slack-bot-token
    Entropy:     4.953909
    File:        test/dummy-secret.txt
    Line:        1
    Fingerprint: test/dummy-secret.txt:slack-bot-token:1
    
    Finding:     api_key = "1234567890abcdef1234567890abcdef12345678"
    Secret:      1234567890abcdef1234567890abcdef12345678
    RuleID:      generic-api-key
    Entropy:     3.970951
    File:        test/dummy-secret.txt
    Line:        3
    Fingerprint: test/dummy-secret.txt:generic-api-key:3
    

    Fileには検知されたファイル、Lineには検知された行、RuleIDにはどのルールで検知されたかが表示されます。

    このように、gitleaksを利用することで、リポジトリ内に含まれるAPIキーや認証情報らしき文字列を検知できます。


    2.4 テスト用ファイルを削除する

    検知確認ができたら、テスト用に作成したファイルを削除します。

    rm -f test/dummy-secret.txt
    rmdir test
    

    手順3. Pre-commit hooksにgitleaksを組み込む

    前の手順では、gitleaksを手動で実行し、ローカル端末上でシークレットを検知できることを確認しました。

    続いて、pre-commitを利用して、コミット時に自動でgitleaksが実行されるようにします。 これにより、シークレットがGitの履歴に残る前に検知し、コミットを中断できます。


    3.1 pre-commitをインストールする

    まず、pre-commitをインストールします。

    pipを利用する場合は、以下のコマンドでインストールできます。

    pip install pre-commit
    

    Ubuntu/WSL環境の場合はこちら

    sudo apt update
    sudo apt install -y pipx
    pipx ensurepath
    source ~/.bashrc
    pipx install pre-commit
    

    インストール後、以下のコマンドでpre-commitが実行できることを確認します。

    pre-commit --version
    

    バージョン情報が表示されればOKです。


    3.2 pre-commit-config.yamlを作成する

    次に、リポジトリのルートディレクトリに.pre-commit-config.yamlを作成します。

    cat << 'EOF' > .pre-commit-config.yaml
    repos:
      - repo: https://github.com/gitleaks/gitleaks
        rev: v8.30.1
        hooks:
          - id: gitleaks
    EOF
    

    この設定により、コミット時にgitleaksが実行されるようになります。


    3.3 pre-commit hookを有効化する

    作成した設定ファイルをもとに、Gitのpre-commit hookを有効化します。

    pre-commit install
    

    以下のように表示されればOKです。

    pre-commit installed at .git/hooks/pre-commit
    

    これで、以降のコミット時にpre-commitが実行され、gitleaksによるシークレットスキャンが行われるようになります。


    3.4 シークレットを含むファイルをコミットしてみる

    pre-commit hookが有効化できたので、実際にシークレットを含むファイルをコミットしようとした場合に、gitleaksによって検知されるか確認します。

    動作確認のため、再度テスト用のシークレットファイルを作成します。

    mkdir -p test
    
    cat << 'EOF' > test/dummy-secret.txt
    slack_token = "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx"
    github_token = "ghp_abcdefghijklmnopqrstuvwxyz0123456789ABCD"
    api_key = "1234567890abcdef1234567890abcdef12345678"
    EOF
    

    作成したファイルをステージングします。

    git add test/dummy-secret.txt
    

    この状態でコミットを実行します。

    git commit -m "Add dummy secret"
    

    gitleaksによってシークレットが検知されると、以下のようにpre-commit hookが失敗し、コミットが中断されます。

    Detect hardcoded secrets.................................................Failed
    - hook id: gitleaks
    - exit code: 1
    
    ○
        │╲
        │ ○
        ○ ░
        ░    gitleaks
    
    Finding:     slack_token = "REDACTED
    Secret:      REDACTED
    RuleID:      slack-bot-token
    Entropy:     4.953909
    File:        test/dummy-secret.txt
    Line:        1
    Fingerprint: test/dummy-secret.txt:slack-bot-token:1
    
    Finding:     api_key = "REDACTED"
    Secret:      REDACTED
    RuleID:      generic-api-key
    Entropy:     3.970951
    File:        test/dummy-secret.txt
    Line:        3
    Fingerprint: test/dummy-secret.txt:generic-api-key:3
    
    12:12PM INF 0 commits scanned.
    12:12PM INF scanned ~187 bytes (187 bytes) in 34.1ms
    12:12PM WRN leaks found: 2
    

    このように、pre-commit hookにgitleaksを組み込むことで、シークレットがGitの履歴に残る前に検知し、コミットを防止できます。

    手順4. GitHub Actionsでgitleaksを動かす

    前の手順では、ローカル端末でgitleaksにより秘密情報を検知し、コミットを停止する仕組みを構築しました。

    しかし、ローカル端末のみの設定では以下のようなケースに対応できません。

    • 開発者がpre-commit hookを設定していない
    • 開発者がpre-commit hookを無効化してコミットしてしまう
    • 過去に混入したシークレットがリポジトリ内に残っている

    そこで、GitHub Actionsにもgitleaksを組み込み、Pull Request作成時にリモート側でもシークレットスキャンを実行できるようにします。


    4.1 GitHub Actionsワークフローを作成する

    リポジトリの.github/workflows/配下に、シークレットスキャン用のワークフローファイルを作成します。

    以下の内容で.github/workflows/secret-scan.ymlを作成します。

    cat << 'EOF' > .github/workflows/secret-scan.yml
    name: Secret Scan
    
    # mainブランチ向けのPull Requestが作成・更新されたときに実行
    on:
      pull_request:
        branches:
          - main
    
    # ワークフローに付与する権限
    permissions:
      contents: read        # リポジトリの内容を読み取るために必要
      pull-requests: read   # Pull Requestの情報を読み取るために必要
    
    jobs:
      gitleaks:
        name: Gitleaks
        runs-on: ubuntu-latest  # GitHub Actionsの実行環境
    
        steps:
          # リポジトリのソースコードをチェックアウト
          - name: Checkout
            uses: actions/checkout@v4
            with:
              fetch-depth: 0  # Git履歴も含めて取得する
    
          # gitleaksを実行してシークレットスキャンを行う
          - name: Run Gitleaks
            uses: gitleaks/gitleaks-action@v2
            env:
              # Pull Requestスキャン時に必要
              # GitHub Actionsが自動生成するトークンを利用する
              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    EOF
    

    このワークフローでは、mainブランチに対してのPull Requestが作成されたタイミングで、gitleaksによるシークレットスキャンを実行します。

    actions/checkout@v4 の fetch-depth: 0 は、Gitの履歴を含めてチェックアウトするための設定です。 gitleaksでリポジトリの履歴も含めてスキャンしたい場合に必要になります。


    4.2 ワークフローファイルをコミットする

    作成したワークフローファイルをコミットします。
    ※先ほど作成したdummy-secret.txtがステージされている場合は、ステージを一旦解除してください。

    git add .github/workflows/secret-scan.yml
    git commit -m "Add gitleaks GitHub Actions workflow"
    

    この時点で、pre-commit hookが実行されます。 ワークフローファイルにシークレットは含まれていないため、gitleaksのチェックが成功し、コミットできればOKです。


    4.3 featureブランチをpushする

    作業中のfeatureブランチをGitHubへpushします。

    git push origin feature/secret-scanning
    

    push後、GitHubのリポジトリ画面に表示されるCompare & pull requestをクリックし、Pull Requestを作成します。

    Pull Requestを作成すると、Secret Scanワークフローが実行されます。 GitHubのPull Request画面で、gitleaksのチェックが成功していればOKです。


    4.4 秘密情報をpushしてみる

    最後に、秘密情報を含むファイルをpushした場合に、GitHub Actions上のgitleaksで検知できることを確認します。

    まず、テスト用のシークレットファイルを作成します。
    ※先の手順で作ったシークレットファイルが残っていればこの手順は不要です。

    mkdir -p test
    
    cat << 'EOF' > test/dummy-secret.txt
    slack_token = "xoxb-123456789012-123456789012-abcdefghijklmnopqrstuvwx"
    github_token = "ghp_abcdefghijklmnopqrstuvwxyz0123456789ABCD"
    api_key = "1234567890abcdef1234567890abcdef12345678"
    EOF
    

    作成したファイルをステージングします。

    git add test/dummy-secret.txt
    

    通常のgit commitではPre-commit hooksによりコミットが失敗するため、検証のために--no-verifyオプションを付けてコミットします。

    git commit -m "Add dummy secret" --no-verify
    

    コミット後、featureブランチをpushします。

    git push origin feature/secret-scanning
    

    push後、GitHub上でPull Requestを作成、または既存のPull Requestを更新します。

    Pull Requestを作成すると、Secret Scanワークフローが実行されます。

    GitHub Actionsの実行結果を確認し、Gitleaksのチェックが失敗していれば成功です。

    GitHub Actions上でgitleaksが実行され、test/dummy-secret.txtに含まれる2件のシークレットが検知されました。

    ○
        │╲
        │ ○
        ○ ░
        ░    gitleaks
    
    Finding:     api_key = "REDACTED"
    Secret:      REDACTED
    RuleID:      generic-api-key
    Entropy:     3.970951
    File:        test/dummy-secret.txt
    Line:        3
    Commit:      <commit-hash>
    Author:      <author>
    Email:       <email>
    Date:        <date>
    Fingerprint: <commit-hash>:test/dummy-secret.txt:generic-api-key:3
    Link:        https://github.com/<owner>/<repository>/blob/<commit-hash>/test/dummy-secret.txt#L3
    
    Finding:     slack_token = "REDACTED"
    Secret:      REDACTED
    RuleID:      slack-bot-token
    Entropy:     4.953909
    File:        test/dummy-secret.txt
    Line:        1
    Commit:      <commit-hash>
    Author:      <author>
    Email:       <email>
    Date:        <date>
    Fingerprint: <commit-hash>:test/dummy-secret.txt:slack-bot-token:1
    Link:        https://github.com/<owner>/<repository>/blob/<commit-hash>/test/dummy-secret.txt#L1
    
    INF 2 commits scanned.
    INF scanned ~290 bytes (290 bytes) in 150ms
    WRN leaks found: 2
    

    検知結果には、検知されたファイル、行番号、ルールID、コミット情報などが表示されます。 今回の例では、generic-api-keyslack-bot-tokenのルールに一致し、最終的にleaks found: 2 と表示されていることが分かります。

    このように、ローカル端末でのPre-commit hooksを回避された場合でも、GitHub Actions上でシークレットを検知できます。


    シークレットファイルpush時にエラーが出力される場合

    4.4の手順で、GitHub側でSecret scanning/Push protectionが有効になっている場合、以下のようなエラーが出力されpushを拒否される場合があります。

    remote: - GITHUB PUSH PROTECTION
    remote:   —————————————————————————————————————————
    remote:     Resolve the following violations before pushing again
    remote:
    remote:     - Push cannot contain secrets
    remote:
    remote:
    remote:      (?) Learn how to resolve a blocked push
    remote:      https://docs.github.com/code-security/secret-scanning/working-with-secret-scanning-and-push-protection/working-with-push-protection-from-the-command-line#resolving-a-blocked-push
    remote:
    remote:      (?) This repository does not have Secret Scanning enabled, but is eligible. Enable Secret Scanning to view and manage detected secrets.
    remote:      Visit the repository settings page, https://github.com/<owner>/<repository>/settings/security_analysis
    remote:
    remote:
    remote:       —— Slack API Token ———————————————————————————————————
    remote:        locations:
    remote:          - commit: <commit-hash>
    remote:            path: test/dummy-secret.txt:1
    remote:
    remote:        (?) To push, remove secret from commit(s) or follow this URL to allow the secret.
    remote:        https://github.com/<owner>/<repository>/security/secret-scanning/unblock-secret/<token>
    remote:
    remote:
    remote:
    To https://github.com/<owner>/<repository>
     ! [remote rejected] feature/secret-scanning -> feature/secret-scanning (push declined due to repository rule violations)
    error: failed to push some refs to 'https://github.com/<owner>/<repository>'
    

    これは、GitHubがpush時点で秘密情報を検知し、リモートリポジトリへの混入を防止したことを示しています。

    上記のエラーを無視して、手順4.4の検証を実施する場合には、以下の手順を実施してください。

    エラーメッセージに表示された以下URLへアクセスすると、検知されたシークレットを例外的に許可するか確認する画面が表示されます。

    https://github.com/<owner>/<repository>/security/secret-scanning/unblock-secret/<token>
    

    今回は検証用のダミー文字列であるため、例外として許可し、再度pushを実行してください。

    git push origin feature/secret-scanning
    

    まとめ

    今回は、APIキーやパスワードなどの秘密情報が誤ってリポジトリに含まれることを検出して防ぐ「シークレットスキャン」を、ローカル端末とGitHub Actionsのパイプラインに組み込みました。

    また、GitHubのPush protectionにより、push時点でもシークレットの混入を検知・ブロックできることを確認しました。

    次の記事では、アプリケーションのソースコードを静的に解析し、脆弱性やセキュアコーディング上の問題を検出する「SAST」をCI/CDパイプラインに組み込んでいきたいと思います。

    最後まで読んでいただき、ありがとうございました!

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。