FastCGI: リバースプロキシ向けには30年経った今でもなお優れたプロトコル
(agwa.name)- 長時間稼働するバックエンドにソケット経由でリクエストを渡すプロキシプロトコルとして、既存のHTTPハンドラ構造をほとんど変えずに適用できる
- HTTP/1.1 リバースプロキシはメッセージ境界の解釈が実装ごとにずれやすく、desync や request smuggling のような深刻なセキュリティ問題を生み続けうる
- FastCGIは1996年から明確なメッセージフレーミングを提供しており、クライアントヘッダとプロキシが追加した信頼情報を構造的に分離してくれる
- Goの
net/http/fcgiはREMOTE_ADDRをRequest.RemoteAddrに入れ、HTTPSかどうかもRequest.TLSに反映するため、信頼情報の受け渡しを追加ミドルウェアなしで処理できる - WebSockets 非対応、弱いツールエコシステム、一部ワークロードでの 低いスループット といった制約はあるが、WebSockets が不要で性能が十分なら今でも実用的な選択肢に見える
FastCGIの位置づけと適用方法
- FastCGIはファイルごとのプロセス実行方式だけで使われるものではなく、長時間稼働するデーモンにTCPまたはUNIXソケットでリクエストを送るプロキシ-バックエンドプロトコルとしても使える
- Goでは
net/http/fcgiパッケージを取り込み、http.Serveをfcgi.Serveに置き換える程度で適用できる- 既存のハンドラはそのまま
http.ResponseWriterとhttp.Requestを使う - アプリケーションの残りの構造もそのまま維持される
- 既存のハンドラはそのまま
- Apache、Caddy、nginx、HAProxy のような主要プロキシは FastCGI バックエンドをサポートしており、設定も比較的シンプル
HTTPをバックエンドプロトコルとして使うときのパース問題
- HTTP reverse proxying はセキュリティの地雷原に近く、Discord メディアプロキシの desync 脆弱性 のように、非公開の添付ファイルをのぞき見できる問題も出続けている
- HTTP/1.1は見た目には単純なテキストプロトコルだが、同じメッセージを表現する方法が多すぎ、例外処理も多いため、実装ごとに解釈がずれやすい
- 最大の問題は、HTTPメッセージに明示的なフレーミングがないこと
- メッセージの終わりをメッセージ自身が複数の方法で説明する
- 実装ごとにメッセージ終了位置と次のメッセージ開始位置を違って解釈しうる
- この不一致は HTTP desync attacks や request smuggling の土台となり、リバースプロキシとバックエンドがメッセージ境界を異なって理解することで深刻なセキュリティ問題を生む
- パーサ差分をひたすらパッチで埋めるやり方は根本的解決になりにくい
- James Kettle は新しいタイプを継続的に見つけている
- 昨年さらに事例を見つけたあと、"HTTP/1.1 must die" という表現まで使った
FastCGIとHTTP/2のメッセージ境界処理
- HTTP/2は、プロキシとバックエンドの間で一貫して使うなら、メッセージ境界を明確にして desync 問題を解決できる
- FastCGIはこうした明確な境界区分を、1996年からより単純なプロトコルで提供してきた
- nginx は最初のリリースから FastCGI バックエンドをサポートしていたが、HTTP/2 バックエンド対応が追加されたのは2025年後半になってから
- Apache の HTTP/2 バックエンド対応は今もなお "experimental" の状態にある
信頼できないヘッダ問題とFastCGIの分離方式
- desync だけの問題ではなく、HTTPには実際のクライアントIP、プロキシが処理した認証済みユーザー名、mTLSでのクライアント証明書情報のように、プロキシが信頼して渡すべきデータを堅牢に運ぶ仕組みも不足している
- 現実にはこうした情報をHTTPヘッダに入れることになるが、プロキシが追加した信頼データとクライアントが送った非信頼ヘッダの間に構造的な区別がない
X-Real-IPのようなヘッダは実際のクライアントIPを伝えるためによく使われるが、安全にするにはプロキシが大文字小文字の変形まで含めた既存ヘッダをすべて完全に除去してから再追加しなければならない- この方式は非常に危険な地形であり、バックエンドが攻撃者の入れたデータを信頼してしまう経路が多い
- プロキシは
X-Real-IPだけでなく、この用途のあらゆるヘッダをすべて消さなければならない - たとえば Chi ミドルウェアは、クライアントの実IPを決める際に
True-Client-IPを先に確認 し、ない場合にだけX-Real-IPを使う- プロキシが
X-Real-IPを正しく処理していても、攻撃者がTrue-Client-IPを送れば問題になりうる
- プロキシが
- FastCGIは、クライアントヘッダとプロキシが追加した情報をドメイン分離の形で区別する
- どちらもキー/値パラメータ一覧として渡されるが、HTTPヘッダ名には
HTTP_接頭辞が付く - そのため、クライアントが送ったヘッダがプロキシの信頼データとして解釈される構造にはならない
- どちらもキー/値パラメータ一覧として渡されるが、HTTPヘッダ名には
GoにおけるFastCGIの信頼情報処理
- FastCGI は、実際のクライアントIPを渡すための
REMOTE_ADDRのような標準パラメータを定義している - Go の
net/http/fcgiは、この値を自動的にhttp.RequestのRemoteAddrに入れるため、追加ミドルウェアなしで動作する - プロキシは、HTTPS使用の有無、ネゴシエートされたTLS cipher suite、クライアント証明書のような情報も非標準パラメータとして渡せる
- Go は、リクエストが HTTPS を使っていた場合、
RequestのTLSフィールドを自動的に nil ではない値に設定する- 中身が空でも、HTTPS強制の確認には役立つ
fcgi.ProcessEnvで、プロキシが送った信頼パラメータ一式にアクセスできる
普及が遅い理由と現実的な限界
- FastCGI のほうが優れているならなぜ広く使われないのかについては、名前自体の古さと HTTP reverse proxy のセキュリティ問題に対する認識不足が重なっているように見える
- Watchfire は2005年の時点ですでに desync 攻撃を扱い、解決が容易でないことも警告していたが、こうした攻撃は10年以上まともに注目されなかった
- FastCGI は現在でも実運用可能で、SSLMate では10年以上本番環境で使われている
- ただし古い技術なので弱点もある
- WebSockets 対応のために更新されていない
- ツールエコシステムが乏しい
- たとえば curl は FTP、Gopher、SMTP までサポートするが、FastCGI リクエストは送れない
- Go FastCGI サーバを複数の reverse proxy の背後でベンチマークすると、一部ワークロードではHTTP/1.1 や HTTP/2 よりスループットが低かった
- これはプロトコル自体の限界というより、FastCGI のコードパスが HTTP ほど最適化されていない結果だと見ている
最終判断
- WebSockets が不要で、現在の性能で十分なら、FastCGI は今でも十分に使える選択肢
- ボトルネックが出たとしても、HTTP reverse proxying の複雑さとセキュリティの悪夢を受け入れるより、ハードウェアを追加するほうがよいと考えている
2件のコメント
Lobsters のコメントで見つけた Twisted の FastCGI に関するコメントが印象的ですね https://web.archive.org/web/20160723091923/…
Hacker Newsのコメント
文章の趣旨には同意する。この用途なら FastCGI は HTTP より優れていると思う
WAS(Web Application Socket) というプロトコルも紹介したい。16年前、職場で FastCGI でも十分によくないと感じて自分で設計した
メインのソケットのフレーミングの代わりに、制御用ソケット1本と raw のリクエスト/レスポンスボディ用パイプ2本を使い、WAS アプリと Web サーバの両方が pipe に対して
splice()を利用できるフレーミングは不要で、リクエストのキャンセルも可能で、3つのファイルディスクリプタを常に復旧できるようにした
何年にもわたって内部アプリケーションや Web ホスティング環境で使ってきており、PHP SAPI も自分で書いた。かなり多くの Web サイトが内部的に WAS 上で動いている
すべてオープンソース
library: https://github.com/CM4all/libwas
documentation: https://libwas.readthedocs.io/en/latest/
non-blocking library: https://github.com/CM4all/libcommon/tree/master/src/was/asyn...
our web server: https://github.com/CM4all/beng-proxy
WebDAV: https://github.com/CM4all/davos
PHP fork with WAS SAPI: https://github.com/CM4all/php-src
HTTP はブラウザとサーバのような両端間でデータを運ぶためのもので、FastCGI はサーバとアプリケーションの間でそのデータを処理するためのものだ
さっき記事をざっと読んだが、著者は両者を互いに置き換え可能であるかのように混同させる書き方をしているように見える。実際にはまったく違う
ちなみに私も Web 顧客サービスで fcgi を10年間使ってきた
この記事は抜けている話が多く、そのぶんむしろ面白い
FastCGI vs. SCGI vs. HTTP 論争が盛んだったころに Web2.0 スタートアップを創業し、フロントエンドスタックを自分で組んだが、最終的に HTTP が勝った理由は単純さだった
ゲートウェイでどうせ処理しなければならない HTTP をそのまま使えば、別のプロトコルをスタックに追加する必要がなく、そのおかげで reverse proxy を何段にも入れたり、認証・セッション・SSL 終端・DDoS フィルタリングのような横断的関心事を役割ごとのサーバに分離したりする構成が非常に簡単だった
開発環境ではアプリサーバに HTTP で直接つなぎ、本番では SSL・認証・不正利用検知を reverse proxy が担当する形で、同じアプリサーバをそのまま再利用できた
当時は nginx が大半の FastCGI/SCGI モジュールよりはるかに高速で安定していたことも大きかった。最初は
HTTP -> Lighttpd -> FastCGI -> Djangoという構成だったが、単に nginx を使うほうがずっと速かったHTTP の利用は Web 版の End-to-End Principle のように機能していた。ネットワークとプロトコルは運ぶ内容に無関係であるべきで、アプリケーションロジックはフィルタリングやリダイレクトを行うネットワークノードではなく終端にあるべきだ、という考え方だ
ただし記事が突いている核心は、セキュリティ面では 最小権限の原則 に従うほうがよい場合が多い、という点だ。想定された通信だけを allowlist で通すようにしないと、別の箇所の侵害にうっかり加担してしまう
結局この2つの間には緊張関係がある。E2E は柔軟性を与えるが、その柔軟性は悪用の余地も広げる。PoLP はセキュリティを与えるが、設計したことしかできなくなり、新しい要求に適応しづらくなる
[1] https://en.wikipedia.org/wiki/End-to-end_principle
[2] https://en.wikipedia.org/wiki/Principle_of_least_privilege
中間ゲートウェイが複数の HTTP リクエストを別の1本の HTTP チャネルに multiplex し、そのチャネルが listening service まで直接つながり、アプリケーションソケットの前で demultiplex されないなら、それは end-to-end の論理をさまざまな意味で根本から壊している
1:1 の接続対称性が保たれる場合にだけ、そのたとえはどうにか成り立つ
reverse proxy の脆弱性はすべて、end-to-end を破ったことから直接生じていると思う
そのたとえが正しいなら、複数の MX を経由する SMTP 配送も end-to-end であるはずだが、実際にはそうではなく、reverse proxy と似た問題、たとえばメッセージ境界の desync も多く起きる
HTTP リクエストをメッセージに対応づけようとする意図はわかるが、実際の TCP・HTTP セマンティクスやさまざまなプロトコル詳細のせいですぐに破綻する
end-to-end 原則はセマンティクスを雑に扱うことを許さない。状態管理とトランスポート層の境界に対して非常に厳格な規律を要求する。なんとなく end-to-end っぽいもの は end-to-end ではない
たとえば multiplexing も HTTP 2.0 以前にはなく、そのため reverse proxy と backend 間の通信に HTTP をそのまま使うのは無駄が大きい
セキュリティ上の問題もある。パーサ同士がリクエスト境界の終わりをどこに見るかすら一致しないことがある
Google もずっと前から、フロントの Web サーバとアプリケーションの間では HTTP を独自の Stubby プロトコルで包んで使っている
HTTP wire protocol よりはるかに高速で機能も多い。普通の会社にはやりすぎだが、規模が大きくなれば、別の wire protocol とその周辺ツール群を自前で作るコストは十分に正当化される
httpd もある時点から設定を難しくする方向に進み、設定フォーマットを突然変えた時点で見限った
適応することもできただろうが、その代わりに lighttpd に移行し、その後は ruby が設定生成を自動化したので技術的には再び httpd に戻ることもできる
それでも戻りたくはない。Web サーバ開発者なら、ユーザに新しいフォーマットを無理やり押しつけることには慎重であるべきだ
本当に単純な判断で設定フォーマットを変えるのなら、少なくとも yaml 設定 のようなものを追加オプションで提供して、突然新しい if-clause 風の設定文を強制しないでほしい
WHATWG streams がブラウザに広く普及した今では、長寿命の HTTP リクエストの上に独自の WebSocket もどきを実装するのはかなり簡単だ
ただバイトストリームを送り、各メッセージの前にヘッダを付ければよく、多くの場合は長さの値ひとつで十分だ
利点もある。WebSocket のようにサーバ層に別個の特殊な経路が要らず、backpressure を使え、HTTP/2・HTTP/3 の改善をそのまま享受でき、フレーミングのオーバーヘッドもより小さい
ただし AFAIK、リクエストボディを流し続けながら同時にレスポンスを受け取ることはまだサポートされていないので、完全な双方向ストリーミングにはリクエストが2本必要になる
古い plain CGI を再発見したのだが、我々のプラットフォームでユーザにカスタムページを vibe code させるには非常によい [1]
標準機能として task list と data viewer はあるが、ユーザはしばしば Kanban ビューやデータフィルタ・チャートを含むカスタムダッシュボードのような、もっと細かなカスタマイズを望む
このボックスには coding agent がいるので、従来型の report builder を作る代わりに、ユーザが望むものを自分でコードで作れるようにしている
Go stdlib はサーバ側とユーザ空間の両方でサポートがよく、coding agent が
page-name/main.goを作って CGI で通信するようにすれば、サーバがリクエストをそこへ委譲するデータ量もページビューもすべて person scale なので、FastCGI のような最適化は特に必要ない
エージェントの時代には古い技術がまた新しくなる
Go の CGI サーバ実装は
$HTTP_PROXYを設定しないためその点は安全だが、それでも CGI が環境変数を使う方式は依然として気に入らないreverse proxy 側はたいてい単純な処理しかしないので、Nginx の組み込み機能だけで十分だった
それでも、より複雑なことが必要なときに FastCGI を使おうという発想は、私には出てこなかったと思う
10年ほど前に C++ コードの一部を Web 上で動かすために FastCGI を少し使ってみたが、その後はほとんど使っていない
アプリケーションの中に HTTP サーバを直接組み込み、ゲートウェイなしで必要なことをそのまま処理すればよい
Red Hat 系で配布される PHP/Apache 構成は FPM(FastCGI Process Manager) だ
RHEL ディストリビューションで FastCGI が他でも使われているかはわからない
$ rpm -qi php-fpm | grep ^SummarySummary : PHP FastCGI Process ManagerFedora の
httpd-coreパッケージには含まれている。RHEL はよくわからない: https://packages.fedoraproject.org/pkgs/httpd/httpd-core/fed...uwsgi protocol もある
これも実質的にはほとんどあらゆるものに対する RPC のような性格だ
FCGI はオーケストレーションシステムでもある
負荷が上がるとサーバタスクを増やし、負荷が下がると減らし、タスクが落ちたら新しいコピーを立ち上げる
一種の単一システム版 Kubernetes のようなものだ
聞こえはよいが、普段の低負荷時にはうまく動いていても、高負荷になると worker を増やしながらメモリを食い尽くすことがよくある
そのため 静的な worker 数 にしておくほうが概ねよかった
ただし crash recovery は必要なら有用だ
HTTP ヘッダの 不条理さ を少し眺めてみてもよいだろう
True-Client-IPがないときだけX-Real-IPを使うのなら、プロキシがX-Real-IPを正しく入れていても、攻撃者がTrue-Client-IPヘッダを送ればそのままやられてしまうX-Forwarded-For、X-Real-IP、CDN ごとにばらばらなカスタムヘッダまであり、しかも中にはカンマ区切りのリストで、たいてい我々 own LB の IP まで役に立たないまま付いてくるものもあるそうなった理由はわかるが、まったく助けになっていない
しかもこうしたヘッダはすべて悪意ある user-agent が挿入できる。信頼できるサーバがパイプライン内で重要な情報をどう渡すべきかについて、誰も合意できなかったかのようだ
この混乱は User-Agent ヘッダの不条理さともよく似合っている
あちらは Apple がプライバシーを名目に、偽の OS バージョンのような完全なデタラメ情報を送る方針を取ったことで、さらに極端になった
この主張にはかなり一理あるが、FastCGI は
PATH_INFOのような部分で CGI/1.1 に従うため、情報の欠落が生じるURL デコードが強制されるので、encoded slash である
%2Fを表現できない実装によってはパス中の
//を/にまとめることもあるが、これは多くの HTTP 実装にもある問題ではある表現力の面では HTTP より劣り、その差が重要かどうかはアプリケーション次第だ
私は URL を正確に扱えるほうを好む