月: 2026年1月 Page 1 of 3

ModSecurity環境下でのWP-Matomo連携設定

こちらの記事の続き。WebアクセスシステムMatomoをWordpressと連携させ、wp-matomoと連携するときに、Mod_Securityが邪魔をしたので処置を行いました。

環境

Matomo側

  • Ubuntu 24.04
  • Apache 2.4
  • Mod Security v2

WordPress専用サーバ

  • Connect Matomoプラグイン

発生していた問題(症例)

1. Expectヘッダーによるブロック(ID: 920450)

Matomoがデータ送信の準備として送った Expect ヘッダーが、WAFのポリシーによって「制限されたヘッダー」と判定されたログです。(IP/ホストなどはダミーに置き換え)

[Wed Jan 21 20:55:18.015915 2026] [security2:error] [pid 34746:tid 133273950865088] [client 192.0.2.100:56166] [client 192.0.2.100] ModSecurity: Warning. String match within "/content-encoding/ /proxy/ /lock-token/ /content-range/ /if/ /x-http-method-override/ /x-http-method/ /x-method-override/ /x-middleware-subrequest/ /expect/" at TX:header_name_920450_expect. [file "/usr/share/modsecurity-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1187"] [id "920450"] [msg "HTTP header is restricted by policy (/expect/)"] [data "Restricted header detected: /expect/"] [severity "CRITICAL"] [hostname "matomo.example.com"] [uri "/"] [unique_id "aXC-pq-hNmFZwWUY4ipYBAAAAEg"]

2. スコア超過によるアクセス拒否(ID: 949110)

上記の Expect ヘッダー検知により、異常スコアがしきい値(5点)に達したため、通信が遮断(403 Forbidden)されたログです。

[Wed Jan 21 20:55:18.044159 2026] [security2:error] [pid 34746:tid 133273950865088] [client 192.0.2.100:56166] [client 192.0.2.100] ModSecurity: Warning. Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [hostname "matomo.example.com"] [uri "/"] [unique_id "aXC-pq-hNmFZwWUY4ipYBAAAAEg"]

3. 相関ログ(ID: 980170)

最終的にどのカテゴリの攻撃として判定されたかをまとめた報告ログです。

[Wed Jan 21 20:55:18.307310 2026] [security2:error] [pid 34746:tid 133273950865088] [client 192.0.2.100:56166] [client 192.0.2.100] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/rules/RESPONSE-980-CORRELATION.conf"] [line "98"] [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=0, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=5)"] [hostname "matomo.example.com"] [uri "/index.php"] [unique_id "aXC-pq-hNmFZwWUY4ipYBAAAAEg"]

行った措置:アプリケーション別除外ルールの作成

サーバー全体のセキュリティを下げるのではなく、特定のホスト(Matomoドメイン)に限定して、干渉しているルールのみを無効化します。

設定ファイルの記述例

/etc/modsecurity/rules/request-900-exclusion-rules-before-crs.conf 等に、以下の内容を追記、または作成します。

# ===================================================================
# アプリケーション別除外ルール: Matomo (matomo.example.com)
# ===================================================================

# 1. ターゲットとなるホスト名を指定し、それ以外は処理をスキップ
# ※ホスト名はご自身の環境に合わせて適宜読み替えてください
SecRule HTTP_HOST "@streq matomo.example.com" \
    "id:5001,phase:1,nolog,pass,chain,skipAfter:END_MATOMO_RULES_PRE"

    # 2. 特定されたホストに対し、干渉しているIDのみを無効化(外科手術)
    SecAction "ctl:ruleRemoveById=920450,ctl:ruleRemoveById=932370,ctl:ruleRemoveById=930130"

# スキップ先マーカー
SecMarker END_MATOMO_RULES_PRE

設定のテストと反映

上記、設定を行ったら

  • 構文チェック
sudo apache2ctl configtest

apache 再起動

sudo systemctl restart apache2.service

apache 再起動確認

systemctl status apache2.service

active(running)を確認します。

その後、wordpressのConnect Matomoのページに難度アクセス。

tail -f /var/log/matomo/matomo_error.log

等として(エラーログの場所は自分の環境に合わせます)

上記のログが出なければOKです。

今回の教訓

「最初にDetectionOnlyにしていたから助かった」に尽きます。

apacheの設定で

# Mod_security
<IfModule security2_module>
## 最初は検知モード
SecRuleEngine DetectionOnly
#SecRuleEngine On

と、コメントでオフオンを切り替えられる運用を組み込んでいたため、失敗を回避しました。

とはいえ

どハマりしたのがその、wp-connectとmatomoの連携だったのはまた改めて記します。

Ubuntu 24.04にMatomoをインストール。(PHP-FPM版)

概要

オープンソースの解析システムであるMatomoをインストールしたときのメモです。

  • Google Analysticにお金を払う余裕がない
  • WordPressのアクセス解析Jetpackは重い

ということで運用しました。

以前のメモがPHP8.3(mod-php)のインストールだったので、php-fpm版を改めて書き起こしています。

参考としたURL

  • https://matomo.org/faq/on-premise/matomo-requirements/

注意点

リアルタイムでアクセスする性質上、PVが非常に多いWebサイトではサーバ自体の冗長化構成が必要です。(上記URL参照)

筆者のサイトは10万ページ/月に満たないので、そこそこのスペックで運用できています。

前提

既に以下のシステムがWAN環境に揃っていること。

  • Ubuntu 24.04
  • Apache 2.4
  • mod_ssl
  • mod_rewrite
  • mod_header
  • ※実行ユーザーはwww-dataです。
  • mysql 8.3
  • PHP 8.3 → インストール方法
  • 必須の拡張機能(curl, gd, mbstring, mysql等)が有効になっていること。
  • PHP-FPM → インストール方法
  • ドメインで名前解決できること。
  • ドメインに沿った証明書があること。

さっくりとした手順

  1. MySQLのDBとユーザを作成します。
  2. ディレクトリにmatomoプログラムを配置します。
  3. Apache設定ファイルを作成します。
  4. matomoサイトにログインできることを確認します。
  5. matomo Web画面で初期設定をおこないます。

データベースを作成します。

  • MySQLログイン
sudo mysql -u root -p
  • DB作成
 CREATE DATABASE IF NOT EXISTS matomo CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 

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

  • DBユーザー、パスワード設定
CREATE USER 'matomo'@'localhost' IDENTIFIED BY 'YOUR_STRONG_PASSWORD';

DBユーザーは自身の環境に合わせます。パスワードはポリシーに沿って強固なものを設定してください。

  • DBユーザーに作成したDBへの権限を付与
GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
  • MySQLコンソールからログアウト
exit;
  • DB作成確認
mysql -u matomo -p

DB名・ユーザー名は適宜自分が設定したものに読み替えてください。

  • DB確認
SHOW DATABASES;

作成したDBがあることを確認します。

EXIT

MySQLコンソールから抜けます。

matomoプログラムをディレクトリに配置します。

  • 保存用ディレクトリに移動
cd /hoge &&pwd

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

  • プログラムダウンロード
wget https://builds.matomo.org/matomo-latest.zip
  • ZIP展開
unzip matomo-latest.zip
  • ディレクトリの所有者変更
sudo chown -R www-data:www-data matomo

ディレクトリ一式をApache実行ユーザー(www-data)に修正します。

  • プログラム実行権に移動
sudo mv matomo /home/www-data/

自分の環境に合わせます。(筆者環境は/home/www-data/matomo で動かします)

ls -ld /home/www-data/matomo

該当ディレクトリにファイル一式があることを確認します。

Apache設定ファイルを作成します。

【】内を自分の環境に合わせます。
コマンド一式をコピー → 別のエディタにペースト
その後、【】内を自分の環境に修正してコピー
コマンド一式をSSHクライアントに貼り付ける

cat <<- __EOF__ | sudo tee  /etc/apache2/sites-available/matomo.conf > /dev/null
<VirtualHost _default_:80>
ServerName 【設定したドメイン名】
 RewriteEngine On
        RewriteCond %{HTTPS} off
        RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName 【設定したドメイン名】
    CustomLog 【/var/log/matomo/matomo_access.log combined】
    ErrorLog 【/var/log/matomo/matomo_error.log】
    # アクセスログとエラーログは自分の環境に合わせて設定します。

    DocumentRoot 【/home/www-data/matomo】
     # PHPファイルの処理をFPMに渡す設定
    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://localhost"
    </FilesMatch>
    # -----------------------
    <Directory 【/home/www-data/matomo】>
    # DcoumentRootとDirectoryは自分の環境に合わせて設定します
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        Require all granted
    </Directory>

#SSL設定
  SSLEngine on
    Protocols h2 http/1.1
  # SSLを有効化します

SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2
#TLS1.3に対応していないクライアントがアクセスする場合は以下を用います
#SSLProtocol -ALL +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder     off
SSLSessionTickets       off

SSLUseStapling On
SSLStaplingCache "shmcb:/var/run/apache2/ssl_stapling(32768)"
# 2025年5月よりLet's EncryptはSSL Staplingに伴うOCSPを廃止しました。そのため、証明書をLet's Encryptにしている場合は上記2行をコメントアウトし、代わりにこちらを用いてください。
# SSLUseStapling Off

SSLCertificateFile 【/etc/certs/example.com.crt】
# SSL証明書を指定します
SSLCertificateKeyFile 【/etc/private/example.com.key】
# 秘密鍵を指定します

# SSLCACertificateFile 【/etc/certs/example.com.CA.crt】
# 中間証明書が発行元から別ファイルで提供されている場合は、この直上をコメントアウトして中間証明書を指定します

    # index.php への転送設定
    DirectoryIndex index.php

#セキュリティヘッダー付与

    Header always set Strict-Transport-Security "max-age=63072000"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-XSS-Protection "1; mode=block"

    # Matomo: 機密情報が含まれるディレクトリへの直接アクセスを禁止
    <DirectoryMatch "/(config|core|lang|tmp|vendor)">
        Require all denied
    </DirectoryMatch>

    # Matomo: .ini や .json といった設定ファイルへの直接アクセスを禁止
    <FilesMatch "\.(ini|json)$">
        Require all denied
    </FilesMatch>

</VirtualHost>
__EOF__
  • ファイル作成確認
ls -l /etc/apache2/sites-available/matomo.conf

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

Apache設定を反映します。

  • .conf読み込み
sudo a2ensite matomo.conf
  • 設定確認
sudo apache2ctl configtest

Syntax OKを確認します

  • Webサービス再起動
sudo systemctl restart apache2.service
  • Webサービス再起動確認
sudo systemctl status apache2.service
  • ブラウザ表示確認。

ブラウザで

https://【matomoを設定したドメイン名】

にアクセスし、初期画面が出ることを確認します。

初期インストール画面の設定

  1. 初期インストール画面が出たら「次へ」をクリックします。
  2. 全てチェックされていることを確認して「次へ」をクリックします。
◎データベースを設定します。
  • ログイン: MySQLのユーザー(matomo)
  • パスワード: 設定したパスワード
  • データベース名:作成したDB (matomo)

をそれぞれ入力し、「次へ」をクリックします。正常に入力されれば「テーブルを作成されました」と出るので「次へ」をクリックします。

◎スーパーユーザーを設定します。
  • スーパーユーザーログイン:ログインするユーザー名
  • パスワード:ログイン時のパスワード
  • パスワード(再入力)
  • メールアドレス

をそれぞれ入力して「次へ」をクリックします。

◎アクセス解析を行うウェブサイトを設定します。
  • アクセス解析対象のウェブサイトの名前
  • ウェブサイトのURL (このmatomoサイトではなく、アクセス解析を行いたいWebサイト)
  • ウェブサイトのタイムゾーン
  • eコマースか否か

を設定して「次へ」をクリックします。

これらを設定後、トラッキングタグが表示されます。これらを控えて「次へ」をクリックします。

「おめでとうございます!」と表示されればインストールの一連の作業は完了します。

Mod_Securityが検知したOpen Proxy スキャニングログ

2026年1月20日、筆者が管理するサーバにて、典型的な Open Proxy スキャニング(公開プロキシ探索攻撃) を検知しました。
WAF(ModSecurity)が、いかにして「身勝手な中継依頼」を門前払いしたか、実際のログからそのメカニズムを記録します。

1. 検知ログの概要(実録)

以下は、攻撃者が CONNECT メソッドを使用して外部ドメイン(www.baidu.com)へ接続しようとした際のログです。

[Tue Jan 20 05:43:59 2026] [security2:error] [client xx.xx.xx.xx] 
ModSecurity: Warning. Match of "within %{tx.allowed_methods}" against "REQUEST_METHOD" required. 
[id "911100"] [msg "Method is not allowed by policy"] [data "CONNECT"] [severity "CRITICAL"]
[hostname "www.baidu.com"] [uri "/"]

2. 攻撃の構造解析:何が起きていたのか?

このログから、攻撃者の明確な意図が読み取れます。

A. メソッドの悪用(CONNECT)

通常、Web閲覧(GET/POST)には使われない CONNECT メソッドが使用されています。これは本来、プロキシサーバに対して「外部サーバへのトンネルを作れ」と命じるためのコマンドです。

B. Hostヘッダの偽装

[hostname "www.baidu.com"]

攻撃者は、筆者のサーバに接続していながら、リクエストの宛先に www.baidu.com を指定しています。これは、筆者のサーバを「踏み台(Open Proxy)」にして、中国の検索エンジンにアクセスしようとする試みです。

これは、ほぼ、金盾の影響下にある者が当局の規制を逃れるため、私のvpsを利用しようとしたという背景でしょう。

3. 防御のロジック:なぜ止まったのか

ID 911100 の発動: ModSecurity CRS は、許可リストにないメソッド(CONNECT)を検知した瞬間、問答無用で CRITICAL 判定を下しました。

4. 結果:404ページへの誘導。

また、筆者のapacheのバーチャルサイトの設定もいい動きをしました。

ErrorDocument 403 404.html

としていたため、ModSecurity がアクセスを拒絶した後、Apache は設定された ErrorDocument に従い、403 forbiddenではなく、内部的に 404 not found を返しています。
攻撃者側から見れば、外部へのトンネルが開通するどころか、「そんなページ(機能)は存在しない」 という素っ気ない返答を掴まされて終わったことになります。

5. まとめ

今回の事例は、WAFのポリシーを「必要なものだけを許可し、不要なものは拒否する」状態に保ったことが防御につながった例です。

オレだけが外に出る事を
許可しろォォォ─────ッ

だが!
ウイルスは
許可しないィィィ────ッ

感染した部分は
出る事は
許可しないィィィィィ───ッ!!

というイルーヅォの言葉は割とセキュリティで重要という言葉を以て、本記事を締めくくります。

Linuxのディスク使用状況をMarkdown形式で一発表示するワンライナー

サーバーの構築メモやドキュメント作成時に、「現在のディスク使用状況をサクッとMarkdownの表にしたい」と思ったのがこちらのワンライナーのきっかけ。

通常、df -h などのコマンドを使いますが、結果をブログやGitHubのIssueに貼り付けるには整形の手間がかかります。

今回は、lsblkjq を組み合わせて、実行するだけで綺麗なMarkdownテーブルを出力する魔法のワンライナーをご紹介します。

使用するコマンド

以下のワンライナーを使用しました。

{ echo -e "| デバイスパス | 全サイズ | 使用率 | 空き容量 | マウントポイント |"; echo -e "| --- | --- | --- | --- | --- |"; lsblk -lnpo NAME,SIZE,FSUSE%,FSAVAIL,MOUNTPOINT --json | jq -r '.blockdevices[] | select(.mountpoint != null and (.name | contains("loop") | not)) | "| \(.name) | \(.size) | \(.["fsuse%"] // "0%") | \(.fsavail // "-") | \(.mountpoint) |"'; }

出力結果のイメージ

実行すると、以下のようなMarkdown形式のテキストが得られます。

デバイスパス全サイズ使用率空き容量マウントポイント
/dev/vda1149G24%109.9G/
/dev/vda15106M6%98.2M/boot/efi
/dev/vda16913M13%702.4M/boot

このワンライナーの仕組み

このコマンドは大きく分けて3つのパートで構成されています。

1.ヘッダーの作成

echo -e を使って、Markdownの表の1行目(項目名)と2行目(区切り線)を出力しています。

2. lsblkによるデータ取得(JSON形式)

lsblk コマンドに複数のオプションを渡しています。

  • -l: リスト形式で表示
  • -n: ヘッダーを非表示にする
  • -p: フルデバイスパスを表示(/dev/vda1など)
  • -o ...: 必要な項目(名前、サイズ、使用率、空き容量、マウントポイント)を指定
  • --json: データを扱いやすいJSON形式で出力します。

3. jqによるフィルタリングと整形

JSONデータを jq で加工し、Markdownの行 | ... | ... | の形に変換しています。

  • select(.mountpoint != null): マウントされていないデバイスを除外します。
  • contains("loop") | not: Snapパッケージなどで増えがちな loop デバイス(仮想デバイス)を除外してスッキリさせています。
  • \(.name) | ...: JSONの値をMarkdownのパイプ記号で囲んで出力します。

まとめ

このワンライナーを使えば、サーバー調査の結果をそのままドキュメントに記録できるため、作業効率が大幅にアップします。特にファイルサーバやWebサーバの定点観測に役立つでしょう。

GROWIスタックの運用を効率化するBashスクリプトの作成

1. はじめに

GROWI(Wikiシステム)をセルフホストする場合、本体の他にMongoDB、Elasticsearch、そしてリバースプロキシとしてのApacheなど、複数のサービスを管理する必要があります。これらを個別に手動で起動・停止するのは手間がかかり、手順ミスも起きやすくなります。

そこで、サービス間の依存関係を考慮し、安全かつ簡潔に管理するためのBashスクリプトを作成しました。

1-1. 筆者環境

  • Ubuntu 24.04
  • Growi (systemdにサービス登録済み)
  • Apacheのバーチャルホストでリバースプロキシ運用
  • mongodb
  • elasticsearch

2. スクリプトの主な特徴

  • 依存関係を考慮した起動順序:
    • DB → 検索エンジン(起動待ち含む) → Apache設定 → GROWI本体の順で確実に立ち上げます。
  • Apache設定の動的切り替え:
    • a2ensite / a2dissite を活用し、サービス停止時にはApache自体は止めず、GROWIへのアクセスパスのみをクリーンに遮断します。
  • 対話モードとコマンドライン引数の両対応:
    • 単体実行ではメニューが表示され、引数を渡せば自動実行が可能です。
  • エラーハンドリング: 各ステップで実行状態をチェックし、異常があれば即座に中断・通知します。

3. 管理スクリプト: manage-growi.sh

systemctlをいじるので、root権限で作っておきます。

#!/bin/bash

# ==============================================================================
# GROWIスタック管理スクリプト
# 対応サービス: MongoDB, Elasticsearch, Apache2, GROWI
# ==============================================================================

# --- [ 設定セクション ] ---
APACHE_CONF_NAME="growi.conf"
SITE_NAME="${APACHE_CONF_NAME%.conf}"
SERVICES=("mongod.service" "elasticsearch.service" "apache2.service" "growi.service")

# カラー出力設定
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# --- [ ユーティリティ関数 ] ---

log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_err()  { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
log_ok()   { echo -e "${GREEN}[SUCCESS]${NC} $1"; }

# 実行権限チェック
if [[ $EUID -ne 0 ]]; then
   log_err "このスクリプトの実行にはroot権限(sudo)が必要です。"
fi

# サービスの状態確認
is_active() {
    systemctl is-active --quiet "$1"
    return $?
}

# --- [ メイン機能 ] ---

start_services() {
    log_info "GROWIスタックの起動を開始します。"

    # 1. MongoDB
    log_info "データベース (MongoDB) を起動中..."
    systemctl start mongod.service
    is_active mongod.service || log_err "MongoDBの起動に失敗しました。"

    # 2. Elasticsearch
    log_info "検索エンジン (Elasticsearch) を起動中..."
    systemctl start elasticsearch.service
    # 応答待ち(最大75秒)
    for i in {1..15}; do
        if is_active elasticsearch.service; then break; fi
        [[ $i -eq 15 ]] && log_err "Elasticsearchの起動がタイムアウトしました。"
        sleep 5
    done

    # 3. Apache (設定の有効化)
    log_info "Apacheのリバースプロキシ設定を有効化しています..."
    if [ ! -L "/etc/apache2/sites-enabled/${APACHE_CONF_NAME}" ]; then
        a2ensite "${SITE_NAME}" > /dev/null
    fi
    systemctl reload apache2.service || systemctl start apache2.service

    # 4. GROWI 本体
    log_info "GROWI本体を起動中..."
    systemctl start growi.service
    sleep 3
    if is_active growi.service; then
        log_ok "すべてのサービスが正常に起動しました。"
    else
        log_err "GROWIの起動に失敗しました。ログを確認してください。"
    fi
}

stop_services() {
    log_info "GROWIスタックを停止します。"

    # サービス影響を最小限にするため、リバースプロキシから先に遮断
    log_info "GROWI本体を停止中..."
    systemctl stop growi.service

    log_info "Apacheの設定を無効化しています..."
    if [ -L "/etc/apache2/sites-enabled/${APACHE_CONF_NAME}" ]; then
        a2dissite "${SITE_NAME}" > /dev/null
        systemctl reload apache2.service
    fi

    log_info "バックエンドサービスを停止中..."
    systemctl stop elasticsearch.service
    systemctl stop mongod.service

    log_ok "すべての関連サービスが正常に停止しました。"
}

show_status() {
    echo -e "\n${YELLOW}--- サービス稼働状況 ---${NC}"
    printf "%-25s %s\n" "サービス名" "ステータス"
    printf "%-25s %s\n" "------------------------" "-------"
    for s in "${SERVICES[@]}"; do
        if is_active "$s"; then
            printf "%-25s ${GREEN}RUNNING${NC}\n" "$s"
        else
            printf "%-25s ${RED}STOPPED${NC}\n" "$s"
        fi
    done

    if [ -L "/etc/apache2/sites-enabled/${APACHE_CONF_NAME}" ]; then
        echo -e "Apache設定 (${APACHE_CONF_NAME}): ${GREEN}ENABLED${NC}"
    else
        echo -e "Apache設定 (${APACHE_CONF_NAME}): ${RED}DISABLED${NC}"
    fi
    echo ""
}

# --- [ エントリポイント ] ---
ACTION=${1:-"menu"}

case "$ACTION" in
    start)   start_services ;;
    stop)    stop_services ;;
    status)  show_status ;;
    restart) stop_services; start_services ;;
    menu)
        echo -e "${YELLOW}実行する操作を選択してください:${NC}"
        select opt in "Start" "Status" "Stop" "Restart" "Exit"; do
            case $opt in
                Start) start_services; break ;;
                Status) show_status; break ;;
                Stop) stop_services; break ;;
                Restart) stop_services; start_services; break ;;
                Exit) exit 0 ;;
                *) echo "無効な選択です。" ;;
            esac
        done
        ;;
    *)
        echo "Usage: $0 {start|stop|status|restart}"
        exit 1
        ;;
esac

4. 利用前の事前準備

スクリプトを使用するには、実行権限の付与と、システム上のファイル名の確認が必要です。

  1. 保存と実行権限の付与

ファイルを manage-growi.sh として保存した場合、以下のコマンドを実行します。

sudo chmod +x manage-growi.sh
  1. 設定の確認(重要)

スクリプト内の APACHE_CONF_NAME="growi.conf" が、実際に /etc/apache2/sites-available/ に存在するファイル名と一致しているか確認してください。

5.使い方(コマンド一覧)

このスクリプトは root権限(sudo) が必要です。

A. 対話型メニューで操作する

引数なしで実行すると、メニューが表示されます。

sudo ./growi-stack.sh

B. 直接コマンドを指定する

特定の操作を即座に実行したい場合に使用します。

  • 起動: sudo ./growi-stack.sh start
  • 停止: sudo ./growi-stack.sh stop
  • 状態確認: sudo ./growi-stack.sh status
  • 再起動: sudo ./growi-stack.sh restart

6. 表示・実行例

ステータス確認時の表示 (status)

現在の各サービスの稼働状況が一覧で表示されます。

--- サービス稼働状況 ---
サービス名                  ステータス
------------------------   -------
mongod.service             RUNNING
elasticsearch.service      RUNNING
apache2.service            RUNNING
growi.service              RUNNING
Apache設定 (growi.conf): ENABLED

起動処理の実行例 (start)

依存関係を考慮し、DBから順に起動していきます。

[INFO] GROWIスタックの起動を開始します。
[INFO] データベース (MongoDB) を起動中...
[INFO] 検索エンジン (Elasticsearch) を起動中...
[INFO] Apacheのリバースプロキシ設定を有効化しています...
[INFO] GROWI本体を起動中...
[SUCCESS] すべてのサービスが正常に起動しました。

7. スクリプトの主な特徴と処理順序

このスクリプトは、単なる一括起動ではなく、GROWIの依存関係を考慮した設計になっています。

順序起動時 (Start)停止時 (Stop)理由
1MongoDBGROWI本体DBがないとアプリが落ちるため / 停止は入り口から。
2ElasticsearchApache設定無効検索エンジンを準備。
3Apache設定有効Elasticsearchインフラが整ってから外部公開。
4GROWI本体MongoDB最後にアプリを立ち上げる。

8. 運用のヒント

  • トラブルシューティング: もし [ERROR] が出た場合は、個別のログを確認してください。
  • GROWIのログ: journalctl -u growi.service -f
  • Elasticsearchのログ: journalctl -u elasticsearch.service -f
  • 自動起動設定: サーバー再起動時に自動で立ち上げたい場合は、このスクリプトを使わず systemctl enable [サービス名] を各サービスに設定するのが一般的です。このスクリプトは、メンテナンス時の手動メンテナンス用として最適です。

王道、ダー・シュワーム。(アプリ版『ガイアプロジェクト』記録)

安定した強さを誇り、カットされなければ覇道を進むことができるダー・シュワーム。

しかも、マジョリティが

  • 施設数
  • 同盟の施設数

と、ダー・シュワームのためにあるようなものだったので、むしろ三番手でなぜ誰も取らなかったのかと思いながらスタート。

研究も3ゴールと上位タイル1つ(鉱山×2点)

得点は188点と、相当の点数をたたき出せました。

  • Pros
    • 受動パワーがとにかく強かった
    • 研究のタイミングがよかった
  • Cons
    • 特になし

と、ミスった部分がほぼない珍しい状況でした。

新たなPCのサプライ購入。

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

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

ダイソーで

  • ワイヤレスイヤホン
  • メモリーカードリーダー

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

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

これに加えて

  • マウス
  • ACアダプター

一式をお気に入りのポーチにまとめることができました。

防衛戦の後始末。広域IPブロックによる外部ストレージ(Wasabi)およびNextcloudの疎通不能と復旧

クリスマスに喰らった広域IPからのDDoSを排除した話。そこでの「友軍相撃(Friendly Fire)」をやらかしたという失敗談です。

事前背景

  1. クリスマスに自分のWebサイトを狙ったDDoSを喰らいました。
  2. そのとき、ipsetで /8という超高域ブロックを実施。

このときのメモはこちらにまとめています

その結果 何が起きたか(事象)

  • s3fsの不調: Wasabi(東京リージョン)のバケットがマウントされず、df -h コマンドが数分間フリーズ(Dステート一歩手前の待機状態)。
  • Nextcloudの孤立: 管理画面に「このサーバーにはインターネット接続がありません」と警告が表示され、アプリストアやアップデートチェックなどの外部通信がすべて遮断された。
  • システム負荷: 通信のタイムアウト待ちにより、VFS(仮想ファイルシステム)レイヤーでプロセスが滞留しました。

2. 原因切り分け(診断)

調査の結果、ネットワーク層およびカーネル層での遮断を確認しました。

エンドポイント疎通確認:

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)結果を引きおこしたのです。

3. 対処(ufw-blocklistの解除)

干渉していた巨大なブロックエントリを ipset から削除し、通信路を復旧させました。

  • Wasabi (Tokyo) への疎通を確保
sudo ipset del ufw-blocklist 103.0.0.0/8
  • Nextcloud 公式サーバーへの疎通を確保
sudo ipset del ufw-blocklist 85.0.0.0/8
  • 設定の永続化
sudo ipset save ufw-blocklist -f /etc/ufw/ipsets.save

この状態では、まだs3fsプロセスが固まっていますので、これも対処。

  • 念のためs3fsプロセスを強制終了。
sudo killall -9 s3fs
  • 固まっていたマウントを解除。
sudo fusermount -u -z /mnt/wasabi2
  • fstabに基づく再マウント
sudo mount -a

4. 復旧確認(検証)

各サービスが正常なステータスに戻ったことを確認しました。

通信確認:

curl -I https://s3.ap-northeast-1.wasabisys.com303 See Other が即座に返ることを確認。

マウント確認:

df -h が遅延なく応答し、Wasabiバケットの容量が表示されることを確認。

Nextcloud確認:

管理画面の「インターネット接続なし」の警告が消え、外部連携機能が復旧したことを確認。

なぜ気づくのが遅れたか?

「このマウントはバックアップのため、普段めったに触れなかった」に尽きます。

今回、バックアップをいじるようになってからようやく気づいた次第です。これに関しては反省。

「使ってなければそれは見えていないのと同じ」というバイアスでした。

今回の教訓

私がipsetの搭載で言及した

これは、対象のIPアドレスをシャットアウトする「慈悲なき王」です。

を身を以て体感しました。なので、これを振るう時は更に注意する必要がありました。

それにしても、「自分だけが使うサーバのため、被害が自分だけで済んで良かった」と改めて思った次第です。こちらの記事にて

「『いい鉄砲は打ち手を選ぶ』ってことわざ知ってるか?
威力のある鉄砲は その分扱いも難しく危険
だから未熟者が使うと打ち手の方がケガをするってことさ」

が自分へ向かうことのないよう、日々、管理/監視を怠らないようにする必要があると知った出来事でした。

が、まさに自分に向かっていったというお話しで、本件を締めます。

WAFの「やりすぎ」と「見逃し」を飼い慣らす:ModSecurityチューニング実践

OSSでのWAFとして非常にメジャーなModSecurityとCRS(Core Rule Set)。

デフォルトでは非常に強力な保護が得られます。しかし、そのままではRedmineやNextcloudといった「複雑なリクエストを投げるアプリ」はまともに動きません。

今回は、筆者の例を元に、偽陽性(誤検知)を回避しつつ、偽陰性(すり抜け)を最小限に抑える設定術を解説します。

筆者環境

  • Ubuntu 24.04
  • Apache 2.4
  • Mod Security v2

動いているWebアプリ

  • Nextcloud
  • Redmine
  • BookStack

と、WAFが偽陽性を誘発するようなWebアプリ群です。

1. そもそも「偽陽性」「偽陰性」とは?

WAFを運用する上で避けて通れない2つの概念です。

用語状態影響対策
偽陽性 (False Positive)正常な通信を攻撃と判定ユーザーがログインできない、投稿が消えるルールの除外設定(Exclusion)を行う
偽陰性 (False Negative)攻撃的な通信を正常と判定脆弱性を突かれ、被害が出るシグネチャの更新、独自ルールの追加

「守りを固めれば不便になり、利便性を取れば危うくなる」。このジレンマを解決するのが、筆者が設定している個別除外ルールの設計です。

2. 確実だけど偽陰性を産むやり方「ID除外」

これは筆者が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ルール名(概要)挙動の説明
911100Method is not allowed by policy許可されていないHTTPメソッド(GET/POST以外など)を検知します。
920420Request content type is not allowed by policyContent-Typeヘッダーが許可リストにない場合に反応します。
949110Inbound Anomaly Score Exceeded重要: これは特定の攻撃を指すものではなく、他のルールの合計スコアが閾値を超えたため「ブロックした」という最終結果を示すIDです。
980130Inbound 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ともなると、

  • 偽陽性になりやすい(取り除かれやすい)ルール
  • 一発アウトになりやすい文章

は極めて多いのです。

特に、技術ブログのように

  • コマンド羅列
  • SQLコマンドをベタ打ち
  • スクリプト文の紹介

などは、投稿した瞬間にエラーとなったため、渋々SecRuleRemoveIdで検知しないようにした方は極めて多いのではないでしょうか。

3. CRSの裏をかく「防御未満の攻撃」

また、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ヘッダー:
  • Connection: keep-alive, close という通常ではありえないヘッダーが含まれていました。これは Slowloris などのDoS攻撃ツールに見られる特徴です。
  • IPアドレスでのホスト指定:
  • ドメイン名ではなくIPアドレス(例: 203.0.113.1)を指定してアクセスしています。これはボットによる無差別なスキャンの典型的な挙動です。

この、Mod_Securityのルールの外を狙った「じわじわとリソースを削っていく」攻撃こそ遮断する必要があります。

4. 「Webは守る」「投稿はスルーする」の「両方」をやる

この「偽陽性は防ぎつつ本来の防御を確立する」ために筆者が行っている手段は「例外ルールによるチューニング」です。

前提として:

こちらのリンク先のような形で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

なぜこれが必要なのか

  1. プロトコルの柔軟性: 標準のCRSは「GET/POST」以外のメソッド(PUT, PATCH, PROPFINDなど)を攻撃の兆候として厳しく制限します。しかし、モダンなWebアプリ(特にNextcloud)にはこれらが不可欠です。
  2. 偽陽性の温床「本文検査」: Redmineで「SQLの書き方」をチケットに書くと、WAFはそれを「SQLインジェクション攻撃」とみなしてブロックします。特定のパスに対してのみ、特定の検査(Tag)をオフにすることで、利便性を確保しています。
  3. 無駄なスキャンの排除: WordPressを使っていないサーバーへのWordPress用スキャンは、リソースの無駄です。これを早期に検知して「スコア加算+404応答」とすることで、後続の重い検査をスキップしつつ攻撃者をあしらいます。
  4. SecRuleRemoveByTag の威力: IDを1つずつ消すと、CRSがアップデートされた際に追加された「新しいSQLi検知ルール」に対応できません。今回のように OWASP_CRS/ATTACK-SQLI というタグ単位で除外することで、将来のアップデート後も「投稿機能だけは常に使える」状態を維持できます。
  5. 「404」で返す心理的効果: 攻撃者(のボット)は、403を返すと「あ、WAFがあるな」と判断して攻撃手法を変えてくることがありますが、404を返すと「このIPには何も存在しない」と判断してリストから外してくれる可能性が高まります。

設定のテストと反映

上記、設定を行ったら

  • 構文チェック
sudo apache2ctl configtest

apache 再起動

sudo systemctl restart apache2.service

apache 再起動確認

systemctl status apache2.service

active(running)を確認します。

偽陽性排除確認

実際にRedmine / Nextcloud等にアクセスして、投稿をしても偽陽性にならない(エラーにならない)を確認できれば成功です。

5. 賢いWAF運用のコツ

WAFは一度設定して終わりではありません。

ログを確認する:

/var/log/apache2/error.log や ModSecurityのアAuditログを監視し、id:xxxxx が出たら「それは本当に攻撃か?」を疑い、必要なら今回のコンフィグに除外ルールを追記します。

身内には優しく、外敵には厳しく:

特定のソースIPや、自社ドメイン宛の正常な操作(Redmineの更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。

この、例外ルールを正しく使うことで、スクリプトキディやクローラーに対して、このような大見得を切ってやりましょう。

『任務は遂行する』
…………
部下も守る
おまえごときに両方やるというのは
そうムズかしい事じゃあないな

2026年1月の統率者会メモ。

  • 釣り合った天秤、テヴァル
  • 世界播種、ハースハル
  • 元ソルジャー、クラウド

がいる中、「高名な者、ミシュラ」デッキにて対戦。

デッキリストはこちら。

身代わり合成機がある中で
金属製の巨像→無情な屍術師で巨像をサクって宝物11個、墓地に落としたミシュラを釣り上げ、残りのトークン使ってゴンティの無限心臓を発明品の唸り。後は無限ターン成立(『ミシュラの戦機』という名前のゴンティの無限心臓が出る→合成機が誘発してアーティファクトが出る→追加ターンの要件をミシュラの戦機を追放。オリジナルは残るので、後はターンを一切返さず同じ手順の繰り返し)

ライブラリーの中にシンクロ解除があるので相手のトークンもケアするまでもなく勝利。

過酷な消耗戦。全除去やピン除去でミシュラの統率者税は6マナ。眼前にはゼンディカーの報復者によるトークン軍団やハーシュハルの猛攻、装備品を山ほど積んだクラウドがいる中での逆転は『グラップラー刃牙 特別編』

プロレスは甘くない!!!
敗北ギリギリまで相手の攻撃を受けきり
紙一重で逆転!!!

を体感です。

Page 1 of 3

Powered by WordPress & Theme by Anders Norén