自宅サーバーを公開しようとすると、SSHへの総当たり攻撃(ブルートフォース攻撃)は想像以上の頻度で発生し、やはり公開はやめておこうという気持ちになります。
「攻撃を受けている」という記録は残りますが、どこの国から、どのIPアドレスが、どの時間帯に攻撃してきたのかまで可視化できたらおもしろいのではないかと思いました。
そこで本記事では、今使っていないRaspberry Pi 3B+ を活用し、ログ収集・可視化基盤を構築します。
構成には fail2ban、Loki、Grafana Alloy、Grafana v13、GeoLite2-City を採用し、SSHへの攻撃ログを世界地図やランキングとしてリアルタイムに確認できる疑似SOC(Security Operations Center)環境を構築します。
企業向けSOCのような大規模分析はできませんが、監視やログ分析を学ぶ教材としては十分実用的な構成です。
本記事では、2026年7月6日点の Raspberry Pi OS Lite(Trixie) で実際に構築と動作確認を行った手順を、確認方法を含めて一工程ずつ解説します。
Raspberry Pi 3B+という非力なマシンでどこまでできるか興味があったのも事実です。
結論
まずは完成しましたという報告をします。今回は次のような処理を疑似SOCで行い、SSHへの攻撃を監視しています。
- SSH攻撃
- fail2banが検知、遮断
- Grafana Alloyがログ収集
- Lokiへ保存
- GeoLite2-Cityで位置情報を張り付ける
- Grafana v13で可視化
最終的には次の情報をリアルタイムでブラウザ上で確認できるようになります。
- 世界地図上への攻撃の表示
- 国別の攻撃回数ランキング
- 攻撃元のIPアドレス
- 時間帯ごとの攻撃の傾向
- fail2banによる遮断
各サービスは独立しており、それぞれ動作確認ができるため、問題が起きても原因を切り分けやすいように設計しています。

システム構成
今回構築システムは次の構成です。
| ソフトウェア | 役割 |
|---|---|
| fail2ban | SSH攻撃を検知・遮断 |
| Grafana Alloy | ログ収集 |
| Loki | ログ保存 |
| GeoLite2-City | IPアドレスを位置情報へ変換 |
| Grafana | ログを可視化 |
使用環境
本記事では、次の環境で構築および動作確認を行いました。
| 項目 | 内容 |
|---|---|
| ハードウェア | Raspberry Pi 3B+(2018) |
| OS | Raspberry Pi OS Lite 64bit(Trixie) |
| OS保存先 | microSDカード |
| ログ保存先 | USBメモリ(フォーマット形式ext4) |
| Grafana | v13(2026年7月6日時点) |
| Grafana Loki | 2026年7月6日最新版 |
| Grafana Alloy | 2026年7月6日最新版 |
| GeoLite2 | GeoLite2-City |
| Python | Python 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 | ファイルの取得 |
| gnupg2 | Grafana公式リポジトリ登録 |
| jq | GitHub APIのJSON解析 |
| Python3 | GeoLite2変換スクリプト実行 |
| git | ソース取得・管理 |
| ufw | ファイアウォール |
| fail2ban | SSH攻撃の検知・遮断 |
| 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

ここにログインをして、GeoLite2のライセンスキー(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
パスワードは初回ログイン後に変更するよう言われるため、忘れないパスワードを入れましょう。

Lokiにデータソースを追加する
ここからがややこしくなります。それはGrafanaがバージョンアップするたびに大きくUIが変更されるからです。メニューや名称すら変わってしまうので気を付けたいところです。
Grafanaの左にあるメニューから色々と選んでいきます。
【左メニュー】→【Connections】→【Data sources】→【Add data source】→ Search【Loki】という流れでLokiをインストールします。

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

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

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

Geomapパネルを作る(世界地図で可視化)
この辺りからようやくSOCらしくなってきます。
【左メニュー】→【Dashboard】→【Create dashboard】→【Panel】→【Configure visualization】→【All visualizations】→ Searchから【Geomap】を選択します。

まずは右メニューのVisualization typeを選びGeomapを選びます。
Geomapパネルでのクエリをつくる
Queryを書くときは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+という貧弱なハードウェアでもここまでがんばってくれることが証明できたと思います。

よくある質問(FAQ)
- QRaspberry Pi 4やRaspberry Pi 5でも動作しますか?
- A
動作します。基本的な手順は同じです。性能にずっと余裕があるため、快適に動作すると思います。
- QUSBメモリではなくSSDでも構築できますか?
- A
構築できます。USB接続のSSDでも同じ構成で利用できます。長期間運用する場合はSSDの方が耐久性に優れているのではないかと考えます。
- QGeoLite2-Countryではだめですか?
- A
だめです。Countryではlatitudeとlongitudeが取れません。世界地図へ表示するためにはこの2つの値が必要不可欠です。そのためこの記事ではGeoLite2-Cityを使用しています。
- QPromtailでも構築できますか?
- A
構築自体は可能です。GrafanaがAlloy移行を推奨している今、余程の理由がない限り逆行する意味がないと思います。
- QRaspberry 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が変更される可能性があるため、その点はあらかじめご了承ください。

コメント