カテゴリー: ガジェット Page 1 of 109

X Server切り替え1年後の雑感。

こちらの記事から約一年。

8/03 : 契約、初期設定(UFW / fail2ban)

とあることから、実質丸一年が経過。その1年での振り返りを行ってみたいです。

2026年8月時点でのリソース使用量

--- 💻 サーバーのCPU状態 ---
  稼働時間 (Uptime): 8 days
  ロードアベレージ (Load Avg): 0.22, 0.22, 0.19
  CPU名 (Model): AMD EPYC-Milan Processor
  スペック (Speed): 2.00 GHz (1996.250 MHz)
  コア数 (Cores): 4 threads

--- メモリとスワップ ---
[物理メモリ(Physical Memory)]
                             total        used        free      shared  buff/cache   available
Mem:           5.8Gi       3.7Gi       447Mi       163Mi       2.1Gi       2.1Gi
Swap:          4.9Gi       1.1Gi       3.8Gi

[zRAM の展開状況]
NAME       DISKSIZE  DATA  COMPR ALGORITHM STREAMS ZERO-PAGES  TOTAL MEM-LIMIT MEM-USED MIGRATED MOUNTPOINT
/dev/zram0     2.9G  1.1G 254.4M lzo-rle         4      50834 261.6M        0B   948.3M     199K [SWAP]

--- ストレージ(Storage Status) ---
Filesystem      Size  Used Avail Use% Mounted on
  /dev/vda1       145G   62G   83G  43% /
  /dev/vda16      881M  222M  598M  28% /boot
  /dev/vda15      105M  6.2M   99M   6% /boot/efi
  s3fs            4.0G     0  4.0G   0% /mnt/wasabi2
  s3fs            4.0G     0  4.0G   0% /mnt/wasabi

--- CPUを利用しているプロセス: CPU Consumers (Top 10) ---
%CPU   %MEM   PID      USER         UNIT                 COMMAND
------------------------------------------------------------------------------------------
8.0    4.3    796409   www-data     apache2.service      Passenger RubyApp: 
4.4    6.8    621372   mysql        mysql.service        /usr/sbin/mysqld                                  
2.8    3.6    796362   www-data     apache2.service      Passenger AppPreloader:     
1.1    2.0    789277   www-data     apache2.service      /usr/sbin/apache2 -k start                        
0.7    2.1    784299   www-data     apache2.service      /usr/sbin/apache2 -k start                        
0.6    2.0    784297   www-data     apache2.service      /usr/sbin/apache2 -k start                        
0.6    3.9    622142   root         growi.service        node --import 
0.6    1.8    620879   mongodb      mongod.service       /usr/bin/mongod --config /etc/mongod.conf         
0.5    0.4    784275   root         apache2.service      Passenger core                                    
0.5    0.2    622750   www-data     apache2.service      /usr/sbin/apache2 -k start                        

--- メモリを利用しているプロセス: Memory Consumers (Top 10) ---
%CPU   %MEM   PID      USER         UNIT                 COMMAND
------------------------------------------------------------------------------------------
0.4    11.2   479114   elastic+     elasticsearch.service /usr/share/elasticsearch/jdk/bin/java -Des.network
4.4    6.8    621372   mysql        mysql.service        /usr/sbin/mysqld                                  
8.0    4.3    796409   www-data     apache2.service      Passenger RubyApp: 
0.0    4.1    786930   www-data     apache2.service      Passenger RubyApp: 
0.6    3.9    622142   root         growi.service        node --import ./bin/runtime/env-preload.mjs dist/s
0.0    3.8    796066   www-data     apache2.service      Passenger RubyApp: 
2.8    3.6    796362   www-data     apache2.service      Passenger AppPreloader:     
0.0    3.2    990      root         mnt-wasabi.mount     
0.2    2.9    789814   www-data     php8.3-fpm.service   php-fpm: pool www                                 
0.4    2.8    794701   www-data     php8.3-fpm.service   php-fpm: pool www                                 

何というか

  • Growi
  • Nextcloud
  • Redmine

というメモリを食いまくるサイト(そのほかLaravel Stack)を運営していてOoM Killerが発生しなかったものだと思っています。

なぜメモリが枯渇しなかったか

アクセスを絞っている

これが一番のポイントです。筆者は広告が嫌いなので、自分のサイトにも作っていません。そのため、SEOや広告料収入によるアクセスアップを狙っていません。

「これは俺のメモ帳だ。節度を持ってアクセスする分にはかまわないがそれ以外は容赦しない」

というスタンスを貫いています。

クローラーや迷惑ボットの排除

  • 一度でも不審な攻撃/その兆候を見せたら次のアクセスを禁じる「ONE OUTS」システム
  • 雑なクローラーを別サイトへと転送。その推論モデルを破綻させるような文言を随所に仕込み、Webアプリの重い処理に到達させない「Jailhouse Lock」システム

更に、tcpレベルでの嫌がらせをやらかす相手にはipsetで防御。

https://barrel.reisalin.com/books/one-outs/page/ipsetddos

zRamによるメモリ圧縮

先にあった、Swapにブロックデバイスを追加することにより、スワップアウトが更に効率化しています。

https://barrel.reisalin.com/books/linux/page/ubuntuzram

AIとの対話によるセキュリティ案強化

これが一番です。Mod_Securityを「偽陽性を踏んだから単にそれをRemoveしよう」という雑な運用から

「偽陽性を踏みがちなアクション(記事の投稿、ファイルのアップロード)だけを無視する」というリアルタイム外科手術的な技法へと変換。

これにより

「攻撃は防ぐ 偽陽性は排除する おまえごときに『両方』やるのは そうそう難しいことじゃあないな」

と言えるようになったのが極めて大きいです。

結局のところ

「4GB→6GB」にスペックアップしたとしても普通に運用したらOoM Killerやアクセス過多に陥ったであろうシステムを

  • 自分のサイト運営方針に則って数を絞り
  • 不穏なアクセスを防ぐことが第一歩と考えた

AIの力を借りての運用という形でした。

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

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

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

暑さ対策グッズ。

体温以上の暑さが続く中、これだけは外せないという暑さ対策グッズはこれに落ち着きました。

アルミ張りの日傘

ワークマンにて購入。

直射日光を遮ることで、体感温度は数度から10℃近く下がると言われています。また、輻射熱を遮る点でも重要です。

サングラス

強い紫外線、まぶしさは目から入って脳に疲労物質を出させる原因になります。

普段の眼鏡と同じ度にすることで、普段の視界のまましっかり減光できます。

携帯式氷嚢

血管が集まる首元、首の横、脇の下、手首などを直接冷やせます。

この対策グッズでもMVPです。ハンディファンで生暖かい風を浴びるより合理的です。

このシンデレラフィットするかのようなヒップバッグも見つけられました。これで鞄にもつけられるので満足。

この暑い夏は始まったばかりですが、なんとか死なないように抑えていきたいものです。

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です。

カラビナつきバッテリー

ワークマン、時々面白いものが見つかります。今回見つけたのはこちら。

モバイルバッテリー&ライト。

一見、ごついカラビナのようですが……

この付け根にUSBケーブルがついています。

つまり、モバイルバッテリーという取り回しに困るものを「ベルトや鞄に固定させる」ことで充電を可能にする仕掛け。

御丁寧に外で使うためIP66という認証までついています。それだけに留まらず

ライトつきです。動力そのものがバッテリーという、盲点のような状態。

注意点として:

  • Type-C:バッテリーそのものの充電時に利用
  • Type-A:他の機器への充電時に利用

と極性が固定されていることのようですのでケーブルに注意が必要でした。

いずれにしても、外出しでき、いざというときに迷わないバッテリーは重要です。

携行文具一式(2026年7月版)

若干のアップデートがありました。

「とりあえず、これとスマートフォンがあれば大概の場所でも作業ができる」厳選道具群です。

下段左から

ほぼ日手帳

こちらは日記帳。2021年の1月に途切れましたが、そこからは日々欠かさず、全ての日付で書いています。

カバーは『SPY×FAMILY』コラボ。

ThinkPad

出先での母艦です。一時期、やたらと画面がオフになり、どうすんだこれ状態に陥りかけましたが、のぞき見防止フィルターが触れた面でそうなっているだけと判明。その位置を調整して事なきを得ています。

iPad mini

混雑した電車の中で用いるメインウエポン。様々なタブレットを触った中で

  • iPhoneとの連携
  • Apple pencilによる最高の入力体験
  • 大容量

と、忙しいときでもブログが書け、電子書籍リーダーとしても八面六臂の活躍。少しバッテリーが怪しいので次世代をどうするかが懸案中です。

PC周辺機器ポーチ

スリザリン×アフターヌーンティーコラボ。

  • ACアダプター
  • マウス
  • 充電ケーブル
  • PC用イヤホン
  • メモリカードリーダー
  • USBメモリ

などのジャストサイズ。

ペンケース

母手製の大容量ペンケース。LAMYぎっしり。課題は万年筆がおざなりなってしまうこと。メンテンアンス頻度を上げていく工夫が求められます。

トラベラーズノート

こちらは利用頻度が上がってきました。メモ帳だけでなく、L判縦の写真がそのまま貼り付けられるサイズ感も気に入っています。

と、それなりに量はありますが、これがないと私の一日は始まらないレベルの必須品です。

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

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

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

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

Linuxエディタ「nano早見表」

論争があるのは知っていますが、それでも、これを紹介する理由があります。

「複数行の選択を可能に出来た」

という今更ながらの感動があったので。

ファイル操作

操作キー
保存Ctrl + O
Enterで保存確定Enter
終了Ctrl + X

カーソル移動

操作キー
上・下・左・右矢印キー
行頭へ移動Ctrl + A
行末へ移動Ctrl + E
1ページ上Ctrl + Y
1ページ下Ctrl + V

検索・置換

操作キー
検索Ctrl + W
次を検索Alt + W
置換Ctrl + \

コピー・切り取り・貼り付け

1行だけ操作

操作キー
行を切り取り(削除)Ctrl + K
貼り付けCtrl + U

複数行を操作

  1. 選択開始位置へ移動
  2. EscA(または Ctrl + ^)でマーク開始
  3. カーソルを移動して範囲選択
  4. 以下を実行
操作キー
選択範囲を切り取り(削除)Ctrl + K
選択範囲をコピーAlt + 6
貼り付けCtrl + U

Undo / Redo

操作キー
元に戻すAlt + U
やり直しAlt + E

その他便利なコマンド

操作キー
ヘルプCtrl + G
カーソル位置表示Ctrl + C
指定行へジャンプCtrl + _
コマンド一覧表示Ctrl + G

Linux端末でよく使うキー

^ = Ctrl

M- = Alt(Metaキー)

Altキーが効かない端末では、

Esc を押して離し、その後キーを押す

例:

  • EscA = Alt + A
  • EscU = Alt + U
  • Esc6 = Alt + 6

よく使う操作の流れ

複数行を削除

Esc → A
↓で範囲選択
Ctrl + K

複数行をコピー

Esc → A
↓で範囲選択
Alt + 6
Ctrl + U

検索・置換

Ctrl + W
検索文字入力
Enter

Ctrl + \
検索文字
Enter
置換文字
Enter

教材価値の高いログ:なぜ攻撃者は最初にWordPressを探すのか

筆者に限らず、Webサーバを運用すると必ずと言っていいほど見かけるアクセスがあります。

GET /wp-login.php
GET /wp-admin/
GET /wp-config.php
GET /config.php
GET /.env

言わずと知れたWordpress/laravel管理画面のURLやDB情報などが飛び交う重要ファイル。

「うちはWordPressじゃないんだけど?」

と思う人も多いでしょう。実際、筆者のVPSもWordPressを詰んでいません。(公開しているWordpressは専用サービスを借りています)

それでも毎日のように飛んできます。では、攻撃者は何を考えて「見たらアクセスするのが礼儀」レベルでアクセスしてきているのでしょうか。

インターネットは「総当たり」の世界

多くの人は、「誰かが自分のサイトを狙っている」と考えがちです。

しかし実際には逆です。攻撃者は運営元をまず見ません。彼らが見ているのは

「世界中に存在する何千万台ものWebサーバ」

です。ですから

WordPress
Laravel
Node.js
Next.js
Joomla
Drupal

といった利用者が非常に多い技術から順番に試します。

WordPressが最優先になる理由

WordPressは世界中のWebサイトのかなりの割合を占めています。つまり、攻撃者は

「WordPressだけ狙う」

だけでも膨大な成果が期待できます。さらに

  • 古いプラグイン
  • 更新されていないテーマ
  • 初期設定のままの管理画面

なども珍しくありません。攻撃者から見ると、WordPressは「狙う価値が非常に高い標的」です。

そして、Wordpressは

/wp-config.php
/wp-config-sample.php
/wp-login.php

など、「アキレス腱となるファイル」がほぼ決まっているという「やりやすい」材料です。

実際のログ

以下は実際に筆者のサーバへ届いたログを、IPアドレスやホスト名をダミー化したものです。

なぜ、この手の攻撃者情報を晒さないかというと「テロリストに名前を与える必要はない」という哲学と、「誰がやったかは問題ではない。私が興味があるのは何をしたかである」という考えです。

[Mon Jul 06 01:06:34.509624 2026] [security2:error] [pid 111111:tid 222222222222222] [client 192.0.2.1:40372] [client 192.0.2.1] ModSecurity: Warning. Matched phrase "config.php" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: config.php found within REQUEST_FILENAME: /config.php"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "example.com"] [uri "/config.php"] [unique_id "DUMMY_UNIQUE_ID_000001AAAAA"]
[Mon Jul 06 01:06:34.510601 2026] [security2:error] [pid 111111:tid 222222222222222] [client 192.0.2.1:40372] [client 192.0.2.1] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/config.php"] [unique_id "DUMMY_UNIQUE_ID_000001AAAAA"]
[Mon Jul 06 01:06:34.511256 2026] [security2:error] [pid 111111:tid 222222222222222] [client 192.0.2.1:40372] [client 192.0.2.1] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "119"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=5, detection=5, per_pl=5-0-0-0, threshold=5) - (Outbound Scores: blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) - (SQLI=0, XSS=0, RFI=0, LFI=5, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=5)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/__jailhouse_lock/topgear.html"] [unique_id "DUMMY_UNIQUE_ID_000001AAAAA"]
[Mon Jul 06 01:06:40.426322 2026] [security2:error] [pid 333333:tid 444444444444444] [client 192.0.2.1:35882] [client 192.0.2.1] ModSecurity: Warning. Matched phrase "wp-config-" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: wp-config- found within REQUEST_FILENAME: /wp-config-sample.php"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "example.com"] [uri "/wp-config-sample.php"] [unique_id "DUMMY_UNIQUE_ID_000002BBBBB"]
[Mon Jul 06 01:06:40.427672 2026] [security2:error] [pid 333333:tid 444444444444444] [client 192.0.2.1:35882] [client 192.0.2.1] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/wp-config-sample.php"] [unique_id "DUMMY_UNIQUE_ID_000002BBBBB"]
[Mon Jul 06 01:06:40.428290 2026] [security2:error] [pid 333333:tid 444444444444444] [client 192.0.2.1:35882] [client 192.0.2.1] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "119"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=5, detection=5, per_pl=5-0-0-0, threshold=5) - (Outbound Scores: blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) - (SQLI=0, XSS=0, RFI=0, LFI=5, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=5)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/__jailhouse_lock/topgear.html"] [unique_id "DUMMY_UNIQUE_ID_000002BBBBB"]

要約するとこんな形です。

[client 192.0.2.1]
GET /config.php

REQUEST-930
Restricted File Access Attempt

↓

REQUEST-949
Inbound Anomaly Score Exceeded

↓

HTTP 403

数秒後、

GET /wp-config-sample.php

REQUEST-930
Restricted File Access Attempt

↓

REQUEST-949
Inbound Anomaly Score Exceeded

↓

HTTP 403

となりました。

このログから読み取れること

筆者がこのログに着目したのは

config.php
↓

wp-config-sample.php

という順番です。これは、

  1. 「PHPかな?」
  2. 「WordPressかな?」

という判定ルーチンそのもの。人間ならこの順番にはなりません。典型的な自動スキャナです。

さらに注目したい点

以前紹介した、ModSecurityのカスタム設定。

HostヘッダーがIPアドレスなら遮断

Hostヘッダーが存在しなければ遮断

というカスタムルール。今回は一切反応しませんでした。

つまり攻撃者は

  • DNSで名前解決
  • 正しいHostヘッダーを付与
  • 普通のHTTPリクエスト

を組み立てています。IPアドレスへ適当に投げるだけのBOTより一段階上です。

そのためにModSecurityは通常の検査を実施し、

REQUEST-930
↓
REQUEST-949
↓
403

という流れで処理しています。

筆者が「教材」と述べた面白い理由

このログには、

OWASP CRSの典型的な動きがそのまま残っています。

CRS:930
危険なリクエストを検知
↓
CRS:949
異常スコアが閾値を超えた
↓
処理:403アクセス拒否

つまり、「ModSecurityがどのように判断して遮断するのか」を学ぶ教材として非常に分かりやすいログです。

そして一番重要なこと

今回の攻撃は、筆者個人を狙ったものではありません。世界中のWordPressサイトを探す途中で、たまたま筆者のLAMPサーバへ届いただけです。(何せ、動いているのは全く別のCMSです)

しかし、その「たまたま」からでも学べることはたくさんあります。

ログには、

  • 攻撃者が何を探しているのか
  • どんな順番で調べるのか
  • WAFがどこで判断したのか

がすべて残っています。

サーバ管理者にとってログは「嫌なもの」ではなく、攻撃者の思考を無料で教えてくれる教材です。

Node.jsを狙った自動スキャン攻撃。その時、攻撃者は何を見ていたのか。

先だっての記事で、「IPアドレス直打ちやヘッダーがないアクセスはたたき落とす」という設定を紹介しました。では、そういう直打ち以外の「きちんとこちらのホスト名、ドメイン名を調べ、そしてリクエストヘッダーも指定した攻撃はどういうものになるのか?」の実例を、筆者が実際に受けたログから紹介します。

1. 検出されたログの抜粋

以下は、検知されたエラーログの要約です(連続して複数のルールに抵触したため、重要なものを抜粋・統合しています)。

今回も攻撃者のIPアドレスは残しません。「テロリストに名前を与えてはいけない」からです。

# 1. Unicode難読化とプロトタイプ汚染の検知
[Date/Time] [security2:error] [client 192.0.2.1] ModSecurity: Warning. Pattern match "(?i)\\\\x5cu[0-9a-f]{4}" at ARGS:0. [file "REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [id "920540"] [msg "Possible Unicode character bypass detected"] [data "ARGS:0={\x22value\x22:\x22{\x5c\x22then\x5c\x22: \x5c\x22$B0\x5c\x22}\x22,\x22then\x22:\x22$1:__proto__:then\x22, ..."] [hostname "example.com"]

# 2. リモートコード実行(RCE)の検知
[Date/Time] [security2:error] [client 192.0.2.1] ModSecurity: Warning. Pattern match ... at ARGS:0. [file "REQUEST-932-APPLICATION-ATTACK-RCE.conf"] [id "932235"] [msg "Remote Command Execution: Unix Command Injection"] [data "Matched Data: eval)(global[\x5c\x22\x5c\x5cu0042... .from('KGFzeW5jIGZ1bmN0aW9uKCl7..."] [hostname "example.com"]

# 3. Node.js インジェクションおよびプロトタイプ汚染ルールの発火
[Date/Time] [security2:error] [client 192.0.2.1] ModSecurity: Warning. Pattern match ... [file "REQUEST-934-APPLICATION-ATTACK-GENERIC.conf"] [id "934100"] [msg "Node.js Injection Attack 1/2"]
[Date/Time] [security2:error] [client 192.0.2.1] ModSecurity: Warning. Pattern match "__proto__" at ARGS:0. [file "REQUEST-934-APPLICATION-ATTACK-GENERIC.conf"] [id "934130"] [msg "JavaScript Prototype Pollution"] [hostname "example.com"]

# 4. 異常スコア超過によるアクセス拒否(403)の実行
[Date/Time] [security2:error] [client 192.0.2.1] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "REQUEST-949-BLOCKING-EVALUATION.conf"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 65)"] [hostname "example.com"]

2. 攻撃の意図

この攻撃の主な意図は、

  • サーバーの制御権の奪取(バックドアの設置)
  • クラウド認証情報の窃取」 です。

ログに記録されたペイロード(ARGS:0)を解析すると、以下の3つのステップを実行しようとしていたことが分かります。

  • プロトタイプ汚染 (Prototype Pollution):
    • JavaScript特有のオブジェクト仕様である __proto__then を悪用し、アプリケーション全体の挙動を書き換えようとしています。
  • 難読化によるWAFの回避:
    • Buffer という文字列を \u0042\u0075\u0066... のようにUnicodeエスケープし、WAFの単純な文字列チェックをすり抜けようとしています。
  • ペイロードの実行(RCE)
    • 送信データ内には Base64 でエンコードされたスクリプト(KGFzeW5j...)が含まれていました。これをデコードすると、// fast_recon_v6 — signature-rotated recon payload(自動偵察用スクリプト)というコメントが出現します。
    • 攻撃が成功すると、サーバー内部で eval() が実行され、AWSやGCPといったクラウド環境のメタデータ(IMDS)から認証情報を盗み出す、あるいは外部からコマンドを自由に入力できる状態(リモートコード実行)に陥る危険がありました。

3. 攻撃者の「技術スタック」

ログから推測される、この攻撃を仕掛けてきたボット(または攻撃者)の技術的な背景やツール群は以下の通りです。

  • 言語・ターゲット環境:
    • JavaScript / Node.js
    • ターゲットを完全に Node.js(Express や Next.js などのWebフレームワーク)に絞り込んでいます。
  • 使用ツール:
    • 既知の脆弱性スキャンツール(自動化ボット)
    • * ペイロード内のコメント(fast_recon_v6)から、アンダーグラウンドやオープンソースで流通している、あるいは独自に開発された高度な自動脆弱性スキャン・バックドア設置ツールキットである可能性が高いです。
  • インフラ(踏み台): クラウドサービス(Google Cloud など)
    • * 接続元IPを調査すると大手のパブリッククラウドに割り当てられたIPであり、攻撃者は自身のアドレスを隠すため、クラウド上のVPSや乗っ取ったサーバーを踏み台にしてスキャンを実行しています。

4. 攻撃が成功してしまった場合のシーケンス(最悪のシナリオ)

もしWebアプリケーション側に脆弱性があり、WAFを導入していなかった場合、攻撃は以下のように進行します。

sequenceDiagram participant Attacker as 攻撃者 participant Server as Webサーバー (Node.js) participant IMDS as クラウドメタデータ (IMDS) Attacker->>Server: 不正なJSONデータ (Prototype Pollution + Base64ペイロード) を送信 Note over Server: 脆弱性によりオブジェクトが汚染され、<br/>内部で eval() が実行される Server->>Server: 悪意あるスクリプト (fast_recon) が作動 Server->>IMDS: クラウドの認証情報 (IAMトークン等) をリクエスト IMDS-->>Server: 認証情報を返却 Server-->>Attacker: 盗み出した認証情報・サーバー情報を送信 Note over Attacker: サーバーの完全な制御権、<br/>およびクラウド全体の権限を掌握

5. 今回の ModSecurity が阻止した実際の動き

実際には、ModSecurity(OWASP CRS)がリクエストの段階で脅威を検知し、アプリケーションにデータが届く前に処理を遮断しました。

sequenceDiagram participant Attacker as 攻撃者 participant WAF as WAF (ModSecurity) participant Server as Webサーバー Attacker->>WAF: 不正なJSONデータを送信 (ホスト: example.com) Note over WAF: ルール 920540: Unicode難読化を検知<br/>ルール 934130: __proto__ (プロトタイプ汚染) を検知<br/>ルール 934100: Node.jsコード注入を検知 Note over WAF: 異常スコアを計算 (合計 65点 / 閾値 5点)<br/>スコア大幅超過のため、アクセスを拒否 WAF-->>Attacker: HTTP 403 Forbidden (アクセス拒否) Note over Server: 攻撃リクエストはサーバー(アプリ層)<br/>に到達せず、無傷で終了

6. そもそもの問題として

筆者のサイトにNode.jsは含まれていないというのがポイントです。筆者の技術スタックが

  • LAMP
  • もしくはRails

という枯れた技術だったのが幸いしていました。攻撃者は「Node.jsだと決めつけていた」のです。

自動スキャンは効率を重視します。相手が誰かではなく、「大量に存在する技術スタック」に対して決め打ちでペイロードを送りつけます。つまり、この攻撃は筆者個人を狙ったものではなく、「Node.jsで動いているなら当たれ」という散弾銃です。そして今回は、その散弾がたまたまLAMP環境に飛んできただけでした。

WAFはその誤射を淡々と403へ変え、ログだけを残しました。ログを読むということは、「何を防いだか」を知るだけではありません。攻撃者が世界をどう見ているかを知ることでもあるのです。

とはいえ、筆者がModSecurityで設定している

# 1-3. 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.'"

を見事すり抜けました。つまり、きちんと筆者VPSのドメインを調べ、正しいHostヘッダーを付与してきたということです。この点については、攻撃者に敬意を表します。少なくとも、IPアドレス直打ちしかできないボットよりは、こちらを相手として認識していたのですから。そのおかげでWAF(ModSecurity)は仕事をし、評価を行い、その上でブロックしました。

この状況をたとえて言うならば『ジョジョの奇妙な冒険 戦闘潮流』の

まず、このワムウに殺される資格はあるッ!

といったところでしょうか。

Page 1 of 109

Powered by WordPress & Theme by Anders Norén