VPSマルウェア感染から復旧した話 - n8n/Dify再構築まで

VPSマルウェア感染から復旧した話 - n8n/Dify再構築まで

セルフホストのVPS上でDifyとn8nを運用していたところ、マルウェアに感染し、外部通信が全断する事態になった。復旧までの過程で得たノウハウをまとめる。


何が起きたか

ある日、VPSホスティング会社から「外部への不審なアクセスを検知したため、ポート制限を実施した」という通知が届いた。

調査してみると、VPSが**仮想通貨マイナー(XMRig)**に感染しており、不正採掘に利用されていた。

感染の証拠

ps aux
# 以下のようなプロセスが稼働中
# pkill -f donate-level   ← XMRigの競合プロセス排除
# pkill -f curl

donate-level はXMRigに特徴的なキーワード。これが見えたら感染確定。

侵入経路

通知メールは届いていたが見逃していたことが最大の失敗。


感染後の状態

アウトバウンド通信:全断
インバウンド:正常(サービスは動作中に見えた)
certbot:Let's Encryptに繋げず証明書更新失敗
Docker pull:タイムアウト
SSH:ポート22のみ通過(ログインは可能)

ホスティング会社がネットワークレベルでブロックしていたため、OS側でいくら設定しても解決しない状況だった。


対応の判断

感染したサーバーは調査・修復よりOS再インストールが確実。理由は以下:

再インストール前にバックアップ

# n8nワークフローのエクスポート
docker exec [n8nコンテナ名] n8n export:workflow --all --output=/tmp/workflows.json
docker cp [n8nコンテナ名]:/tmp/workflows.json /root/workflows_backup.json

# ローカルPCにダウンロード
scp root@[VPSのIPアドレス]:/root/workflows_backup.json ~/Desktop/

注意:バイナリファイルやDockerイメージはバックアップしない。マルウェアが混入している可能性がある。JSONと設定ファイルのみが安全。


再インストール後のセキュリティ強化

SSH設定

# /etc/ssh/sshd_config
PermitRootLogin prohibit-password   # 鍵認証のみ許可
PasswordAuthentication no           # パスワード認証を無効化

再インストール後はホストキーが変わるため、ローカルPCで以下を実行してから接続:

ssh-keygen -R [VPSのIPアドレス]

余談:パスワード認証を無効にしてもブルートフォース攻撃自体は来続ける。ログに Failed password が大量に出るが、全て弾かれているので正常。Fail2banを入れるとさらに安全。


Dify + n8n の再構築

構成方針

サブドメインではなくパスで振り分ける構成を採用。

example.com/        → Dify
example.com/n8n/    → n8n

理由

docker-compose.yamlへのn8n追加

# services セクションの末尾に追加
  n8n:
    image: n8nio/n8n:latest
    container_name: docker-n8n
    restart: always
    ports:
      - "5678:5678"
    environment:
      - N8N_RUNNERS_ENABLED=true
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      - N8N_PATH=/n8n/
      - WEBHOOK_URL=https://example.com/n8n/
      - N8N_EDITOR_BASE_URL=https://example.com/n8n/
    volumes:
      - n8n_data:/home/node/.n8n

# volumes セクションの末尾に追加
  n8n_data:

NginxへのLocation追加

DifyのNginx設定は conf.d/default.conf.template で管理されている。

ハマりポイント:

やってしまったこと 問題
conf.d/n8n.conflocation{} のみ記述 server{} ブロックが必要なためNginx起動失敗
proxy_set_header Upgrade $http_upgrade; を追加 envsubst$http_upgrade が空に置換されてNginx起動失敗
default.conf を直接編集 自動生成ファイルのため再起動で上書きされる

正解:default.conf.template に以下を追加する

    location /n8n/ {
        proxy_pass http://n8n:5678/;
        include proxy.conf;
    }

proxy.conf に必要なヘッダーが含まれているので、余計な proxy_set_header は不要。

追加場所は location / の直前。

# 編集前にバックアップ
cp default.conf.template default.conf.template.bak

# 再起動
docker compose restart nginx

監視の仕組み化

通知メールを見逃さないための仕組みが重要。

不審プロセスの自動検知

# /usr/local/bin/security_check.sh
#!/bin/bash
MAILTO="通知先メールアドレス"
SUSPICIOUS=$(ps aux | grep -E "xmrig|minerd|donate-level" | grep -v grep)
if [ -n "$SUSPICIOUS" ]; then
  echo "$SUSPICIOUS" | mail -s "【警告】VPSに不審なプロセスを検知" $MAILTO
fi
# crontabに登録(1時間ごと)
echo "0 * * * * /usr/local/bin/security_check.sh" | crontab -

DockerイメージのWatchtower自動更新

watchtower:
  image: containrrr/watchtower
  environment:
    - WATCHTOWER_NOTIFICATIONS=email
    - WATCHTOWER_SCHEDULE=0 0 2 * * *  # 毎日深夜2時
    - WATCHTOWER_CLEANUP=true

診断コマンド集

# 不審プロセス確認
ps aux | grep -E "xmrig|minerd|donate-level|kworkerds"

# アウトバウンド通信確認
curl -I https://google.com --max-time 10

# certbot接続確認
curl -v https://acme-v02.api.letsencrypt.org/directory --max-time 10

# Nginxエラーログ
docker logs [nginxコンテナ名] --tail 20

# SSH攻撃ログ
grep "Failed\|Accepted" /var/log/auth.log | tail -30

まとめ