タグ: Linux Page 1 of 34

SSH不正アクセス元の傾向(geoiplookupの使い方)

「取り敢えず乗っ取れそうなサーバがあるなら攻撃する」ぐらいの勢いでSSHに接続する輩。その数は浜の真砂のなんとやらです。

そこでふと思ったのが「どこの国からの不正攻撃が多いのか」という興味。これを調べてみます。

環境

  • Ubuntu 24.04
  • 公開鍵認証
  • fail2ban導入済み

まず、現在のBANリストの傾向を見る

以下を使って調べます。

 sudo fail2ban-client status sshd | grep "Currently banned"

結果

   |- Currently banned: 8473

なんと、8500にも及ぶIP群。これらをnslookup / digで調べるのは非効率。そして、それらを一覧してシェルスクリプトを組むのもDNSのクエリーを食い潰します。

geoiplookupによる調査

そこで、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一本でサーバを公開する」という宣言は自由ではありますが「これだけの悪意と戦う自由」との隣り合わせ。

こちらの記事を再掲しますが、

鍵交換認証にする理由
  • パスワードが送信されない
  • パスワード認証では、パスワード自体がネットワーク上を流れるため盗聴リスクがあります。
  • 鍵認証では、秘密鍵が署名を生成し、署名のみが送信されるため、秘密情報が直接送られることはありません
  • 総当たり攻撃に強い
  • パスワードは文字数が少ないと短時間で破られる可能性があります。
  • 鍵認証では、2048ビット以上の鍵が使われることが多く、現在の一般的なサーバの計算能力では事実上破ることが不可能です。
  • 盗聴されても再利用できない
  • 鍵認証では毎回異なるチャレンジに対して署名を行うため、録音や再送信による攻撃(リプレイ攻撃)が通用しません。
  • フィッシング耐性が高い
  • パスワード認証は偽サイトに入力してしまうリスクがあります。
  • 鍵認証では秘密鍵がローカルに保管されており、外部に送信されないためフィッシングに強いです。

は、心に留めておくべきSSHの運用です。

Growiのsystemdと起動スクリプトの修正。

以下の環境でGrowiを利用。

  • Growi v7.4.1
  • node v20.10.2
  • Ubuntu 24.04
  • Growi実行環境 /home/www-data/growi
  • Growi実行ユーザ:root

v7.4.1で以下の問題点にぶつかったため、growiのスタートアップスクリプトとsystemdで対処したときのメモです。

問題点

  • daemon-reload の遅延: 設定反映に約5分を要していました。
  • 起動プロセスの停滞: サービス開始から実際にアクセス可能になるまで約6分かかっていました。(以前は数秒)
  • 不安定な運用: 異常終了時の自動再起動設定がなく、ログも標準出力のみで追跡が困難でした。

旧設定

  • /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。

  1. systemdの過負荷: ConditionPathExists が大規模なディレクトリ(growi)をチェックする際、OSレベルでスキャン待ちが発生していた可能性。
  2. NVMの初期化コスト: 起動のたびに nvm.sh を読み込んでいた。これは数百行のシェルスクリプトを実行する処理であり、本番環境のサービス起動としては非常に重い。
  3. プロセスの二重管理: シェルスクリプトが npm プロセスを「子プロセス」として抱えていたため、systemdからの制御効率が悪かった。

何が問題だったのか(ボトルネックの正体)

今回の事象で最大の問題は、「本番環境のサービス起動に、開発環境のような動的な初期化プロセスを組み込んでいたこと」にありました。

具体的には、以下の3つの「待ち」が連鎖していました。

  1. システムチェックによる停滞 (ConditionPathExists) systemdのユニットファイルでGROWIのインストールディレクトリをチェックしていましたが、node_modules を含む膨大なファイル群をOSレベルでスキャンしに行った際、I/O待ちやカーネルレベルのオーバーヘッドが発生し、daemon-reload や起動そのものを著しく遅延させていました。
  2. シェルスクリプトによる二重起動のオーバーヘッド 起動のたびに nvm.sh をロード(source)し、Node.jsのバージョン判定を動的に行っていました。これは開発時には便利ですが、本番サービスとしては数百行のシェルスクリプトを毎回実行することになり、CPUリソースと時間を無駄に消費していました。
  3. プロセスの「親子関係」の不備 systemdから見ると、管理対象が「GROWI本体」ではなく「起動用のシェルスクリプト」になっていました。このため、GROWIが内部でハングアップしてもsystemdが検知できず、再起動もかからないという「運用上の死角」が生まれていました。

これを是正した設定ファイル

設定の前に!

  • 設定ファイルのバックアップ
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)
  • diffによるバックアップ確認
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にログインできない」という地獄が待っています。

ファイル差し替え後の挙動

  • systemdリロード
sudo systemctl daemon-reload
  • growi再起動
sudo systemctl restart growi.service
  • growi再起動確認
systemctl status growi.service

active(running) を確認します。

その後、

  1. growiが起動する
  2. 新しいセッション(ゲストセッション)で管理者アカウントにログインできる
  3. 一通りの操作 (Wikiページの作成や編集)が行えればOKです。

設定の比較

■ systemd ユニットファイル (growi.service)

項目旧設定 (遅延の原因)新設定 (最適化済)
依存関係Afterが分散、Redisの指定なしAfter/WantsにRedis含め統合
パスチェックConditionPathExists (5分停滞の疑い)削除(高速化に寄与)
実行ユーザ指定なし (デフォルト)User=root / Group=root 明示
作業ディレクトリスクリプト内で cdWorkingDirectory で定義
再起動設定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... (プロセスを置き換え)

4. 対処方法のポイント

  • 「動的な環境構築」から「静的なパス指定」へ: 本番サーバでは nvm を毎回読み込む必要はありません。パスを直接通すことが最速の解決策でした。
  • systemdの責務を明確にする: ディレクトリの存在チェックやパス移動はスクリプトではなく、ユニットファイルの WorkingDirectory 等に任せることで、systemdの管理サイクルが正常化しました。
  • プロセスの直結 (exec): OS (systemd) -> Bash -> npm となっていた階層を、exec で OS (systemd) -> npm に直結させたことで、シグナルの伝達やメモリ効率が改善しました。

今後のメンテナンス

Node.jsのバージョンを変更した際のみ、growi-start.sh 内の v20.19.2 というパス文字列を書き換えるだけで対応可能です。

【ログ記録】Next.js/Node.js環境を標的にしたサンドボックス脱出と情報窃取試行

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(サーバサイドレンダリング)の脆弱性を想定した、非常にテクニカルなコードを注入しています。

JavaScript実行環境への介入

var res = process.mainModule.require('child_process').execSync('pwd').toString().trim();
  • process.mainModule を経由して、サンドボックス化されている可能性のある環境から child_process(OS操作モジュール)を強制的に呼び出しています。
  • execSync('pwd') を実行することで、「現在、サーバのどのディレクトリでプログラムが動いているか」という、次なる攻撃(設定ファイルの奪取など)のための足がかりとなる情報を取得しようとしています。

Next.jsの内部挙動を悪用した情報の持ち出し

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に強制的に飛ばされます。
  • 攻撃者は自分のサーバのアクセスログを見るだけで、ターゲットサーバの内部パスを手に入れることができる仕組みです。

防御側の対応と結果

  • 検知: ModSecurity CRSの 934100(Node.js Injection)が、child_processexecSync といった危険な関数呼び出しを完全にパターンマッチング。
  • 阻止: 前回同様、アプリケーション層に到達する前に403遮断(設定により404応答)。
  • 分析: 攻撃者はRedmine(Ruby on Rails)に対し、あえてNode.js/Next.js用の高度なペイロードを投げています。これは「何で動いているか分からないが、とりあえず流行りの脆弱性コードを全部試す」という、 スキャンから自動攻撃までシームレスに移行するボット*の挙動です。

技術的考察:2025年を締めくくった「贈り物」

このログが示しているのは、攻撃側がいかに「多様な環境」を想定した多角的な攻撃を自動化しているかという事実です。

しかし、設置したModSecurityは、相手がRubyを狙おうがNode.jsを狙おうが、「外部から実行コードが注入される」という本質的な異常を逃しませんでした。

2025年のサーバ設定のまとめ。

今年はサーバ運用で色々とありましたので、年末らしい振り返りを。

1~4月:地獄だったが学びのあった「150日の亡霊」

「MongoDBをs3fsで繋いでしまった」

ことによる課金地獄。

とはいえ、これにより

  • クラウドストレージの正しい使い道
  • 合う運用/合わない運用

何よりも

「全ての責任が自分である以上、最後まで問題に取り組む」

という、エンジニアの必須スキルを改めて学べました。

5月:取れてしまったドメイン。

元々、VPS運用を更に決定づけたドメイン「reisalin.com」の所有者ではありますが、新たに「ryza.jp」というわずか4文字で意味あるドメインが取れたことで、ますます「サーバ運用に真摯に向き合う」気概が生まれました。

8月:VPS変更。WebArena→XServerに。

月額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

この底上げは何がありがたいかというと、今まで諦めていた

  • Growi
  • Redmine
  • BookStack
  • NextCloud

の同時稼働ができるようになったこと。また、折角だからとmod-phpからphp-fpmへとよりセキュアな構成にできたのもありがたいです。

10~11月「ONE OUTSシステムの刷新」

  • ModSecurity
  • Apache
  • シェルスクリプト

の三構成によりオープンソースでありながら十分なセキュリティ強度を持たせた「ONE OUTS」を

「自分の投稿は偽陽性にならず、相手の疑わしい攻撃を検知する」

ものへと刷新することができました。

12月「クリスマスアタック」

直近の出来事ですが、これをは特に印象深い出来事です。12月25日というクリスマスの朝、自サーバを襲ったDDoS。これを「前もって用意していた」ipsetでカウンターで来たことは何より重畳。

最後に

昨年末の「Wasabiクラウドの重課金」は「これ、私、今後、vpsを運用する資格があるのか?」思いましたが:

「資格? 馬鹿野郎、誰もそんなもの持ってねぇんだ! いいか、あるのは責任だけだ。戦う責任! あの子を傷つけちまった責任! そいつを果たすには、この地球を守るしかねぇんだ!
 俺は慰めねぇぞ。励ますつもりもねぇ。自分の責任は自分でとれ! 立ち上がってこい、ダイモン! そしたら俺たちはいくらでも支えてやる」
 ――『救急戦隊ゴーゴーファイブ』第27話『イエロー戦線離脱!』

この言葉に救われました。このおかげで、今年は乗り切ることができたということで、今年のサーバ運用の締めくくりとしたいです。

サーバのネットワーク情報を一覧で見るためのワンライナー。(RHEL系/Ubuntu系)

設計書を書く際に面倒な「サーバの設定値の抜き出し」を楽にするためのコマンドです。

RHEL系

  • Red Hat Enterprise
  • Rocky
  • Alma

など、dnfで管理するタイプのコマンドです。

{ echo -e "| インタフェース | IPv4 アドレス | ゲートウェイ | DNS |"; echo -e "| --- | --- | --- | --- |"; nmcli -t -f GENERAL.DEVICE,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS device show | awk -F: '/^GENERAL.DEVICE/ {if (dev) printf "| %s | %s | %s | %s |\n", dev, addr, gw, dns; dev=$2; addr=gw=dns="-"; next} /^IP4.ADDRESS/ {addr=$2; next} /^IP4.GATEWAY/ {gw=$2; next} /^IP4.DNS/ {dns=(dns=="-" ? $2 : dns ", " $2); next} END {if (dev) printf "| %s | %s | %s | %s |\n", dev, addr, gw, dns}'; }

| インタフェース | IPv4 アドレス | ゲートウェイ | DNS |

実行と同時に、こういうマークダウンができあがります。(IPはダミーです)

インタフェースIPv4 アドレスゲートウェイDNS
ens192192.0.2.10/24192.0.2.18.8.8.8, 8.8.4.4
ens224198.51.100.50/24198.51.100.11.1.1.1
virbr0192.168.122.1/24--
docker0172.16.0.1/16--
lo127.0.0.1/8--

Ubuntu系

  • Debian
  • Ubuntu
  • LinuxMint

など、aptを用いるLinuxディストリビューションです。

Ubuntuはnmcliを用いないので、同じようにいきません。

{
  echo "| インタフェース | IPv4 アドレス | ゲートウェイ | DNS |"
  echo "| --- | --- | --- | --- |"
  nmcli -t -f GENERAL.DEVICE,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS device show | \
  awk -F: '/^GENERAL.DEVICE/ {if (dev) printf "| %s | %s | %s | %s |\n", dev, addr, gw, dns; dev=$2; addr=gw=dns="-"; next} 
           /^IP4.ADDRESS/ {addr=$2; next} 
           /^IP4.GATEWAY/ {gw=$2; next} 
           /^IP4.DNS/ {dns=(dns=="-" ? $2 : dns ", " $2); next} 
           END {if (dev) printf "| %s | %s | %s | %s |\n", dev, addr, gw, dns}'
}

これの実行結果は

インタフェースIPv4 アドレスゲートウェイDNS
br-dummy0110.0.0.1/16-(br-dummy01):
docker0172.16.0.1/16-(docker0):
eth0192.0.2.15/24192.0.2.1(eth0):, 8.8.8.8, 1.1.1.1
veth_abc123--(veth_abc123):
veth_def456--(veth_def456):
veth_ghi789--(veth_ghi789):

これをどっかに仕込んでおくだけでも管理は楽になります。

クリスマス防衛戦。(ipsetによるDDoS対策)

自分のサーバに組み込んでいるWebセキュリティシステム(と言ってもスクリプトと設定の組み合わせ) 『ONE OUTS』システム。こちらの弱点を見越した追加設定が効力を発揮しました。

何が起きたか?

「自分のvpsがDDoSを喰らったので、カーネルレベルで対処して沈静化」した時のメモです。

状況確認

Redmine サービスダウン

話は2025年12月25日7:40JST。筆者が管理しているサーバにて、サービスダウンを確認。

  1. SSH接続:OK
  2. Growi:OK
  3. BookStack:OK
  4. Redmine:アクセスしようとして「too much connections」

そこで、状況を調べます。

自作ツール「top-procs」にて

--- CPU Consumers (Top 10) ---
%CPU   %MEM   PID      USER         UNIT                           COMMAND
------------------------------------------------------------------------------------------
85.5   5.1    45114    www-data     apache2.service                 Passenger RubyApp: /home/www-data/app (production)

と、CPU利用率85%以上を確認。

確定:DDoS

netstat -tan を実行すると、以下のようなコネクションが大量に表示されました。

tcp6       0  34498 192.0.2.1:443       198.51.100.15:29862     LAST_ACK   
tcp6       0      0 192.0.2.1:443       203.0.113.84:47044      TIME_WAIT  
tcp6       0      0 192.0.2.1:443       192.0.2.55:38844        TIME_WAIT  
tcp6       0      0 192.0.2.1:443       198.51.100.200:57934    ESTABLISHED
...(中略)...
tcp6       0  34081 192.0.2.1:443       203.0.113.120:27327     LAST_ACK   

総計700行にも及ぶコネクション。これは確実に「DDoS」攻撃です。

  • Mod_Security
  • シェルスクリプト
  • Apache設定

で構成されたONE_OUTSシステムはアクセスログを主体としてL7層(アプリケーション層)での防御を行うもの。なので

  • SYNフラッド攻撃
  • アクセスログに残らないレベルの低レイヤー攻撃

と言った「そもそもログに残らない」「ページの閲覧など関係ない」相手には無意味です。(実際、上記をONE OUTSシステムに組み込んでもアクセスログ(という名の執拗なRedmineへのアクセス)が止まりません。

しかも、DDoSというものは実に厄介です。

  • 超・大量のIPから同時にアクセスしてくるためufw/iptablesで制御したとしても遅延が大きい
  • それでマシンパワーを喰う

という、「物理の力でごり押しする破壊行為」です。

漫画『ドリフターズ』にもある

「こりゃ堕とせんと思ったら
その時から目的は変わるのよ
占領からいやがらせに変わる」

この、いやがらせ目的のため、自分のサーバのリソースが奪われるという状況は見過ごせません。

防衛機構:piertotum locomotor

「こんなこともあろうかと」前もって用意していた「ipset」の設定をフルに使いました。

  • ルールを「集合(set)」として管理。
  • 「このIP群をブロック」というリストを一つのルールとしてまとめられる。
  • 内部的にはハッシュテーブル等を利用しており、検索がほぼ定数時間で完了するため非常に高速。

第2オクテット(/16)どころか、悪質なレンジに対しては「第1オクテット(/8)」すら一括でブロックする運用です。

上記リンクの通り

  1. ipsetコマンドをインストールします。
  2. ブロックリストの設定を行います。
  3. ipsetコマンドでSYNフラッド攻撃を行う攻撃者をレンジごとブロックします。

が事前準備です。

このipsetが有効であるかを確認

実はこれがハマった点でした。

 sudo iptables -L ufw-before-input -n --line-numbers | head -n 5

として、

Chain ufw-before-input (1 references)
num  target     prot opt source               destination         
1    DROP       0    --  0.0.0.0/0            0.0.0.0/0            match-set ufw-blocklist src
2    ACCEPT     0    --  0.0.0.0/0            0.0.0.0/0           
3    ACCEPT     0    --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED

と、match-set ufw-blocklist srcが記されていることを確認します。(私はこれに記述違いがあり、後で修正する羽目になりました)

執拗に攻撃してくるIP/NWの正規表現化

これにはAIの力を借りました。netstat -tanの結果やアクセスログを元に

  • 執拗にアクセスを試みるIP群
  • それが属するASN

を第1オクテット/第2オクテットで抜き出してもらいます。

シェルスクリプトで一括登録

#!/bin/bash

# 1. ターゲットのipset名
SET_NAME="ufw-blocklist"

# 2. DDoS主犯格リスト (CIDR表記)
BAN_LIST=(
  "xx.0.0.0/8"
  "yy.0.0.0/8"
  "zzz.0.0.0/8"

  # 執拗な個体 (CIDRではなく単一IPも登録可能)
  "abc.def.0.0/16" 
)

echo "Hogwarts is threatened!: ${SET_NAME}..."

# 3. ループ処理で注入
for ip_range in "${BAN_LIST[@]}"; do
  # -exist オプションをつけることで、既に登録済みでもエラーにせずスキップさせる
  sudo ipset add ${SET_NAME} ${ip_range} -exist
  if [ $? -eq 0 ]; then
      echo "  Checking... ${ip_range} -> Loaded."
  else
      echo "  Error adding ${ip_range}"
  fi
done

echo "Man the boundaries, protest us, do your duty to our school!"
sudo ipset save ${SET_NAME} -f /etc/ufw/ipsets.save

echo "I've always wanted to use that spell!"

というシェルスクリプトで一気に登録しました。(処理中のechoは『ハリー・ポッターと死の秘宝 part2』屈指の名シーンです)

そもそも、私のvpsは

  • 広告を置いていません
  • アフィリエイトもありません

大嫌いだからです。 主目的は

「私が後で閲覧するときのメモ帳」です。なので、私がアクセスしてこないようなアクセス元のブロックは一切の躊躇を行いません。そのため、\/8で切ることに躊躇はしません。

確認

cat /etc/ufw/ipsets.save

で、

cat /etc/ufw/ipsets.save
create ufw-blocklist hash:net family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0xcce80b68
add ufw-blocklist xxx.0.0.0/8

などと表示されればブロック成功です。

沈静化

効果は覿面でした。見られなかったRedmineサイトは無事に表示され、

--- CPU Consumers (Top 10) ---
%CPU   %MEM   PID      USER         UNIT                           COMMAND
------------------------------------------------------------------------------------------
19.3   5.1    45114    www-data     apache2.service                 Passenger RubyApp: /home/www-data/app (production)

CPU利用率も正常に用いました。

まとめ

今回、迅速に対処できたのは、以下の確信があったからです。

  • 「アプリケーション層の防御を突破できない攻撃者は、より原始的なレイヤーでの攻撃に切り替えてくる」
  • 「そして、運用側の注意が散漫になるタイミングを狙ってくる」

事前に「OSの負荷を抑えつつ高速にブロックできる仕組み」を構築していたことが功を奏しました。クリスマスというタイミングを狙ったのは、ホリデーシーズンによる対応の遅れを期待した計画的なものだったのでしょう。

連合艦隊解散の辞にある、

古人曰ク勝ツテ兜ノ緒ヲ締メヨト。

という言葉の重みを再認識する出来事でした。

Growi v7.1.x/v7.2.x→v7.4.0以降へのアップデート

概要

Growi 7.2.x → Growi 7.4.0にアップデートする 手順です。

Growi7.3.3より前のバージョンは脆弱性が存在します。Growiをインターネットで公開している方は、こちらの手順で上げましょう。

注意点

  • Growi 7.4.xはElasticSearchがv9でなければ動きません。
  • また、mongodbの最新版は、古いCPUでは動きません。
  • 上記理由のためGrowiをインターネット環境で動かしている場合は以下を十分検討ください。
    • WAFなどで防御する。
    • ハードウェア環境を最新のmongodb / Elasticsearchが動くものへとアップグレードする。
    • インターネット公開を諦める。

前提

  • 既にgrowi v7.1.x/v7.2.xをインストールしていること。
  • 管理画面トップやトップページ右下からバージョンが7.1.xまたは7.2.xであることを再確認します。
  • systemdによってサービス化されていること。
  • 具体的な手順はhttps://atelier.reisalin.com/projects/zettel/knowledgebase/articles/105
  • 最新版や安定版がリリースされていることを以下のサイトで確認していること。
  • https://github.com/growilabs/growi/releases
  • ※設定ファイルの変更やパッケージインストールの変更、nodeのバージョンアップの必要等があれば、それも事前に済ませます。

さっくりはならない手順

  1. Growiをメンテナンスモードにします。
  2. Growi・Elasticsearchのサービスを停止します。
  3. バックアップを取ります。
  4. gitコマンドで最新版をcheckoutします。
  5. アップグレードを行います。
  6. Elasticsearch・Growiのサービスを再開します。
  7. Growiのメンテナンスモードを解除します。
  8. アップグレードされたことを確認します。

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

  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
  • サービス停止確認
systemctl status elasticsearch.service

inactive(dead)を確認します。

  • Growiサービス停止
sudo systemctl stop growi.service
  • サービス停止確認
systemctl status growi.service

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)

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

リポジトリ設定ファイル名をv9用に変更

Elasticsearchのバージョンを指定するリポジトリをv9に変更します。

  • 現行のリポジトリリストをバックアップ
sudo cp -pi /etc/apt/sources.list.d/elastic-8.x.list /path/to/backup/dir/elastic-8.x.list.$(date +%Y%m%d)
  • リポジトリリストのバックアップ確認
diff -u /path/to/backup/dir/elastic-8.x.list.$(date +%Y%m%d) /etc/apt/sources.list.d/elastic-8.x.list
  • リポジトリリストの名前変更
sudo mv /etc/apt/sources.list.d/elastic-8.x.list /etc/apt/sources.list.d/elastic-9.x.list
  • リポジトリリストの名前変更確認
ls -l /etc/apt/sources.list.d/elastic-9.x.list

ファイルがあることを確認します。

sedコマンドでファイル内の参照先を8.xから9.xに書き換え

sudo sed -i 's/8.x/9.x/g' /etc/apt/sources.list.d/elastic-9.x.list

Elasticsearchのアップグレード

  • パッケージ全体のバックアップ
sudo aptitude update

好みでaptitudeを用いています。必要に応じてaptを用いてください。

  • Elasticsearchのアップグレード
sudo aptitude upgrade elasticsearch

※ Growiインストール時、/etc/elasticsearch/jvm.optionsファイルなどの設定変更を行っているため、アップグレード時の設定ファイルを残すかどうかの確認では、必ずN(残す)を選択します。

  • プラグインのアンインストール

Growiに必要なElasticsearchのプラグインは自動更新されません。この処置を執らないとせっかくアップグレードしたのに起動しないという事態が発生します。

sudo /usr/share/elasticsearch/bin/elasticsearch-plugin remove analysis-icu
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin remove analysis-kuromoji
  • プラグインの再インストール
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-icu
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji

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 【バージョン】

リリースタグは再確認しましょう。今回は 2025/12/24 リリースされたv7.4.0を選択しました。

  • pnpm install
sudo pnpm i
  • ビルド
sudo npm run app:build

ElasticsearchとGrowiの再開

  • Elasticsearchサービス開始
sudo systemctl restart elasticsearch.service
  • サービス開始確認
systemctl status elasticsearch.service

active(running)を確認します。

  • バージョンアップ確認
curl -X GET "localhost:9200"

"number" : "9.1.3",など、9系にアップグレードされていることを確認します。

  • Growiサービス開始
sudo systemctl restart growi.service
  • サービス開始確認
systemctl status growi.service

active(running)を確認します。

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

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

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

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

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

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

Linuxサーバ、稼働中のサービスのバージョンを確認するワンライナー。

概要

Linuxサーバの構築時に非常に面倒で厄介な「動いているサービス(systemctl status hoge.service でenabledになっているもの)だけを取り出し、表に転記するという作業が

  • 限りなく単調で
  • とてもミスが多い

重箱の隅をつつくような作業を一発で解消するワンライナーの紹介です。

前提

RHEL系Linuxで動きます。(筆者環境Rocky Linux)

ワンライナー

※これは

sudo su 

としてから入力した方が無難です。(パッケージによっては一般的権限では見られないため)

 { echo -e "| ソフトウェア名 | バージョン |\n| --- | --- |"; systemctl list-unit-files --type=service --no-legend | awk '{print $1}' | grep -v '@\.service$' | xargs -r systemctl show -p FragmentPath | sed 's/^FragmentPath=//' | grep '^/' | sort -u | xargs -I {} sh -c 'if rpm -qf "{}" >/dev/null 2>&1; then rpm -qf "{}" --qf "| %{NAME} | %{VERSION}-%{RELEASE} |\n"; else echo "| $(basename "{}") | (not owned by any package) |"; fi' | sort -u; } 

スクリプトの解説

このコマンドは大きく分けて「ヘッダーの出力」「サービスファイルのパス特定」「パッケージ情報の取得と整形」の3つのフェーズで動いています。

1. 表のヘッダーを作成

  • { echo -e "| ソフトウェア名 | バージョン |\n| --- | --- |"; ... }
    • 最初にMarkdown形式の表の1行目(項目名)と2行目(区切り線)を出力します。
    • 全体を { } で囲むことで、ヘッダーと後の実行結果を一つの出力ストリームとしてまとめています。

2. サービス一覧からファイルパスを特定

ここからがメインのパイプラインです。

  • systemctl list-unit-files --type=service --no-legend
    • システム上の全サービスユニットを表示します。--no-legend でヘッダー行を省きます。
  • awk '{print $1}'
    • 出力結果から1列目(サービス名)だけを抜き出します(例: sshd.service)。
  • grep -v '@\.service$'
    • インスタンス化されたユニット(user@.service など)を除外します。これらは実ファイルが少し特殊なためです。
  • xargs -r systemctl show -p FragmentPath
    • 各サービスに対して、その定義ファイル(ユニットファイル)がディスク上のどこにあるか(FragmentPath)を取得します。
  • sed 's/^FragmentPath=//'
    • 出力に含まれる FragmentPath= という文字列を削除し、純粋なパス名だけにします。
  • grep '^/'
    • 空行や無効なパスを除外し、/ から始まる絶対パスのみを残します。
  • sort -u
    • 重複したパスを削除します。

3. RPMによるパッケージ照会と整形

特定されたファイルパスを一つずつ RPM データベースと照合します。

  • xargs -I {} sh -c '...'
    • 各パス({})に対して、シェルスクリプトを実行します。
  • if rpm -qf "{}" >/dev/null 2>&1; then ...
    • そのファイルが RPM パッケージによって管理されているかを判定します。
    • 管理されている場合: rpm -qf を使い、パッケージ名とバージョンを Markdown の行形式 | 名前 | バージョン | で出力します。
    • 管理されていない場合: (手動で作成したサービスなど)「(not owned by any package)」と出力します。
  • sort -u(最後)
    • 最終的なリストをソートし、重複を排除して綺麗に並べます。

実行結果のイメージ

コマンドを実行すると、以下のような結果が得られます。

ソフトウェア名バージョン
chrony4.1-3.el9
openssh-server8.7p1-10.el9
my-custom-script.service(not owned by any package)
systemd250-6.el9

このワンライナーの利点

  • 徹底した合理性。
    • なにせ「目視確認」という手段が排除されます。一切のヒューマンエラーがなくなります。
  • 一覧性と整合性。
    • 「最新の情報を全て表示せよ」という要求に対してもコマンドを叩くだけで済みます。
  • 再利用性
    • これが一番大きいです。2020年代ITで最も使いやすいMarkdownのテーブル形式。そのため、
      • Redmine
      • VS Code
      • Notion
        などにいくらでも反映可能。そして、一度テーブルにしてしまえばExcelへの転記も一発です。

まとめ

「ウサギとカメの寓話」は、コツコツやる者が強いパターンではありますが、「60分の道を頑張って55分に縮めるよりも、30分かけて『1分で終わる手段を考える』」方が、本当の「タイムパフォーマンス」だと思った次第です。

備考

Ubuntuサーバでも、このようにすれば動きます。

{
  echo -e "| ソフトウェア名 | バージョン |\n| --- | --- |"
  # 1. サービスファイルのパス一覧を取得
  systemctl list-unit-files --type=service --no-legend | \
  awk '{print $1}' | \
  grep -v '@\.service$' | \
  xargs -r systemctl show -p FragmentPath | \
  sed 's/^FragmentPath=//' | \
  grep '^/' | \
  sort -u | \
  # 2. 1行ずつ読み込んで処理(whileループでシェル起動を最小化)
  while read -r path; do
    # dpkg-query -S は dpkg -S より高速で安定しています
    res=$(dpkg-query -S "$path" 2>/dev/null)
    if [ $? -eq 0 ]; then
      pkg=$(echo "$res" | cut -d: -f1)
      # 複数のパッケージがヒットする場合があるため、最初の1つを取得して詳細表示
      dpkg-query -W -f="| \${Package} | \${Version} |\n" "${pkg%%,*}"
    else
      echo "| $(basename "$path") | (not owned by any package) |"
    fi
  done | sort -u
}

CPU/メモリの利用状況を確認するスクリプト.part2

Webサーバのみならず、サーバ運用において「どのプロセスがCPU/メモリを喰っているか」というボトルネックの把握は重要です。

それを把握するためのスクリプトのご紹介です。

なぜボトルネックの把握が重要なのか

以下の3点が主な理由です:

  1. リソースの最適化と安定運用
     高負荷プロセスを特定することで、不要な消費を抑え、他のサービスへの影響を防げます。
  2. 障害予防と早期対応
     異常なリソース使用は障害の前兆であることが多く、早期発見によりダウンタイムを回避できます。
  3. 攻撃予兆への対応
     DDoS/執拗な攻撃などはリソース寮にダイレクトに現れます。

把握するためのシェルスクリプト

といっても、topwコマンドなどでは煩雑な情報が多いため、シンプルに

  1. CPUを多く使っているプロセス
  2. メモリを多く使っているプロセス

に絞り込みを行います。というのも、プロセスの暴走は先に示したとおり、CPU/メモリを多く使うからです。

それをより分かりやすく視覚化するスクリプト例が以下の通り。

top-procs.sh等の名前で、任意の場所に作成します。

#!/bin/bash

#================================================================
# System Resource Monitor (Refined by Riza)
#================================================================

# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color

# --- Defaults ---
TOP_N=5
MODE="all" # all, cpu, mem

# --- Usage ---
usage() {
    echo -e "${CYAN}Usage: $(basename "$0") [-c] [-m] [-n NUM] [-h]${NC}"
    echo "  -c      : CPU使用率順で表示"
    echo "  -m      : メモリ使用率順で表示"
    echo "  -n NUM  : 表示する行数を指定 (Default: ${TOP_N})"
    echo "  -h      : ヘルプ表示"
    exit 0
}

# --- Argument Parsing (getopts is standard) ---
while getopts "cmn:h" opt; do
  case $opt in
    c) MODE="cpu" ;;
    m) MODE="mem" ;;
    n) TOP_N="$OPTARG" ;;
    h) usage ;;
    \?) usage ;;
  esac
done

# --- Core Function ---
# Arguments: 
# 1: Sort Key (e.g., -pcpu or -pmem)
# 2: Title
# 3: Format String for ps
show_top() {
    local sort_key="$1"
    local title="$2"

    echo -e "\n${BOLD}${CYAN}--- ${title} (Top ${TOP_N}) ---${NC}"

    # Header format specifically tailored for clarity
    printf "${YELLOW}%-6s %-6s %-8s %-20s %s${NC}\n" "%CPU" "%MEM" "PID" "USER" "COMMAND"
    echo "---------------------------------------------------------"

    # ps command explanation:
    # -e : Select all processes
    # -o : User-defined format
    # --sort : Internal sorting (descending with -)
    # head : Limit output

    ps -e -o pcpu,pmem,pid,user,args --sort="${sort_key}" | \
    head -n "$((TOP_N + 1))" | tail -n "$TOP_N" | \
    awk '{
        # Highlighting logic
        cpu=$1; mem=$2;

        # Colorize CPU if > 50% or MEM > 50% (Arbitrary threshold for visual aid)
        color="'"${NC}"'";
        if (cpu > 50.0 || mem > 50.0) color="'"${RED}"'";
        else if (cpu > 10.0 || mem > 10.0) color="'"${GREEN}"'";

        # Reconstruct the line with printf for alignment
        # $1=CPU, $2=MEM, $3=PID, $4=USER, $5...=COMMAND

        # Capture the full command (args) which starts from $5
        cmd=""; for(i=5;i<=NF;i++) cmd=cmd" "$i;

        printf "%s%-6s %-6s %-8s %-20s %s%s\n", color, $1, $2, $3, $4, cmd, "'"${NC}"'"
    }'
}

# --- Main Logic ---

case $MODE in
    cpu)
        show_top "-pcpu" "CPU Consumers"
        ;;
    mem)
        show_top "-pmem" "Memory Consumers"
        ;;
    all)
        show_top "-pcpu" "CPU Consumers"
        show_top "-pmem" "Memory Consumers"
        ;;
esac

echo ""

以前の改良点

  1. ps --sort の活用: これが最大の改良点よです。ps aux | sort -k ... はデータをテキストとして処理するから遅いのですが、ps --sort=-pcpuはカーネルから情報を取得する段階でソートします。
  2. -n オプションの追加: ./top-procs.sh -n 10 と打てばトップ10が見られるようにしています。
  3. しきい値による色付け: awk の中で、CPUやメモリを激しく消費しているプロセス(例えば50%以上)を赤色で表示するようにしています。
./top-procs.sh

を実行することで、

--- CPU使用率 (上位 5 プロセス) ---
 %CPU    PID  COMMAND
-----------------------------------------
 52.10%  12345  ruby_app_server: /var/www/webapp1 (production)
  9.40%   1086  /usr/sbin/database_server [...]
  3.80%  42162  /usr/sbin/web_server -k start
  1.50%  42161  /usr/sbin/web_server -k start
  0.90%   7978  nodejs_process /path/to/nodejs_app/server.js

--- メモリ使用率 (上位 5 プロセス) ---
 %MEM    PID  COMMAND
-----------------------------------------
 13.10%   1984  /usr/bin/java -Xms256m -Xmx256m [...] search_engine -Des.path.home=/usr/share/search_engine [...]
 10.00%   1086  /usr/sbin/database_server [...]
  7.50%  12345  ruby_app_server: /var/www/webapp1 (production)
  3.90%  78630  ruby_app_server: /var/www/webapp2 (production)
  3.80%  76583  ruby_app_server: /var/www/webapp3 (production)

が出てきます。

この例では、rubyアプリが圧倒的にCPUを消費し、ElasticSearchがメモリを食っているというのが分かります。

そして、

  • -a / 引数無し : CPUとメモリの両方を表示
  • -c : CPU情報のみを表示
  • -m : メモリ情報のみを表示
  • -h : これら引数やスクリプトの内容を表示

と、目的に合わせた柔軟な表示も可能にしています。

ついでにコマンド化

こういった障害発生時のボトルネック判定時、いちいちスクリプトの場所を探すという悠長なことはできません。

なので、余裕がある(つまりこのスクリプトを作成した直後です)状況で、

sudo ln -sf /path/to/script/top-procs.sh /usr/local/bin/top-procs

として、どこからでもコマンドを呼び出せるようにします。(スクリプトの場所は自分がこれを保存した絶対パスを指定してください)

which top-procs

/usr/local/bin/top-procs

と表示されればコマンド化は完了。こうすることにより、どのユーザーでもこのコマンド一発で上記のボトルネック判定が可能になります。

Apache/PHP-FPMの構文確認&再起動スクリプト part.2

https://barrel.reisalin.com/books/950a4/page/apachephp-fpm

のスクリプトの改良版となります。

スクリプト特徴

  1. 管理者(root)権限で実行するかのチェック
  2. サービス再起動前に有効なサイトとドメイン名を事前に確認
  3. 構文にミスがないかを確認
  4. 再起動前の最終確認をy/nで行う
  5. PHP-FPMにも対応(インストールされていない場合はスキップ)
  6. 再起動後にサービスの状況を表示する
  7. -r オプションでreloadのみ実施

スクリプト内容

  • apache2-check.sh
#!/bin/bash

#================================================================
# Apache & PHP-FPM Management Script
#================================================================

# --- Colors for Output ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# --- Settings ---
SITES_DIR="/etc/apache2/sites-enabled"
# Default to 8.3, but allow override via ENV like: PHP_VERSION=8.2 ./script.sh
PHP_VERSION="${PHP_VERSION:-8.3}"

# --- Flags ---
AUTO_YES=false
RESTART_APACHE=true
RESTART_PHP=true
EXCLUSIVE_MODE=false
ACTION_TYPE="restart" # default action
ACTION_LABEL="再起動"

# --- Usage ---
usage() {
    echo -e "${CYAN}Usage: $(basename "$0") [-y] [-r] [-a] [-p] [-h]${NC}"
    echo "  -y : 確認をスキップ (Auto-Yes)"
    echo "  -r : restartの代わりに reload を使用 (設定反映のみの場合に推奨)"
    echo "  -a : Apacheのみ対象"
    echo "  -p : PHP-FPMのみ対象"
    echo "  -h : ヘルプ表示"
    exit 0
}

# --- Argument Parsing ---
while getopts "yraph" opt; do
  case $opt in
    y) AUTO_YES=true ;;
    r) 
       ACTION_TYPE="reload"
       ACTION_LABEL="リロード(設定読込)"
       ;;
    a)
      if ! $EXCLUSIVE_MODE; then
          RESTART_APACHE=false; RESTART_PHP=false; EXCLUSIVE_MODE=true
      fi
      RESTART_APACHE=true
      ;;
    p)
      if ! $EXCLUSIVE_MODE; then
          RESTART_APACHE=false; RESTART_PHP=false; EXCLUSIVE_MODE=true
      fi
      RESTART_PHP=true
      ;;
    h) usage ;;
    \?) usage ;;
  esac
done

PHP_FPM_SERVICE="php${PHP_VERSION}-fpm"
PHP_FPM_COMMAND="php-fpm${PHP_VERSION}"

# --- Function: Action & Check ---
manage_service() {
    local service_name="$1"
    local service_label="$2"
    local confirm_action="n"

    # PHP-FPM doesn't support 'reload' gracefully in all versions/configs,
    # but systemd handles it usually. If not, fallback or stick to restart.
    # For this script, we assume systemctl reload works or fails safely.

    if [ "$AUTO_YES" = true ]; then
        confirm_action="y"
        echo -e "${YELLOW}${service_label} を ${ACTION_LABEL} します... (-y)${NC}"
    else
        read -p "${service_label} を ${ACTION_LABEL} しますか? (y/n): " confirm_action
    fi

    if [[ "$confirm_action" =~ ^[Yy]$ ]]; then
        if ! systemctl "$ACTION_TYPE" "$service_name"; then
            echo -e "${RED}エラー: ${service_label} の ${ACTION_LABEL} に失敗しました。${NC}"
            # On failure, show status immediately
            systemctl status "$service_name" --no-pager
        else
            echo -e "${GREEN}${service_label} が正常に ${ACTION_LABEL} されました。${NC}"
            echo "---- ステータス ----"
            systemctl is-active "$service_name"
            echo "--------------------"
        fi
    else
        echo -e "${CYAN}${service_label} の処理はキャンセルされました。${NC}"
    fi
    echo
}

# --- Root Check ---
if [ "$EUID" -ne 0 ]; then
    echo -e "${RED}エラー: root権限が必要です。sudoしてください。${NC}"
    exit 1
fi

# --- 1. Display Sites ---
echo -e "${CYAN}==== 有効なサイト設定 (VHosts) ====${NC}"
if [ -z "$(ls -A "$SITES_DIR" 2>/dev/null)" ]; then
    echo "サイト設定が存在しません。"
else
    shopt -s nullglob
    for site in "$SITES_DIR"/*; do
        echo -e "${YELLOW}File: $(basename "$site")${NC}"
        # Parse Logic (Kept your logic, it works well)
        grep -hi -E "^\s*(ServerName|ServerAlias)\s+" "$site" | sed -E 's/^[[:blank:]]+//;s/[[:blank:]]*#.*//' | awk '{
            orig=$1; dir=tolower(orig);
            proper=(dir=="servername"?"ServerName":(dir=="serveralias"?"ServerAlias":orig));
            for(i=2;i<=NF;i++){
                d=tolower($i); sub(/[;,]*$/,"",d); gsub(/^[[:blank:]]+|[[:blank:]]+$/,"",d);
                if(d) printf "  %s %s\n", proper, d
            }
        }' | sort -u
    done
    shopt -u nullglob
fi
echo -e "${CYAN}==================================${NC}\n"

# --- 2. Syntax Check (Revelio) ---
echo -e "${CYAN}--- 構文チェック (Revelio) ---${NC}"
SYNTAX_OK=true

if [ "$RESTART_APACHE" = true ]; then
    echo -n "Apache: "
    if apachectl configtest 2>&1 | grep -q "Syntax OK"; then
        echo -e "${GREEN}Syntax OK${NC}"
    else
        echo -e "${RED}Syntax Error Detected!${NC}"
        apachectl configtest
        SYNTAX_OK=false
    fi
fi

PHP_FPM_ENABLED=false
if [ "$RESTART_PHP" = true ]; then
    # Simple check for binary existence
    if command -v "$PHP_FPM_COMMAND" &>/dev/null; then
        PHP_FPM_ENABLED=true
        echo -n "${PHP_FPM_SERVICE}: "
        if "$PHP_FPM_COMMAND" -t 2>&1 | grep -q "test is successful"; then
            echo -e "${GREEN}Syntax OK${NC}"
        else
            echo -e "${RED}Syntax Error Detected!${NC}"
            "$PHP_FPM_COMMAND" -t
            SYNTAX_OK=false
        fi
    else
        echo -e "${YELLOW}Warning: ${PHP_FPM_COMMAND} not found. Skipping PHP check.${NC}"
        RESTART_PHP=false
    fi
fi

if [ "$SYNTAX_OK" = false ]; then
    echo -e "\n${RED}構文エラーがあるため、処理を中断します (Protego)。${NC}"
    exit 1
fi
echo

# --- 3. Execute Action ---
if [ "$RESTART_APACHE" = false ] && [ "$RESTART_PHP" = false ]; then
    echo "対象サービスなし。終了します。"
    exit 0
fi

if [ "$RESTART_APACHE" = true ]; then
    manage_service "apache2" "Apache"
fi

if [ "$RESTART_PHP" = true ] && [ "$PHP_FPM_ENABLED" = true ]; then
    manage_service "$PHP_FPM_SERVICE" "$PHP_FPM_SERVICE"
fi

echo -e "${CYAN}👏 Finito!${NC}"

主な改良点

  • -r (Reload) オプションの追加: プロセスを殺さずに設定を読み直す。
  • カラー出力: 重要なメッセージを強調。
  • PHPバージョンの柔軟性: 環境変数でも渡せるように変更。

スクリプトのコマンド化

このスクリプトをコマンドとして実行できるようにします。

sudo ln -sf /path/to/script/apache2-check.sh /usr/local/bin/apache2-check
  • コマンド確認
which apache2-check

/usr/local/bin/apache2-check と表示されることを確認します。

後は、

sudo apache2-check

を実行すればOKです。

引数によるオプション

また、このコマンドは以下の引数での柔軟な処理も特徴です。

  • -y 確認プロンプトを全てスキップし、全てyで応答。(cronなどで威力を発揮)
  • -a Apacheのみを対象。PHP-FPMを組み込んでいないとき、変更対象がApacheのみの場合。
  • -p PHP-FPMのみを対象。PHP-FPMの設定のみを変更した場合。
  • -r Reload。設定変更のみを対象。
  • 引数無し : デフォルトでApacheとPHP-FPMを確認プロンプト込みで再起動確認。
  • -h この引数を表示。

Page 1 of 34

Powered by WordPress & Theme by Anders Norén