fideloper/proxyで理解するリバースプロキシ配下のLaravel設定

概要

ロードバランサやリバースプロキシの背後にLaravelを配置すると、「ローカルでは正常なのに本番だけおかしい」という症状が高確率で発生します。代表的なものは次のとおりです。

  • url() / asset() が https:// ではなく http:// を生成し、Mixed Content でブロックされる
  • リダイレクトが https → http に落ちて無限リダイレクトになる
  • $request->ip() がプロキシのIPになり、ユーザーの実IPが取れない
  • レートリミットやログのIPがすべて同じ値になる

これらの根っこはすべて同じで、「アプリがリクエストの本来の姿(スキーム・ホスト・クライアントIP)を見失っている」ことにあります。本記事では fideloper/proxy(現在はLaravel本体の TrustProxies に統合済み)を軸に、原因と恒久対処を整理します。

環境

  • Laravel(9系以降を想定。8系以前は fideloper/proxy パッケージが別途必要)
  • PHP 8.x
  • 前段にリバースプロキシ / ロードバランサ(ALB、nginx、CDN などを一般化した構成)
  • TLS終端はプロキシ側で行い、プロキシ→アプリ間は平文HTTPで通信
[ Client ] --HTTPS--> [ Reverse Proxy / LB (TLS終端) ] --HTTP--> [ Laravel (PHP-FPM) ]

発生した問題

TLSをプロキシで終端している構成では、アプリに届くリクエストは平文のHTTPになります。PHPから見ると次の状態です。

  • $_SERVER['HTTPS'] は空
  • $_SERVER['SERVER_PORT'] は 80
  • $_SERVER['REMOTE_ADDR'] はプロキシのIP

つまりLaravelは「これはHTTPのリクエストだ」と認識します。結果、URL::to() や route() は http:// のURLを組み立てます。ブラウザはHTTPSでページを開いているため、生成された http:// のアセットが Mixed Content でブロックされる、という流れです。

本来の情報はリクエストヘッダに入っています。プロキシは元の情報を次のようなヘッダで転送します。

X-Forwarded-For: 203.0.113.10
X-Forwarded-Proto: https
X-Forwarded-Host: example.com
X-Forwarded-Port: 443

問題は、Laravelがこれらのヘッダをデフォルトでは信用しない点です。理由は明確で、これらは容易に偽装できるヘッダだからです。信頼できるプロキシからのものだけを採用しなければ、クライアントが X-Forwarded-For を詐称してIP制限を回避できてしまいます。

原因

Laravelは TrustProxies ミドルウェア(旧 fideloper/proxy)を通じて「どのプロキシを信頼するか」を明示的に設定させる設計です。ここが未設定・不適切だと X-Forwarded-* ヘッダが無視され、アプリは平文HTTPのリクエストとして処理を続けます。

原因は「プロキシは正しくヘッダを送っているのに、アプリ側が信頼設定をしていないので採用していない」の一点に集約されます。

対応方法

Laravel 9〜10(TrustProxies が本体に同梱)

app/Http/Middleware/TrustProxies.php を編集します。

<?php

namespace App\Http\Middleware;

use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;

class TrustProxies extends Middleware
{
    protected $proxies;

    protected $headers =
        Request::HEADER_X_FORWARDED_FOR |
        Request::HEADER_X_FORWARDED_HOST |
        Request::HEADER_X_FORWARDED_PORT |
        Request::HEADER_X_FORWARDED_PROTO;
}

$proxies の設定がキモで、環境によって取るべき値が変わります。

// パターンA: プロキシのIP/CIDRが固定・既知の場合(最も安全)
protected $proxies = ['10.0.0.0/16'];

// パターンB: 前段IPが動的で特定しづらい場合(直前1ホップを信頼/注意点参照)
protected $proxies = '*';

Laravel 11以降(bootstrap/app.php で設定)

use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(
        at: ['10.0.0.0/16'],
        headers: Request::HEADER_X_FORWARDED_FOR
            | Request::HEADER_X_FORWARDED_HOST
            | Request::HEADER_X_FORWARDED_PORT
            | Request::HEADER_X_FORWARDED_PROTO,
    );
})

Laravel 8以前(fideloper/proxy を導入)

composer require fideloper/proxy
php artisan vendor:publish --provider="Fideloper\Proxy\TrustedProxyServiceProvider"

config/trustedproxy.php の proxies を設定します。考え方は同一で、信頼するプロキシと採用するヘッダを明示します。

保険としての強制HTTPS

TrustProxies を正しく設定すれば URL::forceScheme は本来不要です。CDNをさらに前段に挟むなど層が深い場合の保険として使うことはありますが、根本対処ではなく対症療法である点に注意してください。

// AppServiceProvider::boot()
if ($this->app->environment('production')) {
    \URL::forceScheme('https');
}

確認方法

Route::get('/_debug/proxy', function (\Illuminate\Http\Request $request) {
    return [
        'isSecure' => $request->isSecure(), // true になるべき
        'scheme'   => $request->getScheme(),// https
        'url'      => url('/'),             // https:// で始まる
        'clientIp' => $request->ip(),       // 実クライアントIP
        'ips'      => $request->ips(),
    ];
});

isSecure() が true、ip() が実クライアントのIPになっていれば成功です。確認後、このデバッグルートは必ず削除してください。

注意点

  • $proxies = '*' はセキュリティトレードオフがあります。 これは「直前のホップを常に信頼する」設定で、アプリに直接到達できる経路があると X-Forwarded-For の偽装によってIP制限やレートリミットを回避されます。アプリのポートが必ずプロキシ経由でしか到達できないことをネットワーク側で保証したうえで使ってください。可能なら CIDR 指定(パターンA)を優先します。
  • 採用するヘッダは必要なものだけに絞ります。
  • CDN→LB→アプリのように層が多いと X-Forwarded-For が複数IPのカンマ区切りになります。信頼ホップ数の考慮が必要です。
  • Mixed Content が残る場合、ビルド済みアセットに http:// がハードコードされていないかも確認します。

まとめ

  • 症状(http URL生成 / リダイレクトループ / 実IPが取れない)は、すべて「アプリがリクエストの本来の姿を見失っている」ことに起因します。
  • プロキシは X-Forwarded-* で正しい情報を送っていますが、Laravelはセキュリティ上デフォルトで信用しません。
  • TrustProxies(旧 fideloper/proxy)で「どのプロキシを・どのヘッダで」信頼するかを明示すれば解決します。
  • '*' は便利ですが、アプリへの直接到達を塞いだうえで使うこと。CIDR指定が本筋です。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。