タグ: Ubuntu Page 1 of 29

Growiを7.5.7→8.0.0へアップデートしたときの作業メモ

概要

筆者にとっての必須エディタ、Growi。そのv8.0.0が出たので、以下の要件に基づきアップデートを行いました。

リリース情報

実施日: 2026年7月29日
対象環境: Ubuntu 24.04 (非Docker環境 / systemd運用)
その他要件:

itemversion
OSUbuntu 24.04
GROWI7.5.7
node.js24.14.1
npm11.13.0
pnpm11.1.1

必要要件の確認

GROWIの最新リリースにおける変更点(ES7の完全廃止、jemalloc対応、メモリ最適化、ESM移行)に対する自環境の適合状況を確認。

Elasticsearchバージョン確認

ES8/ES9のみに対応しているため、以下を確認。

curl -X GET "http://localhost:9200/" |grep number

9.4.4となっていたので、要件を満たすことは確認しました。

OSメモリ最適化モジュールの確認

ldconfig -p | grep jemalloc

libjemalloc.so.2 (libc6,x86-64) => /lib/x86_64-linux-gnu/libjemalloc.so.2と出たのでモジュールも確認済みです。

ここまでやれば、覚悟を決めてアップデートを行います。

さっくりとしているが面倒な手順

  1. Growi実行スクリプトの修正を行います。
  2. 修正結果が反映されたことを確認します。
  3. 関連サービスの停止を行います。
  4. (推奨)mongodbをまるごとバックアップしておきます。
  5. Growiのアップデートを行います。
  6. アップデートの確認を行います。

Growi実行スクリプトの修正

※筆者は

systemdに登録したサービス起動プログラムがGrowiのルートディレクトリ/home/www-data/growi配下にあるgrowi-start.shを参照して各種環境をキックしています。

※ご自身の環境に合わせ修正を行ってください※

  • 修正前ファイル
#!/bin/bash

# nvmのdefaultエイリアスが指すディレクトリを動的に取得してPATHに追加
# これにより、nvm installでバージョンを上げてもこのスクリプトの書き換えが不要になります
DEFAULT_NODE_VER=$(cat "$NVM_DIR/alias/default")
export PATH="$NVM_DIR/versions/node/$DEFAULT_NODE_VER/bin:$PATH"
GROWI_DIR="/home/www-data/growi"

cd $GROWI_DIR

# 環境変数の設定
export NODE_ENV=production
export AUDIT_LOG_ENABLED=true
export FORCE_WIKI_MODE=private
export MONGO_URI=mongodb://localhost:27017/growi
export ELASTICSEARCH_URI=http://localhost:9200/growi
export REDIS_URI=redis://localhost:6379
export PASSWORD_SEED=(設定済みのパスワード)

# npm run app:server を使用する(npmが内部で正しいパスを見つけてくれます)
# execを使うことでsystemdとの親和性を維持します
exec npm run app:server

このファイルを修正していきますが:バックアップを確実に行います。

  • ファイルバックアップ
sudo cp -pi /home/www-data/growi/growi-start.sh /path/to/backup/directory/growi-start.sh.$(date +%Y%m%d)
  • ファイルバックアップ確認
diff -u /path/to/backup/directory/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh

エラー(差分)がなければ両者ともファイルがあり、バックアップが取れています。

バックアップ後、/home/www-data/growi/growi-start.shの編集を行います。(自分の環境に合わせます)

主な変更内容
  • JEMALLOC の有効化: export JEMALLOC_ENABLED=true を追加
  • LD_PRELOAD の明示: export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 を追加
  • MongoDB 接続プールの最適化: MONGO_URI?maxPoolSize=10 パラメータを付与

具体的には以下のような差分が取れていればOKです。

diff -u /path/to/backup/directory/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh
# 環境変数の設定
 export NODE_ENV=production
+export JEMALLOC_ENABLED=true
+export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
 export AUDIT_LOG_ENABLED=true
 export FORCE_WIKI_MODE=private
-export MONGO_URI=mongodb://localhost:27017/growi
+export MONGO_URI=mongodb://localhost:27017/growi?maxPoolSize=10
 export ELASTICSEARCH_URI=http://localhost:9200/growi
 export REDIS_URI=redis://localhost:6379

設定ファイルの修正確認

  • Growiサービスの再起動
sudo systemctl restart growi.service

指定した環境変数および jemalloc ライブラリが GROWI プロセス(Node.js)へ正常にロードされていることを確認しました。

  • プロセスの環境変数確認:
    JEMALLOC_ENABLED=true および LD_PRELOAD が正常に適用されていることを確認。
  • メモリマップ / ファイルハンドラ確認:
    lsof および /proc/{PID}/maps コマンドにより、libjemalloc.so.2 が Node.js プロセスに読み込まれていることを確認。

メンテナンスモード有効化

  1. Growiに管理者権限でログインします。
  2. 管理トップ>アプリ設定に進み、「メンテナンスモードを開始する」をクリックします。
  3. トップページに戻り「メンテナンスモード」が表示されていることを確認します。

バックアップ

以下をバックアップします。

  • mongodbの格納データ
cat /etc/mongod.conf |grep dbPath

として、ここのディレクトリ一式を控えます。(筆者環境 /home/mongodb)

このディレクトリを任意の方法でバックアップします。

  • Growiの添付ファイル一式が納められているディレクトリ(ファイルアップロード先をlocalにしている場合のみ)
/growi/root/directory/apps/app/public

(筆者環境 /home/www-data/growi/apps/app/public)ここも念のためバックアップします。

※ 添付ファイルのアップロード先をAWSやAzureなどにしている場合は不要です

  • vpsや仮想ゲストの場合はシステム全体:推奨

スナップショット機能などでシステム全体をバックアップした方が確実で安心です。

ElasticsearchとGrowiの停止

  • Elasticsearchサービス停止
sudo systemctl stop elasticsearch.service
  • Growiサービス停止
sudo systemctl stop growi.service
  • サービス停止確認
systemctl status elasticsearch.service growi.service | grep Active

inactive(dead)を確認します。

作業前バックアップ

  • データディレクトリを丸ごとコピー (-aオプションでパーミッションを維持)
sudo cp -a /var/lib/elasticsearch/ /path/to/backup/dir/elastic_bk.$(date +%Y%m%d)

自分の環境に合わせます。

  • バックアップ確認
sudo ls -l /path/to/backup/dir/elastic_bk.$(date +%Y%m%d)

バックアップした内容があることを確認します。(※管理者権限でないとこのディレクトリを見ることはできません)

growiディレクトリに移動します

cd /home/www-data/growi && pwd

自分の環境に合わせます。(筆者環境/home/www-data/growi)

リリースタグを確認します。

  • リリースタグ取得
sudo git fetch --tags
  • リリースタグ確認
sudo git tag -l

スペースで確認していき、上記リリースサイトと同じバージョンがあることを確認します。

チェックアウトとインストールを行います。

  • 変更を一時的に退避
sudo git stash
  • チェックアウト
sudo git checkout 【バージョン】

リリースタグは再確認しましょう。今回は 2026/07/28にリリースされたv8.0.0を選択しました。

  • pnpm install
sudo pnpm i

※これができるのは、筆者のgrowi実行環境がrootだからです。このパーミッション設定は確実に確認しましょう。

  • ビルド
NODE_OPTIONS="--max-old-space-size=4096" sudo pnpm run app:build

→ ビルド時のメモリ大量使用を抑えるため上限を抑えています。余談ですが、ここは神に祈る作業です。

ElasticsearchとGrowiの再開

  • Elasticsearchサービス開始
sudo systemctl restart elasticsearch.service
  • Growiサービス開始
sudo systemctl restart growi.service
  • サービス開始確認
systemctl status elasticsearch.service growi.service | grep Active

active(running)を確認します。

メンテナンスモード無効化

  1. Growiに管理者権限でログインします。
  2. 管理トップ>アプリ設定に進み、「メンテナンスモードを終了する」をクリックします。
  3. トップページに戻り「メンテナンスモード」が表示されていないことを確認します。

バージョンアップを確認します。

  1. 画面下部にあるバージョンがチェックアウトしたバージョン(v8.0.0)であることを確認します。
  2. 各種機能(ページ閲覧や編集)などが正常に行えるかを確認します。

追加モジュールが動いていることを確認します。

  • 一時的に特権ユーザー昇格

※一般権限で見られないため

sudo su -
  • モジュール追加確認
 pgrep -f "node.*growi" | xargs -I {} lsof -p {} 2>/dev/null | grep jemalloc
  • 実行例
MainThrea 479521 root  mem       REG  253,1    830656    15385 /usr/lib/x86_64-linux-gnu/libjemalloc.so.2

バージョンアップ後の作業

必要に応じてバックアップしたファイル一式やスナップショットを削除します。

ネットワーク設定移行作業ログ(Wi-Fiから有線LANへの固定IP引き継ぎ)

設定の後始末

中古のLet's noteを「試しにNextcloudのフロントエンドやDNS(AdGuard Home)」にしていたものの、いよいよもって本格的に稼働させることにします。

ネットワーク環境の確認情報

  • 対象機器:
    • Ubuntu 26.04 Desktopノート
  • 有線LAN (NIC)
    • : enp0sxx(NetworkManager接続名: netplan-enp0s31f6
  • 無線LAN (Wi-Fi)
    • : wlp1s0(NetworkManager接続名: Wi-Fi
  • デフォルトゲートウェイ:
    • 192.168.x.1
  • サブネットマスク:
    • /24 (255.255.255.0)

問題と解決

直面した問題

デスクトップ画面で有線LANと無線LANを同時に切り替えようとしたのですが、
無線側がまだ 引き継ぎたいIPアドレスを掴んでいたため、有線側で同じIPを有効化しようとした際にアドレスの重複・競合が発生し、以下のエラーで有効化に失敗しました。

エラー: 接続のアクティベーションに失敗: IP 設定を確保できませんでした (利用可能なアドレスがない、タイムアウトなど)

これに対処していきます。ここからはターミナルやSSH接続です。

解決策

  1. 無線接続を先にダウンさせ、旧IPアドレスを解放します。
  2. 無線のIPv4設定を自動(DHCP)に変更・初期化する。
  3. 有線LAN側に静的IP(192.168.11.16)とゲートウェイ・DNSを設定する。
  4. 有線LAN接続を有効化する。

実行した復旧手順(コマンド)

SSH切断による作業中断を防ぐため、以下のコマンドを一括で実行しました。

sudo nmcli connection down "Wi-Fi" && \
sudo nmcli connection modify "Wi-Fi" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns "" && \
sudo nmcli connection modify "netplan-enp0s31f6" ipv4.method manual ipv4.addresses 192.168.x.x/24 ipv4.gateway 192.168.x.1 ipv4.dns "192.168.x.1" && \
sudo nmcli connection up "netplan-enp0s31f6"

それぞれのステップで何が起きているのか、順番に詳しく解説します。

Step 1: 無線LANの切断

sudo nmcli connection down "Wi-Fi"
  • やっていること:
    • 現在有効になっているWi-Fi接続(Wi-Fi)を即座に無効化(切断)します。
  • 目的:
    • これから有線側に設定しようとしているIPアドレス(192.168.x.x)をWi-Fi側から完全に解放し、IPアドレスの重複(衝突)を防ぐために行っています。

Step 2: 無線LAN設定の初期化 (DHCP化)

sudo nmcli connection modify "Wi-Fi" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""
  • やっていること:
    • Wi-Fi接続の設定情報を変更します。
  • ipv4.method auto:
    • IPアドレスの取得方法を「自動(DHCP)」に変更
  • ipv4.addresses "":
    • 以前割り当てていた固定IPアドレスの登録を削除
  • ipv4.gateway "":
    • 設定されていたゲートウェイ情報を削除
  • ipv4.dns "":
    • 設定されていたDNSサーバー情報を削除
  • 目的: 今後Wi-Fiを単体で使うことがあっても、固定IPではなく自動でIPを取得(DHCP)するように設定をリセットしています。

Step 3: 有線LANへの固定IP・ネットワーク情報の設定

sudo nmcli connection modify "netplan-enp0s31f6" ipv4.method manual ipv4.addresses 192.168.x.x/24 ipv4.gateway 192.168.x.1 ipv4.dns "192.168.x.1"
  • やっていること: 有線LAN(netplan-enp0s31f6)に新しいネットワーク情報を書き込みます。
  • ipv4.method manual: IP設定を「手動(固定IP)」に変更
  • ipv4.addresses 192.168.x.x/24: サーバー用の固定IPアドレスとサブネットマスクをセット
  • ipv4.gateway 192.168.x.1: デフォルトゲートウェイ(ルーター)を指定
  • ipv4.dns "192.168.x.1": 名前解決用のDNSサーバー(ルーター)を指定
  • 目的: サーバー機能(NextcloudやAdGuard等)を引き継ぐための固定IP環境を有線側に割り当てます。

Step 4: 有線LANの有効化(新設定の反映)

sudo nmcli connection up "netplan-enp0s31f6"
  • やっていること:
    • Step 3で書き換えた設定を使って、有線LAN接続を起動(アクティベート)します。
  • 目的:
    • 設定を反映させて通信を開始します。この瞬間に有線側のIPアドレスが 192.168.x.x に変更されるため、旧IPで繋がっていたSSH接続はここで途切れ、新しいIPでの接続が解禁されます。

なぜこれを「1行(ワンライナー)」でやったのか?

SSH接続中にネットワーク設定を変更すると、Wi-Fiを切断した時点(Step 1)でターミナルの操作ができなくなってしまいます。

命令を && で繋いで1行で送信しておくことで、SSHが切れてしまってもUbuntu内部でStep 4まで自動的に突っ走って完了させてくれるという技術的な工夫(SSH切断対策)でした。

最終確認結果

  1. IPアドレス・インターフェース状態の確認 (ip a)
  • 有線 (enp0s31f6): inet 192.168.x.x/24 (正常に固定IP割り当て完了)
  • 無線 (wlp1s0): state DOWN (競合解消)
  1. 疎通確認
  • 有線LANのIP宛て(192.168.x.x)へのSSH再接続: 成功
  • 外向き通信テスト (ping -c 3 google.com): 成功

なんとなくで設定していた宿題に取り組むことが出来ました。

BookStackのヘッダーロゴを高解像度化する

BookStackでは「アプリケーションロゴ」を設定できますが、そのままではアップロードした画像が高さ86pxへリサイズされてしまいます。

そのため、CSSでロゴを大きく表示すると画像がぼやけてしまいます。そもそも86pxは昨今のモダンなCMSにあるまじき解像度ではあるので、手を入れます。

そこでファイル本体を少し修正し、高解像度のロゴ画像をそのまま利用できるようにしました。

環境

  • BookStack
    • v26.05.1
  • Apache
    • 2.4
  • PHP
    • 8.3-fpm
  • Ubuntu
    • 24.04

最初に試したこと

BookStackに管理者権限でログインし、管理>カスタマイズに遷移。

カスタムheadタグで

<style>
.logo-image {
    height: 128px !important;
    width: auto !important;
}
</style>

のような指定はできるものの、超ローレぞ加工され、表示がぼやけます。そこで、要素を調べると、保存された画像自体が86×86になっていました。

この原因を調べます。

調査

インストールされているUbuntuサーバで

cd /path/to/BookStack/directory && pwd

として(筆者環境/home/www-data/Bookstack)BookStackのホームディレクトリに移動。

file public/uploads/images/system/20xx-xx/xxxx.jpg

で、該当のファイルを調べます。

JPEG image data
86x86

つまり、CSSではなくアップロード時に画像が縮小されていました。

分かった原因

いかにCSSで指定してもだめな原因はプログラムそのものにあります。なので、その上流の「ファイルのアップロードを司るファイル」にメスを入れます。

それを司るファイルをチャットAIとともに調査。

BookStackのルートディレクトリからたどれる

app/Settings/AppSettingsStore.php

にそれがありました。

protected function updateAppLogo(Request $request): void
{
    if ($request->hasFile('app_logo')) {
        $logoFile = $request->file('app_logo');
        $this->destroyExistingSettingImage('app-logo');

        $image = $this->imageRepo->saveNew(
            $logoFile,
            'system',
            0,
            null,
            86
        );

        setting()->put('app-logo', $image->url);
    }
}
null,
86

が指定されているため、高さ86pxへリサイズされた画像が保存されます。

ここまで分かれば、後は修正開始です。

さっくりとした手順

  1. サーバ上で該当するファイルを修正します。
  2. 念のためサービスの再起動を行います。
  3. カスタムheadタグを修正します。
  4. 改めてファイルをアップロードします。

ファイルのバックアップ

  • BookStackのルートディレクトリに移動
cd /path/to/BookStack/directory && pwd

自分の環境に合わせます。(筆者環境/home/www-data/Bookstack)

  • 該当ファイルのバックアップ
sudo cp -pi app/Settings/AppSettingsStore.php /path/to/backup/AppSettingsStore.php.$(date +%Y%m%d)

※注意点※

  • プログラム根幹をいじります。つまり、何かあったら直したやつの責任です。バックアップは確実に取ります。
  • 更に、Gitなどでアップデートした場合、確実にこれはMasterから上書きされます。「ここを直す」というのは履歴管理を厳に行いましょう。
  • 該当ファイルのバックアップ確認
diff -u /path/to/backup/AppSettingsStore.php.$(date +%Y%m%d) app/Settings/AppSettingsStore.php 

任意のバックアップディレクトリを指定します。

エラーがないことを確認します。人間「バックアップはやってる」と思いながらもバイアスや手なりでスキップしがちです。この段階でそのミスやバイアスを潰します。

なぜdiffを使うのかは、ls -lで両ファイルを比べるより確実だからと言うのと、後述する修正確認でも使うからです。

該当ファイルの修正

app/Settings/AppSettingsStore.phpファイルを、リサイズを行わないよう変更します。(要管理者権限)

具体的には以下の差分になるように。

- $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
+ $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, null);
  • 修正後のバックアップ確認
diff -u /path/to/backup/AppSettingsStore.php.$(date +%Y%m%d) app/Settings/AppSettingsStore.php 

ここで、なぜdiffを逆にする意味が生まれます。逆にしないと直したところが-、元が+で表示されます。これは視覚的にも感覚的にもよろしくありません。「え? 正しい修正をしたのに消されるの?」となりがちです。

設定反映

sudo -u www-data php artisan optimize:clear

でキャッシュをクリアします。

(推奨)PHP/Apacheを再起動

PHP-FPMなら

sudo systemctl restart php8.3-fpm

Mod-PHP環境なら

sudo systemctl restart apache2

で、実行環境を再起動します。

改めてカスタムヘッダの調整

BookStackに管理者権限でログインし、管理>カスタマイズに遷移。

カスタムheadタグで

<style>
.logo-image {
    height: 128px !important;
    width: auto !important;
}
</style>

のように設定し、(或いは設定されていることを確認し)、保存をクリック。

要してあるロゴをアップロード

管理>カスタマイズ

から、ファイルをアップロードします。

  • ロゴをさらに大きく表示したい場合

BookStack標準ではヘッダー高さが少し窮屈なので、必要に応じてヘッダーも調整します。

header {
    min-height: 72px;
}

.logo-image {
    height: 64px !important;
}

設定反映確認

ここから先はBookStackに戻り、高解像度でロゴが表示されれば設定はOKです。

設定ファイルの所属元を探すコマンド

Ubuntuサーバ設定時、時々迷ってしまう

「どのファイルがどのパッケージに属しているか?」

問題。その道しるべになり得るコマンドのメモです。

先に言ってしまえば

dpkg -S コマンドを使うのが一番手っ取り早くて確実です。apt パッケージ管理システムの実体は、バックエンドで dpkg を使っているため、このコマンドがそのまま使えます。

基本的な使い方は以下の通りです。

dpkg -S /パス/to/ファイル名

具体的な使い方とテクニック

1. 基本:絶対パスで指定する

調べたいファイルの絶対パスがわかっている場合は、そのまま指定します。

dpkg -S /etc/logrotate.d/apache2

出力例:

apache2: /etc/logrotate.d/apache2

コロン(:)の左側に表示されている apache2 がパッケージ名です。

2. 応用:ファイル名だけで検索する

絶対パスが思い出せない場合は、ファイル名の一部だけでも検索できます。

dpkg -S logrotate.d/apache2

注意点: ファイル名だけで検索すると、その文字列にマッチするすべてのパッケージとファイルパスが一覧で出力されます。そのため、できるだけ具体的なパスを指定した方がノイズが少なくなります。

3. まだインストールしていないファイルの場合

「まだシステムに入れていないけれど、/etc/xxxx という設定ファイルを入れるには、どのパッケージをインストールすればいいか?」を知りたい場合は、apt-file というツールが便利です。

まずツールをインストールしてデータベースを更新します。(筆者の好みでaptitudeを用いています)

sudo aptitude install apt-file

コマンドインストール後、DBをアップデート。

sudo apt-file update

その後、以下のように検索します。

apt-file search /etc/logrotate.d/apache2

これを使えば、手元の環境にないファイルでも、どのパッケージに含まれているかを一発で特定できます。

なんかの都合で設定ファイルの名前を変えてしまったときに役立ちます。(やらかしました)

セキュリティデッキを組もう:ModSecurity Request-900-Exclusionの編集意義

MtGやDMを遊んだことがあるのであれば聞いたことがあるだろう

「カードはルールに勝つ
 (カードの効果とルールが直接矛盾した場合、カードの効果を優先する)」

という大原則。

「カードを2枚引く」

というシンプルなテキストであっても、通常、プレイヤーは1ターンに1枚のみカードを引くと言うルールがありますが、カードにそう書かれていればそのルールを無視した挙動が可能になります。

一見すると乱暴な言葉ですが、実際にはゲームデザイン上もっとも重要な考え方の一つです。

基本ルールは全員共通。しかし、カードには「この場合だけは例外」が書かれています。だからこそ何万種類ものカードが共存できます。

実は、この考え方は ModSecurity にも、そのまま当てはまります。

今回、私がModSecurityに導入している「REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf」について解説します。

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confとは?

ModSecurityで本格的な防御を行う際、多くの人が「OWASP CRS(Core Rule Set)」という、世界中のセキュリテイ専門家が作った強力な既製ルール集を導入します。

CRSは非常に優秀ですが、何千もの緻密なルールが詰まっているため、上から順にすべてをチェックするとそれなりの処理コスト(CPUやメモリ)がかかります。

また、これは非常にデリケートでガチガチなので

  1. ファイルをアップロードした
  2. コードを書き込んだ
  3. クリックを繰り返した

だけで「こいつは怪しい動きをしている」として、アクセスそのものを遮断する「偽陽性」が発生します。

そこで、膨大なCRSの本体を書き換えることなく

  1. プログラムのコードやコマンドラインなどを投稿するのはOK
  2. だが、それを使った攻撃は許さない
  3. 複雑なスキャンは不要。「悪・即・斬」レベルで不審な攻撃をたたき落とす
  4. 逆に「こいつは面白い動きをするからハニーポットに誘導しよう」

などの「デッキを作るような感覚で」膨大なCRSを制御する方法として用意されているのが、REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf(以下、900番BEFOREルール)です。

設置例

筆者環境

  • ModSecurity 2.9.7
  • Core Rule Set 3.3.5
  • Apache 2.4
  • Ubuntu 24.04

どこに置くか?

筆者環境の場合は

/usr/share/modsecurity-crs/coreruleset/rules配下のREQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confですが、

/home/hoge/script/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

など、自分がメンテナンスしやすい位置にこのファイルの実体を置いておき、ln -sfでシンボリックリンクを張る方が非常に簡単です。というのも、「このデッキ調整」はデリケートな作業なので何回も何十回も調整が必要になります。そのたびに深い階層を掘ってファイルを編集するのは効率的ではありません。

とはいえ、シンボリックファイル is Evil だったりrootアカウントしかないとかいう方はいるので流儀に合わせてください。

ケーススタディ

効果的なカスタム案

筆者が実際に使っている中で最も効果的なルールがこれです。

# IPアドレス直打ちアクセス対策 
SecRule REQUEST_HEADERS:Host "@rx ^[\d.]+(:\d+)?$" \
    "id:10004,\
    phase:1,\
    deny,\
    status:404,\
    log,\
    msg:'[CUSTOM RULE] Host header is a numeric IP address (incl port). Blocked immediately.',\
    tag:'application-attack',\
    tag:'PROTOCOL_VIOLATION/INVALID_HREQ'"

# Hostヘッダーが存在しない場合は即ブロック
SecRule &REQUEST_HEADERS:Host "@eq 0" \
    "id:10005,\
    phase:1,\
    deny,\
    status:404,\
    log,\
    msg:'[CUSTOM RULE] Missing Host Header. Blocked immediately.'"
  • id:10004:
    • 正規表現 @rx ^[\d.]+(:\d+)?$ を使い、Hostヘッダーの中身が「数字とドットだけ(または末尾にポート番号)」で構成されているかを判定します。IPアドレス直打ちであれば、その瞬間に合致(マッチ)します。
  • id:10005:
    • 変数の頭に & をつけることで、そのヘッダーの「個数」を数えます。@eq 0(=0個、つまりHostヘッダーが存在しない)場合にマッチします。
  • phase:1:
    • これが非常に重要です。リクエストの解析が始まった「最速の段階(フェーズ1)」で検査を行います。
  • deny, status:404:
    • 条件にマッチしたら、荷物の中身(Body)を見るまでもなく、即座に通信を拒否し、404エラーを返して追い払います。

カスタム案が拾ったログ

以下、実際に私のサーバにアクセスしたログです。

# 例①:Hostヘッダー自体が存在しない(欠落)
[ security2:error] ModSecurity: Access denied with code 404 (phase 1). ... [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf"] [line "72"] [id "10005"] [msg "[CUSTOM RULE] Missing Host Header. Blocked immediately."]

# 例②:Hostヘッダーがドメインではなく「IPアドレス直打ち」
[ security2:error] ModSecurity: Access denied with code 404 (phase 1). ... [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf"] [line "63"] [id "10004"] [msg "[CUSTOM RULE] Host header is a numeric IP address (incl port). Blocked immediately."]
sequenceDiagram Note over Crawler: 不正なリクエストを送信<br>(Host: 192.0.2.1) Crawler->>ModSecurity_WAF: HTTP GET / (Host不正) Note over ModSecurity_WAF: phase:1 で瞬時に検知!<br>背後のWebサーバーやアプリには<br>一切パスさせない ModSecurity_WAF-->>Crawler: 404 Not Found (即座に遮断)

という形で、ModSecurityの背後にあるコンテンツに一切触れさせることなく追い払うことができます。

余談:これで追い返して問題は無いのか?

ありません。断言します。なぜなら、ローカル運用ならいざ知らず、ドメインで動くモダンインターネットにおいて

http://203.0.113.6

などと直打ちするケースはほぼありません。なので、IPアドレス直打ちはほぼ確実に「膨大なIPをしらみつぶしに探し回るボット」です。

そして、Webサイトを閲覧するとき、ブラウザとサーバーの間では「データの荷物」がやり取りされています。この荷物は、大きく分けると「ヘッダー(Header)」「ボディ(Body)」の2つで構成されています。

郵便に例えると、以下のようなイメージです。

  • ヘッダー(Header): 封筒の表面。「宛先」「差出人」「中身の形式」などが書かれた管理情報。
  • ボディ(Body): 封筒の中身。「実際のページデータ(HTML)」や「画像」そのもの。

普段目にするWebページは「ボディ」ですが、それを正しく届けて表示するためには、不備のない「ヘッダー(封筒の表面)」を付与するというのがブラウザの挙動です。

900番BEFOREルールである意味

そして、このファイルは「CRSが動く前に対処できる」という、MtGで言う「打ち消し呪文」のようなものとして機能します。

もし、これをRESPONSE-999-EXCLUSION-RULES-AFTER-CRS.confという「最終的に評価するルール」で書いた場合

sequenceDiagram Crawler->>ModSecurity_WAF: HTTP GET / (Host: 192.0.2.1) Note over ModSecurity_WAF: 1.900番BEFORE ルール(何もなし) -> 通過 Note over ModSecurity_WAF: 2. メインCRS審査(数千のルール) Note over ModSecurity_WAF: SQLインジェクションの検査...OK<br>XSSの検査...OK<br>(延々とノーマル審査が続く) Note over ModSecurity_WAF: 3. (AFTERルールに書いた場合) Note over ModSecurity_WAF: ここでようやくルール10004にヒット ModSecurity_WAF-->>Crawler: 404 Not Found (一応遮断はできたが...)

という流れになります。

もしクローラーが超高速で連射してきた場合、この「無駄なフルコンボ審査」のせいでModSecurity自体の処理が追いつかなくなり、サーバーのCPU使用率が100%に張り付いて、一般ユーザーのアクセスが重くなる(あるいは落ちる)という本末転倒な事態が起きます。

このカスタム案の意義

OWASP CRSは非常に優秀です。

SQLインジェクション、XSS、RCEなど、現代的な攻撃に対する膨大な知見が詰め込まれており、何も考えずに導入しても一定以上の防御力を得られます。

しかし、それはあくまで「世界中の誰にでも当てはまる最大公約数」です。

私のサーバーには私のサイト構成があり、私の利用者がおり、私の攻撃ログがあります。

だからこそ

  • このサイトではコードの投稿は許可する
  • このURLへの異常なクロールだけは絶対に許さない
  • この挙動は攻撃ではないので除外する
  • この通信はコンテンツを見る価値すらないので即座に落とす
  • 面白い相手ならハニーポットへ誘導する

という「自分だけのルール」が必要になります。そのための「私のデッキ」が900番BEFOREルールです。

CRS本体を直接編集する必要はありません。

世界中のセキュリティ専門家が更新し続けるルールセットはそのまま利用し、自分の環境だけに必要な判断を、カードを1枚追加するような感覚で差し込めます。

だから冒頭で紹介した

「カードはルールに勝つ」

という考え方が、そのままModSecurityにも当てはまるのです。

CRSという基本ルールがあり、その前に「このサイトではこうする」という例外を定義する。それだけで、自分だけのWAFが出来上がります。

私にとってREQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confは、単なる設定ファイルではありません。

毎日流れてくるログを眺め、

  • 「これは900番で落とせるな」
  • 「このクローラーはハニーポット送りにしよう」
  • 「これはCRSに任せた方がいい」

そんな調整を繰り返しながらデッキをチューニングしていく場所です。Webサイトを運営する人にとって、攻撃ログは鬱陶しいものに見えるかもしれません。

しかし見方を変えれば、それは次の一枚を考えるための対戦ログでもあります。

オリジナルに手を加えない意義

また、この900番BEFOREルールを別ファイルとして管理する最大の理由は、「本家に手を入れない」ことです。

CRSは更新され続けます。

もしCRS本体を書き換えてしまうと、アップデートのたびに差分を確認し、競合を解消し、自分の修正を書き戻す必要があります。

一方、自分のデッキを900番BEFOREだけに閉じ込めておけば、CRSが更新されても自分のカードはそのまま使い続けられます。

新しいサーバーへ移行するときも、自分のデッキを1枚コピーするだけです。デッキは、一度組んで終わりではありません。

  • 新しい攻撃ログを見つけたら1枚差し替え、
  • 不要になったカードは抜き、
  • 新しい環境に合わせて調整していく。

世界中の専門家が作ったルールセットを土台に、自分のサイトで得た対戦ログから一枚ずつカードを選び、デッキを育てていく。それが私にとってのREQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confです。

BookStackにMermaid Viewerを導入する手順

ある種悲願でもあった、「BookStackにmermaid.jsを描画させる」が叶いました。

環境

  • BookStack v26.05.1
  • Apache 2.4
    • 実行ユーザーはwww-data
  • PHP-FPM 8.3
  • MySQL 8
  • Ubuntu 24.04

さっくりとした手順

  1. BookStackのルートディレクトリに移動します。
  2. Mermaid Viewerモジュールをインストールします。
  3. .envの編集をします(ハマった点)
  4. 設定を反映させます。
  5. 動作の確認を行います。

BookStackのルートディレクトリへ移動

cd /home/www-data/BookStack

筆者環境です。お使いの環境に応じてパスは変更してください

Mermaid Viewerモジュールをインストール

sudo -u www-data php artisan bookstack:install-module https://www.bookstackapp.com/hack-modules/mermaid-viewer.zip

途中で

Are you sure you trust this source?

と聞かれるのでyesで返答します。テーマが無い場合は

No active theme folder found...

と表示されるので同様にyesで返答します。

すると

themes/custom/

ディレクトリが作成され、

themes/custom/modules/mermaid-viewer/

ディレクトリへモジュールがインストールされます。

.env にテーマを設定(重要)

ここが今回ハマったポイントです。

.env を編集します。(要管理者/bookstack実行ユーザー権限)

APP_THEME=custom

を追加します。

これを設定しないと、themes/custom以下のディレクトリはは一切読み込まれません。


設定反映

php artisan optimize:clear

または

php artisan cache:clear
php artisan config:clear
php artisan view:clear

でキャッシュをクリアします。

(推奨)PHP/Apacheを再起動

PHP-FPMなら

sudo systemctl restart php8.3-fpm

Mod-PHP環境なら

sudo systemctl restart apache2

で、実行環境を再起動します。

設定反映確認

BookStackにログインして、設定反映されていることを確認します。

エディタの形式をMarkdownなら

```mermaid
flowchart TD
    A --> B
    B --> C
```
flowchart TD A --> B B --> C

保存後、閲覧画面でMermaid図としてレンダリングされます。

※ 編集画面の右ペインには表示されないのでご注意ください。

補足

インストール後のディレクトリは概ね以下のようになります。

BookStack/
└── themes/
    └── custom/
        └── modules/
            └── mermaid-viewer/

今回のポイント

実は一番重要なのはこのメッセージでした。

You will need to set APP_THEME=custom in your BookStack env configuration to enable this theme!

初回インストール時に表示されるのですが、見落としやすい一文です。

筆者も最初は「インストールは成功しているのに表示されない」という状況から別の原因を疑いましたが、このメッセージと APP_THEME の未設定を確認したことで、原因を特定できました。

自宅サーバーに導入したAdGuard Homeを家庭内ルーターに適用する手順とハマったところ。

前回の設定で、AdGuard Homeの常時SSL化とリバースプロキシ環境が整いました。今回は、AdGuardを「自分の部屋のネットワーク全体」に適用していく手順をまとめます。

設定時のハマりどころと失敗談も含めています。

宅内環境

前提として:

家族に迷惑をかけずに自宅サーバーとクライアントをいじるため、私の環境では以下のような二重ルーター構成(インナーネットワーク)を取っています。

[ONU] ──> [家族用メインルーター] ──> [スイッチ]
                                          │
    ┌─────────────────────────────────────┘
    ▼(ここからが自分の環境)
[自室内ルーター] ──(DHCPでAdGuardのIPを配布)──> [自室内端末群]
     │
     └─> [Ubuntuサーバー(DNSを兼ねたAdGuard Home稼働)]

この構成であれば、万が一自分の部屋の設定をトチっても、家族のネット環境には1ミリも影響を与えません。

自室内ルーターの設定手順

この、自室環境のスマホやPCが自動的にAdGuard Home(例: 192.168.1.6)を向くように、ルーターの設定を変更します。

1. ルーターの管理画面を開く

ブラウザに自室内ルーターのIPアドレス(192.168.1.1 など)を入力し、ログインします。

2. LAN側(DHCPサーバー)の設定画面を開く

「詳細設定」や「LAN設定」の中にある 「DHCPサーバー設定」 の項目を探します。

【注意】ここで絶対にやってはいけない罠

ルーターの「WAN側(インターネット接続設定)」のDNSを変更してはいけません。ここを変えると、ルーター自体の挙動がおかしくなることがあります。変更するのは必ず 「LAN側」 です。

3. 配布するDNSサーバーのIPアドレスを固定する

通常は「ルーターのIPアドレスを通知する」になっている部分を、手動設定(カスタム)に変更します。

  • プライマリDNS(DNSサーバー1): 192.168.1.6 (UbuntuサーバーのIP)
  • セカンダリDNS(DNSサーバー2): あえて空欄(またはプライマリと同じIP)

セカンダリDNSのジレンマ

「AdGuardが死んだら困るから」とセカンダリDNSに外のDNS(8.8.8.8 など)を入れておきたくなりますが、これにはジレンマがあります。

OSや端末によっては、プライマリが生きているにもかかわらず、気まぐれにセカンダリのDNSばかりを使って通信を行う仕様があります。ここに外のDNSを書くと、広告ブロックをすり抜けてしまう端末が多発します。

AdGuardによって広告を100%仕留めるなら「空欄」か「AdGuardのIP一本足打法」にするのが鉄則です。

4. クライアント(端末)側での設定

ルーターの設定を変更(またはサーバーを物理復旧)した後は、スマホやPCに新しいDNS情報を強制的に覚え込ませる必要があります。

  • マホ(iPhone / Android)の場合
    • 一度Wi-Fiを「オフ」にしてから再度「オン」にするか、機内モードのON/OFFを行ってください。
  • PC(Windows)の場合
    • コマンドプロンプトを開き、いつものおまじないを実行してDNSキャッシュを完全に吹き飛ばします。
ipconfig /flushdns
ipconfig /renew

DNSを兼ねたことによる自爆

設定が完了し、問題なく運用できていたのに、突然「ローカルNWには繋がっているのに、なぜかインターネットに一切繋がらない」という謎の障害が発生しました。

原因を探ったところ、信じられないほど単純で致命的な物理トラップでした。

サーバーにしていたノートPCの電源アダプターが、いつの間にか外れてバッテリー切れで電源断発生。

先述の通り、広告のすり抜けを防ぐためにルーターのセカンダリDNSは空欄(またはAdGuardのIPのみ)にしています。そのため、サーバー(ノートPC)の電源が落ちてAdGuardもろともサービスダウンすると、部屋の中のスマホやPCは「DNS不在」状態になります。

「通信(ローカル回線)はアクティブなのに、Webサイトの名前解決が一切できないため、結果としてインターネットに完全に繋がらなくなる」というワンミス即死を喰らいました。

対策と教訓

  • ノートPCをサーバーにする場合は、ACアダプターが容易に抜けない位置に配置すること。(電源管理は企業では鉄則ですが家庭はそのあたりが甘かったです)
  • また、上述した家族用のNWから切り離していたことが幸いしました。でなければ、突然ネット閲覧ができなくなる、スマートTVでネトフリやYouTubeに繋がらなくなる事態が発生したでしょう。

「間違えても自分だけが泣けばいい」環境を持っておくのは改めて幸いでした。

22番ポート狙いのボットを完全遮断する、SSHポート変更(36878番)手順書

ようやく、セキュリティでの効果的な一歩である「SSHポート変更」を行いましたので、そのメモです。

例によって前置きは長いです。

変えようとしたきっかけ:怪しいログ

かなり怪しいログを見つけたというのがきっかけ。

不審に思ったログ

198.51.100.120 - - [09/Jun/2026:08:10:12 +0900] "GET /projects/zettel/knowledgebase/categories?tag=Linux%2CAnsible%2Cnginx HTTP/1.1" 200 41768 "https://example.com" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
203.0.113.185 - - [09/Jun/2026:08:10:15 +0900] "GET /projects/zettel/knowledgebase/categories?tag=cron%E8%A8%AD%E5%AE%9A%2CUbuntu HTTP/1.1" 200 41212 "https://example.com" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"

なぜこれが怪しいと思ったのかを述べる前に、普通のログを示します。

通常のアクセスログ

203.0.113.50 - - [09/Jun/2026:08:05:43 +0900] "GET /issues/96 HTTP/1.1" 200 50538 "https://example.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Mobile Safari/537.36"
203.0.113.50 - - [09/Jun/2026:08:05:44 +0900] "GET /plugin_assets/theme/style.css?1754 HTTP/1.1" 200 22610 "https://example.com/issues/96" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Mobile Safari/537.36"
203.0.113.50 - - [09/Jun/2026:08:05:44 +0900] "GET /images/logo.png HTTP/1.1" 200 18871 "https://example.com/issues/96" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Mobile Safari/537.36"
  • 普通のアクセスログ:
    • HTML(1行目)を読み込んだ直後の1秒以内に、ページに必要なCSSやロゴ画像(2〜3行目)を自動で一斉にロードしています。
  • 美しいリファラ・チェーン:
    • 2行目以降の画像などのリファラ(リンク元)に、直前に開いたページログにあります。

それに対して、怪しいログは以下の通りです。

  • 数秒ごとに「IPアドレスが変わる」: 1行目は 198.51.100.120、その3秒後の2行目は 203.0.113.185 です。
    • プロキシのプールから「一般人のIP」を次々に切り替えながらリクエストを送っているため、多くのWAFが取り入れているIP制限をすり抜けてきます。
  • アセットの完全無視:
    • HTML(カテゴリページ)だけにアクセスし、付随する画像やCSS、JavaScriptなどは1文字もダウンロードしません(プログラムにとって不要だからです)。
  • リファラ(リンク元)の不自然さ:
    • どちらのリファラもトップページになっています。何もないトップ画面から、いきなり長大な検索クエリ(?tag=…)へ直接ジャンプする人間はいません。ヘッダーを「それっぽく偽装」した結果、逆にガタ(不自然さ)が出ています。

そして、問題は、この怪しいアクセスログはAbusedIPDB(筆者解説記事) でも普通の一般のISPしか出てきません。

この理由を調べたところ、Residential Proxyの存在が浮かび上がりました。

Residential Proxy(住宅用プロキシ)とは?

一言で言うと、「一般家庭のインターネット回線(光回線やスマホのキャリア回線など)のIPアドレスを借りて、そこを経由してWebにアクセスする仕組み」です。

通常、ハッカーやスクレイピング(自動データ収集)ボットは、データセンター(AWSやGCP、あるいは筆者が用いているようなVPSなど)のIPアドレスから攻撃を仕掛けます。しかし、データセンターのIPは「ボットっぽい」として一発でブロックされやすいという弱点があります。(事実、筆者もこれを利用してブロックしています)

そこで、「一般家庭のPCやスマホ、ルーターなどを踏み台にすれば、普通の人が家からアクセスしているように偽装できるのでは?」という発想で生まれたのがResidential Proxyです。

なぜ一般人の回線が使われてしまうのか?

一般家庭の回線をが利用されるのか。主に以下の2つのパターンがあります。

合法的な報酬・同意(ユーザーが知ってて提供)

「このアプリを入れると、あなたの余った帯域(通信量)をお金やギフト券に換えます」というお小遣い稼ぎアプリ(Pawn.streamやHoneygainなど)を一般人が自らインストールしているケース。

マルウェアや不適切なアプリ(ユーザーが知らずに加担)

無料のVPNアプリや、海賊版ソフト、スマート家電の脆弱性を突くマルウェアなどに「プロキシの中継プログラム」が仕込まれており、本人が気づかないうちに「攻撃の踏み台(中継サーバー)」に仕立て上げられているケース。

私は過日、ASNとは何か?インターネットの“住所録”を支える番号と「盗人宿」の把握

という記事で、

なぜなら、多くの攻撃者は海外の規制が緩いプロバイダ/組織を隠れ蓑にしています。

という見解を述べました。Residential Proxyはこれの超・拡大解釈版です。

どうやって対策していくか? SSHのアクセスポートの変更

規制が緩いVPSはその大本のアクセス元を断ってしまえば対策は可能。しかし、一般人向けのISPとなると話は違います。

  • IPアドレスが極めて多く
  • そのISPからのアクセスを断ってしまうと他の善良なアクセス者が利用できない

という、利用者を人質に取った隠れ蓑です。(尤も、筆者は不正アクセスがあった瞬間、そのIPは二度目のアクセスをさせないのですが)

なので、手っ取り早く「SSHのアクセスポート(22番)」の変更から始めることになりました。

いくらFail2banにより不審なアクセスを弾き、鍵交換形式でセキュリティは担保されると言ったところで:

「攻撃者のアクセス元が数千倍に膨れ上がってきた」のでは話は別です。

Linuxのデフォルトである「22番ポート」を開放し続けることは、要塞の本丸の門の前に、毎日数百万人のゾンビ(ボット)が群がってドアノブをガチャガチャ回し続けているのと同じ状態です。

これがサーバーに与える実害は、単に「ハッキングのリスク」だけではありません。物理的なリソース(パフォーマンス)を極悪なまでに食いつぶします。

  • 暗号計算によるCPUの無駄遣い
    • SSHの接続要求が来ると、OSは律儀に重たい暗号処理や鍵の検証、認証チェックを走らせます。ボットが何重にも同時アタックしてくるだけで、CPU・メモリ・ディスクI/Oを継続的に消費します。
  • メモリの圧迫 SSHは接続が来るたびに、個別の sshd 子プロセスをメモリ上に生成します。
    • ボットの同時訪問により、メモリ空間がジワジワと侵食されていきます。
  • ストレージ(I/O)のボトルネック化 ボットが認証に失敗するたび、システムは「認証失敗(Failed password…)」のログをディスクに猛烈な勢いで書き込み続けます。
    • この連続的な書き込み処理(ディスクI/O)のせいで、本来のWebサーバーのためのディスクの読み書き速度が奪われます。
  • セキュリティを高める
  • パフォーマンスを上げる

の一挙両得を狙っていきます。

作業の前の注意点:

  • これはSSHのポートを変更する作業です。
  • そして、SSHのサービスを再起動します。

つまり、心臓に悪い作業です。下手したら自分自身がアクセスできなくなる事態も十分発生します。

  • 実サーバの場合は実機で作業する
  • VPSの場合はシリアルコンソールで接続する

が必須です。この環境が用意できない方はここから先は作業をしないでください。

※筆者が上手くいったという手順です。参考にする方は必ず裏取りを取ってください※

環境

  • XServer VPSを利用
  • Ubuntu 24.04
  • UFW導入済み
  • Fail2ban導入済み

サクッとはしているけど心臓に悪い手順

  1. UFWが開放するポートを別に変えます。(ここでは22→36878)
  2. XServer側のパケットフィルターを変更します。
  3. Fail2banの設定を変更します。
  4. SSHの待ち受けを変更します。
  5. systemd SSHソケットアクティベーションの書き換えを行います。
  6. 設定を反映して別ポートに変更します。
  7. サーバそのものを再起動して再確認を行います。

Step 0. 推奨:ディスクイメージのバックアップ

何かあったときのために、ディスクそのもののバックアップ(スナップショット)を取っておくことを強く推奨します。

Step 1. OS内ファイアウォール(UFW)の解放

※ここからは実機/シリアルコンソールで作業を行います※
※平行して自分の端末とSSHセッションを張っておきます※

まずはOSの内部で、新ポート(36878)を通すようにルールを追加しました。

他のポートとバッティングしないものを選んでください。

  • 36878番ポート(TCP)へのアクセスを許可
sudo ufw allow 36878/tcp comment 'SSH新ポート'
  • 古い22番ポートの許可ルールを削除
sudo ufw delete allow 22/tcp
  • 設定を反映
sudo ufw reload
  • 設定反映確認
sudo ufw verbose

22/tcpが消えていることと、以下のようなメッセージがあることを確認します。

36878/tcp                  ALLOW       Anywhere SSH新ポート

自端末から新しいSSHセッションを立ち上げ、端末とSSH通信ができないことを確認します。

この段階では既存SSHセッションを絶対に閉じないでください

Step 2. インフラ(XServer)側パケットフィルターの変更

OSにボットのパケットが届く前に、XServerのインフラレベルで遮断するための設定です。

  1. XServer VPSパネル > パケットフィルター設定 を開く。
  2. [ +パケットフィルター設定を追加する ] をクリック。
  3. 以下の内容で追加:
  • フィルター: 手動で設定
  • プロトコル: TCP
  • ポート番号: 単一ポート / 36878
  • 許可する送信元IPアドレス: 全て許可
  • メモ: SSHのポート変更 等を付与。
  1. [ 追加する ] を押して確定。
  2. 既存のルール一覧から、古い SSH TCP 22 の横にある [ 削除 ] ボタンを押し、不要なポートを完全に塞ぐ。

Step 3. Fail2ban(検知ツール)の設定変更

ポートが変わっても、Fail2banが正しく不正アクセスを監視・検知できるように設定を追従させました。

  • バックアップ取得
sudo cp /etc/fail2ban/jail.local /path/to/backup/directory/jail.local.$(date +%Y%m%d)

→ 自分の環境に合わせます。

  • バックアップ取得確認
diff -u /path/to/backup/directory/jail.local.$(date +%Y%m%d) /etc/fail2ban/jail.local 

→ エラーがないことを確認します。

  • /etc/fail2ban/jail.local を編集し、[sshd] セクションのポートを書き換え:
[sshd]
enabled=true
filter=sshd
mode=normal
#port=22                     
#Port変更 
port=36878                   
protocol=tcp
  • 編集後確認
diff -u /path/to/backup/directory/jail.local.$(date +%Y%m%d) /etc/fail2ban/jail.local 
- port=22                     
+ #port=22                     
+ #Port変更 
+ port=36878     

Step4. SSHコンフィグの変更

  • バックアップ
sudo cp -pi /etc/ssh/sshd_config /path/to/backup/sshd_config.$(date +%Y%m%d)
  • バックアップ取得確認
diff -u /path/to/backup/sshd_config.$(date +%Y%m%d) /etc/ssh/sshd_config 

→ エラーがないことを確認します。

  • ファイル編集(筆者例)
#Port 22
Port 36878

編集後に保存。

  • 差分確認
diff -u /path/to/backup/sshd_config.$(date +%Y%m%d) /etc/ssh/sshd_config 
- Port 22
+ #Port 22
+ Port 36878

Step 5. systemd SSHソケットアクティベーションの書き換え

筆者が一番詰まったところです。

これをやらなかったためにSSH接続できませんでした。

Ubuntuには1/init が22番ポートを掴んでしまう仕様があります。

それを回避し、IPv4/IPv6の両方で新しく設定したポート(36878番)を待ち受けさせるための確実な設定です。

  • 設定上書き用のディレクトリを作成:
sudo mkdir -p /etc/systemd/system/ssh.socket.d
  • 設定ファイル /etc/systemd/system/ssh.socket.d/listen.conf を新規作成し、以下の 4行 を記述:
[Socket]
ListenStream=
ListenStream=36878
ListenStream=0.0.0.0:36878

※ ポート番号は自分が設定したものです。

ListenStream=(右側空欄)で、デフォルトの22番の待ち受けを一度綺麗にクリアするのがポイントです。

Step 6. 設定の反映と確認

  • systemdの設定をリロード
sudo systemctl daemon-reload
  • ssh再起動
sudo systemctl stop ssh.service
  • 新しい設定でソケットとFail2banを再起動
sudo systemctl restart ssh.socket
sudo systemctl restart fail2ban
  • UFW(ファイアウォール)を有効化
sudo ufw enable
sudo netstat -lntp | grep 36878

以下のように、36878 番ポートが 1/init によって正常に LISTEN されていれば成功です。

tcp6       0      0 :::36878                :::* LISTEN      1/init
  • 自環境内でのSSH接続確認
ssh -p 36878 localhost

自分の端末からSSH接続できることを確認します。

  • サーバ再起動
sudo reboot

この段階でサーバそのものを再起動します。なぜなら、ここで「SSH接続できるはず」としても、不慮の再起動で設定が違うなどがあり得るからです。

Step 7. 設定反映確認

※ここからはSSH接続クライアントからの接続確認です。※

SSH接続クライアントの接続ポートを22から設定したポート(上記例では36878)に変更して接続します。

接続できれば設定完了です。

まとめ

この作業はサーバ初期設定からやっておくべきものでしたが、ようやく心臓に悪い作業から解放です。

ポート変更後、筆者環境ではWebサーバのレスポンス改善を確認したました。

これは「22番だから侵入される」という話ではありません。

日々押し寄せる大量の自動化ボットによるSSH接続試行そのものをOSへ到達させないことで、限られたサーバ資源を本来のサービス提供へ集中させられたためと考えています。

なお:半ば筆者のVPSは公開情報ではありますが、上記設定とは異なるポート番号で設定されていることは補足しておきます。

AdGuardの管理画面をSSL化するための手順

前回の続き、導入したAdGuardの管理画面を常時SSL化していく作業です。

自宅内NWに立てることを前提としていても

「SSL化しておかないと寝覚めが悪い」

という性分のため、これを実施します。

環境

  • Ubuntu 26.04
  • Apache 2.4
  • AdGuard導入済み(ポートを8080に変更)

そもそもの問題として

「なんでリバースプロキシーなのにnginxじゃあないのか」

という方がいるかと思いますが、

「apacheでもこの設定は十分可能」

という例示のためです。

前提

  • AdGuardの管理画面に紐付くDNS設定が完了している。
  • そのドメインに即した証明書がある。

さっくりとした手順

  1. Apacheのプロキシモジュールを有効化します。
  2. Apacheのログディレクトリを作成します。
  3. AdGuard管理画面用のバーチャルサイトを作成します。
  4. ログのローテーション設定を行います。
  5. 設定を反映させます。
  6. 動作を確認します。

Apacheのプロキシモジュールを有効化する

まずはApacheがポート:8080を待ち受けてリバースプロキシサーバーとして動けるように、必要なモジュールを有効化します。

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod rewrite
sudo a2enmod ssl

有効化したら、一度Apacheを再起動しておきます。

sudo systemctl restart apache2

ログディレクトリの作成

この段階でログディレクトリを作成する理由は

「この段階でやっておかないと忘れるから」

に尽きます。ログは障害の切り分けとして極めて重要です。特にAdGuardは自宅内のNWをほぼ司ります。このときに何か異常が無いかを調べるためにも今の段階で作ります。

  • ログディレクトリの作成
sudo mkdir /var/log/adguard

※名前は自分が管理しやすい者に変更してください。

  • ログディレクトリの所有者変更
sudo chown -R www-data:www-data /var/log/adguard

これは筆者の好みの問題です。(ログローテートの際にwww-dataが参照できるようにするため)

  • ログディレクトリの所有者変更確認
ls -ld /var/log/adguard

所有者とグループがwww-dataを確認します。

管理画面用のバーチャルサイト設定ファイル作成

/etc/apache2/sites-available 内に、adguard.conf等の分かりやすい名前のファイルを管理者権限で作成します。

※ドメイン名は確実に自分の環境に合わせてください

<VirtualHost *:80>
    ServerName adguard.example.com
    # HTTPでアクセスされた場合は自動的にHTTPSへリダイレクト
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName adguard.example.com

    #ログ設定
    ErrorLog /var/log/adguard/adguard_ssl_error.log
    CustomLog /var/log/adguard/adguard_ssl_access.log combined

    # SSL設定
    SSLEngine on
    SSLCertificateFile    /etc/certs/あなたの証明書.crt
    SSLCertificateKeyFile /etc/private/あなたの秘密鍵.key
    # 中間証明書(CA Bundle)がある場合は、下の行のコメントアウトを解除してパスを指定してください
    # SSLCertificateChainFile /etc/certs/中間証明書.crt

    # プロキシ設定(Apacheが受けて後ろのAdGuardに流す)
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>

ログローテーションファイルの作成

/etc/logrotate.d/配下にadguard等の名前をつけて、以下の通り設定します。

※ログディレクトリは設定したディレクトリです。以下は一例なので好みに合わせて設定ください。

/var/log/adguard/*.log {
    daily
    missingok
    rotate 10
    compress
    copytruncate
    notifempty 
    su www-data www-data
}

設定反映

  • バーチャルサイト設定ファイルの登録
sudo a2ensite adguard.conf

※作成したファイル名です

  • ファイルの整合性確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • Apache再起動
sudo systemctl restart apache2.service
  • Apache再起動確認
systemctl status apache2.service

active(running)を確認します。

動作確認

自分のブラウザで

https://adguard.example.com

など、設定したURLにアクセスします。

  1. 設定した管理画面にドメインだけで入れること(ポート番号の付与などが必要ないこと)
  2. SSLで通信できること

を確認します。

ログの所有者変更

これは、筆者の設定の問題。www-dataにしている人向け。

なぜなら、サイトの設定反映後、基本的に/var/log/adguard配下で作成されたログはroot権限で作成されます。しかし、ログローテーションはwww-dataでローテーションするようになっています。これを合わせます。

sudo chown www-data:www-data /var/log/adguard/*.log

以上で「AdGuardを本格的に運用する準備」が整いました。

AdGuard HomeをUbuntu 26.04にインストールしたときのメモと詰まった所。

AdGuard Homeの仕組み:「通信の黒幕」を先回りして止める

通常のブラウザ拡張機能(uBlock Originなど)が「ダウンロードされたページから広告を非表示にする」のに対し、AdGuard Homeは「広告がダウンロードされる前に、接続そのものを遮断する」という方法をとっています。

これを実現しているのがDNSという仕組みのコントロールです。

インターネット上のWebサイトは、すべて「203.0.113.x」のような数字のIPアドレスで動いています。私たちが「google.com」のような分かりやすい名前(ドメイン)を入力したとき、それを数字の住所に変換してくれる案内人がDNSサーバーです。

AdGuard Homeは、家の中のネットワークでの「案内人」の役割を買って出ます。

広告が消える4ステップ

  1. リクエスト: スマホやPCがWebサイトを開こうとすると、ページ内に含まれる広告サーバー(例: ads.example.com)の住所をAdGuard Homeに問い合わせます。
  2. 照合: AdGuard Homeは、あらかじめ登録されている「広告ブロックリスト(ブラックリスト)」と、その問い合わせを照合します。
  3. 遮断: もしリストに載っている広告サーバーだったら、AdGuard Homeは正しい住所を教えず、「そんな住所はありません(NXDOMAIN)」と嘘の返事をします。
  4. 結果: スマホ側は広告のデータをダウンロードすることすらできなくなり、結果として広告が表示されなくなります。

もっと有り体に言うと:ルータ前に位置する『キング・クリムゾン』

本来、スマホやPCがWebサイトを開くときは、以下のような「過程」を踏んでいます。

  • 【通常の過程】
サイトを読み込む → 広告の存在に気づく → 広告サーバーにデータを貰いに行く → 広告が表示される

しかし、AdGuard Homeが介入すると、この時間が消し飛びます。

  • 【AdGuard Home発動】
サイトを読み込む → ×(通信を消し飛ばす)× → 広告の無い「結果」だけが画面に残る!

スマホやアプリからすれば、広告を取得しようとした認識すら残らず、気づいたときには「すでに広告が消滅した快適なページが開いている」という状態になります。

広告サーバー側から見れば、エピタフ(未来予知)で先回りされて、最初から攻撃(広告配信)を無効化されているようなものです。

これを入れるときのメモ

Ubuntu 26.04で実施しました。

注意点

これは、ホームネットワークのDNSを書き換えるための作業です

このヤバさが分からない方は、ここより先は読まない方が破滅から逃れられます。

公式インストーラーを実行します。

curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v

この後、rootパスワードが聞かれるのでそれを入力します。

Ubuntuはデフォルトで systemd-resolved という仕組みが53番ポート(DNS)を占有しています。

更に、AdGuardはDNSを「置き換えます」

これが罠でした。つまり、

  1. OSは「このドメインのIPアドレスは?」とDNSに問い合わせる。
  2. DNSサーバーは必要に応じて上流のDNSへ問い合わせを行う。
  3. 最終的に正しいIPアドレスが返され、通信先が決定される。

仕組みを取っています。

しかし、この、DNSが置き換えられた状態で自分のサイトのドメイン(hoge.example.com)をローカルアドレス127.0.0.1と登録してしまうと、家のネットワーク内にあるクライアントから

「hoge.example.comを探せ」と命令すると、「ああ、ローカルアドレスだね、127.0.0.1だよ」と返します。そうするとクライアントは「OK。127.0.0.1へ接続するんだ」と判断し、自分自身へ接続しようとします。

という、「終わりがないのが終わり」というレクイエムを喰らいます。特に、サーバ内に他のWebサイトを探していたりSSH接続でドメインを指定していると更に致命的です。

これを防ぐためのおまじないをかけておきます。

  • hostsファイルの書き換え
/etc/hosts

に、以下のように入力します。

# ローカルアドレス
127.0.0.1 localhost
# 自分のサーバのドメイン(自分の環境に合わせます)
192.168.1.5 hoge.example.com 
# サーバ内で運用している別サイトのドメイン
192.168.1.5 site.example.com 

というのも、Linuxでは通常、DNSより先にhostsファイルが参照されます。

厳密には名前解決順序はNSS設定に従いますが、Ubuntuの標準構成ではhostsが先に評価されるため、サーバ自身のドメインをここへ記載しておくことで名前解決の事故を避けられます。

ざっくり言うと、まず hosts が参照され、見つからなければ設定されたDNSへ問い合わせが進みます。

なので、これをやっておかないと「サービスを起動した瞬間にSSHからはじき出される」状態も起こり得ます。

  • AdGuard Home用の設定ディレクトリを作成します。
sudo mkdir -p /etc/systemd/resolved.conf.d
  • 競合を避けるための設定ファイルを管理者権限で作成します。
/etc/systemd/resolved.conf.d/adguardhome.conf
[Resolve]
DNS=127.0.0.1
DNSStubListener=no

/etc/resolv.conf の確認

AdGuard Homeを導入した後は、OSが参照するDNSサーバーが正しく設定されているか確認します。

cat /etc/resolv.conf

私の環境では以下のようになりました。

nameserver 127.0.0.1
nameserver 8.8.8.8
nameserver 1.1.1.1
search .

ただし、このファイルは systemd-resolved や NetworkManager によって自動生成される場合があります。

環境によっては直接編集しても再起動時に上書きされるため、設定変更を行う場合は現在の管理方法を確認してから作業してください。

注意点

  • 「お前が最初に見に行くDNSはこれだ!」を決める、Linuxにおける神像のようなファイルです。
  • ワンミス即死があり得ます。
  • そのため、筆者が推奨するバックアップのやりかたは敢えて省いています。

「なあに 心配するな しくじってもたかが死ぬだけよ」

ぐらいの精神で、「自分がネットワークに接続できなくなる」の精神を持ちましょう。

ブラウザから初期設定を行う

ここがApacheが入っている環境での重要なポイントです。

  1. 同じネットワーク内のPCのブラウザから、http://[UbuntuのIPアドレス]:3000 にアクセスします。
  2. 設定ウィザードが始まります。

設定時の重要チェックポイント

  • 管理インターフェース(管理画面のポート)
  • デフォルトでは 80 になっていますが、絶対に 80 のまま進めないでください(すでにApacheが80番ポートを使っているため衝突します)。
  • ここでは一旦、80803000 など、Apacheが使っていない別のポートを指定してください。
  • DNSサーバー(DNSのポート)
  • こちらは 53 番ポートのままでOKです。
  1. ユーザー名とパスワードを設定し、ウィザードを完了させます。

これで無事にAdGuard Homeが起動し、指定したポート(例: http://[UbuntuのIPアドレス]:8080)で管理画面にログインできる状態になりました。

これから

  1. この管理画面の常時SSL化
  2. 肝心要のAdGuardの使い方
  3. そこで詰まった所

などは改めて書きます

Page 1 of 29

Powered by WordPress & Theme by Anders Norén