Raspberry Pi 3B+で疑似SOCを構築する。Loki、Grafana v13、Alloy、GeoLite2でSSH攻撃を可視化(2026年8月21日)

自宅サーバーを公開していると、SSHへの総当たり攻撃は思っている以上の頻度で発生します。

「攻撃を受けている」というログは残りますが、どこから、どのIPアドレスから、いつ攻撃されているのかまで見えるようにしたら面白そうだと思いました。

そこで、使っていなかったRaspberry Pi 3B+を使って、SSH攻撃を収集・保存・位置情報化し、Grafanaで世界地図やランキングとして確認できる疑似SOCを構築しました。

企業向けSOCのような大規模な分析環境ではありません。しかし、ログ監視やセキュリティ分析を実際に触って学ぶ教材としては、十分に面白い構成になりました。この記事では、2026年7月6日時点の環境で実際に構築・動作確認した内容を紹介します。

  1. 結論
  2. システム構成
  3. 使用環境
  4. Raspberry Pi 3B+で削った機能
  5. STEP 0:システムの更新と初期設定
    1. Raspberry PiへSSH接続する
    2. システムを最新状態へ更新する
    3. 必要なパッケージをインストールする
    4. UFWを設定する
    5. fail2banを有効にする
  6. STEP 1:USBメモリの準備(ext4 format、UUID固定、自動マウント)
    1. USBメモリが認識されているか確認する
    2. USBメモリをext4でフォーマットする
    3. マウントポイントを作成する
    4. UUIDを確認する
    5. 起動時に自動マウント
    6. fstabの設定を確認する
  7. STEP 2:Lokiをインストールする
    1. 最新版のLokiを取得する
    2. Lokiをダウンロードする
    3. Loki用保存ディレクトリを作成する
    4. Loki専用ユーザーを作成する
    5. Loki設定ファイル(YAML)を作成する
    6. systemdサービスを作成する
    7. Lokiを起動する
    8. HTTP APIが応答するか確認する
  8. STEP 3:Grafana Alloyを入れてログをLokiへ転送する
    1. なぜPromtailではなくAlloyを採用するのか
    2. Grafana公式リポジトリを登録する
    3. Grafana Alloyをインストールする
    4. Alloy用の保存ディレクトリを作成する
    5. Alloy設定ファイルを作成する
    6. Alloyの環境変数を設定する
    7. systemdへ登録する
    8. Alloyのログを確認する
    9. Lokiが接続できるか確認する
    10. fail2banログを監視できるか確認する
  9. STEP 4:GeoLite2とGrafanaを設定する
    1. GeoLite2とは
    2. MaxMindでアカウントを作成する
    3. GeoLite2-Cityをダウンロードする
    4. Pythonの環境を準備する
    5. 位置情報貼り付けスクリプトを配置する
    6. systemdサービスを作成する
    7. GeoIP変換サービスを有効化して起動する
    8. Grafanaをインストールする
    9. Grafanaの保存先をUSBメモリへ変更する
    10. Grafanaを起動する
  10. STEP 5:Grafanaダッシュボードを作る
    1. Lokiにデータソースを追加する
    2. Geomapパネルを作る(世界地図で可視化)
  11. 疑似SOCの完成
  12. 最終チェック
  13. よくある質問(FAQ)
  14. まとめ
  15. リファレンス
  16. 18日後
  17. 更新履歴

結論

今回の疑似SOCでは、SSHへの攻撃を次の流れで処理しました。

  1. SSHへ攻撃を検知
  2. fail2banが攻撃元を遮断
  3. Grafana Alloyがログを収集
  4. Lokiへログを保存
  5. GeoLite2-CityでIPアドレスから位置情報を取得
  6. Grafana v13で可視化

最終的には、次の情報をブラウザ上で確認できるようになります。

世界地図上のヒートマップ、国別の攻撃回数、攻撃者Top 10、攻撃元IPなどです。各サービスを独立させ、STEPごとに動作確認する構成にしたことで、途中で問題が発生しても原因を切り分けやすくしました。

疑似SOCの完成形
疑似SOCの完成形

システム構成

今回構築システムは次の構成です。

ソフトウェア役割
fail2banSSH攻撃を検知・遮断
Grafana Alloyログ収集
Lokiログ保存
GeoLite2-CityIPアドレスを位置情報へ変換
Grafanaログを可視化

使用環境

本記事では、次の環境で構築および動作確認を行いました。

項目内容
ハードウェアRaspberry Pi 3B+(2018)
OSRaspberry Pi OS Lite 64bit(Trixie)
OS保存先microSDカード
ログ保存先USBメモリ(フォーマット形式ext4)
Grafanav13(2026年7月6日時点)
Grafana Loki2026年7月6日最新版
Grafana Alloy2026年7月6日最新版
GeoLite2GeoLite2-City
PythonPython 3

GrafanaやLokiは更新頻度が高いように思います。そのたびにUIや設定項目が大きく変更されます。今2026年7月6日のバージョンで構成をしています。将来的なバージョンでは画面や設定値が大きく異なる可能性があると思ってください。ら、疑似SOCを体験できる教材みたいな感じの構成にはなっています。

Raspberry Pi 3B+で削った機能

がんばってもスペックの前ではどうにもならないことがたくさんあります。Raspberry Pi 3B+の制約として以下の3点が挙げられます。

  • メモリ1GB
  • CPU Cortex-A53
  • USB2.0

もうどうしようもない、拡張とかそういう問題でもない制限がありました。ならば機能を削るしかありません。削った機能は次の通りです。

  • Docker不採用
  • Kubernetes不採用
  • Prometheus不採用
  • Alertmanager不採用
  • Grafanaギリギリの最小構成
  • Loki保存期間30日

全部入りを考えるのはやめて、止まらないことを最優先としました。

STEP 0:システムの更新と初期設定

このSTEP 0では、疑似SOC構築する前にRaspberry Piの基本環境を整えます。

後から必要なパッケージを追加すると、問題が発生した際に原因の切り分けが難しくなります。そのため、本記事では最初に最低限必要な環境を作り、各STEPごとに動作するか調べて次に進む構成になっています、

また作業は主にRaspberry PiへSSHで接続して行います。PowerShellなどで繋いでください。

Raspberry PiへSSH接続する

まずはSSH接続です。PowerShellなどを開き、次の通りに打ち込んでください。

ssh ユーザー名@ホスト名.local

接続できない場合は、以前接続したホスト情報がWindowsに残っている可能性があります。その場合はファイルを削除することで接続できるようになります。

C:\Users\ユーザー名\.ssh\known_hosts

これを削除するだけです。

システムを最新状態へ更新する

最初にシステム全体を最新の状態へ更新します。

sudo apt update
sudo apt -y full-upgrade
sudo apt -y autoremove
sudo reboot

再起動を行います。30秒ほど待機したのち再度SSH接続を行います。

必要なパッケージをインストールする

今回は必要になったタイミングで追加します。上記を理由としてすべてのパッケージを入れません。

以前は色々いれてごちゃごちゃとなり初期化を繰り返していました。その反省です。

sudo apt install -y curl
sudo apt install -y wget
sudo apt install -y gnupg2
sudo apt install -y ca-certificates
sudo apt install -y lsb-release
sudo apt install -y apt-transport-https
sudo apt install -y unzip
sudo apt install -y jq
sudo apt install -y python3
sudo apt install -y python3-pip
sudo apt install -y python3-venv
sudo apt install -y git
sudo apt install -y ufw
sudo apt install -y fail2ban
sudo apt install -y rsyslog

まず導入するものは次の用途で使用するパッケージです。

パッケージ用途
curl・wgetファイルの取得
gnupg2Grafana公式リポジトリ登録
jqGitHub APIのJSON解析
Python3GeoLite2変換スクリプト実行
gitソース取得・管理
ufwファイアウォール
fail2banSSH攻撃の検知・遮断
rsyslogログ出力

rsyslogを入れ忘れたことで、ローテーションが起こったときにログの出力ができなくなったことがありました。

UFWを設定する

続いてファイアウォールを設定します。

今回はSSH接続用の22番ポートと、Grafanaへアクセスする3000番ポートだけを解放します。

sudo ufw allow 22/tcp
sudo ufw allow 3000/tcp
sudo ufw --force enable
sudo ufw status

正常に設定されたことを確認する

設定後は次のようになっていることを確認してください。

【sudo ufw status】の結果が【Status: active】と表示され、【22/tcpと3000/tcp】が【ALLOW】になっていれば正常です。

本記事ではローカルネットワーク内での利用を前提としています。
honeypodでインターネットへ直接公開する場合は、ルーターやVPNを含めたネットワーク全体のセキュリティ設計を見直してください。

fail2banを有効にする

疑似SOCでは、SSHへの攻撃ログを起点として分析します。
そのため、最初にfail2banが正常に動作していることを確認しておく必要があります。

今回の想定では、fail2banが攻撃ログを生成する起点となります。

SSHへの総当たり攻撃やブルートフォース攻撃を検知すると、fail2banがアクセス元を遮断してログに書き込みます。

そのログを後ほど活用するため、このfail2banが正常に動作していなければなりません。

sudo systemctl daemon-reload
sudo systemctl enable fail2ban
sudo systemctl enable rsyslog
sudo systemctl restart fail2ban
sudo systemctl restart rsyslog

状態を確認します。

sudo systemctl status fail2ban --no-pager
sudo systemctl status rsyslog --no-pager

正常動作を確認する

ここでどちらもこのような表示になっていれば正常です。

【Active: active (running)】

このように表示されていれば正常に起動しています。

続いてSSH監視用Jailが有効になっているか確認します。

sudo fail2ban-client status sshd

正常ならば【Jail list: sshd】が表示されていることを確認する。

STEP 1:USBメモリの準備(ext4 format、UUID固定、自動マウント)

このSTEPでは、LokiやGrafanaが保存するログデータをUSBメモリへ保存するための準備を行います。

Lokiは大量のログを書き込み続けるため、microSDカードへ保存すると書き込み回数が増え、寿命を縮める原因になります。

そのため、本記事ではログ保存先をUSBメモリへ分離する構成を採用します。

Raspberry Pi 3B+はUSB 2.0までの対応ですが、ログ用途であれば十分実用的です。

LokiやGrafanaは大量のログを書き込みます。microSDに書き込むとカードの寿命が縮まります。

そこで今回はログの書き込み先をUSBメモリにしました。SSDの方がいいのですが、Raspberry Pi 3B+は全てUSB2.0なので、オーバースペックです。

USBメモリが認識されているか確認する

まずはUSBメモリが認識されているか確認をします。

lsblk

/dev/sdaや/dev/sdb、/dev/sda1など表示されます。その表示されたものの情報も確認します。
後にフォーマットをするため、容量、パーティション構成がおかしくないかを確認します。

今回はパーティションがあり[/dev/sda1]が表示されたと便宜上扱います。

間違ったデバイスを選択しない

USBメモリをLinux標準のext4でフォーマットします。

間違ったデバイスを指定してしまうと、そのデバイス内のディスク内のデータは失われます。

今回はパーティションがあり【/dev/sda1】だったという設定でフォーマットをします。

USBメモリをext4でフォーマットする

認識を確認したら、Linux標準ファイルシステムであるext4へフォーマットします。

sudo mkfs.ext4 /dev/sda1

仮にFAT32でフォーマットされていた場合で/dev/sdaではなく、sdaとだけ表示されていたとします。
その時も同じくsudo mkfs.ext4 /dev/sdaで上書きしてext4にフォーマットできます。

ext4を採用する理由

LinuxなのでFAT32やexFATも利用できます。

LokiやGrafanaは多数の小さなファイルを読み書きします。そのため、Linuxネイティヴ、権限管理の都合、ジャーナリングの都合、Linuxでの安定性を考えるとext4がふさわしいと思います。

マウントポイントを作成する

USBメモリを保存先とするため、マウントポイントを作成します。

今回は適当に、【/mnt/socdata】という名前を使用します。まずはディレクトリを作成します。

sudo mkdir -p /mnt/socdata

UUIDを確認する

まずUUIDの特定します。sdaと表示されていても、sdbやsdcになったりして、USB機器の接続順によって変化します。そこで起動時も同じUSBメモリを認識できるようにUUIDを使用します。

sudo blkid /dev/sda1

これで【/dev/sda: UUID=”xxxx-xxxx-xxxx” TYPE=”ext4″ ~】、と表示されます。UUIDの後ろにある文字列がそのUSBメモリのUUIDです。後で必要になるため、メモを取ってください。

UUIDとは

UUID(Universal Unique Identifier)は、ストレージごとに割り当てられる固有の識別子です。

デバイス名が変更されてもUUIDは変わらないため、自動マウントではUUIDを使用することが推奨されています。実際に自動マウントの際はUUIDの方が便利です。

起動時に自動マウント

起動時の自動マウントやデバイスの固定のためにfstabに記入します。

sudo nano /etc/fstab

2行ほど書いてあると思いますが、一番下に次の1行を加えてください。

UUID=xxxx-xxxx-xxxx  /mnt/socdata  ext4  defaults,noatime  0  2

ここで先ほどメモをしたUUIDをxxxx-xxxx-xxxxの部分へ貼り付けます。

noatimeを指定する理由

noatimeを指定すると、ファイルへのアクセス時刻(atime)更新を行わなくなります。
今回の用途ではログにアクセス日時を保存する必要がないため、不要な書き込みを減らすことができます。

結果としてUSBメモリやmicroSDカードの負荷を軽減できます。

fstabの設定を確認する

設定を書き終えたら、一度設定を反映して確認します。

sudo systemctl daemon-reload
sudo mount -a
df -h /mnt/socdata

sudo mount -aでマウントをします。エラーが出なければマウントができています。よくsudo systemctl daemon-reloadをせずにマウントしてエラーがでます。何かを書き換えたときはdaemon-reloadと思ってもいいかもしれません。

df-hでは/mnt/socdataが表示されていて、容量もだいたい合っていれば成功です。

STEP 2:Lokiをインストールする

このSTEP 2では、ログを保存するためのデータベース【Loki】を導入します。

LokiはGrafana Labsが開発しているログ集約システムで、Prometheusのようなラベルベースでログを管理できることが一番の特徴です。

この記事では、デバイスの都合上PrometheusではなくLokiを採用し、fail2banで収集したログをAlloy経由でLokiへ保存します。それをGrafanaから可視化する構成を構築します。

※この記事の中で最も設定ミスが起こりやすい所です※

YAMLのインデントやパス、権限が少しでも異なるとサービスが動きません。
途中で問題が発生した場合は、一度に色々変えず、一つずつ確認して直すことをお勧めします。

最新版のLokiを取得する

まずはGitHub APIを利用し、公開されている最新版のLokiを取得します。

curl -s https://api.github.com/repos/grafana/loki/releases/latest | jq -r '.tag_name'

これでバージョンが番号が表示されたら環境変数へ保存します。

LOKI_VERSION=$(curl -s https://api.github.com/repos/grafana/loki/releases/latest | jq -r '.tag_name' | sed 's/^v//')

確認を行います。

echo "$LOKI_VERSION"

3.x.xと表示されれば正常です。

GitHub APIを利用する理由

aptでインストールできる環境もありますが、リポジトリは更新タイミングによって最新版より古いバージョンが配布されている場合があります。

そのため今回はGitHub Releasesから直接取得することで、記事公開時点(2026年7月6日)の最新版を利用できるようにしています。

Lokiをダウンロードする

現時点での最新版を取得します。

curl -LO "https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/loki-linux-arm64.zip"

ダウンロードしたzipファイルを解凍します。

unzip loki-linux-arm64.zip

そして、実行ファイルを配置します。

sudo mv loki-linux-arm64 /usr/local/bin/loki
sudo chmod +x /usr/local/bin/loki

一応の確認としてバージョン情報が返ってくるかを見ます。

loki --version

バージョン情報が返ってくれば正常です。

Loki用保存ディレクトリを作成する

今回はmicroSDカードへの書き込みをなるべく減らすため、USBメモリ上へログを逃がす構成を取っています。

今回はログをUSBメモリへ保存するために、必要なディレクトリをたくさん生成します。

sudo mkdir -p /mnt/socdata/loki/chunks
sudo mkdir -p /mnt/socdata/loki/index
sudo mkdir -p /mnt/socdata/loki/cache
sudo mkdir -p /mnt/socdata/loki/wal
sudo mkdir -p /mnt/socdata/loki/compactor
sudo mkdir -p /etc/loki

Loki専用ユーザーを作成する

次はユーザーの作成です。Lokiをroot権限で動かす必要はないです。
専用のユーザーを作り、必要なディレクトリだけアクセスできるようにすることで、安全簡単にな設定にします。

まずは専用ユーザーを加えます。

sudo useradd --system --no-create-home --shell /bin/false loki

その後は権限の設定を行います。

sudo chown -R loki:loki /mnt/socdata/loki
sudo chown -R loki:loki /etc/loki

専用ユーザーを利用する理由

root権限でサービスを実行できなくはないのですが、設定ミスやセキュリティの穴が発生した場合の影響範囲が本当に大変なことになります。

systemdサービスでは、必要最小限の権限で動作させることが良いです。

Loki設定ファイル(YAML)を作成する

LokiはYAML形式で設定を管理します。まずは設定ファイルを作成します。

sudo nano /etc/loki/loki-config.yaml
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /mnt/socdata/loki
  storage:
    filesystem:
      chunks_directory: /mnt/socdata/loki/chunks
      rules_directory: /mnt/socdata/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 64

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

storage_config:
  tsdb_shipper:
    active_index_directory: /mnt/socdata/loki/index
    cache_location: /mnt/socdata/loki/cache

limits_config:
  retention_period: 30d
  ingestion_rate_mb: 4
  ingestion_burst_size_mb: 8

compactor:
  working_directory: /mnt/socdata/loki/compactor
  retention_enabled: true
  retention_delete_delay: 2h
  delete_request_store: filesystem

analytics:
  reporting_enabled: false

この疑似SOC最大の山場です。

YAMLで間違えやすいところ

YAMLは一般的な設定ファイルとは異なり、少しの間違いも許されません。

  • タブ文字
  • スペース数
  • インデント

これらが厳密に決められており、タブやスペース1つでLokiが動かなくなります。そのものが構文になっています。

  • server
  • common
  • storage_config
  • schema_config

インデントを変更すると起動できなくなるため、前述のYAMLをコピーした後はスペースが崩れていないか確認してください。

systemdサービスを作成する

続いてLokiを自動起動できるようにsystemdへ登録します。

sudo nano /etc/systemd/system/loki.service
[Unit]
Description=Grafana Loki
After=network.target

[Service]
User=loki
Group=loki
ExecStart=/usr/local/bin/loki -config.file=/etc/loki/loki-config.yaml
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
MemoryMax=256M

[Install]
WantedBy=multi-user.target

大事なところは【ExecStart=】ここのパスが自分の環境のパスと合っているか確認してください。

ExecStartを確認する

一番多い設定ミスはパスです。今回の記事のパスは/usr/local/bin/lokiを使用しています。

ExecStart=/usr/local/bin/loki

今回はこのパスですが、環境によって違います。自分の環境ではどこに配置したのかを覚え、パスの変更もしてください。

Lokiを起動する

今まで記入してきたLokiの設定を反映させます。

sudo systemctl daemon-reload
sudo systemctl enable loki
sudo systemctl restart loki

状態を確認します。

sudo systemctl status loki --no-pager

正常にきどうしたことを確認する

ステータスが【Active: active (running)】となっていることが確認できれば成功です。
もし【failed】の場合は、YAML、ディレクトリ権限、ファイル名、パスのどれかです。

HTTP APIが応答するか確認する

この段階ではサービスが起動しているということしかわかりません。
そこでHTTP APIが正常に応答しているのかも確認します。

sleep 30 && curl -s http://localhost:3100/ready

このコマンドで【ready】が返ってくればLokiは正常にリクエストを受け付けられる状態にあります。sleep 30を入れているのは、すぐに入力をすると正確に返事が返ってこないからです。

STEP 3:Grafana Alloyを入れてログをLokiへ転送する

このSTEP 3では、Grafana Alloyを導入し、fail2banが出力するログをLokiへ転送します。

Grafana AlloyはGrafana Labsが開発する統合エージェントで、今までのPromtailやGrafana Agentの後継として位置づけられています。

この記事ではfail2banのログの監視とLokiへリアルタイムで送信する役割を担います。

なぜPromtailではなくAlloyを採用するのか

以前はLokiへログを送信する方法として、Promtailが広く利用されていました。

しかし現在はGrafana LabsよりGrafana Alloyへの移行が推奨されています。理由としては、AlloyにはPromtail、Grafana Agent、OpenTelemetryなどの機能を統合したエージェントであり、今後はAlloyが標準となるからです。

そのため、将来的にAlloyになるのならば、と考えてAlloyを採用しました。

Grafana公式リポジトリを登録する

まずはGrafana公式リポジトリを登録します。これでaptから最新版のAlloyをインストールできます。

sudo mkdir -p /etc/apt/keyrings

curl -fsSL https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg

echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list

リポジトリの追加が終わったらパッケージ情報を更新します。

sudo apt update

リポジトリ登録を確認する

更新している最中に【Hit: https//apt.grafana.com stable InRelease】と表示されていれば正常です。

エラーが表示される場合は、ネットワーク接続の確認、GPGキーが正しいか、grafana.listあたりを確認してください。

Grafana Alloyをインストールする

準備は整っているため簡単にインストールすることができます。

sudo apt install -y alloy

一応確認しておきます。

alloy --version

ここでバージョンが表示されていれば正常にインストールされています。

Alloy用の保存ディレクトリを作成する

Alloy用の保存ディレクトリを作成し、所有者を設定します。

sudo mkdir -p /mnt/socdata/alloy
sudo chown -R alloy:alloy /mnt/socdata/alloy

ここでもLoki同様にUSBメモリ側へ保存する構成にしています。

Alloy設定ファイルを作成する

ここでは最初の設定として、fail2banのログだけをLokiへ送信する構成を作ります。

GeoLite2で変換したJSONログはまだ作りません。そのため、ここではfail2ban.logだけ設定されていれば良しとしています。

sudo nano /etc/alloy/config.alloy

サンプルが既に記入されています。全部消して次の内容を記入しなおしてください。

local.file_match "fail2ban_logs" {
  path_targets = [{
    __path__ = "/var/log/fail2ban.log",
    job       = "fail2ban",
  }]
}

loki.source.file "fail2ban" {
  targets    = local.file_match.fail2ban_logs.targets
  forward_to = [loki.write.local.receiver]
}

local.file_match "fail2ban_geo" {
  path_targets = [{
    __path__ = "/mnt/socdata/fail2ban_geo.json",
    job       = "fail2ban_geo",
  }]
}

loki.source.file "fail2ban_geo" {
  targets    = local.file_match.fail2ban_geo.targets
  forward_to = [loki.write.local.receiver]
}

loki.write "local" {
  endpoint {
    url = "http://127.0.0.1:3100/loki/api/v1/push"
  }
}

保存をしていったん閉じます。

Alloyの環境変数を設定する

続いてAlloyがどの設定ファイルを読み込むのかについて指定をします。
そのため環境変数を書き換えます。

sudo nano /etc/default/alloy

次の3行を書きます。

CONFIG_FILE=/etc/alloy/config.alloy
CUSTOM_ARGS="--storage.path=/mnt/socdata/alloy"
RESTART_ON_UPGRADE=true

全て設定してから、起動と確認を行います。

Alloy設定で重要なポイント

今回利用する設定は、fail2banログ監視、Loki転送、ラベル付けの3つです。
特にloki.source.fileとloki.writeの接続関係を間違えるとログは送信されません。

コピー後はブロックの対応関係を確認してください。

systemdへ登録する

設定が終わったらサービスを登録し、起動します。

sudo systemctl daemon-reload
sudo systemctl enable alloy
sudo systemctl restart alloy

そして状態を確認します。

sudo systemctl status alloy --no-pager

正常に起動しているか確認する

正しく設定が行われて入ればstatusが【Active: active (running)】と表示されます。

ここでfailedになっている場合は、設定ファイルに問題がある可能性を疑ってください。

Alloyのログを確認する

問題が発生した場合はsystemctlだけでは原因が分からないことが多くあります。

その場合はjournalctlで確認します。

sudo journalctl -u alloy -n 100 --no-pager

よくあるエラー

よく起こったエラーとしては次のようなものがあります。

  • config.alloyの構文エラー
  • LokiのURL間違い
  • fail2banログのパス間違い
  • 権限間違い

Lokiが接続できるか確認する

Lokiが起動している状態で確認をします。

curl http://localhost:3100/ready

これで【ready】が返ってきたら正常に動作しています。

続いてAlloy側でもエラーが発生していないかを確認します。

sudo journalctl -u alloy -f

ログ送信時にエラーが出ていなければ正常です。

fail2banログを監視できるか確認する

まずログファイルを確認します。

sudo tail -f /var/log/fail2ban.log

続いてAlloy側でもログを監視します。

sudo journalctl -u alloy -f

fail2banへログが追加されたタイミングでAlloy側にも処理済みの内容が表示されます。

STEP 4:GeoLite2とGrafanaを設定する

このSTEP 4では、攻撃元IPアドレスへ位置情報(緯度、経度、国名)を張り付け、Grafanaで可視化ができるようにします。

LokiにはIPアドレスしか保存されていないため、このままでは世界地図には表示できません。そこで今回導入するGeoLite2-Cityデータベースを利用して、IPアドレスから位置情報を取得します。

最終的にはGrafanaのGeomapパネルで、攻撃元を地図上へ表示できるようにします。

GeoLite2とは

GeoLite2は、IPアドレスから位置情報を取得するためのデータベースです。

本記事ではlatitude(緯度)とlongitude(経度)が必要となるため、MaxMind GeoLite2-Cityを使わさせていただきます。

取得できる主な情報は次のようなものです。

項目内容
Country国名
City都市名
Latitude緯度
Longitude経度

今回は緯度経度を利用してGeomapに表示し、攻撃元を視覚的にわかるようにします。

MaxMindでアカウントを作成する

まずはMadMindアカウントを取得します。
https://www.maxmind.com/en/home

MAXMINDのページ右上にアカウントがあります。
MAXMINDのページ右上にアカウントがあります。

ここにログインをして、GeoLite2のライセンスキー(License Key)を取得します。ライセンスキーは後ほどつかうことになるため、絶対にダウンロードしておきましょう。

このLicense keyが後ほど必要となります
このLicense keyが後ほど必要となります

GeoLite2-Cityをダウンロードする

まずは保存用ディレクトリを作ります。

sudo mkdir -p /etc/GeoIP

そして、GeoLite2-City.mmdbをダウンロードします。この時にライセンスキーが必要になります。

sudo curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=あなたのライセンスキー&suffix=tar.gz" -o /tmp/GeoLite2-City.tar.gz
sudo tar -xzf /tmp/GeoLite2-City.tar.gz -C /tmp/
sudo find /tmp -name "GeoLite2-City.mmdb" -exec mv {} /etc/GeoIP/ \;
ls -lh /etc/GeoIP/GeoLite2-City.mmdb

今回は都市情報だけではなく、Geomapで使用する緯度経度も必要になるため、GeoLite2-Countryではなく、GeoLite2-Cityを使用します。Countryだとlatitudeもlongitudeも取得できず、Geomapで詰みました。

Pythonの環境を準備する

位置情報を付与するスクリプトを実行するため、Pythone仮想環境を作成します。

GeoLite2を扱うPythonライブラリをシステム全体へ直接入れることもできます。今回は色々ハードウェア上の事情があったため、仮想環境を利用します。仮想環境ならばOSの環境に影響を与えず管理ができます。

sudo mkdir -p /opt/geoip-converter
sudo python3 -m venv /opt/geoip-converter/venv
sudo /opt/geoip-converter/venv/bin/pip install maxminddb

これでGeoLite2を扱うことができるようになります。

仮想環境を利用する理由

Pythonライブラリをシステム全体へインストールすると、他のアプリケーションと依存関係が競合する可能性がでてきます。

仮想環境を利用することで、必要なライブラリだけを使うことができます。

位置情報貼り付けスクリプトを配置する

Pythonのスクリプトを書くのですが、内容としては監視して、抽出して、検索して、JSONで保存するといった内容になります。

  • fail2ban.logを監視
  • IPアドレスを抽出
  • GeoLite2で検索
  • GeoJSON形式として保存

具体的にはこのような処理ができるようにしてみました。

sudo nano /opt/geoip-converter/geoip_converter.py

次のように書いてみました。

#!/usr/bin/env python3
import os
import re
import json
import time
import maxminddb
from datetime import datetime, timezone

FAIL2BAN_LOG = "/var/log/fail2ban.log"
OUTPUT_LOG   = "/mnt/socdata/fail2ban_geo.json"
MMDB_PATH    = "/etc/GeoIP/GeoLite2-City.mmdb"

IP_RE = re.compile(
    r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+ fail2ban\.actions\s+\[.*?\]: (NOTICE|WARNING)\s+\[sshd\] Ban (\d+\.\d+\.\d+\.\d+)"
)

def lookup(reader, ip):
    try:
        r = reader.get(ip)
        if not r:
            return None, None, None, None
        lat     = r.get("location", {}).get("latitude")
        lon     = r.get("location", {}).get("longitude")
        country = r.get("country", {}).get("names", {}).get("en", "Unknown")
        city    = r.get("city",    {}).get("names", {}).get("en", "Unknown")
        return lat, lon, country, city
    except Exception:
        return None, None, None, None

def tail(path):
    # inodeを監視してlogrotate後に新しいファイルを自動で開き直す
    current_ino = None
    f = None
    while True:
        try:
            new_ino = os.stat(path).st_ino
            if f is None or new_ino != current_ino:
                if f:
                    f.close()
                f = open(path, "r")
                f.seek(0, 2)
                current_ino = new_ino
            line = f.readline()
            if not line:
                time.sleep(1)
                continue
            yield line
        except FileNotFoundError:
            time.sleep(1)
            continue

def main():
    with maxminddb.open_database(MMDB_PATH) as reader:
        for line in tail(FAIL2BAN_LOG):
            m = IP_RE.search(line)
            if not m:
                continue
            _, _, ip = m.group(1), m.group(2), m.group(3)
            lat, lon, country, city = lookup(reader, ip)
            if lat is None:
                continue
            record = {
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "ip":        ip,
                "latitude":  lat,
                "longitude": lon,
                "country":   country,
                "city":      city,
            }
            with open(OUTPUT_LOG, "a") as out:
                out.write(json.dumps(record) + "\n")
            print(json.dumps(record), flush=True)

if __name__ == "__main__":
    main()

今回はどんどん継ぎ足しで作り動作することを最優先に作っています。数週間粘りましたが、降参したと思っています。ただし、計画通りの動きはしています。しかし、もっと効率的な実装ができるとおもいます。

もし別環境で停止する場合は、前述のYAMLとこのPythonスクリプトあたりを重点的に見直してみてください。

GeoLite2のパスやPythonライブラリ、ログファイルの場所など環境依存になりやすいものが集中しています。それだけ怪しいということです。

systemdサービスを作成する

特に特別なことはないと思います。

sudo nano /etc/systemd/system/geoip-converter.service

次のように書きます。

[Unit]
Description=GeoIP Converter for fail2ban
After=network.target fail2ban.service

[Service]
Type=simple
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/geoip-converter/venv/bin/python3 /opt/geoip-converter/geoip_converter.py
Restart=on-failure
RestartSec=5s
MemoryMax=64M

[Install]
WantedBy=multi-user.target

GeoIP変換サービスを有効化して起動する

スクリプトを保存したら、systemdサービスとして起動します。

sudo systemctl daemon-reload
sudo systemctl enable geoip-converter
sudo systemctl restart geoip-converter
sudo systemctl status geoip-converter --no-pager

statusがActive: active (running) であれば成功です。

sudo journalctl -u geoip-converter -n 10 --no-pager

ここでjournalにログが出力されていることを確認します。

Grafanaをインストールする

Grafana公式リポジトリはAlloy導入時に登録済みのため、この時点ですぐにインストールができます。
20%くらいで止まって見えますが正常です。

sudo apt install -y grafana
sleep 30 && grafana-server -v

バージョンが表示されれば成功です。

Grafanaの保存先をUSBメモリへ変更する

Grafanaはデータベースやプラグイン、各種設定を書き込み続けます。microSDカードにそんなことをされたら書き込み回数が増えすぎてカードの寿命を縮める原因となります。

今回はLokiと同様にUSBメモリ側へ保存するようにしています。

sudo mkdir -p /mnt/socdata/grafana
sudo chown -R grafana:grafana /mnt/socdata/grafana

続いてGrafanaの環境設定を書き換えます。

sudo nano /etc/default/grafana-server

初期設定が記入されていますが、次の内容に置き換えてください。

GRAFANA_USER=grafana
GRAFANA_GROUP=grafana
GRAFANA_HOME=/usr/share/grafana
LOG_DIR=/var/log/grafana
DATA_DIR=/mnt/socdata/grafana
MAX_OPEN_FILES=10000
CONF_DIR=/etc/grafana
CONF_FILE=/etc/grafana/grafana.ini
RESTART_ON_UPGRADE=true
PLUGINS_DIR=/mnt/socdata/grafana/plugins
PROVISIONING_CFG_DIR=/etc/grafana/provisioning
PID_FILE_DIR=/var/run/grafana

DATA_DIRとPLUGIN_DIRがUSBメモリ側になっていることを確認してください。この環境で運用していますが不具合はいまのところ発生していません。

Grafanaを起動する

設定を保存したらGrafanaを起動します。

sudo systemctl daemon-reload
sudo systemctl enable grafana-server
sudo systemctl restart grafana-server
sudo systemctl status grafana-server --no-pager

statusでActive: active (running) が確認できたら成功です。

すぐにGrafanaの確認をしたいところですが、Grafanaは初回起動時に数秒の内部処理を行っているふしがあります。かなり長い間処理がつづくので、sleep 60をつけて時間を待ってから状態を確認します。

sleep 60 && curl -s http://localhost:3000/api/health | jq

正常ならば[“database”: “ok”]と返事があります。

全サービス動作確認

sudo systemctl status fail2ban --no-pager
sudo systemctl status loki --no-pager
sudo systemctl status alloy --no-pager
sudo systemctl status geoip-converter --no-pager
sudo systemctl status grafana-server --no-pager

全サービスが[Active: active (running)]なっていると成功です。

各サービスのエンドポイントの確認をする

curl -s http://localhost:3100/ready
curl -s http://localhost:12345/-/ready
curl -s http://localhost:3000/api/health | jq

再起動をして自動起動するかどうかを確認する。

sudo reboot
sudo systemctl status fail2ban --no-pager
sudo systemctl status loki --no-pager
sudo systemctl status alloy --no-pager
sudo systemctl status geoip-converter --no-pager
sudo systemctl status grafana-server --no-pager
sudo journalctl -u geoip-converter -n 20 --no-pager

journalにログが出力されているか確認します。

sudo fail2ban-client status sshd

sshdのjailが動いているかどうかの確認です。

STEP 5:Grafanaダッシュボードを作る

ブラウザでRaspberry PiのIPを打ち開きます。

http://ラズベリーパイのIP:3000

Raspberry PiのIPアドレスの調べ方は次のコマンドで出てきます。

hostname -I

Grafanaへ初めて入るときのユーザー名とパスワードは次の通りです。

  • ユーザー名:admin
  • パスワード:admin

パスワードは初回ログイン後に変更するよう言われるため、忘れないパスワードを入れましょう。

Grafanaのログイン画面
Grafanaのログイン画面

Lokiにデータソースを追加する

ここからがややこしくなります。それはGrafanaがバージョンアップするたびに大きくUIが変更されるからです。メニューや名称すら変わってしまうので気を付けたいところです。

Grafanaの左にあるメニューから色々と選んでいきます。

【左メニュー】→【Connections】→【Data sources】→【Add data source】→ Search【Loki】という流れでLokiをインストールします。

Grafanaの左のメニュー
Grafanaの左のメニュー

【Add data source】で【Loki】を検索すると次のようにヒットするのでインストールします。

Loki
Loki

Lokiの編集や設定は1か所のみです。【URL】と書かれている個所に【http://localhost:3100】と入力してください。それ以外はいじらなくていいです。

URLにはhttp://localhost:3100と記入する
このピンクの欄にhttp://localhost:3100と記入する

最後に一番下にある【Save & test】を押すと【Data source successfully connected】が表示されたら成功です。

緑のメッセージボックスが現れたら成功だと思ってください。
Data source successfully connected

Geomapパネルを作る(世界地図で可視化)

この辺りからようやくSOCらしくなってきます。

【左メニュー】→【Dashboard】→【Create dashboard】→【Panel】→【Configure visualization】→【All visualizations】→ Searchから【Geomap】を選択します。

空のダッシュボードの右側に+のマーク、ここがPanelです。
空のダッシュボードの右側に+のマーク、ここがPanelです。

まずは右メニューのVisualization typeを選びGeomapを選びます。

Geomapパネルでのクエリをつくる

Queryを書くときはCodeモードで書いた方が楽です。

Queries、右にTransformationsのタブ、右端にCode切替、カラフルな文字がクエリ
Queries、右にTransformationsのタブ、右端にCode切替、カラフルな文字がクエリ

Geomapパネルでは次のクエリを使います。

{job="fail2ban_geo"} | json

Transformationsの設定をする

そして【Transformations】→【Add transformatin】は【Extract fields】を選択します。

  • Source → Line
  • Format → JSON

この二つを設定します。

ヒートマップを作る

Geomapの項目をいくつか設定していきます。次の通りに設定をしてください。

使う【Visualization type】は【Geomap】です。

  • Map layers → Heatmap
  • Location → Coord
  • Latitude field → latitude
  • Longitude → longitude
  • Layer type → OpenStreetMap

Autoでも良さそうですがCoordでlatitudeとlongitudeを指定した方が良いです。

LongitudeとLatitudeに何も表示されない場合は、Map layersをMarkersに変えたり、一度セーブしてEditし直すと現れる場合があります。

国別ランキングパネルを作る

国別にグラフという形でどの国から攻撃されているか、その数を可視化します。

今度の【Visualization type】は【All visualizations】から【Bar gauge】を選びます。

QueryはまたCodeで入力する方がいいと思います。

sum by (country) (count_over_time({job="fail2ban_geo"} | json [7d]))

グラフのタイプを選んだら、またTransformationsを追加します。

【Transformations】→【Add transformatin】→【Labels to fields】を選択します。

  • Value Options → show → All values

また、標準でなっていると思いますが、パネルメニューの【Panel options】が【Horizontal】になっていることを確認してください。

攻撃者 Top 10パネルを作ります

【All Visualization types】は【Table】を選びます。

QueryはCodeで書く方がずっと便利です。

topk(10, sum by (ip, country) (count_over_time({job="fail2ban_geo"} | json [7d])))

そして【Transformations】→【Add transformation】→【Labels to fields】を選びます。

疑似SOCの完成

これで疑似SOCは完成といっていいのではないかと思います。

  • ヒートマップ
  • 国別ランキング
  • 攻撃者Top 10

望んでいたものを全て詰め込むことができたと思います。

最終チェック

最後に本当に全てが期待通りに動いているかstatusでチェックします。Raspberry Pi側でチェックをします。

sudo systemctl status fail2ban
sudo systemctl status loki
sudo systemctl status alloy
sudo systemctl status geoip-converter
sudo systemctl status grafana-server

全てがactive (running)であれば大丈夫です。

あとは、再起動したときに設定が崩れないかのチェックです。

sudo reboot

再起動をします。30秒くらい待ってから次のコマンドを入力してください。

sudo systemctl status fail2ban
sudo systemctl status loki
sudo systemctl status alloy
sudo systemctl status geoip-converter
sudo systemctl status grafana-server

すべてがactive (running)になっていることを確認してください。

一つずつ打ち込むのが面倒だと思う方はこのようにまとめることもできます。

sudo systemctl status fail2ban loki alloy geoip-converter grafana-server

これで本当に疑似SOCの完成です。本当にお疲れさまでした。また、Raspberry Pi 3B+という貧弱なハードウェアでもここまでがんばってくれることが証明できたと思います。

Grafanaで可視化された世界。攻撃しているのはその国の人というわけではない
Grafanaで可視化された世界。攻撃しているのはその国の人というわけではない

よくある質問(FAQ)

Q
Raspberry Pi 4やRaspberry Pi 5でも動作しますか?
A

動作します。基本的な手順は同じです。性能にずっと余裕があるため、快適に動作すると思います。

Q
USBメモリではなくSSDでも構築できますか?
A

構築できます。USB接続のSSDでも同じ構成で利用できます。長期間運用する場合はSSDの方が耐久性に優れているのではないかと考えます。

Q
GeoLite2-Countryではだめですか?
A

だめです。Countryではlatitudeとlongitudeが取れません。世界地図へ表示するためにはこの2つの値が必要不可欠です。そのためこの記事ではGeoLite2-Cityを使用しています。

Q
Promtailでも構築できますか?
A

構築自体は可能です。GrafanaがAlloy移行を推奨している今、余程の理由がない限り逆行する意味がないと思います。

Q
Raspberry Pi 3B+でも十分動作しますか?
A

はい、動作しています。今回の構成は、Raspberry Pi 3B+で実際に動かすために作られたものです。また動作確認も行っています。性能から色々と削ぎ落す必要がありましたが、ログ監視用としては十分実用的です。

まとめ

使っていなかったRaspberry Pi 3B+でしたが、ログ収集と可視化環境を整えることで、自宅サーバーへの攻撃状況をリアルタイムに分析できる疑似SOCを作ることができました。

この記事では、fail2ban、Grafana Alloy、Loki、GeoLite2-City、Grafana v13を組み合わせて、攻撃元を世界地図やランキングとして可視化する疑似SOCを完成させることができました。

重要なのは、完成したことや、ダッシュボードに表示されたことではありません。各STEPで動作確認を行いながら、慎重に作り上げることで、問題が発生した場合でも原因を切り分け、何がエラーを出しているのかがわかりやすい構成にしたことだと思います。

また、PromtailではなくGrafana Alloyを採用、Prometheusを不採用にしたことは、この記事で実際に悩んだ結果に基づく設計判断です。これらは2026年7月6日時点の環境に合わせた構成です。同様の環境を構築する方にとって再現性の高い情報になればいいと思っています。

リファレンス

Raspberry Pi Documentation:
https://www.raspberrypi.com/documentation/
Raspberry Pi OS Documentation:
https://www.raspberrypi.com/software/

Grafana Documentation:
https://grafana.com/docs/
Grafana Loki Documentation:
https://grafana.com/docs/loki/latest/
Grafana Alloy Documentation:
https://grafana.com/docs/alloy/latest/
LogQL Documentation:
https://grafana.com/docs/loki/latest/query/
Geomap Visualization:
https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/geomap/

Grafana Loki Releases:
https://github.com/grafana/loki/releases
Grafana Alloy:
https://github.com/grafana/alloy

GeoLite2 Databases:
https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
GeoLite2 Database Documentation:
https://dev.maxmind.com/geoip/docs/databases/

Python Documentation:
https://docs.python.org/3/
Python venv:
https://docs.python.org/3/library/venv.html

Debian Administrator’s Handbook:
https://www.debian.org/doc/
systemd Documentation:
https://systemd.io/

fail2ban Documentation:
https://www.fail2ban.org/wiki/index.php/Main_Page

fstab:
https://man7.org/linux/man-pages/man5/fstab.5.html
systemctl:
https://man7.org/linux/man-pages/man1/systemctl.1.html
journalctl:
https://man7.org/linux/man-pages/man1/journalctl.1.html

本記事の手順は2026年7月6日時点の Raspberry Pi OS Lite(Trixie)環境で動作確認を行っています。将来のバージョンアップにより設定項目やUIが変更される可能性があるため、その点はあらかじめご了承ください。

18日後

疑似SOCを起動してからおよそ18日後の2026年7月24日の状況です。

18日後のヒートマップ、構築当初よりも赤の範囲が大きい
18日後のヒートマップ、構築当初よりも赤の範囲が大きい

勘違いをしてはいけないことは、このヒートマップや国ランキングが表しているのは、その国の人が私のRaspberry Pi 3B+に攻撃をしているわけではないということです。

踏み台、VPN出口ノード、ボットネット(感染端末)の所在地として解釈すべきだと思います。

逆に怖いのは1桁から10台のところでしょうか。
これは偵察のアプローチの可能性があります。少しだけ接続を試みてポートの開き状態、認証画面が返ってくるかを確認し、大規模ブルートフォースをかけるかふるいにかけている可能性があります。

検知回避を狙ったローパス攻撃も気になります。意図的に時間をばらばらにすることで1分に10回失敗でIPブロックを回避している場合があります。

最後にたちが悪いのは、手動操作の可能性です。慣れている人だと試行錯誤の形跡がわかるようです。セキュリティ研究者やクラッカー個人が手元でツールやブラウザから直接アクセスしている可能性があります。

このように、デバイスの能力の都合上短期間しかログが保存できませんが、どの国が踏み台にされやすいか、VPNで使われやすいかがわかるのは面白いです。

更新履歴

  • 2026年8月21日
  • 2026年8月5日
  • 2026年7月6日
P
P

【この記事を書いた人】

黒瓜 勉(Lonely Lab運営):

PC-9821時代からのインターネット老人。英文学博士課程前期で培った英文一次ソースの解読を武器に、PC-98からWindows 3.1、そして現代のRaspberry Piまで、世代を超えた実機検証を行っている。

「今あるものを、もっと長く楽しむ」をテーマに、技術の流行り廃りに左右されない公式仕様に基づいた堅牢な環境構築を追求。実機検証による第一に安全性、第二に可能性、第三に再現性を担保した技術情報を発信中。

※ 本記事は実機環境で検証した内容をもとに執筆しています。動作環境やソフトウェアのバージョンが異なる場合は結果が変わる可能性があります。

詳しいプロフィール・運営方針はこちら
https://lonely1.jp/helloworld/

コメント