我が家にやってきて一週間ほどの猫たち。
驚くほど馴染んできています。

まだサークルの中にいる状態ですが、誰がいるかという顔認識は完了。

凄まじい食欲を見せ(日に三食)、元気に遊び回るようになりました。

現行の猫はまだまだおっかなびっくりで見ていますが、威嚇すると言うことはないようで

そして、先だってはケージも導入。より立体的な動きも見せるようになっていて

よく食べ、よく遊び、よく寝ているという健やかな日々が続いています。
我が家にやってきて一週間ほどの猫たち。
驚くほど馴染んできています。

まだサークルの中にいる状態ですが、誰がいるかという顔認識は完了。

凄まじい食欲を見せ(日に三食)、元気に遊び回るようになりました。

現行の猫はまだまだおっかなびっくりで見ていますが、威嚇すると言うことはないようで

そして、先だってはケージも導入。より立体的な動きも見せるようになっていて

よく食べ、よく遊び、よく寝ているという健やかな日々が続いています。
こちらのipsetによる防御。これを運用して分かったのが
の2点。そこで、これを回避するためのスクリプトを改良しました。
#!/bin/bash
# セット名の定義
SET_NAME="ufw-blocklist"
SAVE_FILE="/etc/ufw/ipsets.save"
# 1. root権限チェック
if [[ $EUID -ne 0 ]]; then
echo "エラー: このスクリプトは sudo または root 権限で実行してください。"
exit 1
fi
# 保存用関数
save_ipset() {
read -p "設定をファイルに保存しますか? (y/n): " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
ipset save "$SET_NAME" -f "$SAVE_FILE"
echo "保存完了: $SAVE_FILE"
else
echo "保存をスキップしました(メモリ上の設定のみ更新)。"
fi
}
# ipsetが存在しない場合に作成
if ! ipset list "$SET_NAME" > /dev/null 2>&1; then
echo "情報: $SET_NAME が見つからないため新規作成します。"
if ipset create "$SET_NAME" hash:net; then
echo "作成成功"
else
echo "エラー: ipsetの作成に失敗しました。"
exit 1
fi
fi
# フェイルセーフ関数:接続中の自分のIP(SSHポータ22)を遮断しようとしていないかチェック
is_ssh_connected_ip() {
local target_ip=$1
# 現在22番ポートでサスペンド/エスタブリッシュしている接続元のIPをすべて取得
# (awkで不要なヘッダーやコロン以降のポート番号を削る)
local my_ssh_ips=$(ss -nt state established sport = :22 | awk 'NR>1 {print $5}' | sed 's/:[^:]*$//' | sort -u)
# テスト対象のターゲットからサブネット部分(/24など)を除外してIPのみにする
local clean_target=$(echo "$target_ip" | cut -d'/' -f1)
for ssh_ip in $my_ssh_ips; do
# 1. 完全一致チェック
if [[ "$ssh_ip" == "$clean_target" ]]; then
return 0 # ロックアウトの危険あり
fi
# 2. サブネットマスクごとの巻き込みチェック
if [[ "$target_ip" == */24 ]]; then
local target_net=$(echo "$clean_target" | cut -d'.' -f1-3)
local ssh_net=$(echo "$ssh_ip" | cut -d'.' -f1-3)
if [[ "$target_net" == "$ssh_net" ]]; then
return 0
fi
elif [[ "$target_ip" == */16 ]]; then
local target_net=$(echo "$clean_target" | cut -d'.' -f1-2)
local ssh_net=$(echo "$ssh_ip" | cut -d'.' -f1-2)
if [[ "$target_net" == "$ssh_net" ]]; then
return 0
fi
fi
done
return 1 # 安全
}
# IP/NWの形式を整形する関数
format_target() {
local ip=$1
echo "=== サブネット選択 ==="
echo "1) そのまま登録 (${ip})"
echo "2) /24 で登録 (${ip%.*}.0/24)"
echo "3) /16 で登録 (${ip%.*.*}.0.0/16)"
read -p "選択肢を選んでください [1-3]: " subnet_choice
case $subnet_choice in
2) echo "${ip%.*}.0/24" ;;
3) echo "${ip%.*.*}.0.0/16" ;;
*) echo "$ip" ;;
esac
}
# メインループ
while true; do
echo "------------------------------------------"
echo " ipset 管理メニュー ($SET_NAME)"
echo "------------------------------------------"
echo "1) IP/ネットワークを追加"
echo "2) IP/ネットワークを削除"
echo "3) ステータス表示 (list)"
echo "q) 終了 (Exit)"
echo "------------------------------------------"
read -p "番号を選択してください: " choice
case $choice in
1)
echo "--- 追加モード ---"
local added_any=false
while true; do
read -p "追加する IPアドレス: " raw_ip
if [ -n "$raw_ip" ]; then
# サブネットの選択
target=$(format_target "$raw_ip")
# フェイルセーフ発動チェック
if is_ssh_connected_ip "$target"; then
echo "【警告】現在あなたが接続しているSSH元IPが含まれているため、登録をブロックしました! ($target)"
else
if ipset add "$SET_NAME" "$target" 2>/dev/null; then
echo "追加成功: $target"
added_any=true
else
echo "エラー: 追加に失敗したか、既に登録されています。 ($target)"
fi
fi
fi
# 続けて入力するか確認
read -p "続けて他のIPを入力しますか? (y/n): " continue_add
if [[ ! "$continue_add" =~ ^[Yy]$ ]]; then
break
fi
echo "------------------------------------------"
done
# 追加が1件でもあれば一括で保存確認
if [ "$added_any" = true ]; then
save_ipset
fi
;;
2)
echo "--- 削除(解除)モード ---"
read -p "削除する IPアドレス: " raw_ip
if [ -n "$raw_ip" ]; then
target=$(format_target "$raw_ip")
# 削除時も念のためセーフティ(既存の接続を誤って切らないようにするため)
if is_ssh_connected_ip "$target"; then
echo "【警告】現在接続中のSSHセッションが含まれています。削除を中止します。 ($target)"
else
if ipset del "$SET_NAME" "$target" 2>/dev/null; then
echo "削除成功: $target"
save_ipset
else
echo "エラー: 削除に失敗したか、リストに存在しません。 ($target)"
fi
fi
fi
;;
3)
echo "--- 現在のリスト ---"
ipset list "$SET_NAME"
;;
q)
echo "終了します。"
exit 0
;;
*)
echo "無効な選択です。"
;;
esac
echo ""
done
まずはスクリプトをサーバーに保存し、実行できるように権限を付与します。
blocklist-mgr.sh)。sudo chmod +x blocklist-mgr.sh を実行して、スクリプトに実行権限を与えます。sudo ./blocklist-mgr.sh で起動します。起動すると、以下のような対話型メニューが表示されます。
1) IP/ネットワークを追加
2) IP/ネットワークを削除
3) ステータス表示 (list)
番号を選択してください:
悪意のあるアクセス元IPを見つけたら、「1」を入力して追加モードに入ります。
192.168.1.50)を入力します。/24:周辺のIP 256台をまとめてブロック)/16:大規模なネットワークごとブロック)y で続けて入力できます。追加が終わると「設定をファイルに保存しますか? (y/n)」と聞かれます。ここで y を選ぶと、OS再起動後もブロックが有効になります。
もし間違えて「自分が今SSH接続しているIP(またはその所属サブネット)」を入力してしまった場合、スクリプトが自動でそれを検知し、
【警告】登録をブロックしました! と表示して処理を中断します。これにより、リモートワーク中にサーバーから締め出される悲劇を防ぎます。
誤ってブロックしてしまったIPや、制限を解除したいIPがある場合は「2」を選択します。
追加時と同様にIPを入力し、サブネット範囲を選択すれば、リストから安全に削除されます。ここでも自分が接続中のIPを誤って削除(変更)しないようセーフティが働きます。
「3」を選択すると、現在 ipset に登録されてリアルタイムでブロックされているIPの一覧がズラリと表示されます。正しく反映されているか確認したいときに便利です。
「q」を入力すると、安全にメニューを閉じます。
自分のサーバへのアクセスログ。いつもの挨拶の他、「これはさすがに」と思うものがあったのでご紹介です。
例によってIPアドレスやホスト名を無害化(ダミーデータへの置き換え)を行っています。テロリストに名前を与える気はないからです。
[Fri May 22 00:01:58.357561 2026] [security2:error] [pid 563988:tid 133631884367552] [client 192.0.2.10:13346] [client 192.0.2.10] ModSecurity: Multipart parsing error: Multipart: No boundaries found in payload. [hostname "example.com"] [uri "/"] [unique_id "ag8eZi5y5sdCQ_cRAbbcQAAAAFc"]
[Fri May 22 00:01:58.357663 2026] [security2:error] [pid 563989:tid 133632756782784] [client 192.0.2.10:13348] [client 192.0.2.10] ModSecurity: Multipart parsing error: Multipart: No boundaries found in payload. [hostname "example.com"] [uri "/projects/"] [unique_id "ag8eZuLCu5fVcQiqqP37FAAAAIo"]
[Fri May 22 00:02:00.980490 2026] [security2:error] [pid 563989:tid 133632656135872] [client 192.0.2.10:53854] [client 192.0.2.10] ModSecurity: Multipart parsing error: Multipart: No boundaries found in payload. [hostname "example.com"] [uri "/"] [unique_id "ag8eaOLCu5fVcQiqqP37HQAAAIw"]
[Fri May 22 00:22:32.285606 2026] [security2:error] [pid 563989:tid 133632790353600] [client 192.0.2.20:48723] [client 192.0.2.20] ModSecurity: Access denied with code 404 (phase 1). Pattern match "^[\\\\d.]+(:\\\\d+)?$" at REQUEST_HEADERS:Host. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf"] [line "53"] [id "10004"] [msg "[CUSTOM RULE] Host header is a numeric IP address (incl port). Blocked immediately."] [tag "application-attack"] [tag "PROTOCOL_VIOLATION/INVALID_HREQ"] [hostname "203.0.113.50"] [uri "/"] [unique_id "ag8jOOLCu5fVcQiqqP3_ewAAAIY"]
[Fri May 22 00:22:38.213267 2026] [security2:error] [pid 563989:tid 133632647743168] [client 192.0.2.20:56013] [client 192.0.2.20] ModSecurity: Access denied with code 404 (phase 1). Operator EQ matched 0 at REQUEST_HEADERS. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf"] [line "62"] [id "10005"] [msg "[CUSTOM RULE] Missing Host Header. Blocked immediately."] [hostname "example.com"] [uri "/"] [unique_id "ag8jPuLCu5fVcQiqqP3_gwAAAI0"]
[Fri May 22 00:36:55.821987 2026] [security2:error] [pid 563989:tid 133632370915008] [client 192.0.2.30:38808] [client 192.0.2.30] ModSecurity: Access denied with code 404 (phase 1). Pattern match "^[\\\\d.]+(:\\\\d+)?$" at REQUEST_HEADERS:Host. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf"] [line "53"] [id "10004"] [msg "[CUSTOM RULE] Host header is a numeric IP address (incl port). Blocked immediately."] [tag "application-attack"] [tag "PROTOCOL_VIOLATION/INVALID_HREQ"] [hostname "203.0.113.50"] [uri "/.env"] [unique_id "ag8ml-LCu5fVcQiqqP0CpgAAAJU"]
[Fri May 22 02:56:01.000854 2026] [security2:error] [pid 563988:tid 133632387700416] [client 192.0.2.40:46164] [client 192.0.2.40] ModSecurity: Access denied with code 404 (phase 1). Operator EQ matched 0 at REQUEST_HEADERS. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf"] [line "62"] [id "10005"] [msg "[CUSTOM RULE] Missing Host Header. Blocked immediately."] [hostname "example.com"] [uri "/"] [unique_id "ag9HMS5y5sdCQ_cRAbbtWQAAAEw"]
[Fri May 22 03:18:21.209460 2026] [security2:error] [pid 563989:tid 133632396093120] [client 192.0.2.50:53678] [client 192.0.2.50] ModSecurity: Warning. Matched phrase "Mozlila" at REQUEST_HEADERS:User-Agent. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-913-SCANNER-DETECTION.conf"] [line "56"] [id "913100"] [msg "Found User-Agent associated with security scanner"] [data "Matched Data: Mozlila found within REQUEST_HEADERS:User-Agent: Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"] [severity "CRITICAL"] [hostname "example.com"] [uri "/"] [unique_id "ag9MbeLCu5fVcQiqqP0kggAAAJI"]
[Fri May 22 03:18:51.242531 2026] [security2:error] [pid 563989:tid 133632396093120] [client 192.0.2.50:58448] [client 192.0.2.50] ModSecurity: Warning. Matched phrase ".env" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "145"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .env found within REQUEST_FILENAME: /.env.live"] [severity "CRITICAL"] [hostname "example.com"] [uri "/.env.live"] [unique_id "ag9Mi-LCu5fVcQiqqP0kmwAAAJI"]
[Fri May 22 03:18:51.349415 2026] [security2:error] [pid 563989:tid 133632379307712] [client 192.0.2.50:58432] [client 192.0.2.50] ModSecurity: Warning. String match within "..." at TX:extension. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1105"] [id "920440"] [msg "URL file extension is restricted by policy"] [data ".old"] [severity "CRITICAL"] [hostname "example.com"] [uri "/.env.old"] [unique_id "ag9Mi-LCu5fVcQiqqP0knQAAAJQ"]
[Fri May 22 03:19:17.202887 2026] [security2:error] [pid 563988:tid 133632807139008] [client 192.0.2.50:48772] [client 192.0.2.50] ModSecurity: Warning. Matched phrase ".gitlab-ci.yml" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "145"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .gitlab-ci.yml found within REQUEST_FILENAME: /.gitlab-ci.yml"] [severity "CRITICAL"] [hostname "example.com"] [uri "/.gitlab-ci.yml"] [unique_id "ag9MpS5y5sdCQ_cRAbbvZQAAAEM"]
ここから気になった攻撃ログを見てみましょう。
これは攻撃者がファイルアップロード(Multipart形式)を模したリクエストを送っているものの、データ構造が壊れている(Boundaryがない)ためにWAF(ModSecurity)が激怒している状態です。
手動の攻撃ではなく、雑に作られた自動スキャンツール(ボット)が、手当たり次第に脆弱性を探す「偵察フェーズ」としてパケットを送りつけてきたと考えられます。
Webアプリケーションの設定ファイルである .env(環境変数ファイル)を狙っています。
ここには「データベースのパスワード」「AWSやstripeなどのAPI秘密鍵」といった、一発でシステムが崩壊するレベルの情報が書かれています。
ご丁寧に .env だけでなく .env.old や .env.test などの「バックアップっぽいファイル名」まで推測して探している点です。
開発者がうっかり残した過去の遺物を探す、攻撃者の「ずる賢さ」がよく出ています。
CI/CD(自動ビルド・デプロイ)の設定ファイルです。ここを覗かれると、システムの内部構造や、ソースコードが保管されているリポジトリの場所、デプロイ用の認証情報などが漏洩します。開発環境のセキュリティ設定が甘いサイトを見つけ出そうとしています。
攻撃者は、普通のブラウザ(Mozilla)からのアクセスに見せかけようとUser-Agentを偽装しています。しかし、よく見ると Mozlila(ロとリが逆:モズリラ) になっています。
これはチープなハッキングツールの作成者がスペルミスをしたまま配布しているケースで、ModSecurityのシグネチャ(検出ルール)に「チープなスキャナ特有の文字列」として一発で見破られてブロックされています。
というよりも、この「モズリラ」を紹介したくてこの記事を書いたまであります。
もちろん、これらのログは抜粋であり、その数十倍。サイトによっては数百・数千倍のログがあることは日常茶飯事。それでも
と、「ログに残る攻撃があるうちは」対策するのは嗜みだと思いました。真に巧妙な攻撃者はログをいかに残さないかに全力を挙げるわけですし。
Nextdloudを更に統合プラットフォームとして使うため、以下の手順が必要でした。
iOSリマインダー等Nextdloud側で適当なタスクを作成して、iOS側で表示されることを確認します。
iOSの「リマインダー」に、Nextdloudで設定したタスクが表示されることを確認します。
iOS側で適当なタスクを作成して、Nextdloud側で表示されることを確認します。
我が家に猫たちがやって来ました。

現行の猫と同じくスコティッシュフォールド。しかも
両方ともサバトラの折れ耳です。
我が家に来て思ったのが、昨年の3月末、虹の橋を渡っていた茶トラの子の子猫時代と面影が重なっているということ。

俗に「着替えて帰ってくる」と言われていますが、まさにそんな言葉を実感しています。
RHEL系Linuxに備わっているfirewalld。非常に柔軟で(比較的)直感的に使える仕組みだったのでメモを残します。
特に驚きだったのがzoneの概念。
ufwが基本的に「システム全体に対してポートを開けるか閉じるか」をシンプルに管理するのに対し、firewalldは「接続するネットワークの信頼度に応じて、ファイアウォールのルールを瞬時に切り替える」という柔軟性がありました。
一言で言うと、ゾーンとは「接続元のネットワークやインターフェース(LANカードなど)の『信頼度』に応じたグループ分け」のことです。
従来の iptables などでは、「このIPアドレスからのこのポートへの通信を許可する」といった細かいルールを1つずつ書く必要があります。
一方、firewalld では以下のようなステップで考えます。
eth0)や、特定のIPアドレスをその箱に割り当てます。これにより、「カフェのWi-Fiに繋いだ時は『パブリック』ゾーンに切り替える」「会社のLANに繋いだ時は『社内』ゾーンに切り替える」といった管理が、一瞬でできるようになります。
firewalld には、最初からいくつかのゾーンが用意されています。
| ゾーン名 | 信頼度 | 主な用途・特徴 |
|---|---|---|
drop | 最低 | すべての受信パッケージを破棄します(応答すら返さない)。こちらからの送信は可能です。 |
block | 低 | すべての受信を拒否します。drop と違い、相手に「拒否しました」という通知(ICMP)を返します。 |
public | 低〜中 | デフォルトのゾーン。 不特定多数がいる公共のネットワーク用。自分が許可した通信(sshなど)だけを通します。 |
external | 中 | ルーターとして使う場合の「外側(インターネット側)」用。マスカレード(NAT)が有効になります。 |
**home / internal** | 高 | 自宅や社内LANなど、周囲のコンピューターを信頼できる場合用。お互いの通信が少し緩く許可されています。 |
trusted | 最高 | すべてを許可します。完全に安全だと分かっているネットワーク専用です。 |
ネットワークカード(eth0 や wlan0 など)は、必ずいずれか1つのゾーンに紐付けられます。何も設定していない場合、自動的に public(デフォルトゾーン)に属します。
「インターフェース全体は public だけど、上司のPCのIPアドレス(192.168.1.50)だけは trusted ゾーンとして扱う」といった柔軟な設定が可能です。
「Webサーバー(80番ポート)を開放したい」となったら、「public ゾーンに対して80番ポートを許可する」というように、ゾーンに対して設定を紐付けます。
『ジョジョの奇妙な冒険』第5部における『マン・イン・ザ・ミラー』です。
ここには『スタンド力』は おれの許可なくしては入る事はできない
『おまえ本体』だけ入る事を許可した
ここにある物は全て命のない『物質』だけだ―――おまえとオレだけ!他に『生きてる物』はいない………
という「許可」と「許可しない」を、鏡の世界ではなく「ゾーン」ごとに決められる能力、と言っていいでしょう。
ゾーンの状態を確認・操作するための、代表的な firewall-cmd コマンドです。
firewall-cmd --get-default-zone
firewall-cmd --list-all-zones
firewall-cmd --zone=home --change-interface=eth0
firewall-cmd --zone=public --add-service=http --permanent
`
--permanent をつけた後は、設定を反映させるために以下が必要ですfirewall-cmd --reload
Linuxサーバーを構築する際は、まず「このサーバーはどこに置かれていて、どのゾーンを適用すべきか」を考えていきましょう。
そのケーススタディを行っていきます。
各部署が持っているサーバを管理するためシステム部が一括でzabbixのエージェントを入れたいという状況。
まずは 192.168.1.0/24 だけが所属する専用のゾーン illuso を作ります。
sudo firewall-cmd --permanent --new-zone=illuso
sudo firewall-cmd --reload
作成したゾーンに「部内NWのIPセグメント」を紐付ける
sudo firewall-cmd --permanent --zone=illuso --add-source=192.168.1.0/24
Step 1 で作ったゾーンに、ssh, http, https の鍵(許可)を配置します。
sudo firewall-cmd --permanent --zone=illuso --add-service=ssh
sudo firewall-cmd --permanent --zone=illuso --add-service=http
sudo firewall-cmd --permanent --zone=illuso --add-service=https
社内のZabbixサーバー(192.168.12.6)だけが所属する man-in-the-mirror を作り、Zabbix Agent用の 10050 ポートを許可します。
sudo firewall-cmd --permanent --new-zone=man-in-the-mirror
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --zone=man-in-the-mirror --add-source=192.168.12.6
sudo firewall-cmd --permanent --zone=man-in-the-mirror --add-port=10050/tcp
public の掃除誰からでも受け付ける Zabbix Server(10051)や SMTP(25)を public に設定し、同時に、先ほど部内限定へお引越しさせた不要なサービス(ssh, http, https)を public から削除します。
sudo firewall-cmd --permanent --zone=public --add-port=10051/tcp
sudo firewall-cmd --permanent --zone=public --add-service=smtp
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --permanent --zone=public --remove-service=http
sudo firewall-cmd --permanent --zone=public --remove-service=https
ここまでの --permanent(次回起動用の設定)を、一気に本番環境へ反映(リロード)させます。
sudo firewall-cmd --reload
反映が完了したら、正しく設定できているか各ゾーンを覗いてみましょう。
sudo firewall-cmd --zone=illuso --list-all
sudo firewall-cmd --zone=man-in-the-mirror --list-all
sudo firewall-cmd --zone=public --list-all
以上、手順は多いものの
を極めて柔軟に行えるのはRHEL系Linuxの持つ特権だと思いました。とはいえ、
この条件は「これから入ろうとする邪魔者」を拒否するには有効ですが、入ってしまったものを取り除くのは極めて厄介です。とくにウィルス(マルウェア)の除去は
『マン・イン・ザ・ミラー』オレだけが外に出る事を許可しろォォォォーーーーッ
うおおおががががが だが! ウイルスは許可しないィィィィィーーーッ
感染した部分は出る事は 許可しないィィィィィィィーーーッ!!
とはならないので注意が必要です。
(最後のこれが言いたいだけのエントリーを書き終えました)
昨日設定したfailbanとの連携。ufw.aggressive。
結論から言うと「あまりにも閾値が低すぎて自分自身がロックアウトを喰らう」結果になりました。
自分のIPからのアクセスが全てのサービスにつながらなくなった。
これはSSH接続はのみならずWeb閲覧でも弾かれるを意味します。
取り急ぎ、「確実にignoreipされている」場所からアクセスし、
sudo fail2ban-client status ufw
をしたところ、ものの見事にアクセスしていたIPアドレスが含まれています。
そのため、
sudo fail2ban-client set ufw unbaip IPアドレス
として条件を解除。
そんな中で見つけた挙動。Redminetneは比較的単純な通信が発生するため、
[ufw]
enabled = true
filter = ufw-aggressive
action = iptables-allports
logpath = /var/log/ufw.log
maxretry = 1
bantime = -1
# ignoreipには、自分自身のローカルホストと、巻き添えを防ぎたい大手検索エンジン(Googlebotなど)のIP帯を指定します
では、大量のretryが発生し、fail2banはご丁寧に「こいつは大量アクセスを繰り返している」としてban。更に、当然ながら、iptables-allportsが含まれているため、自分自身が締め出しを食らったという次第。
「閾値の緩和」に尽きます。「人間の通常の作業は問題ないがbotが引っかかる」を目安に修正しました。この時の注意点は 対象サイトに過度にアクセスしないことに尽きます。iptablesと手を組んでいる以上、無効の怒りを買わないよう、接続はSSHのみにとどめます。
修正ファイル /etc/fail2ban/jail.local
[ufw]
enabled = true
filter = ufw-aggressive
action = iptables-allports
logpath = /var/log/ufw.log
# ↓ 1 から 30 〜 50 程度に大幅緩和(ボットは防げて人間は誤検知しない絶妙なライン)
maxretry = 50
# ↓ 10分〜15分(600〜900秒)の間に規定回数叩いたらアウトにする
findtime = 900
bantime = -1
# ignoreipは、自分が今アクセスしているIPを真っ先に入れます
ignoreip = 127.0.0.0/8 ::1
設定後、
sudo fail2ban-client reload
を実行して設定完了。
を操作して通常通りの作業が行えることを確認。
今回、慌てずに作業できていたのが「どこからアクセスすれば安全か」を知っていたこと。
「頑固で融通が利かない門番」
を意図通りに動かすためには、門番が動く条件をきちっと動かす必要があるというお話でした。
インターネット上にWebサーバーを公開すると、ものの数分で世界中から自動スキャンやブルートフォースアタック(総当たり攻撃)の嵐に見舞われます。
アクセス元のIPアドレスが固定されていれば接続元を絞れますが、「自宅や出先からリモートアクセスして作業する」場合、ファイアウォールの門を広く開けざるを得ません。
この記事では、そんな環境でもサーバーを鉄壁に守るため、UFW(ファイアウォール)とFail2ban(ログ監視型自動遮断ツール)を組み合わせ、不審者を検知した瞬間に「すべてのポートから永久追放(永久BAN)」する強力なネットワーク保護の設定手順を解説します。
まずは不要なポートをすべて閉じ、必要な通信だけを通す基本的な防壁を作ります。
単なる許可(allow)ではなく、短時間の連続アクセスを制限する limit を使うことで、ブルートフォースアタックの速度を鈍らせます。
sudo ufw limit proto tcp from any to any port 22
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Ubuntuのデフォルト(low)では、UFWが自動で弾いた不審なアクセスのログが残りません。これではFail2banが検知できないため、ログレベルを medium に引き上げます。
sudo ufw logging medium
※ この作業は何度やっても心臓に悪い作業です。
を確実に確認してから作業を行います。
sudo ufw enable
注意:
Command may disrupt existing ssh connections. Proceed with operation (y|n)?と聞かれたら、落ち着いてyを入力して続けます。
sudo ufw status verbose
以下のように、状態が「アクティブ(ロギング: on (medium))」になり、ルールが適用されていることを確認します。
状態: アクティブ
ロギング: on (medium)
Default: deny (incoming), allow (outgoing), deny (routed)
To Action From
-- ------ ----
22/tcp LIMIT Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
22/tcp (v6) LIMIT Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
sudo reboot で再起動し、再起動後も接続できることを確認します。ログを監視して自動でブロックする必須ツールです。
sudo aptitude update && sudo aptitude install fail2ban
筆者の好みでaptitudeを用いています。
systemctl status fail2ban.service
active (running) と表示されていればOKです。
UFWの拒否ログ([UFW BLOCK])をFail2banに認識させるための判定ルールを作成します。
※Ubuntu 24.04環境の仕様に合わせ、フィルターファイル名は ufw-aggressive.confとします。
sudo tee /etc/fail2ban/filter.d/ufw-aggressive.conf > /dev/null << 'EOF'
[Definition]
failregex = \[UFW BLOCK\].+SRC=<HOST> DST
ignoreregex =
EOF
Fail2banの挙動を定義するローカル設定ファイルを作成します。
以下のファイルを教義・信仰に沿ったエディタで編集します。
/etc/fail2ban/jail.local
[ufw]
enabled = true
filter = ufw-aggressive
action = iptables-allports
logpath = /var/log/ufw.log
maxretry = 1
bantime = -1
# ignoreipには、自分自身のローカルホストと、巻き添えを防ぎたい大手検索エンジン(Googlebotなど)のIP帯を指定します
ignoreip = 127.0.0.0/8 ::1 66.249.64.0/19
[sshd]
enabled = true
filter = sshd
mode = normal
port = 22
protocol = tcp
logpath = /var/log/auth.log
maxretry = 3
bantime = -1
ignoreip = 127.0.0.0/8 ::1
# ignoreipには、自分自身のローカルホストと、ある程度回線が固定されているIP帯を指定します。
bantime = -1:
maxretry = 1(ufwセクション):
action = iptables-allports:
ignoreip = ... 66.249.64.0/19:
maxretry = 1 は強力すぎるため、Webサイトを巡回するGoogleのクローラー(Googlebot)を誤って永久追放しないよう、あらかじめホワイトリストで保護しています。設定が完了したら、Fail2banに読み込ませます。
sudo fail2ban-client reload
設定してわずか数分〜数十分。ログファイル(/var/log/fail2ban.log)を覗いてみると、その効果は一目瞭然です。
2026-05-17 07:14:26,476 fail2ban.filter [1720]: INFO [ufw] Found xxx.xxx.xxx.xxx
2026-05-17 07:14:26,623 fail2ban.actions [1720]: NOTICE [ufw] Ban xxx.xxx.xxx.xxx
2026-05-17 07:14:44,198 fail2ban.filter [1720]: INFO [ufw] Found yyy.yyy.yyy.yyy
2026-05-17 07:14:44,647 fail2ban.actions [1720]: NOTICE [ufw] Ban yyy.yyy.yyy.yyy
世界中から飛んでくる不審なアタックやスキャンを、Fail2banが次々と検知し、その場で息の根を止めて(Ban)くれているのが分かります。
現在の捕獲状況は、以下のコマンドでリアルタイムに確認できます。
sudo fail2ban-client status ufw
インターネットの荒波に晒されているサーバーであれば、10分も経たないうちに数十〜100以上の悪質なIPアドレスが Banned IP list: に積み上がっていきます。
万が一、設定ミスなどで必要なIPをBANしてしまった場合は、以下のコマンドで個別に救出(BAN解除)が可能です。
sudo fail2ban-client set ufw unbanip <解除したいIPアドレス>
固定IPを持たない環境であっても、「UFWで怪しい動きを検知し、Fail2banですべてのポートを即座に塞ぐ」という二段構えを構築することで、驚くほど強固なサーバーへと進化させることができます。
筆者はかれこれ4年ほどvps運用を続けている中、重篤な攻撃に晒されていないのは基本であるこのufwとfail2banのおかげ。
休息も慈悲も与えぬ。何があってもだ。
No rest, no mercy. No matter what.
のレベルで攻撃者にいかなる躊躇も容赦もしないのがVPSサーバの基本です。
RHEL9系ディストリビューション(Rocky Linux 9.7)にMySQLを導入したときのメモです。
一言で言えば、「特定のルールに従って、整理整頓されたデータの集まり」です。
言うなれば「超高性能な図書館」のようなものです。
閲覧者、借りている人の帳簿を司り、膨大な本から一瞬で目的の1ページを探し出し、同時に何百人もの人が本を借りようとしても混乱が起きないように管理されています。
MySQLは正確には「リレーショナルデータベース管理システム(RDBMS)」と呼ばれます。
MySQLが担っている主な役割は、大きく分けて以下の4つです。
なぜExcelファイルやテキストファイルで管理するのではダメなのでしょうか?
テキストファイルだと、100万件のデータから1件を探すのに上から順に読み込む必要があり、時間がかかりすぎます。DBは「インデックス(索引)」という仕組みを持ち、瞬時にデータを見つけ出せます。
銀行振込を想像してみましょう。
もし「1」の直後にシステムがダウンしたら、1万円が消えてしまいます。
こうならないよう、DBには「トランザクション」という仕組みがあり、「すべて成功するか、すべてなかったことにするか」のどちらかしか認めません。これが社会インフラを支える信頼性の正体です。
Linuxサーバー上で動く
など、バラバラな入り口から入ってくる要求を、一つの窓口(MySQL)が交通整理して処理してくれます。
MySQLは、システムにおける「記憶の番人」です。
LinuxにDBを入れるということは、そのサーバーに「確かな記憶力」と「厳格な管理能力」を持たせるということに他なりません。
ここまで踏まえ、LinuxにDBを入れていきましょう。
MySQLサーバーのパッケージを導入し、MySQLサービスが利用するディレクトリの権限をあらかじめ適正化。
sudo dnf install -y mysql-server
MySQLサービスを有効化し、初期状態でのログインを確認。
sudo systemctl enable --now mysqld
mysql -u root
ログイン直後にMySQLのroot顕現のパスワードを設定します。
※先ほどのDBの話に戻ります。DBは「システムのデータそのもの」を管理します。先ほどの図書館の例で言うと
まで全て記録されている状態です。ここで、例えば、悪意ある者が「『ハムレット』の作者は『クリストファー・マーロウ』である」としたい場合、悪意ある者はそのような行為ができてしまいます。
そのため、攻撃者はDBのroot権限を真っ先に奪います。それを防ぐためにも最初にrootパスワードを設定します。
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Your_Strong_Password';
パスワードは自分の環境に合わせます。パスワード設定後、そのデータを適切な方法・手段・保管場所に格納してください。
FLUSH PRIVILEGES;
EXIT
mysql -u root -p
設定したパスワードでログインできることを確認します。確認後、EXITで抜けます。
mysql_secure_installation)対話型スクリプトを用い、商用・本番環境に耐えうるセキュリティ設定を一括で適用します。 以下のように行ってください。
Enter password for user root:
で、先ほどのrootパスワードが聞かれます。
その後、いくつかの確認事項があるのでYで答えます。
| 設定項目 | 内容 | 理由 |
|---|---|---|
| Remove anonymous users | Yes | 誰でも接続できる穴を塞ぐため |
| Disallow root login remotely? | Yes | ローカル管理に限定し攻撃経路を遮断するため |
| Remove test database and access to it? | Yes | 不要なオブジェクトの排除 |
| Reload privilege tables now? | Yes | 設定内容を有効化するため |
「KVMで作成したディスク(LVM)を500→200程度に切り詰めようとしたところ失敗した」。
まず、ディスク容量を空けるために /home を削除・再作成し、使用量を削減しました。
LVMの「末尾」にあるデータを物理的にディスクの「先頭」へ移動させました。
sudo pvmove --alloc anywhere /dev/vda2pvdisplay -m にて、使用中セグメントが 0 ~ 73153 PE(約180GB圏内)に固まり、それ以降が FREE になったことを確認。管理情報を 180GB に書き換えようと試みました。
sudo pvresize --setphysicalvolumesize 180G /dev/vda2
cannot resize to 46079 extents as later ones are allocated により失敗。「データは前に寄せた」という事実に基づき、ホスト側から物理的に180GB 分だけを切り出す作戦を敢行。
dd コマンドで旧LVから 180GB 分を抽出コピー。lvrename を使い、VMが参照するターゲットを 180GB の新ディスクにすり替え。virsh console 等で応答なし。pvmove でデータを寄せても、LVM自身の管理領域(Metadata Area)の整合性を保ったまま物理サイズを削るのは、OS稼働中や単純な dd では極めて困難である。dd で削る手法は、パーティションテーブルとLVMヘッダの整合性が 1 バイトでも狂うとシステム停止に直結する。やはり、この手のリサイズは「新たにサーバを作成し(リサイズした上で)データを流し込む」という地道な主だが一番です。
Powered by WordPress & Theme by Anders Norén