osqueryでLinuxにセルフホスト型EDRを構築する:SQLでプロセス、ネットワーク、ファイル変更を監視する

Security tutorial - IT technology blog
Security tutorial - IT technology blog

導入背景:osqueryに乗り換えた理由

6か月前、数台のVPSサーバーでOSSECを運用していましたが、ログの雑音に溺れており、システムに対して的を絞った質問をする簡単な方法がありませんでした。本当に欲しかったのは「過去10分間に外部IPへ接続を開いたすべてのプロセスを表示してくれ」というような問いに即答できる仕組みでした。従来のロギングスタックでそれに答えるには、ログファイルを解析し、grepを連鎖させ、必要なデータが適切なフォーマットでキャプチャされていることを祈るしかありませんでした。

osqueryはそれを解決します。ログを追いかける代わりに、リレーショナルデータベースのようにシステムにクエリを実行できます。実行中のプロセス、オープンソケット、ロード済みカーネルモジュール、cronジョブ、ユーザーアカウントがすべてSQLテーブルの行になります。テーブルをJOINし、WHERE句でフィルタリングし、30秒ごとにクエリをスケジュール実行できます。6か月の本番運用を経て、これを中心に軽量なセルフホスト型EDRを構築した率直な感想をお伝えします。

位置づけについて補足すると、osqueryはWazuhやSuricataの代替品ではありません。それらのツールはログ集約とネットワークIDSに優れています。osqueryはエンドポイントの状態に関するオンデマンドのフォレンジック質問のために作られています——今何が動いているか、過去1時間に何が変わったか、どのプロセスがそのソケットを開いたか。これらは組み合わせてうまく機能します。

インストール

Ubuntu / Debian

Metaは2014年にosqueryをオープンソース化し、2019年にLinux Foundationに寄贈しました。公式パッケージはpkg.osquery.ioにあります。セットアップは約2分で完了します:

# 最新の鍵管理方法(Ubuntu 22.04+ / Debian 11+)
# apt-keyは非推奨 — signed-byオプションでgpg --dearmor を使用する
curl -fsSL https://pkg.osquery.io/deb/pubkey.gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/osquery.gpg

echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/osquery.gpg] https://pkg.osquery.io/deb deb main" | \
  sudo tee /etc/apt/sources.list.d/osquery.list

# インストール
sudo apt-get update && sudo apt-get install osquery -y

# 確認
osqueryi --version

CentOS / RHEL

curl -L https://pkg.osquery.io/rpm/GPG | \
  sudo tee /etc/pki/rpm-gpg/RPM-GPG-KEY-osquery
sudo yum-config-manager --add-repo https://pkg.osquery.io/rpm/osquery-s3-rpm.repo
sudo yum install osquery -y

システムに2つのバイナリがインストールされます:osqueryi(対話型SQLシェル、アドホック調査に最適)とosqueryd(スケジュールクエリを実行して結果をディスクに継続的に書き込むデーモン)です。

設定

メイン設定ファイルの作成

osquerydは/etc/osquery/osquery.confから設定を読み込みます。ここでスケジュールクエリ、ファイル整合性監視(FIM)パス、ログの出力先を定義します。

sudo mkdir -p /etc/osquery
sudo nano /etc/osquery/osquery.conf

本番環境で何度も試行錯誤した末に落ち着いた設定はこちらです:

{
  "options": {
    "config_plugin": "filesystem",
    "logger_plugin": "filesystem",
    "logger_path": "/var/log/osquery",
    "disable_logging": "false",
    "log_result_events": "true",
    "schedule_splay_percent": "10",
    "utc": "true"
  },
  "schedule": {
    "running_processes": {
      "query": "SELECT pid, name, path, cmdline, uid, parent FROM processes WHERE path != '';",
      "interval": 60
    },
    "network_connections": {
      "query": "SELECT p.pid, p.name, lc.local_address, lc.local_port, lc.remote_address, lc.remote_port FROM process_open_sockets lc LEFT JOIN processes p USING(pid) WHERE lc.remote_address NOT IN ('', '0.0.0.0', '::');",
      "interval": 30
    },
    "user_accounts": {
      "query": "SELECT username, uid, gid, directory, shell FROM users;",
      "interval": 300
    },
    "cron_jobs": {
      "query": "SELECT command, path, minute, hour FROM crontab;",
      "interval": 300
    },
    "suid_binaries": {
      "query": "SELECT path, username, groupname, permissions FROM suid_bin;",
      "interval": 3600
    },
    "kernel_modules": {
      "query": "SELECT name, size, status, address FROM kernel_modules;",
      "interval": 3600
    }
  },
  "file_paths": {
    "system_configs": [
      "/etc/%%",
      "/etc/ssh/%%"
    ],
    "user_ssh": [
      "/root/.ssh/%%",
      "/home/%/.ssh/%%"
    ],
    "system_binaries": [
      "/usr/bin/%%",
      "/usr/sbin/%%",
      "/bin/%%",
      "/sbin/%%"
    ]
  },
  "exclude_paths": {
    "system_configs": ["/etc/mtab"]
  }
}

scheduleの下で、各クエリは定義された間隔で実行され、出力をJSONとして記録します。file_pathsの下で、osqueryはそれらのディレクトリをカーネル監査サブシステムに登録し、CREATE、MODIFY、またはDELETEイベントを発行します。globの構文に注意してください:%%はサブディレクトリ全体を再帰的にマッチし、単一の%はちょうど1つのパスセグメントにマッチします。

FIMのためのLinux Auditフレームワークの有効化

FIMイベントにはカーネル監査サブシステムが動作している必要があります。/etc/osquery/osquery.flagsを作成します:

--disable_audit=false
--audit_allow_config=true
--audit_allow_process_events=true
--audit_allow_sockets=true

デーモンの起動

sudo systemctl enable osqueryd
sudo systemctl start osqueryd
sudo systemctl status osqueryd

ログは/var/log/osquery/osqueryd.results.logに改行区切りのJSONとして出力されます。各エントリにはクエリ名、Unixタイムスタンプ、アクションフィールド(差分クエリの場合はadded/removed)、および行データ自体が含まれます。

初期に学んだこと:これらのログの前に置くログ集約エンドポイントや管理パネルには、強力でユニークな認証情報を生成してください。サーバーのパスワードにはtoolcraft.app/ja/tools/security/password-generatorのパスワードジェネレーターを使用しています——ブラウザ内で完全に動作し、ネットワーク経由でデータは送信されません。セキュリティインフラを強化する際には、その違いが重要です。

確認とモニタリング

osqueryiを使った対話型調査

対話型シェルに入り、すぐにフォレンジック調査を始められます。新しいクエリ言語を学ぶ必要はありません——普通のSQLだけです:

-- 非標準ポートでリッスンしているすべてのプロセス
SELECT p.pid, p.name, p.path, lp.port, lp.protocol
FROM listening_ports lp
JOIN processes p USING(pid)
WHERE lp.port NOT IN (22, 80, 443, 3306)
ORDER BY lp.port;

-- パブリックIPへのアウトバウンド接続があるプロセス
SELECT p.name, p.pid, p.cmdline,
       s.remote_address, s.remote_port
FROM process_open_sockets s
JOIN processes p USING(pid)
WHERE s.remote_address NOT LIKE '10.%'
  AND s.remote_address NOT LIKE '192.168.%'
  AND s.remote_address NOT LIKE '172.16.%'
  AND s.remote_address != '127.0.0.1'
  AND s.remote_address != '';

-- 過去1時間に変更された/etc内のファイル
SELECT path, mtime, size
FROM file
WHERE path LIKE '/etc/%'
  AND mtime > (strftime('%s','now') - 3600)
ORDER BY mtime DESC;

-- 既知の安全リストにないrootが所有するプロセス
SELECT pid, name, uid, path, cmdline
FROM processes
WHERE uid = 0
  AND name NOT IN ('systemd','sshd','cron','rsyslogd','agetty');

変更検出のための差分クエリ

差分モードはEDR作業におけるosqueryのキラーフィーチャーです。osquerydがスケジュールクエリを実行すると、現在の結果セットを前回の実行と比較します。変更された行のみがログに書き込まれます——新しい行にはaddedタグが、削除された行にはremovedタグが付きます。

これにより、上記のSUIDバイナリクエリが自動変更検出器になります。ディスクに新しいSUIDバイナリが出現すると、1時間以内にaddedの行として表示されます。これが私がサーバーの1台でクリプトマイナーを発見した方法です——マルウェアがsetuidラッパーバイナリをドロップし、osqueryが侵害から約2時間後、2回のスケジューリングサイクル以内にそれを検出しました。

ファイル整合性イベントの読み取り

-- 過去10分間のFIMイベント(auditが有効である必要があります)
SELECT target_path, action, time, md5, sha256
FROM file_events
WHERE time > (strftime('%s', 'now') - 600)
ORDER BY time DESC;

/etc/passwd/etc/sudoers、または/usr/bin配下のバイナリでメンテナンスウィンドウ外にUPDATEDイベントが検出された場合、変更されるべきでないものが変更されたことを意味します。ルーティンだと決めつける前に必ず調査してください。

シンプルなアラートパイプラインの構築

小規模なサーバー群——例えば5〜20台——であれば、結果ログを追跡してアラートをスクリプト化するだけで、完全なSIEMなしで十分機能します:

#!/bin/bash
# /usr/local/bin/osquery-alert.sh
# osqueryの結果を追跡し、不審なパターンをアラートする

LOGFILE="/var/log/osquery/osqueryd.results.log"
ALERT_LOG="/var/log/osquery-alerts.log"

tail -F "$LOGFILE" | while read -r line; do
  # 新しいSUIDバイナリに対してアラートを発する
  if echo "$line" | grep -q '"name":"suid_binaries"' && \
     echo "$line" | grep -q '"action":"added"'; then
    echo "$(date -u): NEW SUID BINARY: $line" >> "$ALERT_LOG"
    # 通知を挿入(Telegramへのcurl、sendmailなど)
  fi

  # 高ポートへのアウトバウンド接続にアラートを発する
  if echo "$line" | grep -q '"name":"network_connections"'; then
    REMOTE_PORT=$(echo "$line" | python3 -c \
      "import sys,json; d=json.loads(sys.stdin.read()); \
      print(d.get('columns',{}).get('remote_port','0'))" 2>/dev/null)
    if [[ "$REMOTE_PORT" =~ ^[0-9]+$ ]] && [[ "$REMOTE_PORT" -gt 1024 ]]; then
      echo "$(date -u): SUSPICIOUS OUTBOUND $REMOTE_PORT: $line" >> "$ALERT_LOG"
    fi
  fi
done
# バックグラウンドサービスとして実行する
sudo chmod +x /usr/local/bin/osquery-alert.sh
nohup /usr/local/bin/osquery-alert.sh &

スケジュールクエリの実行確認

-- osqueryデーモンのステータスを確認する
SELECT pid, version, config_hash, config_valid
FROM osquery_info;

-- スケジュールクエリが実行されていることを確認する
SELECT name, executions, last_executed, average_memory, last_status
FROM osquery_schedule;

-- このホストで利用可能なすべてのテーブルを一覧表示する
SELECT name, description
FROM osquery_registry
WHERE type = 'table'
ORDER BY name;

6か月後、最大の驚きはアクティブな侵害を検出したことではありませんでした——osqueryは確かにそのクリプトマイナーを2時間以内に検出しましたが。本当の収穫はベースラインの可視性です:何が動いているか、何がどこに接続しているか、先週火曜日の午前3時に/etcで何が変更されたかを正確に把握できることです。その状況認識は、毎回リアクティブなアラートよりも優れています。

SQLはチームメンバーのオンボーディングも簡単にします。Linuxセキュリティについてほとんど知らない開発者でも、SELECT * FROM processes WHERE name = 'python3'と書けば、見ているものをすぐに理解できます。低い参入障壁こそが、より重い商用ツールが利用可能な場合でも、osqueryが私の標準サーバーセットアップに残り続ける理由です。

Share: