1 ポイント 投稿者 GN⁺ 1 시간 전 | 2件のコメント | WhatsAppで共有
  • 長時間稼働するバックエンドにソケット経由でリクエストを渡すプロキシプロトコルとして、既存のHTTPハンドラ構造をほとんど変えずに適用できる
  • HTTP/1.1リバースプロキシはメッセージ境界の解釈が実装ごとにずれやすく、desync や request smuggling のような深刻なセキュリティ問題を継続的に生みうる
  • FastCGIは1996年から明確なメッセージフレーミングを提供しており、クライアントヘッダとプロキシが追加した信頼情報を構造的に分離してくれる
  • Goの net/http/fcgiREMOTE_ADDRRequest.RemoteAddr に入れ、HTTPSかどうかも Request.TLS に反映するため、信頼情報の伝達を別個のミドルウェアなしで処理できる
  • WebSockets 非対応、弱いツールエコシステム、一部ワークロードでの 低いスループット といった制約はあるが、WebSockets が不要で性能が十分なら、依然として実用的な選択肢に見える

FastCGIの位置づけと適用方法

  • FastCGIはファイルごとのプロセス起動方式にだけ使われるものではなく、長時間稼働するデーモンに TCP または UNIX ソケットでリクエストを送るプロキシ-バックエンドプロトコルとしても使える
  • Goでは net/http/fcgi パッケージを取り込み、http.Servefcgi.Serve に置き換える程度で適用できる
    • 既存ハンドラはそのまま http.ResponseWriterhttp.Request を使う
    • アプリケーションの残りの構造もそのまま維持される
  • ApacheCaddynginxHAProxy といった主要プロキシは 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_ 接頭辞が付く
    • そのため、クライアントが送ったヘッダがプロキシの信頼データとして解釈される構造が成り立たない

GoにおけるFastCGIの信頼情報処理

  • FastCGI は実際のクライアントIPを渡すための REMOTE_ADDR のような 標準パラメータ を定義している
  • Go の net/http/fcgi はこの値を自動的に http.RequestRemoteAddr に入れるため、別個のミドルウェアなしで動作する
  • プロキシは HTTPS 利用の有無、ネゴシエートされた TLS cipher suite、クライアント証明書といった情報も 非標準パラメータ として渡せる
  • Go はリクエストが HTTPS を使っていた場合、RequestTLS フィールドを 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件のコメント

 
rtyu1120 56 분 전

Lobsters のコメントで見つけた Twisted の FastCGI に関するコメントが印象的ですね https://web.archive.org/web/20160723091923/…

 
GN⁺ 1 시간 전
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

    • FastCGI と HTTP は同じレイヤーではない
      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

    • そのたとえはあまり当てはまらないと思う。特に connection caching や multiplexing の文脈ではなおさらだ
      中間ゲートウェイが複数の 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 ではない
    • Web アプリ開発者にとって HTTP semantics は有用だが、HTTP wire protocol 自体はひどい
      たとえば multiplexing も HTTP 2.0 以前にはなく、そのため reverse proxy と backend 間の通信に HTTP をそのまま使うのは無駄が大きい
      セキュリティ上の問題もある。パーサ同士がリクエスト境界の終わりをどこに見るかすら一致しないことがある
      Google もずっと前から、フロントの Web サーバとアプリケーションの間では HTTP を独自の Stubby プロトコルで包んで使っている
      HTTP wire protocol よりはるかに高速で機能も多い。普通の会社にはやりすぎだが、規模が大きくなれば、別の wire protocol とその周辺ツール群を自前で作るコストは十分に正当化される
    • データセンター内部で end-to-end principle を適用するのはほとんど意味がなく、記事が示しているように、むしろ危険な動作を許してしまう
    • 私が nginx で嫌いなのは ドキュメント だ。実質的にほとんど役に立たないと感じる
      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 のような最適化は特に必要ない
    エージェントの時代には古い技術がまた新しくなる

    1. https://housecat.com
    • CGI は FastCGI と違って HTTP ヘッダを 環境変数 として渡すので、かなり大きな落とし穴があることに注意すべきだ: https://httpoxy.org/
      Go の CGI サーバ実装は $HTTP_PROXY を設定しないためその点は安全だが、それでも CGI が環境変数を使う方式は依然として気に入らない
  • reverse proxy 側はたいてい単純な処理しかしないので、Nginx の組み込み機能だけで十分だった
    それでも、より複雑なことが必要なときに FastCGI を使おうという発想は、私には出てこなかったと思う
    10年ほど前に C++ コードの一部を Web 上で動かすために FastCGI を少し使ってみたが、その後はほとんど使っていない

    • 今では embedded server のほうがずっと一般的だ
      アプリケーションの中に HTTP サーバを直接組み込み、ゲートウェイなしで必要なことをそのまま処理すればよい
  • Red Hat 系で配布される PHP/Apache 構成は FPM(FastCGI Process Manager)
    RHEL ディストリビューションで FastCGI が他でも使われているかはわからない
    $ rpm -qi php-fpm | grep ^Summary
    Summary : PHP FastCGI Process Manager

  • uwsgi protocol もある
    これも実質的にはほとんどあらゆるものに対する RPC のような性格だ

  • FCGI はオーケストレーションシステムでもある
    負荷が上がるとサーバタスクを増やし、負荷が下がると減らし、タスクが落ちたら新しいコピーを立ち上げる
    一種の単一システム版 Kubernetes のようなものだ

    • 私の経験では、その機能はあまりよくなかった
      聞こえはよいが、普段の低負荷時にはうまく動いていても、高負荷になると worker を増やしながらメモリを食い尽くすことがよくある
      そのため 静的な worker 数 にしておくほうが概ねよかった
      ただし crash recovery は必要なら有用だ
    • 我々もまさにその使い方をしていた
  • HTTP ヘッダの 不条理さ を少し眺めてみてもよいだろう
    True-Client-IP がないときだけ X-Real-IP を使うのなら、プロキシが X-Real-IP を正しく入れていても、攻撃者が True-Client-IP ヘッダを送ればそのままやられてしまう
    X-Forwarded-ForX-Real-IP、CDN ごとにばらばらなカスタムヘッダまであり、しかも中にはカンマ区切りのリストで、たいてい我々 own LB の IP まで役に立たないまま付いてくるものもある
    そうなった理由はわかるが、まったく助けになっていない
    しかもこうしたヘッダはすべて悪意ある user-agent が挿入できる。信頼できるサーバがパイプライン内で重要な情報をどう渡すべきかについて、誰も合意できなかったかのようだ
    この混乱は User-Agent ヘッダの不条理さともよく似合っている
    あちらは Apple がプライバシーを名目に、偽の OS バージョンのような完全なデタラメ情報を送る方針を取ったことで、さらに極端になった

  • この主張にはかなり一理あるが、FastCGIPATH_INFO のような部分で CGI/1.1 に従うため、情報の欠落が生じる
    URL デコードが強制されるので、encoded slash である %2F を表現できない
    実装によってはパス中の /// にまとめることもあるが、これは多くの HTTP 実装にもある問題ではある
    表現力の面では HTTP より劣り、その差が重要かどうかはアプリケーション次第だ
    私は URL を正確に扱えるほうを好む