サーバー運用者の危機対応

依存パッケージが乗っ取られていたとき

乗っ取られた版が更新で入った場面を無害なダミーで起こし、ロックファイルの監視とおとりの通知から、影響範囲の特定、鍵の入れ替えの判断、版の固定までを学びます。

  • レベル: 実践
  • 約45分
  • 2026-11-12

#状況

デプロイの直後に、通知が2件届きました

1件目は「依存関係ロックファイルの変更を検知」。2件目は「おとりの認証情報ファイルが開かれました」で、開いたのは node setup.js です。

さきほど、本番のサーバーで npm update を実行して、依存パッケージを新しくしたところでした。

依存パッケージの乗っ取りでは、正規の名前のまま、悪意のある版が公開されます。いつもどおりの更新で、それが入ります。このモジュールは、入ったあとの運用の手順を学ぶ手順書です。仕組みと点検の機能そのものは、サプライチェーン攻撃 で扱っています。

ここでは、1つの場面を、起こす、検知を見る、調べる、対応する、元に戻す、の順に進めます。扱う機能は、ロックファイルの改ざん監視、認証情報のおとり、パッケージの既知の脆弱性の照合、インストール時に動くコードの点検、プロセス実行の記録と攻撃手口の検知、インシデントのタイムライン、通知の履歴、API キー・秘密鍵の検出 です。

題材は、実際にあった debug の件です。2025 年 9 月 8 日、npm の debug の公開アカウントが、フィッシングで乗っ取られました。4.4.2 が公開され、修正版は 4.4.3 です。演習では、この名前と版で、中身が無害なダミーを作ります。

#練習の場面を作る

変更を伴う操作です

この演習は、サーバーの設定を2つ変え、/srv にプロジェクトを作ります。使い捨ての VM で試してください。元に戻す手順は、最後の章にあります。

ダミーのパッケージ、レジストリ、プロジェクトを用意する

npm と git が要ります(Ubuntu では sudo apt-get install -y npm git)。debug の 4.4.1、4.4.2、4.4.3 を作ります。中身は、何もしない関数だけです。4.4.2 にだけ、インストール時に動くスクリプト(postinstall)を持たせます。このスクリプトは、おとりの鍵のファイルを開いて読むだけで、どこにも送りません。

mkdir -p ~/lab/registry/debug/- && cd ~/lab
for v in 4.4.1 4.4.2 4.4.3; do
  mkdir -p src/$v
  echo 'module.exports = () => () => {};' > src/$v/index.js
  echo "{ \"name\": \"debug\", \"version\": \"$v\", \"main\": \"index.js\" }" > src/$v/package.json
done
# 4.4.2 にだけ、インストール時に動くスクリプトを持たせます
# おとりの鍵のファイルを開いて読むだけで、中身はどこにも送りません
cat > src/4.4.2/package.json <<'J'
{ "name": "debug", "version": "4.4.2", "main": "index.js",
  "scripts": { "postinstall": "node setup.js" } }
J
echo "try { require('fs').readFileSync('/root/.aws/credentials'); } catch (e) {}" > src/4.4.2/setup.js
for v in 4.4.1 4.4.2 4.4.3; do (cd src/$v && npm pack --silent --pack-destination ~/lab/registry/debug/-); done

次に、127.0.0.1 だけで待ち受けるレジストリを動かします。publish.py に渡した版だけが、npm から見えます。外のレジストリには、何も公開しません。

cat > ~/lab/publish.py <<'P'
# レジストリに並べる版を決める(この VM の中だけ。外へは何も公開しません)
import base64, hashlib, json, os, sys, tarfile
root = os.path.expanduser("~/lab/registry/debug")
doc = {"name": "debug", "versions": {}, "dist-tags": {"latest": sys.argv[-1]}}
for v in sys.argv[1:]:
    path = f"{root}/-/debug-{v}.tgz"
    data = open(path, "rb").read()
    with tarfile.open(path) as t:
        meta = json.load(t.extractfile("package/package.json"))
    if "postinstall" in meta.get("scripts", {}):
        meta["hasInstallScript"] = True
    meta["dist"] = {"tarball": f"http://127.0.0.1:4873/debug/-/debug-{v}.tgz",
        "integrity": "sha512-" + base64.b64encode(hashlib.sha512(data).digest()).decode()}
    doc["versions"][v] = meta
json.dump(doc, open(f"{root}/index.json", "w"))
print("published:", " ".join(sys.argv[1:]))
P
cat > ~/lab/registry.py <<'P'
# 127.0.0.1 だけで待ち受ける、読み出し専用のレジストリ
import http.server, os
ROOT = os.path.expanduser("~/lab/registry")
class H(http.server.SimpleHTTPRequestHandler):
    def translate_path(self, path):
        full = os.path.join(ROOT, path.split("?")[0].strip("/"))
        return os.path.join(full, "index.json") if os.path.isdir(full) else full
http.server.ThreadingHTTPServer(("127.0.0.1", 4873), H).serve_forever()
P
nohup python3 ~/lab/registry.py >/dev/null 2>&1 &
python3 ~/lab/publish.py 4.4.1

続いて、本番のアプリ /srv/shop と、別のバッチ /srv/batch を作ります。どちらも "debug": "^4.4.1" に依存し、いまは 4.4.1 が入ります。/srv/shop には、ダミーの鍵を書いた .env を置き、ロックファイルを git に記録します。

sudo mkdir -p /srv/shop /srv/batch
for app in shop batch; do
  echo "{ \"name\": \"$app\", \"private\": true, \"dependencies\": { \"debug\": \"^4.4.1\" } }" | sudo tee /srv/$app/package.json >/dev/null
  echo 'registry=http://127.0.0.1:4873/' | sudo tee /srv/$app/.npmrc >/dev/null
  (cd /srv/$app && sudo npm install --no-audit --no-fund --silent)
done
echo 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE' | sudo tee /srv/shop/.env >/dev/null
cd /srv/shop && sudo git init -q && sudo git add package.json package-lock.json .npmrc
sudo git -c user.name=ops -c user.email=ops@example.com commit -qm "debug 4.4.1"

次に、RoamSwitch の設定を2つ変えます。lockfile_watch_dirs は、ロックファイルを監視するプロジェクトのフォルダです。既定は空で、何も見ません。package_cve_map_languages_enabled は、npm などの依存パッケージの脆弱性データを受け取る設定です。既定はオフです。どちらも、反映には再起動が要ります。

sudo roamswitch server config set lockfile_watch_dirs /srv/shop
sudo roamswitch server config set package_cve_map_languages_enabled true
sudo roamswitch server restart
sudo systemctl start roamswitch-update.service
cat /var/lib/roamswitch/package_cve_map_npm.VERSION
出力の抜粋
  ✓ サーバー設定を更新・保存しました。 (lockfile_watch_dirs = /srv/shop)
  ✓ サーバー設定を更新・保存しました。 (package_cve_map_languages_enabled = true)
  ✓ roamswitch-server サービスを正常に再起動しました。
2026-11-11

最後の行は、受け取った npm の脆弱性データの日付です。このファイルがなければ、データはまだ届いていません。届くまでは、依存パッケージの照合は働きません。

#通知を読む

乗っ取られた版が出て、更新で入る

レジストリに 4.4.2 を並べます。公開アカウントが乗っ取られ、悪意のある版が出た、という場面です。そこで、いつもどおり npm update を実行します。

python3 ~/lab/publish.py 4.4.1 4.4.2
cd /srv/shop && sudo npm update --no-audit --no-fund
sleep 10
sudo roamswitch notifications | head -40
出力の抜粋
  2026-11-12T01:43:28.597332685+00:00
  おとりの認証情報ファイルが開かれました
    /root/.aws/credentials (aws_credentials) は本物ではありません。
    プロセス: root / /usr/bin/node / コマンド: node setup.js (pid 7931)
    親プロセス: dash ← node ← sudo ← bash ← tart-guest-agent ← systemd / サービス: tart-guest-agent.service

  2026-11-12T01:43:26.159321391+00:00
  依存関係ロックファイルの変更を検知 (1 件)
    ホスト: ubuntu (IP: 192.168.64.18)
    変更されたロックファイル:
    /srv/shop/package-lock.json: 内容の変更
      プロセス: root / /usr/bin/node / コマンド: npm update (pid 7913)
      SSH のログインは見つかりませんでした(systemd サービス tart-guest-agent.service のプロセスです)
    検知と同時にnpm/yarn/pnpmが動いていたようです。デプロイ自体のinstall/ciによる可能性が高いですが、心当たりがなければ確認してください。
    推奨対応: `sudo roamswitch lockfile-fim verify` で変更内容を確認し、問題なければ `lockfile-fim update` でベースラインを更新してください。
    時刻: 2026-11-12 01:43:26

この下には、サーバーの点検の通知(「オンにした保護が働いていません」など)が続きます。

2件の通知は、別々の仕組みから出ています。

通知出した機能分かること
依存関係ロックファイルの変更を検知ロックファイルの改ざん監視/srv/shop/package-lock.json が、記録した内容から変わった。書いたのは npm update
おとりの認証情報ファイルが開かれました認証情報のおとり/root/.aws/credentials のおとりを、node setup.js が開いた。親は node、その親は sudo

ロックファイルの変更だけなら、ふつうの更新でも起きます。通知も「npm/yarn/pnpm が動いていた」と添えて、デプロイによる変更の可能性が高いと書いています。おとりが開かれたことは、ふつうの更新では起きません。本物のアプリが、おとりの鍵を読む理由はないからです。この2件がそろったら、乗っ取られた版が入ったものとして調べます。

おとりは、honeytokens_home(既定は /root)の下に置かれます。演習の VM では、操作が tart-guest-agent を通っているので、通知の親にもそれが出ます。SSH でログインして操作した場合とは、ここが違います。

#いつ、どこに入ったか

該当する版、入った時刻、動いたものを突き止める

まず、ロックファイルを、既知の脆弱性のデータと照合します。フォルダを並べると、まとめて照合できます。

sudo roamswitch scan-packages /srv/shop /srv/batch | sed -n '/npm\/PyPI/,$p'
出力の抜粋
  要対応 1 件 / 受容中 0 件(うち 14 日以内に期限が来るもの 0 件) / 期限切れで要対応に戻ったもの 0 件

  CVE-2025-59144  [npm] debug 4.4.2 (CVSS 7.0)
    EPSS 0.5% (2026-11-10)  (悪用が確認された数字ではなく、悪用される見込みの目安です)
    📁 /srv/shop/package-lock.json
    修正版: 4.4.3
    Impact On 8 September 2025, the npm publishing account for debug was taken over after a phishing attack. Version 4.4.2 was published, functionally identical to the previous patch version, but with a malware payload added attempting to redir…

該当したのは /srv/shop だけで、/srv/batch は出ていません。パッケージの既知の脆弱性の照合 は、ロックファイルに書かれた版を読むだけで、何も実行しません。説明文は、元のデータの英語のままです。

次に、ホスト全体で、その版がどこに置かれているかを探します。あわせて、変わった内容と、入った時刻を確かめます。

sudo find / -xdev -path '*/node_modules/debug/package.json' -exec grep -H '"version"' {} +
cd /srv/shop && sudo git diff package-lock.json | grep '^[-+] *"version"'
stat -c '%y %n' /srv/shop/node_modules/debug/package.json
sudo sh -c 'grep -H "argv" /root/.npm/_logs/*.log'
出力
/srv/batch/node_modules/debug/package.json:{ "name": "debug", "version": "4.4.1", "main": "index.js" }
/srv/shop/node_modules/debug/package.json:{ "name": "debug", "version": "4.4.2", "main": "index.js",
-      "version": "4.4.1",
+      "version": "4.4.2",
2026-11-12 01:43:26.008933274 +0000 /srv/shop/node_modules/debug/package.json
/root/.npm/_logs/2026-11-12T01_42_51_869Z-debug-0.log:21 verbose argv "install" "--no-audit" "--no-fund" "--loglevel" "silent"
/root/.npm/_logs/2026-11-12T01_42_52_480Z-debug-0.log:21 verbose argv "install" "--no-audit" "--no-fund" "--loglevel" "silent"
/root/.npm/_logs/2026-11-12T01_43_25_934Z-debug-0.log:21 verbose argv "update" "--no-audit" "--no-fund"

4.4.2 があるのは /srv/shop だけです。git の差分で 4.4.1 から 4.4.2 に変わったことが、ファイルの時刻と npm のログで、01:43(UTC。日本時間は 10:43)の npm update だと分かります。Ubuntu の apt で入れた npm は、自分の使う debug を /usr/share/nodejs/debug に置きます(この VM では 4.3.4)。node_modules の下ではないので、この find には出ません。

最後に、入った版が、インストール時に何を実行するかを一覧にします。そのあと、実行の記録から、実際に動いたかを確かめます。events tree の PID は、手元の値に読み替えてください。

sudo roamswitch scan-scripts /srv/shop | sed -n '/一覧表示です/,$p'
sudo roamswitch events search --since 1h --text setup.js | grep -v 'events search'
sudo roamswitch events tree 7931
出力の抜粋
  これは脅威判定ではなく一覧表示です。何も実行しません。⚠️は誤検知を含む軽量ヒューリスティックです。

    debug 4.4.2 [postinstall]
    node setup.js

  確認済みの状態がまだありません。今のインストールスクリプトを確認したら、roamswitch scan-scripts --accept を実行してください。次回からは、増えた分だけを知らせます。

2026-11-12T01:43:26.015Z pid=7931 ppid=7930 root /usr/bin/node setup.js
2026-11-12T01:43:26.015Z pid=7930 ppid=7913 root /usr/bin/dash -c node setup.js
1 /usr/lib/systemd/systemd
694 /usr/bin/tart-guest-agent
7842 /usr/bin/bash
7912 /usr/bin/sudo
7913 /usr/bin/node
7930 /usr/bin/dash
              7931 /usr/bin/node setup.js

インストール時に動くコードの点検 で、4.4.2 が postinstall を持つと分かります。プロセス実行の記録と攻撃手口の検知 の記録では、node setup.js が root で動いています。親の 7913 は npm update です(sudo roamswitch events search --pid 7913 で引数まで見られます。--pid に空の値や数でない値を渡すと、全件は出さずに終了コード 2 で終わります)。

#鍵が盗まれた可能性を見積もる

インストール時のスクリプトは、npm を実行した人の権限で動きます。今回は sudo npm update なので、root です。

時系列と、置いてある鍵を確かめる

インシデントのタイムライン で、検知を1つの時系列にまとめて見ます。次に、API キー・秘密鍵の検出 で、プロジェクトに置いてある鍵を探します。見つかった鍵が、入れ替えの候補です。

sudo roamswitch timeline
sudo roamswitch audit-secrets /srv/shop
出力の抜粋
  2026-11-12T01:43:26.108053902+00:00  [Honeytoken]  対応中
    ハニートークン(/root/.aws/credentials)への実アクセスを受け、関連する新規デコイを追加配置しました: /root/.aws/config
    MITRE ATT&CK: T1552 (Unsecured Credentials)
    実施した対応: おとりへのアクセスを記録

  2026-11-12T01:43:26.078560448+00:00  [FIM]  記録のみ
    /srv/shop/package-lock.json: 内容の変更(SSH のログインは見つからない: service tart-guest-agent.service)
    プロセス: node (pid 7913)
    実施した対応: 記録して通知

  2026-11-12T01:43:26.074832374+00:00  [Honeytoken]  対応中
    ハニートークン(aws_credentials)へのアクセスを検知: /root/.aws/credentials — アクセス元: プロセス: root / /usr/bin/node / コマンド: node setup.js (pid 7931) / 親プロセス: dash ← node ← sudo ← bash ← tart-guest-agent ← systemd / サービス: tart-guest-agent.service
    プロセス: node (pid 7931)
    親プロセス系譜: dash ← node ← sudo ← bash ← tart-guest-agent ← systemd
    MITRE ATT&CK: T1552 (Unsecured Credentials)
    実施した対応: おとりへのアクセスを記録
(…)
  /srv/shop/.env
    ⚠️  AWS Access Key ID (AWS) — 行番号 1
        マスク済み: AKIAIO...MPLE
        ↳ 推奨対応: AWS IAMコンソールからアクセスキーを非アクティブ化してください。

  合計 1 件の露出(1 ファイル)。コミット・共有前に対処してください。

時系列では、ロックファイルの変更と、おとりが開かれたことが、同じ秒(01:43:26)に、新しい順に並びます。おとりの行には、開いたコマンドと親プロセスの系譜が、通知と同じ名前で出ます。

おとりが読まれたあと、RoamSwitch は /root/.aws/config に、おとりを1つ足しています。audit-secrets は、.env の鍵を、先頭と末尾だけ出して伏せます。

鍵が見つかったので、終了コードは 1 です(見つからなければ 0)。

入れ替える鍵を決める
  1. スクリプトは動きましたか。 動いていなければ、インストールで鍵が読まれることはありません。今回は、実行の記録に node setup.js が残っています
  2. どの権限で動きましたか。 その権限で読めるものは、すべて読まれたとみなします。今回は root なので、/root の下も、/srv/shop/.env も、/etc の設定も入ります
  3. おとりは読まれましたか。 読まれていれば、鍵を探すコードだったという強い根拠です。読まれていなくても、盗まれていないとは言えません

1 と 2 が「はい」なら、その権限で読める本物の鍵を、すべて入れ替えます。新しい鍵を発行してアプリに入れ、動くことを確かめてから、古い鍵を無効にします。古い鍵が使われた跡は、鍵を発行した側(クラウドの管理画面など)の記録で確かめます。

確認問題

おとりの通知は出ませんでした。インストール時のスクリプトは root で動いていました。言えることはどれですか

解説を見る

おとりは、鍵を探すコードを見つける手がかりです。本物の鍵だけを狙って読むコードには、おとりは反応しません。判断の基準は、スクリプトが動いたことと、その権限です。

#版を固定して入れ直す

修正版に固定し、スクリプトを止めて入れ直す

レジストリから 4.4.2 を外し、4.4.3 を並べます。乗っ取られた版が取り下げられ、修正版が出た、という場面です。

--save-exact で、package.json に ^ のない版を書きます。^4.4.1 のままだと、次の npm update で、条件に合う別の版が入りえます。--ignore-scripts は、インストール時のスクリプトを動かさない npm の指定です。node_modules は消して、ロックファイルどおりに npm ci で入れ直します。

python3 ~/lab/publish.py 4.4.1 4.4.3
cd /srv/shop
sudo rm -rf node_modules
sudo npm install debug@4.4.3 --save-exact --ignore-scripts --no-audit --no-fund
grep '"debug"' package.json
sudo rm -rf node_modules && sudo npm ci --ignore-scripts --no-audit --no-fund
sudo roamswitch scan-packages /srv/shop | sed -n '/npm\/PyPI/,$p'
sudo roamswitch scan-scripts /srv/shop | grep -F 'インストールスクリプトは'
sudo git -c user.name=ops -c user.email=ops@example.com commit -qam "debug を 4.4.3 に固定" && sudo git log --oneline
出力の抜粋
published: 4.4.1 4.4.3
{"name":"shop","private":true,"dependencies":{"debug":"4.4.3"}}
  照合できた依存パッケージ 1 件に、既知の重大CVE(CVSS 7.0以上)に該当するものは見つかりませんでした。
  インストールスクリプトは見つかりませんでした。
8588234 debug を 4.4.3 に固定
cfa7a5e debug 4.4.1

照合の該当はなくなり、インストール時に動くスクリプトもなくなりました。直したロックファイルは、git に記録します。

ロックファイルを書き換えたので、監視からは、もう一度通知が届きます。

通知の抜粋(sudo roamswitch notifications)
  依存関係ロックファイルの変更を検知 (1 件)
    ホスト: ubuntu (IP: 192.168.64.18)
    変更されたロックファイル:
    /srv/shop/package-lock.json: 内容の変更
      プロセス: root / /usr/bin/node / コマンド: npm install debug@4.4.3 (pid 8484)

書いたコマンドが、自分の実行した npm install debug@4.4.3 と一致しています。git の記録とも合います。自分の変更だと、通知の中身で確かめてから、次へ進みます。

scan-scripts は、.npmrc に min-release-age(公開から日の浅い版を入れない設定)がないことも示します。使っている npm がこの設定に対応しているかを確かめてから、入れてください。

やってはいけないこと
  • 通知を見てすぐに node_modules を消す。調べる前に消すと、どの版が入っていたかと、ファイルの時刻が分からなくなります
  • 修正版を入れただけで終わる。スクリプトが動いていたなら、鍵の入れ替えが残っています
  • ^ のついた範囲のまま、本番で npm update を続ける。次の乗っ取りも、同じ経路で入ります
  • 本番のサーバーで、root のまま依存を入れる。スクリプトが root で動き、読める範囲が最も広くなります

#元に戻す

設定と練習のファイルを戻す

設定を既定に戻して再起動し、レジストリを止めて、練習のファイルを消します。lockfile_fim.db と lockfile_fim_notified.txt は、ロックファイルの監視の記録です。

sudo roamswitch server config set lockfile_watch_dirs ''
sudo roamswitch server config set package_cve_map_languages_enabled false
sudo roamswitch server restart
kill $(sudo ss -ltnpH 'sport = :4873' | sed -n 's/.*pid=\([0-9]*\).*/\1/p')
sudo rm -rf /srv/shop /srv/batch ~/lab /root/.npm
sudo rm -f /var/lib/roamswitch/lockfile_fim.db /var/lib/roamswitch/lockfile_fim_notified.txt
sudo grep -E '^(lockfile_watch_dirs|package_cve_map_languages_enabled)=' /etc/roamswitch/server.conf
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4873/debug
出力の抜粋
  ✓ サーバー設定を更新・保存しました。 (lockfile_watch_dirs = )
  ✓ サーバー設定を更新・保存しました。 (package_cve_map_languages_enabled = false)
  ✓ roamswitch-server サービスを正常に再起動しました。
lockfile_watch_dirs=
package_cve_map_languages_enabled=false
000

最後の 000 は、レジストリが止まり、接続できなかったという意味です。受け取った npm の脆弱性データ(/var/lib/roamswitch/package_cve_map_npm.json など)は、オフにしても残ります。おとりの /root/.aws/config は、RoamSwitch が置いたものなので、そのままにします。

#見えない部分

この手順で分からないもの
  • スクリプトが何をしたかは、実行の記録とおとりから分かる範囲だけです。本物の鍵のファイルを読んだことは、おとりの通知には出ません
  • 外への送信は、この演習では起こしていません。送られたかどうかは、別の記録(外向きの通信のログなど)で調べます
  • ロックファイルの監視は、lockfile_watch_dirs に書いたフォルダだけを見ます。演習の /srv/batch は、登録していないので見ていません
  • scan-packages の照合は、受け取った脆弱性データにある版だけが対象です。データに載る前の乗っ取りは、照合では出ません
  • scan-scripts は、インストール時のスクリプトの一覧です。require したときに動くコードは、一覧に出ません
確認問題

修正版に固定して入れ直したあとも、残っている作業はどれですか

解説を見る

版を直すと、これから入るものは止まります。すでに動いたスクリプトが読んだかもしれないものは、版を直しても戻りません。

次に学ぶこと

扱う機能

対応する攻撃手法 (ATT&CK): T1195.001 T1552.001

確認した内容

2026-11-12に確認しました。対象は Linux 1.17.39 です。使い捨ての Ubuntu 24.04 の VM(公開版 1.17.39 の Server Edition、apt の npm と git)で、127.0.0.1 だけのダミーのレジストリと無害な debug 4.4.1〜4.4.3 を使い、本文の手順を最初から実行しました。英語の出力は、同じ VM で LANG を切り替えて取りました(server.conf の language は ja のまま)。

コマンドの出力は、製品の版や環境によって変わります。違いが出たら、まず製品の版を確かめてください。