리버스 프록시와 HTTPS 환경의 Discourse

Apache 리버스 프록시 뒤에 Discourse를 설정하려고 하는데, HTTPS로 제대로 작동하지 않습니다.

지금까지 이 지점까지 도달하는 데 많은 문제를 겪었습니다. 현재 서버에 Discourse를 설치하고, 그 앞에 Apache 서버를 리버스 프록시로 배치해 두었습니다. Discourse가 app.yaml에 설정된 호스트 이름으로 리디렉션하려고 해서 리버스 프록시 뒤에서 실행하는 것 자체가 많은 문제를 일으켰습니다.

어쨌든 지금은 작동은 하지만, 브라우저에서 혼합 콘텐츠(Mixed-content) 경고가 나타납니다.
Apache에 HTTP에서 HTTPS로의 리디렉트가 설정되어 있어 그 부분은 정상적으로 작동합니다. 하지만 Discourse가 여전히 일부 콘텐츠를 HTTP로 서빙하고 있는데, HTTPS로 변경하는 방법을 찾지 못하고 있습니다.

예를 들어, 파비콘(favicon)이 HTTP로 서빙되고 있는데, 이 부분을 어떻게 변경해야 할지 모르겠습니다.

Discourse가 HTTPS 트래픽을 직접 처리하지 않으면서도 모든 링크를 HTTPS로 변경하도록 할 수 있을까요?

Apache에서 다음을 설정해 보았지만 도움이 되지 않았습니다:

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

Discourse의 force https 플래그를 활성화해도 마찬가지입니다. 이 경우 HTTP로 오는 모든 요청을 무시하게 되어 사이트가 깨져 버립니다.
혼합 콘텐츠 문제를 해결하기 위해 무엇을 해야 할까요?

Apache2는 많은 문제를 일으킬 수 있습니다. nginx, caddy, traefik 또는 haproxy로 전환하는 것을 고려해 보세요.

컨테이너 내 유닉스 소켓으로 Apache2를 리버스 프록시로 사용하는 테스트 환경에서 Apache2가 “문제없이” 작동하는 것을 확인했습니다:

발견한 유일한 차이점(참고: 테스트는 몇 시간밖에 하지 않았으며, 완전한 검증은 아닙니다)은 다음과 같습니다:

  • 컨테이너의 공유 볼륨에 있는 유닉스 소켓으로의 심볼릭 링크를 사용할 경우 Apache2가 작동하지 않습니다;
  • 대략적인 테스트에서 Apache2가 약간 느렸지만, 그 정도는 크지 않았습니다.

개인적으로 기술에 대한 종교 전쟁 같은 것을 좋아하지 않습니다. 따라서 "Apache2를 사용하면 많은 문제가 발생할 것이다"라는 주장에는 동의하지 않습니다. 제 테스트 과정에서 Apache2로 인해 부정적인 문제를 경험한 적은 없습니다.

Apache2(참고: LETSENCRYPT와 함께 HTTP로 잘 작동했습니다)에 사용했던 핵심 설정은 다음과 같습니다:

# cat discourse.example.conf
<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  ServerName  discourse.example.com
  DocumentRoot /website/discourse

  RewriteEngine On
  ProxyPreserveHost On
  ProxyRequests Off
  ProxyPass / unix:/var/discourse/shared/socket-only/nginx.http.sock|http://localhost/
  ProxyPassReverse  / unix:/var/discourse/shared/socket-only/nginx.http.sock|http://localhost/
  ErrorLog /var/log/apache2/discourse.error.log
  LogLevel warn
  CustomLog /var/log/apache2/discourse.access.log combined

  RewriteCond %{SERVER_NAME} =discourse.example.com
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

참고: force_https가 설정되어 있음에도 HTTP로 서빙되는 문제를 경험한 유일한 경우는 /uploads 디렉토리에 파일이 누락되었을 때였으며, 이는 (물론) Apache2와 nginx를 리버스 프록시로 사용하는 것과는 무관합니다.

답변 주셔서 감사합니다. 하지만 저는 Discourse와 동일한 서버에 Apache를 설치해 두지 않았습니다. 그 부분이 명확하지 않았던 것 같습니다.
저는 이미 여러 웹사이트를 호스팅 중인 기존 Apache 서버가 있고, 다른 서버에 있는 Discourse를 리버스 프록시해야 하므로 소켓을 사용할 수 없습니다.

감사합니다.

이 방법을 직접 시도해 보지는 않았지만, 원격 파일 시스템을 마운트하여 유닉스 소켓에 접근할 수 있는지 확인해 볼 수 있습니다. 특히 서버가 동일한 데이터센터에 있고 광역 네트워크(WAN)를 통한 네트워크 성능이 문제가 되지 않는다면 더욱 그렇습니다.

아키텍처, 운영체제 세부 정보, 네트워크 구성 등을 게시하지 않는 한 답변하기가 어렵고, 어쩌면 메타 디스코urs의 범위 밖의 문제일 수도 있습니다.

창의적으로 생각해 보세요!

마지막으로 그 설정을 apache2와 함께 사용해 봤을 때, discourse 메시지 버스에 대한 연결 실패가 발생했습니다. 이미 1년 넘게 지난 일이긴 하지만요. 사이트는 정상적으로 로드되는 것처럼 보였지만, F12 개발자 콘솔에서는 초기 몇 번의 시도 후 wss 연결이 타임아웃되었다고 명확히 표시되어 있었습니다.

제가 올린 예시 설정에서 분명히 볼 수 있듯이, 우리는 wss를 사용하지 않았습니다.

wss !== apache2 reverse proxy (이것은 방법 중 하나일 뿐이며, 우리는 wss를 사용하지 않습니다)

사실, 우리는 다음과 같은 이유로 unix domain sockets를 사용하여 nginx와 apache2 reverse proxy 설정만 사용하고 있습니다:

  • 저는 게으르고 단순하며 디버깅이 쉬운 설정을 좋아합니다.
  • unix domain sockets는 단순하고 디버깅이 쉽습니다.
  • nginx에서는 심볼릭 링크를 사용하여 reverse proxy와 컨테이너 간을 전환할 수 있습니다.
  • apache2 (컨테이너로 reverse proxy)는 심볼릭 링크와 함께 작동하지 않으므로 웹 서버 재시작이 필요합니다.

그러나 @Grunskin 님은 아직 설정하지 않은 것에 대해 질문하셨습니다. 즉, 한 호스트에서 reverse proxy를 수행하고 다른 호스트에서 컨테이너를 실행하는 것입니다.

시간이 나면, 원격 파일 시스템을 마운트하고 unix socket를 사용하여 이것이 작동하는지 확인하기 위해 같은 데이터 센터 내에서 nginx와 apache2 모두에 대해 테스트해 보겠습니다.

그때까지…

참고: 제 생각에, 이 문제는 단순히 reverse proxy로만 작동하는 nginx나 apache2와 관련이 없습니다 (다만, 원격 액세스 설정은 아직 테스트하지 않아 더 이상의 코멘트는 할 수 없습니다).

왜 이것이 필요한가요?

Discourse는 웹사이트가 아닌 애플리케이션입니다. 초기 JavaScript 페이로드가 브라우저로 전달된 후, 많은 기능이 Discourse 서버와의 빠르고 안정적인 연결에 의존합니다. 다른 시스템을 통해 프록시 처리를 하면 지연 시간이 발생하여 사용자 경험이 크게 저하됩니다.

요구 사항에 대한 이유를 설명해 주시겠습니까?

리버스 프록시를 사용하는 이유는 많습니다.
예를 들어, 공용 IP가 하나뿐인데 포트 80/443으로 여러 웹 서버를 대외 공개해야 하고, 해당 웹 서버에서는 이 예시처럼 Discourse를 실행할 수 없는 경우가 있습니다.
SSL 오프로딩으로 사용하여 최종 서버가 암호화를 처리하지 않도록 할 수 있습니다.
리버스 프록시 뒤에 최종 서버를 배치하면 공격 표면이 줄어듭니다.
합법적인 이유가 많음에도 불구하고, 이것이 불가능한 이유를 이해할 수 없습니다.

잠시 후, 이미 Apache 서버 뒤에 Discourse가 실행 중인 다른 서버가 있으며, 최소 2년 동안 잘 작동해 왔다는 것을 떠올랐습니다. 새로운 Discourse를 동일한 방식으로 설정했지만, 일부 콘텐츠를 http로 서빙하는 것을 멈추게 할 수 없습니다. 두 Discourse 서버 간의 유일한 차이는 구버전이 2.4, 신버전이 2.5로 실행되고 있다는 점이며, 이 사이에 차이가 있는지 알 수 없습니다.

첫 번째 게시글에서 언급했듯이 force_https는 사이트를 깨뜨려 로그인, 초대 수락 등이 불가능하게 만듭니다. http로 서빙되는 것으로 보이는 일부 JavaScript가 실행되지 않는 것 같습니다.
force_https가 링크를 폐기하는 대신 모든 http 링크를 https로 재작성하도록 하는 것이 더 합리적이지 않나요? 최소한 옵션으로 제공해 주면 좋겠습니다.

Discourse를 대외 공개하기 위한 권장 설정 방법은 무엇인가요? DMZ에 자체 외부 IP가 있는 서버를 설정하는 것이 권장되나요?

Traefik을 한번 살펴보는 것을 추천합니다.
매우 잘 작동하며 SSL 인증서를 자동으로 관리합니다.

리버스 프록시 뒤에 실행해야 하는 이유는 여러 가지가 있습니다. Discourse.org의 인프라가 HAproxy 뒤에 실행되고 있다고 확신합니다.

저는 traefik과 caddyserver를 사용하며, 과거에 nginx, HAproxy, 심지어 apache까지도 동작하도록 설정해 본 경험이 있습니다(서브폴더 설치 환경의 워드프레스용).

이것이 당신의 문제입니다. force_https를 활성화하고 왜 깨지는지 파악해야 합니다. 이를 끄는 것은 옵션이 아닙니다. 당신은 무료 지원을 요청했고, 응답한 사람들은 apache에 대한 해결책을 가지고 있지 않으므로, 당신은 apache 밴드의 리더가 되어야 합니다.

네. 저희도 완전히 동의합니다.

현재 저희는 메인 마이그레이션을 스테이징하는 서버를 제외하고, 프로덕션, 테스트 및 스테이징 환경의 모든 사이트에서 “리버스 프록시를 사용하는 두 개의 컨테이너” 구성을 사용하고 있습니다.

이유는 postgres, mysql, discourse 앱 코드를 모두 단일 컨테이너에서 실행할 때 다양한 마이그레이션 스크립트를 실행하기가 더 쉽기 때문입니다. 이는 프로덕션 환경이 아니므로 디버깅하기도 쉽습니다. 하지만 문제가 없음을 확인하면 백업을 프로덕션으로 이동하여 복원합니다.

이 구성의 한 가지 문제는, 리버스 프록시를 갖춘 초고성능 두 컨테이너 구성이라 하더라도 DB가 하나만 존재하기 때문에 DB 복원 중 발생하는 다운타임을 상쇄할 수 없다는 점입니다.

향후에는 DB 컨테이너를 두 개 사용하여 하나를 복원하고 데이터 측면에서도 전환할 수 있는 구성을 시도해 볼 수 있을 것입니다.

아니면, 더 좋게 말하면, 다른 이름의 데이터베이스로 복원한 후 실시간으로 전환하는 방법이 있지만, 아직 어떻게 해야 하는지 모릅니다. 프로덕션 DB 이름을 discourse에서 discourse2로 변경하는 코드가 어디에 있는지, 앱을 전체적으로 다시 빌드하지 않고 변경할 수 있는 방법을 아신다면 알려주시면 좋겠습니다 :slight_smile: 아마 창의적이고 혁신적인 @pfaffman 수석 컨설턴트가 아실지도 모르겠네요?

업데이트: templates/postgres.template.yml 파일에 있습니다 :slight_smile:

(여기서 postgres 폴더 관련 mv 명령어가 몇 가지 흥미롭게 보입니다 :slight_smile: :). )

스탠드얼론과 멀티 컨테이너 구성 모두 장단점이 있지만, 프로덕션 환경에서는 리버스 프록시를 사용하는 두 컨테이너 구성을 완전히 채택했습니다. 마이그레이션 테스트와 스테이징 환경에서는 스탠드얼론 구성이 저희에게 가장 적합합니다.

Apache2에 관해서는, 리버스 프록시를 하나의 서버에서, 컨테이너를 다른 서버에서 설정하고 테스트할 시간을 가졌으면 합니다. 죄송합니다…

또한, 두 컨테이너 구성에서 DB 복원을 수행할 때 사이트를 가동 상태로 유지하는 방법을 찾아내야 합니다. (지금 보니) templates/postgres.template.yml 템플릿은 단일 스탠드얼론 컨테이너 구성에 맞춰져 있는 것 같습니다.

Discourse는 WSS를 사용하지 않는다는 점을 추가로 말씀드리고자 합니다. 메시지 버스는 청크 인코딩(스트리밍)을 사용하는 롱 폴링(Long Polling)을 사용합니다.

해당 호스트 이름을 Discourse에 접근할 때 사용될 호스트 이름으로 설정해야 합니다. app.yml의 도메인 이름이 사람들이 브라우저에 입력하여 사이트에 접속하는 것과 일치하지 않으면 작동하지 않습니다.

app.yml에 ssl 또는 letsencrypt 템플릿이 없나요?

force_https를 켜야 합니다.

그리고 롱 폴링(long polling)을 작동시키려면 몇 가지 복잡한 설정을 거쳐야 합니다. 정확히 어떤 설정인지 기억나지 않습니다.

@Grunskin 님, 안녕하세요.

분노를 잘 이해합니다. 하지만 다음을 설정할 때:

force_https = true

이것이 문제의 원인은 아닙니다. @pfaffman 님이 위에서 언급했듯이 (최소 두 번, 메타의 최상위 마이그레이션 전문가 중 한 분):

“반드시” force_https = true를 유지한 상태에서 "실제 문제"를 찾아야 합니다.

제가 그 자리에 있다면, 제가 읽은 내용들을 바탕으로.

첫째, 문제 해결을 단순화하기 위해 테스트 및 트러블슈팅 목적으로만 리버스 프록시를 discourse 컨테이너와 동일한 서버에 설정하겠습니다. 이 간단한 테스트 케이스가 완벽하게 작동하는지 확인하세요. 그런 다음, 동일한 호스트에서 작동하는 것을 확인한 후, 해당 테스트용 리버스 프록시를 끄고 원하는 이중 서버 구성으로 이동하세요.

이것은 흥미로운 문제입니다. force_https = true를 유지하고 체계적이고 단계적인 트러블슈팅 방법을 유지하면 이 문제를 해결할 수 있습니다. 이러한 유형의 문제는 해결되길 바라는 IT 퍼즐에 불과합니다.

당신은 할 수 있습니다.


P.S.: 이 퍼즐에 지치거나 지루해지면, 항상 @pfaffman 또는 다른 경험 많은 메타 직원에게 돈을 지불하여 이 장애물을 넘게 하고 앞으로 더 나은 미래를 향해 나아갈 수 있습니다.


참고: 우리는 동일한 서버에서 유닉스 도메인 소켓을 사용하여 Apache2를 리버스 프록시로 설정하는 데 전혀 문제가 없었습니다. 두 서버 구성을 설정하고 두 다른 서버에서 Apache2 리버스 프록시 방법을 작동시킬 개인적인 시간이 없다는 점에 진심으로 사과드립니다. 우리는 "미친 bbcode 남용"을 마크다운으로 정리하는 최종 마이그레이션 작업으로 바쁘며, 원래 예상했던 것보다 더 많은 시간이 소요되고 있습니다.

저는 로컬호스트의 포트 8443에서 SSL로 Discourse를 실행하고, UFW(방화벽)로 8443 포트를 잠근 뒤, apache2를 통해 443 포트를 8443으로 프록시하는 방식을 선택했습니다. 지금 시도해 보고 있습니다.

                ProxyPass / "https://localhost:8443"
                ProxyPassReverse  / "https://localhost:8443"

새 환경으로 Discourse를 마이그레이트한 후 로그인 시 POST 세션에서 HTTP 403 오류가 발생하고 있습니다. CSRF 문제라고 추측되지만, 현재 디버깅을 어디서 시작해야 할지 막막한 상태입니다. /var/log/discourse-var-log/production.log와 production_error.log 파일 모두 0바이트 상태입니다…

이 문제를 올바르게 디버깅하는 방법에 대한 아이디어가 있으신가요?

현재 구성은 다음과 같습니다:
1. haproxy를 중앙 HTTPS 로드 밸런서/가속기로서 사용 (Discourse 및 기타 서비스 모두 대상)

forum >> develd apache rev. proxy p.82
backend forum-backend
   mode http
   server forum.netzwissen.de 10.10.10.14:83 cookie A check
   http-request set-header X-Forwarded-Port %[dst_port]
   http-request add-header X-Forwarded-Proto https if { ssl_fc }
   # HSTS 헤더, 16000000초: 약 6개월 이상
   http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"

2. 로컬 apache를 리버스 프록시로 사용

   <IfModule proxy_module>
    ## <https://meta.discourse.org/t/running-other-websites-on-the-same-machine-as-discourse/17247>
    ProxyPreserveHost On
    # ProxyRequests Off     
    RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
    RequestHeader set X-Real-IP expr=%{REMOTE_ADDR}
    ProxyPass /  unix:/var/discourse/shared/web-only/apache.http.sock|http://localhost/
    ProxyPassReverse  / unix:/var/discourse/shared/web-only/apache.http.sock|http://localhost/
    </IfModule>

3) Discourse는 별도의 web_only와 data 컨테이너로 운영되며, web_only는 "templates/web.socketed.template.yml"로 배포됨

로그인 시 실패하는 세션 요청입니다:

POST /session HTTP/1.1
Host: forum.netzwissen.de
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: */*
Accept-Language: de,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 89
X-CSRF-Token: dtV0N6faVQSWZsg6z9ZGOxQBjuTpBZk6tAMRxaXJdwozF1kObw9UuiFnxbLf5OGDeL1DWDgZ5W3oJP7CY+LwRw==
Discourse-Present: true
X-Requested-With: XMLHttpRequest
Origin: https://forum.netzwissen.de
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Referer: https://forum.netzwissen.de/
Connection: keep-alive

web_only 컨테이너의 웹서버 응답:

HTTP/1.1 403 Forbidden
date: Sun, 13 Mar 2022 16:41:56 GMT
server: nginx
content-type: text/plain; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-download-options: noopen
x-permitted-cross-domain-policies: none
referrer-policy: strict-origin-when-cross-origin
vary: Accept
x-request-id: 778da942-3c1c-493b-946b-478984f53a8c
x-runtime: 0.003623
transfer-encoding: chunked
strict-transport-security: max-age=16000000; includeSubDomains; preload;

CSRF 관련 사항이나 nginx에 대해 잘 모르지만 (외부 웹서버는 apache 2.4입니다), CSRF가 제 문제일 것이라고 확신합니다. Discourse는 로그인 없이 정상적으로 작동하며, 로그인에 사용되는 POST 요청만 여기서 실패하기 때문입니다. 제 중앙 haproxy의 내부 IP는 10.10.10.21이므로, web_only 컨테이너의 discourse.conf yml 파일에 다음을 추가했습니다:

set_real_ip_from 10.10.10.21/24;

기본값인 127.0.0.1/24;로 시도해 보기도 했지만, 둘 다 로그인 시 동일한 403 오류가 발생합니다.