ピボット旅行2026-07『Come Sail Away!(後編)』

はじめに

この、安定した船旅がもたらした時間は非常に有意義でした。

逆説的に「普段と全く変わらない部屋での過ごし方」をしただけで目的地寸前まで到着するという形です。

最初のトラブルとバックアッププラン

とはいえ、最初から「どーすんだこれ」のトラブルがいきなり降りかかります。

「ThinkPadのACアダプターがない」

詰め忘れです。出発寸前までこのノートPCを開いていたため、鞄に持ち込んだ後、それをサプライバッグに入れてませんでした。ですが脳内シャアの

「ええい、打ちどころが悪いとこんなものか!」

とはならず。というのも、スマートフォンやiPadなどの充電に備えて「USBマルチアダプター」があったからです。PD対応。後はケーブルが…… ありました。それも、体のすぐそばに。

「Type-C給電可能なケーブル」が備わっているネックストラップ。この「見せてもらおうか……」をまさか本番でぶち込むとは思わず。

結果は成功。

でなければ、この船旅はかなりの制限が加わっていたと思います。「当初の予定を外しながらもバックアッププランでどうにかする」を象徴する出来事。

お風呂→頭の悪い夕飯

お風呂という性質上、テキストのみの描写になりますが…… 艦内大浴場は感動そのものでした。キチッと座って汗を流し、湯量たっぷりの浴槽に浸かる。そして眼前に広がる川﨑の夜景。

で、部屋着に着替えて自室に戻り、楽しいお弁当タイムです。正直、ピボット(コメダで夕食)という手じゃあなくてよかったと思いました。でなければ、この頭の悪い弁当に巡り会うことはできませんでした。

こちらです。

そぼろと牛タン焼き肉。それ以上に目を引くのが辛子明太子一腹。「欲望をその場に詰め込んだ」という。これを、先の「広い船室」でいただく。最高にも程があります。

フェリー飯・船内オペレーション

テンションが上がると胃袋の容量が上がるもの。頭の悪いお弁当を食べてなお「もう少しに胃に入る」ということで、オーシャン東九フェリーの名物「冷食販売機」を利用します。

とても簡単な仕組みです。

  1. 気に入った商品を自販機で購入する。
  2. パックを袋から開ける。
  3. ラウンジに置かれている業務用電子レンジで「買ったものと番号が一致する」ボタンを押す。
  4. それを食べる

見かけとか更に別守とかそんなお行儀のいいものはありません。冷食のトレイでそのままいただく。これは船客も提供側も満足いく仕掛けだと思いました。

船客のメリット

  • 船上価格が若干入っていても「熱々の食べ物が即手に入る」
  • 調理器具も食べる場所も用意されている。
  • 個室利用者なら船室でも食べられる。
  • 航行中であればどのタイミングでも食べられる。

提供側のメリット

  • 人件費の最小化。
  • 船客用の厨房を廃することで船室面積を増やせる。
  • 輸送と保管費用も最低限。

はいいとして「海を見ながら、身近な冷食を食べる」というテーマとテンションは、背徳感がマシマシです。

船室のベッド

これは、豪華なダブルベッドとかではないですが、筆者としては満足いく広さです。何せ4人部屋を1人で利用。床にそのまま寝ることも可能です。かなり深く眠ることができました。

船内アクティビティ

食事と就寝以外は本当に普段やっていることでした。即ち、

  • PCを広げる
  • ブログの記事を書く
  • オフラインで日記を書く
  • ゲームをする

です。このゲームをするというのはスマートフォンゲームではなくSteamでの『Blue Reflection Quartet』をプレイしてました。ThinkPadがしっかりと軽めの3Dゲームも動くということで。

朝風呂と食事

紀伊半島を見ながらの朝風呂も最高でした。朝ご飯は、夕飯と合わせて買っていた『キムパとチャプチェ』です。これも、「ある程度、味がしっかりしているし旅情も味わえる」パーツ。

夜明け→接岸

そこからは以下の通り。起きたのが紀伊半島の東側。

そこから朝風呂入ったりデッキでねんどろライザと記念撮影をしたり。

そして、「潮岬」で電波が安定してきたのでThinkPadで自分のサーバにSSH接続をしたりfirefly-iiiの記帳をしたり。

もちろん、着替えとパッキングも済ませています。

ゆっくりしながらいよいよ、船は徳島港に入港。瀬戸内の青い海が出迎えてくれました。(この日だけ)

ピボット旅行2026-06『Come Sail Away!(前編)』

はじめに

この旅そのものの成否を分ける「フェリーの18時間超の航海」は、結論から言えば大成功でした。

船の全貌

国際展示場駅から相当歩くだけのことはあります。船そのものが大きく、ターミナルからも、あの独特の桟橋での入船です。

東京湾に浮かぶ大きな船。私の旅はまさにこれから始まるのかというワクワク感でいっぱいでした。

案内された船室

個室を使うのは譲れなかったため、割高と分かっていてもスパッと室料を支払って正解でした。で、案内された船室に入って

「ええい、このフェリーの船室は化物か?」

がいきなり出ました。広いです。それもすっごく。横になるのが精一杯だったサンライズ瀬戸と比べるまでも無く

  • 立って歩ける
  • 大の字になれる
  • 航行中に食事やPCを広げる座卓
  • 冷蔵庫

「なんだこの浮かぶ旅館は」という感覚です。「18時間超も海の上か」ではなく「こんな場所に18時間もいていいのか」です。普通の宿が11時にチェックアウトするのが通例なのに、こっちは13時。

快適な施設の数々

豪華客船というには憚られますが、快適でした。

大浴場

これが最高でした。「なぜお風呂があるのか」が疑問でしたが、フェリー(船舶)はエンジン冷却/発電機/造水機/冷房の熱交換などで「巨大な熱源」を内包しています。

そのため、その廃熱を利用して船客に還元することが可能です。これは、「移動時/到着前の疲れを取りたいし汗も流したい」パターンにおいてとても役立ちます。

いい気分転換になる歩行スペース

夜間は閉められていますが、展望デッキから

  • 東京湾の夜景
  • レインボーブリッジ

までは見ることができます。朝も潮岬や紀伊水道、瀬戸内海の入口の潮風を感じることができます。

というか、「移動中の乗り物内を自由に歩ける」というのは飛行機はおろか電車でもお目にかかれない体験です。

冷食オペレーションという合理性の極み

特に気に入りました。船内食堂が「カップ麺と給湯器」「冷食の自動販売機」です。それを取り出し、ずらっと並ぶ業務用レンジで温める。「旅の情緒」とか「豪華な食事」という点では一歩劣りますが:

「深夜の海を眺めながらのたこ焼きや焼き鳥」

という、背徳的&愉悦を味わえる。素晴らしかったです。(これは後編で改めて述べます)

もちろん、船ならではの欠点はありました

天候に完全依拠

こればかりは天候ガチャに頼る。神に祈るレベルではあります。割と深い眠りができたのも、台風の合間の出航という、ピンポイントでの天候運があったおかげです。

これが時化気味だった場合は船内浴場が利用禁止になり「船内トイレに設置されている特有のブツ」に頼る事もあり得ます。

携帯電話の電波問題

相模湾や駿河湾、紀伊半島の東側では4Gも届かず。サパッと寝るか船内Wi-Fiサービスに頼るのが正解です。

次への宿題

は後編で述べます。

ピボット旅行2026-05『そうだリフィル、買おう。(My “Favourite” Things)』

「お土産」は旅の途中や最後に買うというのが通例ではありますが、私の場合は「0日目」に始まっていました。

はじめに

道具論のお話ではあるのですが、これもまた旅行記なのでそのカテゴリーに入れます。

拙稿にあるように

筆者はアナログの筆記具「も」信頼しています。

なぜなら、

  • iPhone
  • iPad mini
  • Thinkpad

といったデジタル筆記具と

  • ほぼ日手帳
  • トラベラーズノート

を併用。というのも、『MASTERキートン』に曰く

「やめておけ」
「…………」
「拳銃の方が、ナイフよりも速いと思っているんだろう。
 だが、拳銃はデリケートな道具だ。
 弾が出ないかもしれないし、
思い通り的に当たるとは限らん。
おまけに拳銃は、
抜き、構え、引き金を引くまでに三動作(スリーアクション)……
 その点ナイフは、一動作(ワンアクション)で終わる。
この距離なら、絶対に俺が勝つ!!
どうする? それでもやってみるかね?」

の通り、それぞれ「記録する手段」は同じなのですが「得意とする射程距離」と「初速」が違うため、どうしても併用する。

(↑建前↑)
(↓本音↓)
「カッコいいから」

です。

また、昨年の谷川岳・湯檜曽温泉にて筆者は「トラベラーズノート」という極めてシンプルな作りなのにカスタマイズ性と利便性を兼ね備えた手帳を手に入れています。

このトラベラーズノートを使って分かったこと

シンプルなのに強い

これが一番の印象です。最初は縦長すぎないかと思ったあの独特のサイズ感。『ほぼ日手帳』になれている筆者としては、公式リフィルの厚い紙は慣れるのかどうかと不安だったのにスラスラ書けます。特に万年筆のインクの吸い付きが再考です。

割と雑に使える

旅に携行するだけあって

  • 重い荷物の隙間
  • 少々の水分

は耐えられます。使えば使うほど味が出てくる革の風合いも、その割と雑に使っていいんだという安心感をもたらしてくれます。

豊富なカスタマイズ

手帳本体が止め紐をつけた革。なのに、

  • 追加のリフィル
    • 無地
    • 罫線
    • スタンプ
    • スケジュール……
  • クリアファイル
  • リフィル自体を増やすアタッチメントバンド

等、用途に合わせて成長します。

ここまで旅行記に書く理由

「この旅行記群を書くにあたって活躍したから」に他なりません。「トラベラーズ」の名は伊達では無いです。

  • 旅先のメモ帳
  • ちょっとした気づき
  • 使ったお金

何よりも「レシートの台紙」として。

旅行0日目での「そうだリフィル、買おう。」

今回は東京からのスタートが19時と、サンライズ瀬戸よりも2時間ほど早い出立となります。なので、自宅からの出発も早く済みます。

最初の予定では

  1. 夢の島熱帯植物館で時間を潰す
  2. 閉館と出航の間をコメダで時間つぶし
  3. 同時に早めの夕食
  4. 船内の朝食をそこで包んでもらい
  5. フェリーターミナルに着く

予定でしたが「時は9/24」が立ちはだかります。今年の大連休がある意味災いし、「夢の島熱帯植物館、休館日」と、いきなり0日目の時間を潰すプランが無くなりました。

そうなると、「出航までどこで時間を潰すか?」の問いが生まれたときの結論が「そうだリフィル、買おう。」という、脳内に『My "Favourite" Things(英国英語で)』のあの歌い出しが聞こえてきた次第です。

この時いる場所は新木場駅。東京駅まで10数分です。そこで、こういう回答がありました。

以前購入しているこの、トラベラーズファクトリー、東京駅。ここなら夕飯・翌朝の朝食を買うことも可能です。オマケに、このタイミングで気づいた点として、「買ったばかりのノートに備え付けのスタンプを押してもらえる」おあつらえ向きのカスタマイズです。

そうして出航

乗船手続き前、その戦利品がこちら。この、表紙に押印された

  • 香川
  • 広島
  • 岡山

が旅で出回ったところ、という辺りが「ええぃ、ままよ!」のアドリブで行動しつつ「天候という変数」のピボットが成立できたわけで。

ピボット旅行2026-04『心の中のシャアとゴリラルーム』

はじめに

2026年9月――フェリーで四国へ渡り、高松を拠点に一人旅をしてきました。

今回の旅行については、すでに「ピボット旅行」と名付けています。

そもそもこの言葉を使い始めたきっかけは、WWEでのジョン・シナのストーリーでした。

2025年のシナはヒールターンという大きな方向転換を行いました。しかし、その反応を受けて、今年は再びベビーフェイスへと方向を変えた。

ここで面白かったのは、 それまで起きたことを全部なかったことにするのではなく、起きてしまったことを材料にして、次の展開へ進んだこと です。

今回の旅行も、まさにそんな感じでした。

  • 予定を立てる。
  • 予定が崩れる。
  • そこで別の手を考える。

これを繰り返しているうちに、いつの間にか「ピボット旅行」になっていました。

心の中のアズナブル

そして、もう一つ。今回の旅行には、常に 脳内のシャア・アズナブル がいました。

旅先では、見知らぬ土地を歩き、初めて乗る船に乗り、初めて食べるものを食べます。当然、予定通りにいかないこともあります。

そんな時に、

「見せてもらおうか……○○の性能とやらを!」

と、とりあえず試してみる。うまくいけば、

「ええぃ! ○○は化物か?」

となります。思ったほどではなければ、

「どうということはない」

で済ませられます。予定が外れれば、

「そうそう当たるものではない」

と言えばいいし、そして、自分の判断が盛大に外れていたことに気付いたら、

「これでは道化だよ」

にもなります。極めて重要なマインドセットとして、大ピンチに陥ったときにはあの言葉。

「まだだ…… まだ終わらんよ!」

実に便利です。

しかも、あの人は「戦いは常に二手三手先を読むものだ」と言いながら、普通に判断を間違えます。そのくせ、妙に生存率が高い。

あの白い悪魔と何度も交戦して、アクシズ・ショックまで生き残った人ですから、 「判断は必ずしも正しくないが、とりあえず現場に投入して帰ってくるテスター」 としては非常に優秀です。

今回も、フェリーには、

「見せてもらおうか……フェリー東九の性能とやらを!」

と乗り込みました。尤も、そこに至るまでに2つほどピボットがありましたが

心の中のゴリラルーム

そして、こうした「予定外の展開」を見て、もう一人、私の心中にいたのがゴリラルームのシナリオライターです。

WWEのプロレスラーは、何が起きてもその場で試合を成立させなければなりません。それも、コンマ1秒単位でのタイムコントロールが求められます。

予定していた展開が崩れたら、そこで試合そのものを止めるのではなく、次の展開を作ります。

今回の旅行も、それにかなり近いものでした。

  • 「尾道へ行く」という目的は残す。
  • しかし、そこへ至る手段が変わる。
  • フェリーの帰りがなければ、新幹線にする。
  • 自転車を持っていく予定だったが、雨だったので未練たらたら置いていく。

当初の予定が使えなくなったら、その時点で残っているカードから次の展開を組み立てる。

予定通りに進むことが目的ではない。旅行そのものを成立させることが目的です。

そう考えると、今回の旅行は最初から最後まで、ずいぶんとプロレス的でした。

シナリオライターが用意した展開に対して、

「そうはならんやろ」「なっとるやろがい!」のコンボ

が炸裂し、その横でシャアが、

「見せてもらおうか……」

と言っている。そんな旅行です。

まずは、東京・有明から乗船する前から。

見せてもらおうか……フェリー東九の性能とやらを!

は、このお話の後の後、となります。前回のエントリーで

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

と言いましたが……「アレは嘘だ」のメイトリックス語録でこのエントリーは〆ます。

ピボット旅行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 ディレクトリを丸ごと載せ替える前に、一度「本当にクラウドへ置くべきものは何か」を考えてみることをおすすめします。

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

Page 4 of 309

Powered by WordPress & Theme by Anders Norén