FastCGI: 리버스 프록시용으로는 30년이 지나도 여전히 더 나은 프로토콜

5 hours ago 3
  • 장기 실행 백엔드에 소켓으로 요청을 넘기는 프록시 프로토콜로서, 기존 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의 기반이 되며, 리버스 프록시와 백엔드가 메시지 경계를 다르게 이해하면서 심각한 보안 문제를 만듦
  • 파서 차이를 계속 패치하는 방식은 근본 해법이 되기 어려움

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.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의 복잡성과 보안 악몽을 감수하기보다는 하드웨어 추가를 택하는 쪽이 낫다고 봄
Read Entire Article