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 を正確に扱えるほうを好む