AI事業本部 AIオペレーションテクノロジーカンパニーの金綱雅也(@masaaya)です。

2025年11月24日、Shai-Hulud 2.0 と呼ばれる npm サプライチェーン攻撃が発生し大量の npm パッケージ・リポジトリが影響を受けました。

今回の事例が示しているのは プロセスに露出しているシークレットは組織規模に関係なく常に狙われ得る という事実です。

本記事ではこの教訓を踏まえ、GitHub 認証において今回の攻撃の主要ターゲットとなった Personal Access Token (PAT) に依存する運用を見直しさまざまなケースでよりセキュアに接続するためのアプローチを紹介していきます。

Shai-Hulud 2.0 の概要

Shai-Hulud 2.0 は、npm エコシステムを標準とした観測史上最速レベルのサプライチェーン攻撃です。攻撃のメカニズムはおおまかに次のとおりです。

  • 正規の npm パッケージメンテナーの認証情報を不正入手してアカウントを乗っ取る
  • 乗っ取ったアカウントを使って人気パッケージをトロイの木馬化する
  • 感染パッケージがインストールされると preinstall スクリプトでマルウェアが自動実行され、exfiltration 用リポジトリを作成して環境内のシークレットを push する
  • 盗んだ npm トークンを使ってパッケージの不正バージョンを公開する

一部の感染ホストでは盗まれたトークンを用いて self-hosted runner が登録されるなどの侵入行為も報告されています。

本攻撃の詳細については、以下のレポートをご参照ください。

GitHub 運用における PAT の問題点

PAT は発行の手軽さとスコープ設定の柔軟さから幅広く利用されてきましたが、「スコープを細かく設定できる」ことと「安全に運用できる」ことは別の問題とみなせます。

特に現場では常にセキュリティだけが最優先されているわけではないため、開発スピードや一時的な対応を優先した結果次のような状況が生まれがちです。

  • とりあえず動かすために必要以上の権限を与えてしまう
  • 一時的だからと環境変数に平文のトークンを置いてしまう
  • CI の secrets 経由で渡したトークンがコンテナイメージに残留してしまう

これらを 1 つずつ潰していく「終わりのないモグラ叩き」だけで十分な防御力を確保することは難しく、現代のサプライチェーン攻撃に対しては

「プロセスの中でシークレットを直接参照する」という構造そのものが、リスク増大要因になり得る

という一段階高い抽象度で問題を捉え直す必要があります。

言い換えると

  • 「シークレットの権限を適切に絞って運用する」だけでは不十分であり、
  • 「プロセスに対するシークレット露出を最小限に抑え、必要な時に必要な権限だけを外部から間接的に受け取る」

という設計思想へとシフトしていく必要があります。

GitHub 認証を PAT から SSH へ移行する基本方針

ローカル / Dev Container / CI/CD 環境における GitHub へのアクセスは、大きく次の 2 つに分類できます。

  • Git 操作(clone / fetch / pull / push)
  • GitHub API

本記事では、これを次のように切り分けて運用する方針を紹介します。

  • Git 操作
    SSH
  • GitHub API
    authentication token (web-based browser flow)

例えば gh auth login を実行するとき

  • 「HTTPS or SSH」
  • 「Login with a web browser or Paste an authentication token」

といった選択肢が出ますが、このとき選ぶべきなのは 「SSH」「Login with a web browser」 の組み合わせです (ローカルのみ)。

ブラウザログインを選ぶと GitHub API 向けの OAuth Token が OS のキーチェーンに保存されます。キーチェーンへのアクセスは静的なファイルスキャンや単純な環境変数のダンプで容易に行えるものではないため、環境変数やファイルに平文で置く場合と比べて攻撃者に発見・悪用されるリスクを大きく低減できます。

ローカル / Dev Container 環境における SSH キー管理

SSH キーの扱いにおいて特に重要なのは次の 2 点です。

  1. 秘密鍵をディスクにベタ置きしない
  2. プロセスから生の秘密鍵を読めない形で Git 操作を行う

1 に対してはパスワードマネージャー(Bitwarden、1Password など)、2 に対しては SSH Agent を用いたアプローチが有効です。

SSH Agent は Unix ドメインソケットを介して別プロセスとして署名だけを代行してくれる仕組みです。SSH クライアント側は SSH_AUTH_SOCK で指定されたソケットに署名要求を送信し、実際の秘密鍵は SSH Agent プロセス内部のメモリ上にのみ保持されます。クライアント側には署名結果だけが返されることから、クライアントプロセスから秘密鍵そのものを直接読み取ることはできないため窃取リスクを大きく下げることができます。

また、Bitwarden を利用している場合はこれに加えて Bitwarden SSH Agent を使う構成が有効です。Bitwarden SSH Agent は復号済みの秘密鍵をエージェントプロセスのメモリ上にのみ保持するため、マルウェアが ~/.ssh をスキャンしても鍵ファイルは見つからず、生の秘密鍵が窃取されるリスクをさらに抑制できます。一度ログインすればローカル環境と Dev Container 双方で同じエージェントを利用できる点も Good Point です。

まとめると、ローカルでの作業フローは次のようになります。

  1. パスワードマネージャーにログインし、Vault をアンロックする
  2. GitHub 用の SSH キーペアを生成する
  3. 公開鍵を GitHub に登録する
  4. SSH Agent(例:Bitwarden SSH Agent)を有効化し、SSH_AUTH_SOCK を設定する
  5. 以降の Git 操作はすべて SSH Agent 経由で行い、秘密鍵本体には直接触れない

Dev Container での再現

Dev Container 環境でも基本方針は同じで、ホスト側で動作している SSH Agent ソケットだけをコンテナに渡す構成をとります。簡単な設定例は次のとおりです。

// .devcontainer/devcontainer.json
{
  "name": "ssh-agent-auth-in-devcontainer",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspaces/app",
  "remoteUser": "root",
  "features": {}
}
# docker-compose.yml
services:
  app:
    image: mcr.microsoft.com/devcontainers/base:debian
    volumes:
      - .:/workspaces/app
      - ${SSH_AUTH_SOCK}:/ssh-agent:ro  # ホストの SSH Agent ソケットをマウント
    environment:
      SSH_AUTH_SOCK: /ssh-agent        # コンテナ内でのパスを上書き
    working_dir: /workspaces/app
    tty: true
    stdin_open: true
  • ホストで動作する SSH Agent (例:Bitwarden SSH Agent) が署名処理を担当する
  • コンテナには SSH Agent ソケットだけが渡される(ssh-add -l を実行するとホストと同じ鍵が見える)
  • マウントしているのはソケットのみであり、秘密鍵ファイルそのものではない
  • コンテナ内のファイルシステム上には秘密鍵ファイルが存在しないため、コンテナ内から直接読み取ることはできない

Dev Container 内での gh auth login がシークレットを露出する問題

Dev Container で GitHub API 操作をするためにはコンテナ内部で gh auth login を実行する必要がありますが、この場合 OAuth Token はローカル実行時のように OS キーチェーンに保存するのではなく /root/.config/gh/hosts.yml に平文で保存されてしまうため注意が必要です (実際にcatコマンドで取得してみました ↓ )。

root ➜ ~/.config/gh $ cat hosts.yml
github.com: 
    users: 
        username: 
            oauth_token: gho_xxx 
    git_protocol: ssh
    oauth_token: gho_xxx 

これではマルウェアにも Dev Container 内を動くコーディングアシスタント AI (Claude Code、 Codex、Gemini Code Assist など) にも簡単に読み取られてしまう上、web-based browser flow で発行される OAuth Token はスコープが広いため流出時のインパクトが大きくなります。Dev Container 内で gh を利用する場合は最小権限の PAT を用いて、Git 操作は SSH に切り分ける構成が現実的な最適に近い判断となります。

CI/CD 環境における SSH Agent と Deploy Key の活用

CI/CD 環境 (ここでは GitHub Actions) においても基本方針は変わりませんが、パスワードマネージャーが利用できないため Deploy Key + SSH Agent の組み合わせが現実的な選択肢になります。

Shai-Hulud 2.0 では盗まれた PAT を用いて self-hosted runner の登録・乗っ取りが行われたケースも確認されており、CI/CD においても秘密鍵を環境に露出させず SSH Agent 経由の署名だけで GitHub にアクセスする設計が必要です (この構成は GitHub 公式ドキュメントの推奨に沿っています)。

以下に webfactory/ssh-agent を使った簡単な例を示します。

# actions-ssh-agent.yaml
...
# SSH Agent を起動し Deploy Key をメモリに読み込む
- uses: webfactory/ssh-agent@v0.9.0
  with:
    ssh-private-key: ${{ secrets.DEPLOY_KEY }}

# 以降の Git / Docker ビルドは SSH Agent 経由
- name: docker build
  run: |
    docker build --ssh default .
...
# Dockerfile
...
RUN mkdir -p -m 0700 /root/.ssh \
    && ssh-keyscan github.com >> /root/.ssh/known_hosts
RUN --mount=type=ssh \
    git clone git@github.com:orgname/privaterepo.git
...
  • 秘密鍵は Actions の一時的な実行環境と SSH Agent のメモリ上にのみ存在し、コンテナイメージには残らない (echo などの出力ミスには要注意)
  • 個人の SSH キーではなく、そのリポジトリ専用の Deploy Key を払い出す
  • Deploy Key の権限は read-only を基本とし、必要な場合にのみ write を付与する

まとめ

本記事では、GitHub における Git 操作を SSH に一本化し PAT に依存しないための認証設計・運用方法の一例を紹介しました。

  • Git 操作は SSH (SSH Agent) で行う
  • GitHub API には ローカル実行時は authentication token (web-based browser flow)、Dev Container 内では最小権限に絞った PAT を利用する
  • CI/CD では Deploy Key + SSH Agent を基本構成とする

これらに共通する本質は、

プロセスに対するシークレット露出を最小限に抑え、必要なときに署名だけを安全な外部プロセスへ委譲する

という考え方にあります。

完璧な防御は存在せず攻撃手法も日々高度化していますが、エンジニア個人の PAT 運用に依存する形から SSH Agent を用いた間接的な認証方式へ切り替えるだけでも防御レイヤーを確実に厚くすることができます。

Shai-Hulud 2.0 を受けて各所でさまざまな対策が検討されているかと思いますが、本記事が GitHub 認証運用の見直しや議論の一助となれば幸いです。