投稿者: manualmaton Page 4 of 308

ピボット旅行2026-03『筆者の異常な愛情 または私は如何にして輪行することを止めて未練を持つことになった か』

地味に、今回の大型旅行は国内初の「旅行代理店を一切介さない」旅でした。そもそも、フェリー東九を使う以上、ノーマルな旅行代理店の取り扱いはありません。

だからこそ、先述した母の「先に宿を取る」が最後まで響いたわけです。

そこで発生した「2つの大きすぎる軌道修正」が今回のテーマとなります。

-1ヶ月目:帰りの船がない

フェリー東九は1ヶ月前から予約可能と言うことで、時は8/24。出発日の9/24に行きの船を

  • 東京→徳島
  • 個室付き

の乗船予約を取ることに成功。後は帰りの9/28のフェリーを取るため8/27に予約をする…… 復路は割引がきくから更にOKと思っていた矢先

「この日付は選択できません」

の非情なメッセージ。待って、定期便のはず…… ということで改めてスケジュールを確認したのですが「この日付は徳島寄港せず」の×印があります。

これも確かに道理です。いくら4隻のローテーションとは言え、メンテナンスや「物理的にそこに寄港しないスケジュール」は存在します。

これは、飛行機や鉄道での旅行に慣れきっていたため「船便が毎日来るとは限らない」という当たり前の事実に気づきました。

とはいえ、この事態が行きの便でなくて本当に助かりました。

-1ヶ月目の解決策:新幹線を使う

極めて単純な手です。高松→岡山は1時間足らずで瀬戸大橋を渡って到着します。即座にスマートExで予約を行い、自転車が運べる最後列の指定席も確保。もしもの時に備えて変更可能なプランを選択。

これで、

  • 行きのルート(東京→徳島→高松)
  • 宿(高松3泊)
  • 帰りのルート(高松→岡山→東京)

が決まったわけです。

準備は上々

これが整ったことで

  • 自分と一緒に運ぶ荷物(輪行一式)
  • 先んじて別便で送るもの(衣服)

のジャンル分けが完了し、おぼろげながら主な旅程を考えていました。

最初のプラン

  • 0日目:フェリー泊。新木場なり有明なりで自走してブロンプトンを輪行する。
  • 1日目:徳島港から徳島駅まで自走して、そこから輪行して高松。
  • 2日目:高松駅を朝一で出発して特急で今治まで輪行し、しまなみ海道を走りつつ尾道。
  • 3日目:鷲羽山ハイランドで「世界一怖い」とも言われるスカイサイクルにチャレンジ。
  • 4日目:岡山まで輪行してさっと岡山駅周辺を見て東京に帰る

当時の私は「抜け漏れがない」良いプランだと思っていました。実際は

「全日程が好天である限り通用する」プランです。

これに関しては全く心配していませんでした。「ほぼ絶対レベルで私の天候運に揺るぎはない。この2024年の尾道旅行でも同じ、秋空が出迎えてくれる。これは事実である」というレベルで晴れ男ぶりを確信していました。

もっと言うと、「That's not a prediction, that's a SPOILER!!」の心境でした。

-2日目:大きく崩れたバイアス

9/22。高松の宿に向け別便で荷物を発送。これで輪行の準備をするだけであるというタイミングで天気予報を確認し、強烈な天気図を確認。

「秋雨前線の大停滞」

です。「これは何かのミスでは?」が最初の一声です。いくら2026年9月は雨が多かったとは言え、旅行中には逸れるはずだと

  • Yahoo! 天気予報
  • Appleの天気アプリ
  • 気象庁

などを確認しても「9/26~9/28は雨である」は回避不能。ピンポイントで晴れると言うこともまずないだろうということを天気図で確認。

思いっきり、秋雨前線が四国の南辺りに固定されています。「自分の運が、こんな時に、崩れる?」という、重力が逆さになったような常識が崩れました。

-2日目の決断:輪行を諦める

今回の最大のピボットです。全日程で雨。であれば、ブロンプトンは「自分の行動範囲を広げてくれる相棒」から「ほぼ全ての状況で余計な手間を増やす11キロの物体」に成り下がります。

デメリットを敢えて言いますが

  • 水濡れはもちろん、錆の心配。
  • それに伴う体の冷え。
  • 視界不良やスリップの心配。
  • 雨の中での輪行の難易度の高さ。

これらがのしかかります。そして、雨の時は使わない手も採れるとは最初に思っていましたが「その間、宿で留守番させるのか?」がつきまといます。であれば、

自転車は未練がましく諦め、雨で楽しめるプランにする

にしたほうが賢明です。本当に幸いなことに

  • 雨に煙る尾道は今回、初めて撮影することになる。良い機会だ
  • 高松駅至近の宿。その地の利を活かす。

と思い直して、大方の荷物をブロンプトンのロールバッグからキャリーケースへと詰め替えます。

「雨なら雨の楽しみがあるはず」というマインドセットがあったとは言え、これは正直耐えがたい決断。

また、2024年の旅行の通り、

「この子は晴天でこそ似合う」に思い直し、留守番させます。

しかし、この決断がなければ確実に、この旅は失敗でした。

そんなこんなで、次からはいよいよ実際の写真を伴いながらの旅行記となります。

ピボット旅行2026-02『動かなかった軸』

初めに

前回の記事で、「今回の旅行はピボットの連続だった」と書いてきましたしかし、最初から最後まで変わらなかったものがあります。

それは、瀬戸内海へ行くこと。特に、尾道に行くこと。

ここだけは、最後まで残りました。ですが、そのための手段は大きく変わりました。

最初に考えていたのは、サンライズ瀬戸です。東京から夜行列車に乗り、朝に四国へ入り、そこから自転車で瀬戸内を走る。

2024年でも実施したことです。ですが、この2024年の経験を思い返すと、どうしても引っかかるものがありました。

瀬戸内海方面の個室の夜行=サンライズというバイアス

サンライズは確かに便利です。東京から四国まで寝ている間に運んでくれます。

しかし、実際に乗ってみると、車内での身動きはそれほど自由ではありません。そして何より、前回は空調と乾燥の影響もあって喉を痛め、その後の旅行にまで響きました。

旅行のスタート地点に立つ前に、すでにコンディションを崩してしまうという本末転倒がありました。

そこで、最初の大きなピボットが発生しました。

「サンライズで行く」を捨てて、「船で行く」に変える。

ただし、ここでも目的は変わっていません。

瀬戸内海を拠点として、尾道に行きたい。そのために、移動手段を変えただけです。

そこで候補になったのが、オーシャン東九フェリーでした。これも「船旅がしたかったから」というほどロマンチックな選択ではありません。

むしろ、かなり現実的な消去法です。

  • 個室がある。(これは絶対の条件だったので、夜行バスという選択肢はそもそも外れます)
  • 東京から出る。
  • そして四国に着く。

この条件を並べていくと、かなり自然に候補が絞られていきました。何より、「移動で体力を削らずに、四国へ入る」という当初の目的を維持できます。

時間はもちろん、フェリーの方が遅くなります。

移動手段発車/出港到着所要時間
サンライズ瀬戸21:26(東京)翌07:27(高松)10時間01分
オーシャン東九フェリー19:00(有明)翌13:20(徳島)18時間20分

この差は8時間。ほぼ一日違いますが、フェリーは非常に魅力的なオプションでした。

サンライズ瀬戸の憂慮点

  • 昨今、大人気のサンライズ瀬戸。いくら宿を取ったところで一ヶ月前の10時打ち争奪戦には巻き込まれたくありません。
  • 食事や飲み物事情。寝台に特化しているため、車内販売や自販機がありません。
  • 空調。これは筆者の体調の問題です。2024年の時に喉を痛めたということで、ここは避けたい。

フェリーの憂慮点

  • 何せ初めて。勝手が分からない。再現性は不明。
  • 海の上で快適に眠ることはできるのか?
  • 空調がここでも再現したらどうしよう

とはいえ、以下の魅力がありました。

フェリーで良さそうだと思った点

  • 2人部屋/4人部屋でも1人で乗れる。
    • 温泉宿のようにお一人様お断りが無かった。
  • 横になるだけでも体力の回復は全然違う。
    • この8時間の差をむしろアドバンテージと考えました。
  • 食事販売がある。
    • 船内レストランの代わりに冷凍食品の自販機と業務用電子レンジという割り切ったオペレーションは大好きです。
  • 船内大浴場がある。
    • 最後の決め手になりました。シャワー兼転売とかいう人道に悖る行為が普通にあるのと比べ、フェリーはその性質上、「大量の熱湯が確実に発生する」乗り物です。それを船客に還元するというアイディア。そして、出港時や就寝前、そして朝にお風呂というのは心躍ります。
    • 更に、お風呂という強烈な湯気と湿気で喉を保護できるのもアドバンテージです。

こうして、サンライズからフェリーへと交通手段が変わりました。

宿(拠点)の問題

そして、交通手段は勘案できました。フェリーで四国へ入るなら、どこを拠点にするのか。

当初のイメージだけなら、尾道でもOK。むしろ、しまなみ海道を走るのであれば、尾道は非常に分かりやすい目的地です。ところが、帰りのことまで考え始めると、話が変わってきます。

フェリーは徳島から東京へ戻ります。だったら、行きだけを見て尾道を拠点にするより、四国側に拠点を置いたほうが全体として動きやすいと判断して、拠点として出てきたのが、高松でした。

高松なら、徳島へ戻るのも比較的動きやすいです。(というよりも瀬戸内海の玄関口です)。朝早く動けるなら、今治まで移動して、そこからしまなみ海道をアタックすることもできます。

つまり、 「しまなみへ行くための宿」ではなく、「瀬戸内のどこへでも振れるための拠点」

として高松に白羽の矢を立てました。

ここでも、旅行の軸そのものは変わっていません。変わったのは、軸を中心にした配置です。

そして、もう一つ。最初から三泊四日で行こうと決めていたわけでもありませんでした。当初は二泊三日でもいいのではないかと考えていました。

  • フェリーでの移動時間を考える。
  • しまなみへ行く可能性を考える。
  • (これはほぼ考慮していなかったのですが……)天候によって予定が変わる可能性を考える。
    • 自分の天候運には絶対の自信を持っていました。

何よりも2024年は、

  • 0日目:東京→高松
  • 1日目:高松→今治→自転車と高速線で尾道→宿泊
  • 2日目:尾道での自由行動
  • 3日目:尾道から東京に戻る

そう、お気づきの通り、「まるまる一日使える日が2日目しか存在しない」のです。ここで自転車で走った体力をリカバリーしつつ尾道で過ごすのはかなり無理があります。事実、2024年は旅の間喉の痛みに悩まされ、帰宅翌日に内科のお世話になったほどです。

そう考えて、予算増はやむなしとして三泊四日にしました。これも後から考えれば、今回の「ピボット旅行」を成立させるための重要な判断だったと思います。

そんな計画を母に告げたところ

「まず宿を取りなさい。移動は新幹線に変えられる。でも宿はそうはいかない」

その通りでした。交通機関や観光地のことばかり考えていると、どうしても「何をするか」から決めたくなります。しかし、旅行には寝る場所が必要です。

だったら、まず拠点を確保。そこから先を考えればいい。特に、先に言ったとおり高松は

  • 四国全県
  • 岡山
  • 小豆島

などの交通アクセスが極めて良好な都市です。なので勧められるまま高松の宿を取りました。

結果として、この「まず宿から」という判断が、今回の旅行の自由度をかなり高くしてくれましたし、この一言が今回の旅の成功の要素でした。

  • 宿が決まっている。
  • 荷物を置く場所がある。
  • 帰ってくる場所がある。

だったら、その日の天候や体力、気分に合わせて、行き先を変えられます。尾道も日帰りで帰ることができます。

予定を一つ捨てても、旅行そのものは壊れません。ここまで来て、今回の旅行の「軸」がようやく見えてきました。

  • 瀬戸内海を見る。
  • 尾道を歩く。
  • なんなら天候や気分、体力に合わせて他のオプションを自由に決められる。

変わったのは、旅行の方法でした。変わらなかったのは、旅行の目的でした。

今回の旅行で何度も予定を変えることになったのは、最初から何も考えていなかったからではありません。

高松という絶対的なハブがあったからこそ柔軟に旅程を変えられた(ピボットが成立した)

今回の「ピボット旅行」は、実際にはこの2026年6月下旬から始まっていたのです。

ピボット旅行2026年01-定義説明-

ピボット(pivot)という言葉があります。

もともとは「軸」「旋回軸」といった意味ですが、ビジネスやプロジェクトの文脈では、それまで進めていた方針を、状況に合わせて大きく転換することを指します。

当初の計画を全部捨てるわけではありません。「このままでは上手くいかない」と判断したところで、目的そのものや現在地を見直し、別の方向へ舵を切る。

今回、この言葉を妙に気に入ってしまいました。

なぜなら、今回の旅行そのものが、まさにピボットの連続だったからです。

さっくりとした旅行の概要

2026年9月24日から28日まで、高松を拠点とした瀬戸内旅行に行きました。 ……と書けば、いかにも予定通りの旅行だったように見えます。

実際には、予定通りとは決して行かない旅行でした。

  • 出発前から予定を変更
  • 移動中にも変更
  • 現地でも変更
  • 天候によって変更
  • 腹が減ったので変更
  • 目の前に面白そうなものがあったので変更

など、予定通りだったのは行きと帰りの予約していた交通機関、そして宿のみ。それでも旅行そのものは、最後までちゃんと成立しました。

むしろ振り返ってみると、予定を変更したからこそ、成立した旅行だったというのが一番近い表現かもしれません。

そこで、この旅行を私は「ピボット旅行」と呼ぶことにしました。

なぜ「ピボット」なのか

この言葉を使うきっかけになったのは、WWEの絶対的エースだったジョン・シナのラストイヤーでした。2025年から始まったジョン・シナの引退ロードは、最後の最後まで大きな物語を作りました。

その中でも象徴的だったのが、ヒールターンです。

長年WWEの顔として君臨してきたシナが、ついにヒールへ転じる。演出そのものは見事でした。ところが、実際にファンが示した反応は、作り手が想定していた以上に大きな失望と困惑だった。

そこで物語は、さらに軌道修正されました。ヒールターンそのものを無かったことにするのではない。そこで生まれた感情も、そこで起きた出来事も拾い上げた上で、最終的にはフェイスターンへ向かっていく。

つまり、

「予定していた筋書きを最後まで押し通す」のではなく、実際に起きたことを見て、そこから物語を組み直す。

これが今回、私が「ピボット」という言葉を使いたくなった理由です。そして、今回の旅行もまったく同じでした。

今回の「ピボット」ぶり

最初に立てた旅行計画があります。

そこには「こう移動する」「ここへ行く」「これを持っていく」という予定がありました。

しかし、現実には

  • 天候があります。
  • 時間があります。
  • 交通機関があります。
  • 現地に着いてから初めて分かることがあります。

そして何より、

そのときの自分が何をしたくなるかなんて、出発前には完全には分かりません。

だから、計画通りに進まなくなったところで、「予定が崩れた。どうしよう」ではなく、「では、ここから何に切り替えるか」 と考える。

今回の旅行では、それを何度も繰り返しました。そして面白いことに、そのたびに旅行が悪くなるわけではありませんでした。

むしろ、 「ああ、こっちへ行けばいいのか」 という具合に、次の道が見えてくる。

最終的には、出発前には想像していなかったものが大量に残りました。

  • 写真の撮り方
  • 食べ物への認識
  • 衝動買いした数々
  • 移動手段や町そのものの認識
  • 自分の旅行について

今回の記事では、最初から旅程を全部並べるつもりはありません。

「9月24日はこれをして、25日はこれをして……」という旅行記にすると、今回の旅での印象的な部分が見えなくなってしまうからです。

  • なぜその場で予定を変えたのか。
  • 何を見て、何を諦め、何を拾ったのか。
  • そのピボットによって、当初の予定にはなかった何が生まれたのか。

そこを中心に書いていきます。2026年9月24日から9月28日まで。高松を拠点に、瀬戸内を巡った5日間。

予定通りとは決して行かなかった旅行でした。だからこそ、今回は 「ピボット旅行」 として記録しておこうと思います。

「Jailhouse Lock」の運用・トラブルシューティング(mod_rewriteで詰まったところ)

この記事の修正となります。

Webサーバーを公開していると、日常的に /wp-admin や /.git、生IPアドレスへのスキャン(Probe)が大量に飛んできます。

単に 404 や 403 を返すだけでも防御にはなりますが、今回は「悪意あるスキャナーやボットを検知し、専用のトラップ(檻)ページへ自動転送・隔離する」という多層防御システム(通称: Jailhouse Lock)を Apache + ModSecurity で構築しました。

構築の過程でハマった「設定の競合」と「ModSecurityのフェーズ問題」の解決策を記録として残します。

1. 目的

  1. 不正スキャンの隔離:
    隠しファイル(/.git)、WordPress探索(/wp-admin)、CGI探索(/cgi-bin)、生IP直撃などの不審なリクエストを検知し、アプリケーション(Rails/Node/PHP等)へ届く前に専用のトラップページ(檻)へ 302 Redirect する。
  2. 正常通信の保護:
    通常のブラウザからのアクセスや Let's Encrypt(ACME チャレンジ)などの正常な通信に一切影響を与えない。
  3. 無駄なログ・負荷の削減:
    ボットによる無駄なリクエスト処理コストを最小限に抑える。

2. 最初の設定と準備

(1) トラップページ(檻)ディレクトリとファイルの準備

トラップ先のコンテンツを配置するディレクトリとダミーファイルを作成します。

# 檻となるディレクトリの作成
sudo mkdir -p /var/www/jailhouse_trap

# トラップページの作成(例: Git用、WordPress用、CGI用、生IP/ボット用)
sudo touch /var/www/jailhouse_trap/git.html
sudo touch /var/www/jailhouse_trap/login.html
sudo touch /var/www/jailhouse_trap/cgi.html
sudo touch /var/www/jailhouse_trap/topgear.html

# 権限の調整
sudo chown -R www-data:www-data /var/www/jailhouse_trap

(2) Apache VirtualHost の基本設定(修正後)

諸々ハマったあとで最終的に決定した内容がこれです。

  • ドメイン名: example.com
  • ドキュメントルート: /var/www/my_app/public
  • トラップディレクトリ Alias: /__jailhouse_lock -> /var/www/jailhouse_trap
<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/my_app/public

    # ----- 檻(トラップベースディレクトリ)の定義 -----
    <IfModule mod_alias.c>
        Alias /__jailhouse_lock /var/www/jailhouse_trap

        <Directory /var/www/jailhouse_trap>
            Options -Indexes -ExecCGI
            AllowOverride None
            Require all granted
        </Directory>
    </IfModule>

    # ----- Jailhouse Lock (mod_rewrite トラップ群) -----
    <IfModule mod_rewrite.c>
        RewriteEngine On

        # ----- 0. ループ防止および例外通過(最優先判定) -----
        # 檻(トラップページ自身)と ACME チャレンジ(SSL更新)は即座に通過
        RewriteCond %{REQUEST_URI} ^/__jailhouse_lock [NC,OR]
        RewriteCond %{REQUEST_URI} ^/\.well-known/acme-challenge/ [NC]
        RewriteRule ^ - [L]

        # ----- 1. トラップ1: 隠しファイル / ドットディレクトリ探知 -----
        # /.git, /.env, /.htaccess 等
        RewriteRule ^/\. /__jailhouse_lock/git.html [R=302,L,E=dontlog:1]

        # ----- 2. トラップ1.5: WordPress探知 -----
        # /wp-admin, /wp-admin/*, /wordpress配下, /wp-login.php を隔離
        RewriteRule ^/(wp-admin|wordpress)(/.*)?$ /__jailhouse_lock/login.html [R=302,L,NC,E=dontlog:1]
        RewriteRule ^/wp-login\.php$ /__jailhouse_lock/login.html [R=302,L,NC,E=dontlog:1]

        # ----- 3. トラップ2: CGI探索者探知 -----
        # /cgi-bin配下、または .cgi, .pl, .py 拡張子を隔離
        RewriteRule ^/(cgi-bin|cgi-sys|cgi-mod)(/.*)?$ /__jailhouse_lock/cgi.html [R=302,L,NC,E=dontlog:1]
        RewriteRule \.(cgi|pl|py)$ /__jailhouse_lock/cgi.html [R=302,L,NC,E=dontlog:1]

        # ----- 4. トラップ3: 生IP直撃 / Hostヘッダー不正判定 -----
        # HostヘッダーがIPアドレス形式、または空の場合
        RewriteCond %{HTTP_HOST} ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(:[0-9]+)?$ [OR]
        RewriteCond %{HTTP_HOST} ^$
        RewriteRule ^.*$ /__jailhouse_lock/topgear.html [R=302,L,E=dontlog:1]

        # ----- 5. トラップ4: 悪質ボット隔離 -----
        RewriteCond %{ENV:bad_bot} ^1$
        RewriteRule ^.*$ /__jailhouse_lock/topgear.html [R=302,L,E=dontlog:1]

    </IfModule>
</VirtualHost>

3. 想定外の動きと原因(ハマりポイント)

構築中、/wp-login.php は正常に 302 で login.html に転送されるものの、/wp-admin や /wp-admin/admin-ajax.php などのディレクトリ配下へのアクセスが 404 Not Found になる現象が発生しました。

原因の調査で分かった「罠」は以下の2点です。

原因1:ModSecurity(Phase 1)による先行拒否

過去に ModSecurity 側に「WordPress 探索者を 404 で弾く」独自ルール(phase:1)を記述していました。

# [原因となった旧 ModSecurity ルール]
SecRule REQUEST_URI "@rx /(?:wordpress|wp-admin)" \
    "id:10002, phase:1, deny, status:404"

ModSecurity の phase:1(リクエストヘッダー受信直後)は、Apache の mod_rewrite 処理(Phase 2 相当)よりも前に実行されます。
そのため、mod_rewrite でリダイレクト(302)をかける前に ModSecurity が 404 を返してリクエストを終了させていました。

原因2:Alias と mod_rewrite のパス展開の競合

当初 /wp-admin を Alias /wp-admin /var/www/jailhouse_trap/login.html で処理しようとしたところ、/wp-admin/admin-ajax.php へのアクセスが /var/www/jailhouse_trap/login.html/admin-ajax.php という存在しないファイルパスへ展開され、Apache 内部で 404 が発生していました。

4. 修正内容

  1. ModSecurity 側の旧ルールの撤去:
    ModSecurity 側で 404 拒否していた旧ルールを削除し、URI 判定と転送ロジックをすべて mod_rewrite へ統一・一元化しました。
  2. Alias から mod_rewrite(正規表現捕獲)への切り替え:
    サブパスを持つ可能性のあるディレクトリトラップ(wp-admin や cgi-bin)は、Alias ではなく RewriteRule の正規表現 ^/(wp-admin|wordpress)(/.*)?$ を使用して配下ファイルごと一網打尽に捕捉するように変更しました。

5. 修正後の検証テスト

設定反映後、curl を使用して各エンドポイントの挙動をテストしました。

1 WordPress探索のテスト

# 末尾スラッシュなし
curl -i -A "Mozilla/5.0" "https://example.com/wp-admin"

# 配下ファイル
curl -i -A "Mozilla/5.0" "https://example.com/wp-admin/admin-ajax.php"

【結果】
いずれも HTTP/1.1 302 Found が返り、Location: [https://example.com/__jailhouse_lock/login.html](https://example.com/__jailhouse_lock/login.html) へ正しく誘導されました。

2 CGI探索のテスト

curl -i -A "Mozilla/5.0" "https://example.com/cgi-bin/test.cgi"

【結果】
HTTP/1.1 302 Found で Location: [https://example.com/__jailhouse_lock/cgi.html](https://example.com/__jailhouse_lock/cgi.html) へ誘導。

3 生IPアドレスアクセスのテスト

curl -i -k "https://192.0.2.1/"

【結果】
HTTP/1.1 302 Found で Location: [https://192.0.2.1/__jailhouse_lock/topgear.html](https://192.0.2.1/__jailhouse_lock/topgear.html) へ誘導後、ModSecurity との連携により無効化完了。

4 正常アクセス・ループ防止テスト

# 正常なブラウザ通信
curl -i -A "Mozilla/5.0" "https://example.com/"

# トラップページへの直接アクセス(ループ確認)
curl -i -A "Mozilla/5.0" "https://example.com/__jailhouse_lock/git.html"

【結果】
いずれもリダイレクトループを起こさず HTTP/1.1 200 OK が返り、正常なコンテンツが表示されました。

まとめ

今回のハマりポイントを通して、「WAF (ModSecurity) と Apache (mod_rewrite) の処理フェーズの違い」を理解することの重要性を痛感しました。

多くのクローラーは

  • IPアドレス直打ちである
  • ヘッダが欠損している
  • wp-adminやwp-loginを真っ先に狙ってくる
  • 隠しファイルを執拗に狙う

が私の経験則です。であれば、

  • IPアドレス直打ちやヘッダの欠損はまともにリクエストをさせない。(前段で弾く)
  • 存在しないパスや隠しファイルはダミーページに飛ばす

で、多くのノイズを減らすことができます。

攻撃者は何を探していたのか? ModSecurityのログから読み解く自動スキャンの狙い

Webサーバーを運用していると、ModSecurityやApacheのエラーログには毎日のように不審なアクセスが記録されます。

その多くは機械的なスキャンですが、中には「攻撃者が何を考えてこのURLを叩いているのか」がよく分かるものがあります。

今回は、筆者のサーバーで実際に検知したログをもとに、「攻撃者は何を探していたのか」「WAFはどこを危険と判断したのか」を読み解いてみます。

ログの内容抜粋

※ IPアドレスやドメイン名はダミーに置き換えています。

[Mon Sep 07 02:49:34.840627 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [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_x-middleware-subrequest. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1228"] [id "920450"] [msg "HTTP header is restricted by policy (/x-middleware-subrequest/)"] [data "Restricted header detected: /x-middleware-subrequest/"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL-ENFORCEMENT"] [tag "capec/1000/210/272"] [hostname "example.com"] [uri "/_image"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.841346 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?i)(?:[/\\\\x5c]|%(?:2(?:f|5(?:2f|5c|c(?:1%259c|0%25af))|%46)|5c|c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|(?:bg%q|(?:e|f(?:8%8)?0%8)0%80%a)f|u(?:221[56]|EFC8|F025|002f)|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|1u)|0x(?:2f|5c))(?:\\\\.(?:%0[01]|\\\\?)?|\\\\?\\\\.?|%(?:2( ..." at REQUEST_URI_RAW. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "54"] [id "930100"] [msg "Path Traversal Attack (/../) or (/.../)"] [data "Matched Data: /../ found within REQUEST_URI_RAW: /_image?href=/../../../.env"] [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 "/_image"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.842572 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] 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: 35)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/_image"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.842998 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=35, detection=35, per_pl=35-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=30, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=35)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/var/www/app/public/404.html"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.849032 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [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_x-middleware-subrequest. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1228"] [id "920450"] [msg "HTTP header is restricted by policy (/x-middleware-subrequest/)"] [data "Restricted header detected: /x-middleware-subrequest/"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL-ENFORCEMENT"] [tag "capec/1000/210/272"] [hostname "example.com"] [uri "/google-credentials.json"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep 07 02:49:34.849243 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] ModSecurity: Warning. Matched phrase "credentials.json" 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: credentials.json found within REQUEST_FILENAME: /google-credentials.json"] [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 "/google-credentials.json"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep 07 02:49:34.850394 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] 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: 10)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/google-credentials.json"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep07 02:49:34.850677 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=10, detection=10, per_pl=10-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=10)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/var/www/app/public/404.html"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep 07 02:49:34.990300 2026] [security2:error] [pid 136775:tid 137621634270912] [client 192.0.2.100:55394] [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_x-middleware-subrequest. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1228"] [id "920450"] [msg "HTTP header is restricted by policy (/x-middleware-subrequest/)"] [data "Restricted header detected: /x-middleware-subrequest/"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL-ENFORCEMENT"] [tag "capec/1000/210/272"] [hostname "example.com"] [uri "/keys/service-account.json"] [unique_id "ap2nrn53LMPC8yKIKKnxogAAAYU"]
[Mon Sep 07 02:49:34.991569 2026] [security2:error] [pid 136775:tid 137621634270912] [client 192.0.2.100:55394] [client 192.0.2.100] 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 "/keys/service-account.json"] [unique_id "ap2nrn53LMPC8yKIKKnxogAAAYU"]

普通のブラウザは送らないHTTPヘッダ

最初にModSecurityが反応したのは次のルールでした。

HTTP header is restricted by policy
Restricted header detected: x-middleware-subrequest

この X-Middleware-Subrequest は、一般的なWebブラウザが送るヘッダではありません。これは Next.js が内部処理で利用する特殊なヘッダとして知られており、近年では

「このヘッダを偽装すれば認証を回避できないか」

という攻撃が数多く試されています。つまり攻撃者は、

「このサーバーは Next.js ではないか?」

という前提でアクセスを始めています。ModSecurityは「通常の利用者が送るはずのないヘッダ」と判断し、この時点で危険なアクセスとして記録しました。

本命は .env ファイル

続いて現れたのがこちらです。

/_image?href=/../../../.env

一見すると画像を要求しているように見えますが、本当の目的は画像ではありません注目すべきなのは

../../../.env

という部分です。

../../../ が意味するもの

../ は

「一つ上のディレクトリへ移動する」

という意味を持ちます。

つまり

../../../.env

とは、

Web公開ディレクトリ
        ↓
      ../
        ↓
      ../
        ↓
      ../
        ↓
       .env

というように、公開ディレクトリの外側にある設定ファイルを読もうとしていることになります。これは Path Traversal(ディレクトリトラバーサル) と呼ばれる典型的な攻撃手法です。

なぜ .env を狙うのか

最近のWebアプリケーションでは、

.env

に重要な設定が保存されています。

例えば

DB_PASSWORD=
API_KEY=
SECRET_KEY=
AWS_SECRET_ACCESS_KEY=
SMTP_PASSWORD=

などです。もし .env が取得できてしまえば、

  • データベースへ接続
  • クラウドサービスへの接続
  • メールサーバーの悪用
  • 他システムへの横展開

など、一気に被害が拡大する可能性があります。攻撃者にとって .env は「宝箱」のような存在なのです。

WAFは何を見ていたのか

今回のログでは

930100

と

930110

という二つのルールが繰り返し反応していました。

Rule 930100

こちらは

パストラバーサルらしい文字列

を検知するルールです。

単純な

../

だけではなく、

URLエンコードやUnicode表現など、回避を試みた表現まで幅広く検査します。

Rule 930110

こちらはもっと単純です。実際に

../

という並びを検出します。

つまり、

  • URI全体
  • パラメータ
  • エンコード表現

それぞれを別々に検査しているため、同じリクエストでも複数回ヒットしています。

次に探し始めたもの

攻撃者は .env の次に、

/google-credentials.json

へアクセスしました。さらに

/keys/service-account.json

というリクエストも続いています。これは Google Cloud や Firebase などで利用される サービスアカウント鍵 を探しています。これらのJSONファイルには

private_key
client_email
project_id

などが含まれており、環境によってはGoogle Cloud APIを操作できる権限が保存されています。そのためOWASP CRSには、

Restricted File Access Attempt

という専用ルールが用意されており、今回も即座にブロックされました。

WAFが35点という高スコアを付けた理由

最後には

Inbound Scores

blocking = 35
LFI = 30

という結果が記録されています。

OWASP CRSは、一つのルールだけでブロックするのではなく、「怪しい行動の積み重ね」を点数化します。

今回であれば、

  • 通常使われないHTTPヘッダ
  • パストラバーサル
  • .env を狙うアクセス
  • 危険なファイル名へのアクセス

といった複数の危険要素が重なった結果、35点 という非常に高い危険度になりました。

既定では5点程度でブロックされるため、このアクセスは「かなり悪質」と判断されたことになります。

このログから見える攻撃者の思考

このログを見ていると、攻撃者は無差別にURLを叩いているようでいて、実際には一定の手順に沿って行動していることが分かります。

  1. まず特殊なHTTPヘッダを送ってNext.js系のアプリケーションではないかを探る。
  2. 次に画像APIを悪用できないか試し、.env を読み出せるか確認する。
  3. 失敗すると今度はGoogle Cloudの認証情報やサービスアカウント鍵を探し始める。

つまり、この一連のアクセスは

「秘密情報(Secrets)がどこかに置きっぱなしになっていないか」

を高速で探し回る自動スキャンなのです。

サーバーそのものを壊すことが目的ではなく、認証情報を盗み出して、その先にあるデータベースやクラウドサービスへ侵入することが本当の狙いと言えるでしょう。

ログを一つひとつ眺めるだけでは「また攻撃が来た」で終わってしまいます。しかし、攻撃者の行動を時系列で追ってみると、「何を探していたのか」「WAFは何を危険だと判断したのか」が見えてきます。

WAFは単に「怪しい文字列」を検出しているのではありません。攻撃者の行動全体を点数化し、その積み重ねから危険度を評価しています。

【Nextcloud】Backblaze B2へデータを移した話:filesだけをbindマウントしたときの手順

以前の記事では、Backblaze B2を rclone でマウントし、systemdサービスで安定運用する方法を紹介しました。

これで「クラウドストレージをローカルディスクのように扱える」環境はできたわけですが、Nextcloudではもう一つ考えなければならない問題があります。

それが 「何をクラウドへ置くのか」 です。最初は筆者も

dataディレクトリごとB2へ持っていけばいいだろう

と思っていました。ところが調べていくと、これは運用上かなり危険な構成でした。

今回は、Nextcloudのデータ領域のうちユーザーの実ファイルだけをBackblaze B2へ移し、それ以外はローカルSSDへ残す構成にした話です。

dataディレクトリを丸ごと移すと何が起きるのか

Nextcloudのデータ領域には、ユーザーが保存したファイル以外にも様々なものが置かれています。

例えば

  • サムネイル
  • プレビュー画像
  • キャッシュ
  • アプリ内部データ
  • ログ

などです。

特に問題になるのが appdata_xxx 以下です。画像を開くだけでもサムネイルが作られ、不要になれば削除されます。

つまり、 大量に作って、大量に消す という処理を何度も繰り返しています。ローカルSSDなら何の問題もありません。

しかし、これをオブジェクトストレージ上で行うと話は変わります。API呼び出しは増えますし、オブジェクトの作成・削除も大量に発生します。

Wasabiで痛い目を見った経験

以前、筆者はWasabiを使っていました。

Wasabiには「90日以内に削除されたオブジェクトでも、90日分の利用料金が発生する」というルールがあります。

つまり、

  • サムネイルを作る
  • 数秒後に削除される

という処理でも、90日分の容量として計算されます。その結果、

「実際には存在しないサムネイルの料金だけが積み上がる」

という状態になりました。見た目の容量はほとんど増えていないのに、請求対象だけが増えていくという、なかなか厄介な運用になります。

Backblaze B2ではWasabiほど厳しい制約はありませんが、それでも大量のAPIアクセスや不要なI/Oをわざわざクラウドへ投げる理由はありません。

そこで残すものと移すものを分ける

最終的に採用した構成はシンプルです。

ローカルSSDへ残すものは、

  • appdata_xxx
  • プレビュー
  • キャッシュ
  • ログ
  • .ncdata

だけです。逆にB2へ置くのは

data/<ユーザー名>/files

だけにしました。ユーザーが保存した実データだけをクラウドへ置く構成です。

今回の環境

今回確認した環境は次の通りです。

  • Ubuntu 24.04 LTS
  • Nextcloud
  • Backblaze B2
  • rclone(FUSE3)
  • マウントポイント:/mnt/b2

ユーザー名などは例としてダミーのものを使用しています。対象となるディレクトリは、

/home/www-data/example/data/demo/files

です。

まずはB2へ同期する

先にローカルのデータをB2へコピーします。まずはドライランで確認します。

rclone sync /home/www-data/example/data/demo/files example-b2:example-bucket/nextcloud/files --dry-run -P -v

問題がなければ本番同期です。

rclone sync /home/www-data/example/data/demo/files example-b2:example-bucket/nextcloud/files -P

同期が終われば、ローカル側を退避します。

cd /home/www-data/example/data/demo

mv files files_backup_local
mkdir files

chown www-data:www-data files
chmod 750 files

シンボリックリンクでは動かない

最初に試したのはシンボリックリンクでした。

ln -sfn /mnt/b2/nextcloud/files files

ところが、

php occ files:scan

を実行すると、

Following symlinks is not allowed

で停止します。最初は権限かと思いました。しかし原因はNextcloud側でした。Nextcloudはデータ領域内のシンボリックリンクを追跡しない仕様になっています。

セキュリティ上は正しい動作なのですが、この用途では少々困ります。

bindマウントなら問題なく動く

そこで使ったのがLinux標準のbindマウントです。

sudo mount --bind \
/mnt/b2/nextcloud/files \
/home/www-data/example/data/demo/files

Nextcloudから見ると、これは普通のディレクトリです。そのため、

sudo -u www-data php /home/www-data/example/occ files:scan demo

も問題なく完走しました。シンボリックリンクでは拒否されても、bindマウントなら普通に扱ってくれます。

fstabだけでは少し危ない

bindマウントが成功したので、そのまま /etc/fstab に書けば終わり…… と思ったのですが、ここにも落とし穴がありました。起動直後は、まだ rclone がB2をマウントしていません。

その状態でbindマウントすると、 空ディレクトリをbindしてしまいます。

そこで、

/mnt/b2/nextcloud/files \
/home/www-data/example/data/demo/files \
none \
bind,nofail,x-systemd.after=rclone-b2.service \
0 0

のように、 x-systemd.after を指定して、rcloneサービスの起動後にbindマウントされるようにしました。これなら再起動しても順序が崩れません。

この構成にして良かったこと

結果として、今回の構成にはかなり満足しています。

まず、サムネイルやキャッシュはローカルSSDで処理されるため、クラウドストレージに不要なI/Oが流れません。

一方で、ユーザーが保存した実データだけはBackblaze B2へ退避されるため、ローカルディスク容量を気にし続ける必要もなくなりました。

さらに、Nextcloudのシンボリックリンク制限にも引っ掛からず、Linux標準のbindマウントだけで自然に動いてくれます。

結果として、「速いものはローカル、保存したいものはクラウド」という、それぞれの長所を活かした構成になりました。

Nextcloudをオブジェクトストレージへ移そうとしている方は、data ディレクトリを丸ごと載せ替える前に、一度「本当にクラウドへ置くべきものは何か」を考えてみることをおすすめします。

その一手間だけで、後々の運用はかなり楽になります。

Backblaze B2をLinuxへ常駐マウントする:/etc/fstabを使わず、systemdサービスで自動起動と自己修復を実現する

前回の記事では、WasabiからBackblaze B2へのデータ移行についてまとめました。これでデータそのものは移動できましたが、それだけではWebアプリケーションから利用することはできません。

Redmineから見れば、これまで通り「files」ディレクトリが存在しているだけであり、その保存先がローカルSSDなのか、Backblaze B2なのかは意識しない構成が理想です。

そのためには、Backblaze B2をLinuxへマウントし、通常のディレクトリとして扱えるようにする必要があります。

今回は、そのための仕組みとしてrclone mountを利用しました。問題は、「どうやって常駐させるか」です。

古くからLinuxでは/etc/fstabへ記述して起動時にマウントする方法が紹介されています。しかし今回は、その方法は採用しませんでした。理由は単純です。クラウドストレージはネットワークの向こう側にあります。

ローカルディスクと同じ感覚でOS起動時にマウントしようとすると、起動順序や通信断など、物理ディスクでは考えなくてよい問題が発生します。

今回はそうしたリスクを避けるため、systemdサービスとしてrclone mountを常駐させる構成にしました。

systemdを選んだ理由

/etc/fstabは非常に便利ですが、クラウドストレージでは少し事情が変わります。

OS起動時、まだネットワークやDNSが利用できないタイミングでマウント処理が実行されると、ホスト名の名前解決に失敗し、そのまま起動処理が止まってしまうことがあります。

設定によってはレスキューモードへ入ってしまうため、「ストレージが利用できない」だけでは済みません。

また、運用中に通信断などでrclone mountが終了してしまった場合も、/etc/fstabには再起動する仕組みがありません。

つまり、一度落ちると手動で復旧するまで、Redmineから添付ファイルへアクセスできない状態が続いてしまいます。systemdであれば、この問題を比較的素直に解決できます。

After=network-online.targetによってネットワークが利用可能になるまで起動を待機でき、さらにRestart=alwaysを指定すれば、万が一プロセスが終了しても自動的に再起動してくれます。

単に「起動時にマウントする」のではなく、「サービスとして運用する」という考え方です。

FUSEの準備を行う

rclone mountはFUSE(Filesystem in Userspace)を利用して動作します。

今回は一般ユーザーでマウントを実行しつつ、Webサーバー(www-data)から読み書きできるようにしたかったため、まずFUSE側の設定を変更しました。

念のためバックアップを取得した上で、/etc/fuse.confのuser_allow_otherを有効にします。

  • バックアップ
sudo cp -p /etc/fuse.conf /etc/fuse.conf.orig

user_allow_other を有効化

sudo sed -i 's/#user_allow_other/user_allow_other/' /etc/fuse.conf

続いてマウントポイントを作成します。

sudo mkdir -p /mnt/app_storage
sudo chown -R $USER:www-data /mnt/app_storage
sudo chmod 775 /mnt/app_storage

今回は実行ユーザーが管理しつつ、Webサーバーからも書き込み可能な権限にしました。

systemdサービスを作成する

続いて、rclone mountをsystemdサービスとして登録します。

sudo tee /etc/systemd/system/rclone-b2-app.service << 'EOF'
[Unit]
Description=Rclone Mount Backblaze B2 for Application Storage
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
User=appuser
Group=appuser
ExecStart=/usr/bin/rclone mount b2-remote:dest-backup/app_data /mnt/app_storage \
  --allow-other \
  --vfs-cache-mode full \
  --vfs-cache-max-size 10G \
  --vfs-cache-max-age 24h \
  --uid 33 --gid 33 \
  --umask 002 \
  --log-level INFO \
  --log-file /var/log/rclone-b2-app.log
ExecStop=/bin/fusermount3 -u -z /mnt/app_storage
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

今回の設定で特に意識したのは、アプリケーションとの互換性です。

--vfs-cache-mode fullを指定することで、読み書きをローカルキャッシュ経由で処理し、多くのWebアプリケーションが通常のファイルシステムと同じように扱えるようになります。

また、

--uid 33
--gid 33

を指定し、見かけ上の所有者をwww-dataへ変更しています。これにより、Redmineなどから見ても通常の添付ファイルディレクトリとして扱うことができます。

ログについてもsystemd任せにせず、専用ファイルへ出力するよう設定しました。

sudo touch /var/log/rclone-b2-app.log
sudo chown appuser:appuser /var/log/rclone-b2-app.log

問題が起きた際に、原因を追いやすくするためです。

サービスとして起動する

設定が終わったら、systemdへ登録します。

sudo systemctl daemon-reload
sudo systemctl enable --now rclone-b2-app

状態は、

sudo systemctl status rclone-b2-app

で確認できます。Active: active (running)となっていれば正常です。

続いて、

ls -la /mnt/app_storage/files/

などで、Backblaze B2上のデータがマウントされていることを確認します。ここまで来れば、Linuxからは通常のディレクトリとして扱える状態になっています。

Redmineを止めずに切り替える

最後は、Redmineが参照する添付ファイルディレクトリを切り替えます。今回はシンボリックリンクを利用していたため、

cd /var/www/redmine
ln -sfn /mnt/app_storage/files files

だけで切り替えられました。Linuxではシンボリックリンクそのものを置き換える処理は一瞬で完了します。

つまり、Redmineから見ると「files」という入り口が別の場所を指すようになるだけであり、中途半端な状態が発生しません。

このような、一度に切り替える方法は一般にアトミックな切り替えと呼ばれます。アプリケーションを停止することなく保存先だけを差し替えられるため、運用中のシステムでは非常に扱いやすい方法です。

最後は実際に読み書きを確認する

切り替えたら、必ずWebアプリケーションから動作を確認します。

まずは既存チケットを開き、添付ファイルや画像が問題なく表示・ダウンロードできることを確認しました。

続いて、新しいチケットを作成し、テスト用ファイルを添付します。

Redmine上で正常に保存できることに加え、Backblaze B2の管理画面でもファイル数と使用容量が増えていることを確認しました。

読み込みだけでなく、書き込みまで正常に動作することが確認できれば、移行作業は完了です。

おわりに

Backblaze B2への移行は、データをコピーしただけでは終わりではありません。

Webアプリケーションからこれまで通り利用でき、通信断が発生しても自動的に復旧し、運用者が特別なことを意識しなくても済む状態になって初めて、「移行が終わった」と言えるのだと思います。

今回はrclone mountをsystemdサービスとして管理することで、起動順序や通信断といったクラウドストレージ特有の課題にも対応できる構成になりました。

今後は、この構成でRedmineやPiwigoなどを実際に運用しながら、VFSキャッシュのサイズやメモリ消費、レスポンスへの影響なども継続して検証していく予定です。

rcloneを使ったクラウド間データ移行:WasabiからBackblaze B2へ直接ストリーミング同期する

前回の記事では、WasabiからBackblaze B2へ移行する理由と、B2のアカウント作成から接続確認までをまとめました。

今回は実際に、Wasabiへ保存されているデータをBackblaze B2へ移行します。

「クラウドからクラウドへデータを移す」と聞くと、一度ローカルへダウンロードし、それを再びアップロードするイメージを持つ方も多いかもしれません。

筆者も当初はその方法を考えていました。

しかし、この方法ではサーバーのディスク容量を一時的に消費するだけでなく、ダウンロードとアップロードの二重の転送が発生します。データ量が増えるほど、作業時間もストレージ使用量も無視できません。

そこで今回は、rclone が持つリモート間同期機能を利用し、WasabiからBackblaze B2へ直接データを転送しました。

Linuxサーバーはデータを一時的に中継するだけで、ローカルディスクへ保存することはありません。ストレージAPI同士がストリームとしてデータを受け渡すため、容量を気にすることなく移行できます。

本記事では、接続確認からドライラン、本番同期、そして移行後の整合性確認までの流れをまとめます。

今回の移行環境

今回の環境は次の通りです。

  • サーバー:Ubuntu 24.04 LTS
  • ツール:rclone
  • 移行元:Wasabi(S3互換)
  • 移行先:Backblaze B2
  • 移行対象:app_data ディレクトリ

今回はs3fsなどでマウントしたディレクトリは一切利用せず、rclone syncによるクラウド間同期のみで移行を行いました。

まずは接続できることを確認する

同期を始める前に、移行元・移行先の両方へ問題なく接続できることを確認します。今回は、それぞれ次のようなRemoteを登録しました。

Wasabi

[wasabi-remote]
type = s3
provider = Wasabi
access_key_id = <WASABI_ACCESS_KEY>
secret_access_key = <WASABI_SECRET_KEY>
region = ap-northeast-2
endpoint = s3.ap-northeast-2.wasabisys.com

Backblaze B2

[b2-remote]
type = b2
account = <B2_APPLICATION_KEY_ID>
key = <B2_APPLICATION_KEY>
hard_delete = true

設定後は、それぞれのバケット一覧が取得できることを確認します。

# Wasabi
rclone lsd wasabi-remote:

# Backblaze B2
rclone lsd b2-remote:

ここでエラーが出るようであれば、APIキーやアクセス権限を見直しておきます。

最初に移行対象を確認する

同期コマンドを実行する前に、まず対象となるデータ量を確認します。

容量だけではなく、オブジェクト数もこの段階で記録しておくと、移行後の確認が非常に楽になります。

rclone size wasabi-remote:src-storage/app_data

筆者の環境では、

Total objects: 769 (769)
Total size: 454.601 MiB (476683987 Byte)

という結果になりました。

今回は約455MBと比較的小規模ですが、この数字はあとでBackblaze B2側と比較する重要な基準になります。

いきなり同期せず、まずはドライランを行う

rclone syncは非常に便利ですが、その名前の通り「同期」を行うコマンドです。

移行先との差分によっては削除や上書きも実行されるため、筆者はいきなり本番を流すことはほとんどありません。

まずは--dry-runを付け、実際には転送を行わず、どのような処理が実行されるかだけを確認します。

rclone sync \
    wasabi-remote:src-storage/app_data \
    b2-remote:dest-backup/app_data \
    --dry-run \
    -P \
    -v

今回の結果は次の通りでした。

Transferred:      454.601 MiB / 454.601 MiB, 100%, 318.497 MiB/s, ETA 0s
Transferred:          769 / 769, 100%
Elapsed time:          3.0s

全769オブジェクトについて問題なく処理できることを確認できました。

ドライランで異常がなければ、本番へ進みます。

本番同期を実行する

確認が終わったら、--dry-runを外して実際の同期を行います。

rclone sync \
    wasabi-remote:src-storage/app_data \
    b2-remote:dest-backup/app_data \
    -P \
    --transfers 4 \
    --checkers 8

今回は並列転送数を4、差分確認を8として実行しました。この値は回線速度やCPU性能によって最適値が異なりますが、個人サーバー程度であれば十分扱いやすい設定です。

今回のデータ量では、同期自体は数秒程度で完了しました。ローカルディスクを経由していないため、サーバー側の空き容量を気にすることなく作業できたのは大きな利点でした。

移行後は必ず整合性を確認する

同期が終了したら、「終わった」で済ませず、必ず移行先を確認します。まずはBackblaze B2側で同じように容量を集計します。

rclone size b2-remote:dest-backup/app_data

結果は、

Total objects: 769 (769)
Total size: 454.601 MiB (476683987 Byte)

となりました。

移行前に確認した

  • オブジェクト数
  • 総容量

の両方が完全に一致しています。さらにBackblaze B2のWebコンソールでもファイル数と容量を確認し、問題なく反映されていることを確認しました。

ここまで確認できれば、データ移行は完了です。

おわりに

今回の移行作業そのものは数分で終わりました。しかし、本当に時間を掛けたのは「どのストレージへ移るか」を考えることだったように思います。

以前のWasabiでは、Timed Deleted Storageを意識してアプリケーションの設定まで調整する必要がありました。

もちろん、その経験があったからこそ、料金体系だけではなく「普段どのようなファイルが生成・削除されるのか」という視点でストレージを選ぶようになりました。

オブジェクトストレージは、容量や価格だけを比較してしまいがちです。

しかし実際には、アプリケーションの動作と料金体系が噛み合っているかどうかの方が、長く運用していく上では重要なのだと改めて感じています。

次回は、このBackblaze B2をLinuxへマウントし、NextcloudやPiwigoなどのWebアプリケーションからローカルストレージと同じ感覚で利用できるようにする構成をまとめます。

Wasabiの90日削除問題(Timed Deleted Storage)から脱出する。Backblaze B2とrcloneによるオブジェクトストレージ再構築

以前の記事でも触れましたが、筆者はWasabiの「Timed Deleted Storage(90日最低保存期間)」によって、一度約8万円という高額請求を経験しました。

もちろん、Wasabiそのものが悪いサービスというわけではありません。

バックアップ用途のように、一度保存したデータを長期間保持する使い方であれば非常に優秀です。問題だったのは、私が動かしていたアプリケーションとの組み合わせでした。

当時の環境では、MongoDBを利用するサービスが大量の一時ファイルや更新データを短時間で生成・削除しており、その挙動がWasabiの90日最低保存期間と最悪の相性になってしまいました。

さらにNextcloudでも、サムネイル生成やプレビュー画像、一時ファイルなどが絶えず作られては削除されます。

アプリケーションとしてはごく正常な動作です。しかし、Wasabiでは「90日以内に削除されたオブジェクト」であっても、残りの保存期間分がTimed Deleted Storageとして課金対象になります。

結果として、利用容量以上に「削除したデータ」が積み上がり、気付いた頃には非常に大きな請求になっていました。

この経験以降、筆者は運用を大きく見直しました。

NextcloudのデータはローカルSSDへ退避し、一時ファイルの保持期間も短縮。キャッシュやサムネイルの扱いも見直したことで、Timed Deleted Storageは12MB程度まで抑え込めています。

現在では当時のようなペナルティはほぼ発生しておらず、運用自体は十分安定しています。それでも、根本的な問題は残っていました。

実際に保存しているデータは20GB程度しかありません。(というか、Nextcloudを限定的な使い方しかしていないため増やせないという)

さらに、今後導入を考えているPiwigoのようなフォトギャラリーでは、サムネイル生成や画像整理が日常的に行われます。

「この操作をするとTimed Deleted Storageが増えるかもしれない。」

そんなことを気にしながらアプリケーションを導入したり、設定を考えたりするのは、本来あるべき運用ではありません。

サービスに合わせてアプリケーションを制限するのではなく、アプリケーションが本来の動きをしても問題にならないストレージを選びたい。

そう考え、オブジェクトストレージそのものを見直すことにしました。

Backblaze B2を候補に選んだ理由

移行先として最初に候補へ挙がったのは、Cloudflare R2とBackblaze B2でした。

どちらも個人利用では人気の高いサービスですが、筆者が重視したのは「料金」そのものではなく、「運用中に余計なことを考えなくて済むか」という点です。

Wasabiで最も苦労したのは、保存容量ではありません。「削除する」という、ごく普通の操作にコストが発生することでした。Backblaze B2では、料金はByte-Hoursという時間単位で計算されます。

サムネイルや一時ファイルを数分だけ作成して削除した場合、その数分間しか課金されません。アプリケーションが普通に動けば、料金も普通に計算される。

もちろん、小容量利用でのコスト面も魅力です。

最低利用容量はなく、10GBまでは無料。筆者のように20GB程度しか保存していない環境であれば、月額は数十円程度に収まります。

また、以前は「日本から利用するには代理店経由なのでは」と思い込んでいましたが、実際には本家サイトからそのまま個人契約でき、クレジットカードやPayPalにも対応していました。

Backblaze B2のアカウントを作成する

移行先をBackblaze B2に決めたら、まずはアカウントを作成します。

公式サイトから 「B2 Cloud Storage」 を選択し、メールアドレスとパスワードを登録するだけで利用を開始できます。個人利用でも特別な手続きは必要なく、クレジットカードやPayPalでそのまま契約できます。

途中で保存先リージョンを選択しますが、 US West を選択しました。物理的に日本へ比較的近く、レイテンシも十分実用的です。

認証メールで本人確認を済ませると、管理画面へログインできます。

Bucketを作成する

続いて、データを保存するためのBucketを作成します。

左メニューから B2 Cloud Storage → Buckets を開き、「Create a Bucket」を選択します。

設定は次のようにしました。

  • Bucket Name:任意(グローバルで一意)
  • Visibility:Private
  • Default Encryption:今回は無効
  • Object Lock:無効

Object Lockはランサムウェア対策や改ざん防止には非常に有効な機能ですが、通常のファイルサーバー用途では「削除できない」という制約が先に立ってしまいます。

バックアップ専用のバケットであれば検討する価値がありますが、今回は日常的に更新されるストレージなので無効のままとしました。

最初に変更しておきたい設定

Bucketを作成したら、最初に確認しておきたい項目があります。

それが Lifecycle Settings です。

初期設定では Keep all versions となっており、同じファイルを上書きすると以前のバージョンが残り続けます。Backblaze B2はデフォルトでファイルの世代管理を行うため、このままでは古いバージョンも保存容量に含まれます。

筆者の用途では世代管理は不要だったため、

Keep only the last version of the file

へ変更しました。これで最新版のみを保持する運用になります。もちろん、バックアップ用途で利用するのであれば「Keep all versions」の方が適しています。

ここは「どちらが正しい」という話ではなく、用途に合わせて選択する部分です。

アプリケーションキーを発行する

続いて、Linuxから接続するためのAPIキーを作成します。

管理画面の Application Keys から Add a New Application Key を選択します。

今回は次のように設定しました。

  • Key Name:任意
  • Bucket:作成したBucketのみ
  • Access:Read and Write
  • Allow List All Bucket Names:有効

最後の Allow List All Bucket Names は忘れやすい項目です。

rcloneなどがBucket一覧を取得する際に利用するため、有効にしておく方が扱いやすくなります。

キーを作成すると

  • keyID
  • applicationKey

が表示されます。

このうち applicationKeyはこの画面でしか表示されません。 あとから確認できないため、安全な場所へ保管しておきます。

Ubuntu 24.04へrcloneを導入する

Backblaze B2への接続には、今回は rclone を採用しました。

以前はs3fsを利用していましたが、VFSキャッシュや同期機能などを考えると、現在ではrcloneの方が扱いやすい場面が多くなっています。

Ubuntu 24.04であればAPTからそのまま導入できます。筆者は好みでaptitudeを用いています。

sudo aptitude update
sudo aptitude install -y rclone fuse3

導入後以下で確認します。

rclone version

rcloneを設定する

設定は一般ユーザーで行います。

rclone config

を実行し、新しいRemoteを作成します。主な設定は以下の通りです。

  • New remote
  • 名前:任意
  • Storage:Backblaze B2
  • Account:keyID
  • Key:applicationKey

途中で

Permanently delete files on remote removal, otherwise hide files.

という質問が表示されます。筆者はここだけ true を選択しました。Backblaze B2は削除しても「Hidden Version」として残すことができますが、今回の目的は「不要なデータを残さない運用」です。

そのため、削除時は完全削除となるよう hard_delete=true を設定しています。

なお、ファイルを上書きした場合の旧バージョン保持はLifecycle設定の対象になるため、先ほどBucket側で「Keep only the last version」に変更した意味もここで生きてきます。

接続を確認する

設定が終わったら、最後に接続できることを確認します。

rclone lsd 設定した名前:

設定が正しければ、作成したBucket一覧が表示されます。

続いて簡単なファイルをアップロードしてみます。

echo "Hello, Backblaze B2 from Linux!" > test-b2.txt
rclone copyto -P test-b2.txt \
    名前:バケット名/test-b2.txt

アップロード後は、

rclone ls 名前:バケット名

で存在を確認できます。さらに削除も試してみます。

rclone delete \
    名前:バケット名/test-b2.txt

今回は、削除後もTimed Deleted Storageのような最低保存期間を気にする必要はありません。短時間だけ存在したファイルは、その存在していた時間だけが課金対象となります。

これで、ようやく「削除すると料金が増えるかもしれない」という制約から解放されました。

もちろん、料金が安くなったことも嬉しいのですが、それ以上に「アプリケーションが本来の動作をしても気にしなくて良い」という安心感の方が、筆者にとっては大きな収穫だったように思います。

『ライザのアトリエ3』採取のTIPS:「金属」が付与された「英雄の心得」を入手する

この記事にて

ロール(役職)こそが火力の命

ステータス値そのものよりも遥かに重要なのが

  • ロールレベル
  • ロール効果

でした。アルケミストの消費CC軽減やアイテム威力補正、ブレイバー等の乗算レート。狙ったロールを確実に発現させることこそが、数百万から2000万への跳躍を分ける決定打となります。

と記しましたが、「そのロールレベルを上げる鍵となる」○○の心得を手に入れるのは終盤ほど厄介になるという仕組みがあります。

仕組み:レアリティの差と不可逆的なスキルツリー

英雄の心得はレアリティ(☆3つ)です。

反して、

鷹の目は☆5。

この違いは「採取ランクが高ければ高いほど低レアリティは手に入れにくくなる」を意味します。

これはやりこみ勢にとっては厄介な仕組みとなり得ます。

なぜなら、

スキルツリーは一度入手すると「それを無効化する」ことはできない仕様になっているからです。

「採取ランク1」→「採取ランク2」はスキルポイントの消費で行えますが、その逆はできないという決定的な罠があります。

採取ランクを敢えて落とすやり方

ここでは既に

「生き物が採れる・優」で発現を止めた捕獲網

を利用しての採取方法を紹介していますが

  • 通常採取
  • 杖スウィングでの採取

は不可逆です。ですが、ハイエンドの武器/装飾品を作るには割と

  • 金属
  • 神秘の力

が有用です。

タオの最強武器、イフェスティオや

スキルで戦うキャラクターに装備させたい投資のバングルなど。

これらの素材から超特性を取得する方法を見つけました。

灯台下暗し――「実家」という採取場所

方法と言うより、場所を見つけた、という方が正解です。

  • 金属と神秘の力を持つ素材
  • それらが大量に手に入る(一回での採取確率を増やす)

そんな素材が取れる場所、あるのか……? を改めて探したところ、

ここでした。クーケン島周辺エリア、それも「ライザの家」です。

この家近くにある木。

シリーズの代名詞「うに」が取れるだけではなく、採取ランク3に高めると「黄金うに」が手に入ります。

都合が良いことに

  • うに
  • 金属
  • 神秘の力

持ちです。これに超特性を付与できる方法があります。

レア採取ポイント出現の秘密の鍵

無垢の鍵からキーメイクできる「アンコモン/レア」の鍵から確率で「レア採取ポイント出現」を発動できる鍵を採取できます。

この鍵を、上記の木の前で利用すると

木そのものがレア採取ポイントになります。後は、チャージスウィングで一気に刈り取ることで

筆者の体験則で行くと、全てのスキルを発現させている状態でのチャージスウィングで、「1~2回に1回は○○の心得」を持つ黄金うにが採取できました。

まとめ――運よりも確率を上げる採取法

ほぼ全てが論理的に数値を上げられる本作において、かなりままならないのが「超特性ガチャ」です。

欲しい超特性が出るかどうかそのものを、こちらから直接指定することはできません。

しかし、

  • 欲しい属性を持つ素材を選ぶ
  • その素材が大量に取れる採取場所を選ぶ
  • レア採取ポイントを発生させる
  • チャージスウィングで一気に採取する

ところまでは、こちらで組み立てることができます。

特に今回の「黄金うに」は、金属・神秘の力を持ち、しかもライザの家という非常に近い場所で大量に確保できます。

「○○の心得」が欲しいからといって、採取ランクを下げるためにスキルツリーを巻き戻すことはできません。

ならば、ランクを下げるのではなく、現在の採取環境のまま、欲しい超特性を引く試行回数を増やす。

これが今回見つけた方法です。

もちろん、最後の一個が出るかどうかは運です。

ですが、「どこで」「何を」「どうやって」採るかまで詰めれば、その運任せの部分をかなり小さくすることはできます。

本作のやりこみは、必ずしも「最高の装備を作るために、最高ランクまで進める」だけではありません。

一度進めてしまった採取ランクを戻せないからこそ、戻れないことを前提に、別のところから確率を組み立て直す。

今回の「実家」は、そのための灯台下暗しでした。

更に余談――謝辞に代えて

今回の検証を始めるきっかけとなったのが、以前の記事にいただいた読者様(jhop様)のコメントでした。

3点ほど、助言いただけたら幸いです。
1.武器の特性・超特性に対して
2.ロールに対して
3.属性防御について

このコメントを受けて、

  • 今までステータス上昇系だけで組んでいたが、まだ伸びしろがあるのではないか
  • まだ検証し切れていないダメージのロジックがあるのではないか
  • もしかすると、これまでの記録(一回のアイテムラッシュで1000万ダメージ)を更新できるのではないか

と考え、ライザ3DXを立ち上げました。

そこから2026/09/12より集中して検証を行った結果、

  • ロールレベルによるリニア直線とも言うべき係数
  • アイテムラッシュにおけるライザ自身のダメージバフ
  • 一回のアイテムラッシュで2000万ダメージという大幅な記録更新
  • ディアンによる異次元のフェイタルドライブ、3900万ダメージ

といった、これまで確認できていなかったデータを見つけることができました。

最初は「まだ伸びるのではないか」という程度の疑問でした。

それが実際に調べてみると、ロール、アイテムラッシュ、バフ、そして超特性と、これまで見えていなかった要素が次々と繋がっていきました。

こうして、以前の記録を更新するだけではなく、ダメージ計算そのものについて新しい発見ができたことは、大きな収穫でした。

2026/09/12から始まった今回の集中検証ですが、取り急ぎ、ここでいったん区切りを置きます。

検証のきっかけをいただいたjhop様には、この場を借りてお礼申し上げます。

ありがとうございます。法則の発見、再現性、何よりも今までのバイアスを打破するロジック。非常にエキサイティングで知的好奇心あふれた再発見の日々ですが

「これを持って更に他の記事を書きたい」という欲望の方が強くなっています。

反面、読者様の意に沿わない「なんでゲーム攻略と同じような感じでLinuxシステムやログ解析をしているんだろう」という形になるかと存じますが

「これは私のブログである以上、何を書くかは私が決める」

という原理原則を持ちまして、この集中検証は一区切りとします。また新たな発見があればこの場でご紹介するでしょう。

Page 4 of 308

Powered by WordPress & Theme by Anders Norén