ハクソク

世界を動かす技術を、日本語で。

FastCGI: 30年経ってもなおリバースプロキシにとって優れたプロトコル

概要

  • HTTPリバースプロキシは多くの脆弱性を抱える危険な領域
  • FastCGIは30年前から存在する安全なプロキシ-バックエンド通信プロトコル
  • HTTP/1.1はパースの曖昧さや信頼できないヘッダー問題を抱える
  • FastCGIは明確なメッセージフレーミングと信頼データ分離を提供
  • 一部の制限はあるが、FastCGIは今でも実用的な選択肢

HTTPリバースプロキシの危険性とFastCGIの再評価

  • HTTPリバースプロキシ経由の通信は、Discordのメディアプロキシ脆弱性のようなdesync攻撃(リクエストスマグリング)リスク
  • HTTPがプロキシ-バックエンド間通信に広く使われているが、本来その用途には不適切
  • FastCGIは1996年に公開されたプロキシ-バックエンド通信専用のワイヤープロトコル
  • FastCGIはプロセスモデルではなく、通信プロトコルとして利用可能
    • .fcgi拡張子ファイル用のプロセス自動生成だけでなく、長寿命デーモンとのTCP/UNIXソケット通信にも対応

Go言語とFastCGIの実装例

  • Goでは、標準ライブラリnet/http/fcgiをimportし、http.Servefcgi.Serveに置き換えるだけ
    • アプリの他の部分やハンドラーはそのまま利用可能
  • 主要プロキシ(Apache, Caddy, nginx, HAProxy)はFastCGIバックエンドをサポート
    • 設定例:
      • nginx:
        • HTTP: proxy_pass http://localhost:8080;
        • FastCGI: fastcgi_pass localhost:8080; include fastcgi_params;
      • Apache:
        • HTTP: ProxyPass / http://localhost:8080/
        • FastCGI: ProxyPass / fcgi://localhost:8080/
      • Caddy:
        • HTTP: reverse_proxy localhost:8080 { transport http { } }
        • FastCGI: reverse_proxy localhost:8080 { transport fastcgi { } }
      • HAProxy:
        • HTTP: backend app_backend server s1 localhost:8080
        • FastCGI:
          • fcgi-app fcgi_app docroot /
          • backend app_backend use-fcgi-app fcgi_app server s1 localhost:8080 proto fcgi

HTTPの問題点:desync攻撃・リクエストスマグリング

  • HTTP/1.1はテキストベースで一見シンプルだが、パースの曖昧さエッジケースが多い
  • メッセージフレーミングが明確でなく、実装ごとに解釈の違いが発生
    • これがdesync攻撃(リクエストスマグリング)の温床
  • HTTP/2は明確なフレーミングでこの問題を解決するが、FastCGIは1996年から同様の仕組みを持つ
  • nginxは初期からFastCGI対応、HTTP/2バックエンド対応は2025年後半から
  • ApacheのHTTP/2バックエンド対応は未だ「実験的」扱い

HTTPの問題点:信頼できないヘッダー

  • プロキシがクライアント情報(IPアドレス、認証ユーザー名、クライアント証明書情報など)をHTTPヘッダーで伝達
    • ヘッダー名の重複や大文字小文字違い、他ヘッダー利用などで攻撃者による偽装リスク
  • 例:X-Real-IPTrue-Client-IPなど、複数ヘッダーの扱いが複雑
  • FastCGIは、クライアント由来のヘッダーとプロキシ追加情報のドメイン分離をプロトコルレベルで実現
    • HTTPヘッダーは「HTTP_」プレフィックス付きで渡され、信頼データと区別
    • REMOTE_ADDRパラメータで本来のクライアントIPを安全に伝達
    • Goのnet/http/fcgiはこのパラメータを自動でhttp.Request.RemoteAddrに反映
    • fcgi.ProcessEnvで全ての信頼パラメータにアクセス可能

FastCGIの課題と現状

  • FastCGIが広まらなかった理由は、CGIの古臭いイメージやHTTPリバースプロキシの問題認識不足
  • desync攻撃は2005年にWatchfireが指摘したが、長年無視されてきた経緯
  • FastCGIは現在も実用的で、SSLMateでは10年以上運用実績
  • WebSocket非対応ツール不足(curl未対応)、最適化不足によるスループット低下などの弱点
  • それでもWebSocket不要な場合や、十分な性能が得られる場合はFastCGIの採用価値
  • HTTPリバースプロキシの悪夢を回避できる点が最大の魅力
  • FastCGIの30周年に寄せて、再評価のすすめ

Hackerたちの意見

面白いね。リバースプロキシの設定は大体簡単で、Nginxに組み込まれてる機能を使ってたけど、もっと複雑なことが必要だったらFastCGIを使おうなんて思いつかなかったな。10年くらい前に自分が書いたC++のコードをウェブで動かすためにFastCGIをちょっと使ったけど、それ以来あんまり使ってないな。
それに、埋め込みサーバーが今はめちゃくちゃ人気だよ。アプリケーションにHTTPサーバーを直接組み込んで、ゲートウェイなしで好きなことができる。
この記事は、欠落してる部分があって面白いね。FastCGIとSCGI、HTTPの戦争を思い出すよ。Web2.0のスタートアップを立ち上げてた時期に、これらの技術が普及し始めて、フロントエンドのスタックを構築する責任があったんだ。HTTPが勝ったのはシンプルさのおかげ。スタックに別のプロトコルを追加する代わりに、すでにゲートウェイで扱う必要があるHTTPを使えばよかったから。これで複雑なネットワークトポロジーが簡単になった。容量が足りなくなったらリバースプロキシを複数レベルで導入できるし、認証やセッション管理、SSL終端、DDoSフィルタリングなどを専門にするサーバーを持てる。リクエストチェーンの位置を知らなくても大丈夫だし、開発環境でも本番環境でも同じアプリケーションサーバーを使える。nginxは当時のFastCGI/SCGIモジュールよりもずっと速くて、堅牢だったのも良かった。最初はHTTP -> Lighttpd -> FastCGI -> Djangoってスタックを組んでたけど、nginxを使う方がずっと速かった。HTTPの使用は、TCP/IPのエンドツーエンド原則のウェブ版みたいなもので、ネットワークとそのプロトコルは何が送信されているかに無関係で、すべてのアプリケーションロジックはパケットをフィルタリングしてリダイレクトするネットワークのノードにあるべきだって考え方。これは非常に強力な原則で、軽視すべきじゃない。この記事が指摘しているのは、セキュリティのためには、情報を盲目的に渡すのではなく、最小権限の原則に従う方が良いということ。コミュニケーションを期待するものだけに許可リストを作って、ネットワークの他の部分での妥協に無意識に貢献しないようにしよう。この記事は明示的ではないけど、この二つの原則の間の緊張関係を強調してる。E2Eは柔軟性を与えてくれるけど、その柔軟性には誰かがそれを悪用する可能性がある。最小権限の原則はセキュリティを提供するけど、柔軟性が失われて、システムは設計されたことしかできなくなり、新しい要件に簡単に適応できなくなる。
データセンター内でのエンドツーエンド原則はあまり意味がなくて、この記事が示すように、不安定な行動を助長することになる。
HTTPのセマンティクスはウェブアプリを開発する人には便利だけど、HTTP自体のワイヤプロトコルはひどいよ。例えば、マルチプレクシングはHTTP 2.0まで来なかったし。だから、リバースプロキシとバックエンドの間でHTTPを使うのはすごく無駄だよ。セキュリティの問題もあって、異なるパーサーがリクエストの境界をどこで終わるかで意見が食い違うこともある。例えば、GoogleはずっとHTTPを自社のStubbyプロトコルにラップして、フロントラインのウェブサーバーとアプリケーションの間で使ってる。HTTPのワイヤプロトコルを使うよりもずっと速くて機能が豊富なんだ。典型的な企業には必要ないけど、スケールが増えると、新しいワイヤプロトコルを使ってその周りのツールを開発する価値が出てくる。
nginxの嫌なところは... ドキュメントだね。ほとんど役に立たないと思ってる。残念ながらhttpdは「設定を難しくしよう」って方向に行っちゃった。突然設定フォーマットが変わった時に見切りをつけたんだ。調整できたかもしれないけど、lighttpdに切り替えた(それ以降はrubyに自動生成させてるから、技術的にはhttpdに戻れるけど、戻りたくないな。ウェブサーバーを開発する人は、新しいフォーマットに人々を適応させることを強制することについて考えるべきだと思う。もし「シンプル」な決定で設定フォーマットを簡単に切り替えるなら、例えばyaml設定を追加で有効にして、突然新しいif文の設定文を通過しなくても済むようにしてほしい。
> HTTPの使用は、基本的にTCP/IPのエンドツーエンド原則のウェブ版だった。アナロジーはうまくいかないと思う、接続キャッシングやマルチプレクシングの文脈ではね。中間ゲートウェイが別のHTTPチャネルを介して複数のHTTPリクエストをマルチプレクシングする場合、そのチャネルはリスニングサービスへの端末の足であり(つまり、リクエストはアプリケーションソケットに到達する前にデマルチプレクスされない)、エンドツーエンドの論理を根本的に複数の方法で侵害している。アナロジーが成り立つのは、1:1の接続対称性を保つ場合だけだ。すべてのリバースプロキシの悪用は、エンドツーエンドを侵害することに直接起因する。もしアナロジーが正しければ、複数のMXを介したSMTP配信もエンドツーエンドであるはずだ。でもそうじゃないし、リバースプロキシと同じ問題が多く見られる、メッセージ境界の非同期も含めてね。HTTPリクエストをメッセージとしてアナロジーを取ろうとしてるんだろうけど、すぐにすべての厄介な詳細の文脈で崩れちゃう。TCPとHTTPのセマンティクスの性質や、さまざまな具体的なプロトコルの詳細が物事を複雑にして、予測可能な結果をもたらす。エンドツーエンド原則は、セマンティクスを緩く扱うことを許さない。状態管理やトランスポートレイヤーに関して非常に厳格な境界を要求する。それが全体のポイントなんだ。「ほぼ」エンドツーエンドはエンドツーエンドではない、少しもね。
Red Hatファミリーに配布されているPHP/Apacheの設定は「FastCGIプロセスマネージャー」(FPM)だよ。他のRHELディストリビューションがFastCGIを使っているかは分からないけど。$ rpm -qi php-fpm | grep ^Summary で、Summary : PHP FastCGI Process Managerって出てくる。
あなたが探しているのはFPMじゃなくてmod_proxy_fcgiだよ。これはFedoraのhttpd-coreパッケージに含まれてる。RHELについては分からないけど。https://packages.fedoraproject.org/pkgs/httpd/httpd-core/fed...
記事に同意だね。FastCGIはこういうことにはHTTPよりも優れてる。ただ、もう一つのプロトコル、Web Application Socket(WAS)を紹介したいな。16年前に仕事でデザインしたんだけど、FastCGIはまだまだ足りないと思ったから。WASはメインソケット内にデータを詰め込む代わりに、コントロールソケットと2つのパイプ(生リクエスト+レスポンスボディ)を持ってるんだ。WASアプリとウェブサーバーは、例えばsplice()を使ってパイプで操作できる。フレーミングは不要だし、リクエストはキャンセル可能で、3つのファイルディスクリプタは常に回復できるよ。これまでの数年間、内部アプリケーションにWASを使ってきたし、ウェブホスティング環境のためにWAS用のPHP SAPIも書いた。結構多くのウェブサイトが内部でWASを使ってるんだ。全部オープンソースだよ: - ライブラリ: https://github.com/CM4all/libwas - ドキュメント: https://libwas.readthedocs.io/en/latest/ - ノンブロッキングライブラリ: https://github.com/CM4all/libcommon/tree/master/src/was/asyn... - ウェブサーバー: https://github.com/CM4all/beng-proxy - WebDAV: https://github.com/CM4all/davos - WAS SAPI付きのPHPフォーク: https://github.com/CM4all/php-src
昔ながらのCGIを再発見したんだけど、ユーザーがうちのプラットフォームでカスタムページを「バイブコード」するのにすごくいい方法だよ。[1] シナリオとしては、うちにはファーストパーティのタスクリストやデータビューアがあるけど、ユーザーはそれをかなりカスタマイズしたがるんだ。例えば、カンバンビューやデータフィルターとチャート付きのカスタムダッシュボードを作りたいとか。ボックスにはコーディングエージェントがあって、ユーザーは何でもコーディングできるんだ。従来のレポートビルダーを作る代わりにね。Goの標準ライブラリはサーバーサイドとユーザースペースの両方で良いサポートがある。コーディングエージェントは、CGIと話すpage-name/main.goを作成して、サーバーはリクエストをそれに委譲する。すべて「人間規模」のデータとページビューだから、FastCGIで最適化する必要もないよ。エージェントにとっては、古いものが新しくなったね! 1. https://housecat.com
CGIはFastCGIとは違って、HTTPヘッダーを伝えるために環境変数を使うため、かなり大きな足元をすくうリスクがあることに注意してね: https://httpoxy.org/ GoのCGIサーバー実装は$HTTP_PROXYを設定しないから、その点では安全だけど、CGIが環境変数を使うのはあまり好きじゃないな。
Perlが人気だった頃、FastCGIでいい経験をしたことがあるよ。最近では、WebTransportが新しいセクシーなものだね。多分、FastCGIの本当の代替にはならないけど。
これって本当に悪いアドバイスに思えるんだけど、何か見落としてる? FastCGIを使うには、アプリをFastCGIに対応させる必要がある。HTTP/1.1を使う利点は、開発者がブラウザで即座にテストできること。逆に、マシンにリバースプロキシを設定する必要がないからね。HTTP/1.1の悪い部分は、HTTP/2.0とFastCGIの両方で同じように修正されてる。だから、HTTP/2.0を使えば、適切なフレーミングとブラウザサポートが得られるよ。
信頼できないヘッダーについてのセクションを見てみて - これはHTTP/2では修正されてないよ。ブラウザをアプリに直接向けられるのはすごく便利だよね。Goでは、http.Serve(開発用)とfcgi.Serve(本番用)を切り替えるコマンドラインフラグが使えるよ。
> とはいえ、ビンテージ技術を使うことにはいくつかの欠点がある。WebSocketsをサポートするために更新されることはなかった。WHATWGストリームの広範なブラウザサポートがあるので、長寿命のHTTPリクエスト上で自分のWebSocketsを実装するのはかなり簡単だよ。基本的には、バイトストリームを送信し、各メッセージの前にヘッダーを追加するだけ。多くの場合、ヘッダーはサイズだけで済む。WebSocketsに対する利点: * WebSocketのためにサーバーレイヤーに特別なパスが必要ない。 * バックプレッシャー * HTTP/2/3の改善を無料で利用できる * フレーミングオーバーヘッドが低い 残念ながら、私の知る限り、レスポンスを受け取っている間にリクエストボディをストリーミングし続けることはまだサポートされていないので、完全な双方向ストリーミングにはペアのリクエストが必要だね。
FCGIはオーケストレーションシステムでもあるんだ。負荷が上がるとサーバータスクを増やして、負荷が下がるとそれをシャットダウンする。もしタスクがクラッシュしたら、新しいコピーを立ち上げる。要するに、シングルシステムのKubernetesみたいな感じだね。
> 負荷が上がるとサーバータスクを増やして、負荷が下がるとそれをシャットダウンする。 俺の経験から言うと、これはあんまり良い機能じゃないと思う。聞こえはいいけど、負荷が低いときは問題なく動くけど、負荷が高くなるとワーカーを増やしてメモリが足りなくなることが多い。静的なワーカー数を持っている方がずっといいよ。クラッシュからの復旧は必要なら便利だけどね。
(u)WSGIもここで言及されるべきじゃない?