업그레이드 이후 혼합 SSL 문제

기본적으로 정상 작동하는 리버스 프록시 설치를 가지고 있었습니다.
모든 것이 설계대로 HTTPS로 제공되고 있었습니다.

몇 가지 버전을 건너뛰어 업그레이드한 결과, 여기서 보고된 것과 동일한 화면이 흰색으로만 표시되는 '죽음의 업그레이드’를 경험했습니다: Upgrade discourse doesn't work - #6 by Andrew_Bond

./launcher rebuild app을 수동으로 실행하여 Discourse를 업데이트했으며, 현재 빌드 2.9.0.beta14(0da79561c3)를 사용하고 있습니다.

초안을 저장하려고 하거나 게시, 로그인 등 대부분의 다른 작업을 수행할 때 403 Forbidden 오류가 발생한다는 것을 확인했습니다(크롬 인스펙터를 통해 확인).

다만 사이트 왼쪽 상단의 로고는 정상적으로 작동합니다.

명령줄을 통해 force https 요소를 수동으로 비활성화하면, http 요소가 차단되는 혼합 모드(mixed mode) 상태에 있기 때문에 로드가 실패합니다. 사이트의 다른 모든 요소는 계속 정상 작동합니다.

리버스 nginx 설정에는 아무런 변경을 가하지 않았습니다. 앱을 다시 재빌드해 보았지만 해결되지 않았습니다.

업그레이드한 빌드에서 Discourse의 작동 방식이 변경되었을 것으로 추정됩니다. force https 옵션이 정상 작동했던 마지막 빌드 ID는 e9f53dbe0028ca7a5d8926711a1944765d34347입니다.

현재 포착된 오류:

안드류님, 보고해 주셔서 감사합니다. 이 문제는 요청이 애플리케이션 컨테이너에 도달하기 전에 여러 개의 X-Forwarded-Proto 헤더가 추가되는 리버스 프록시 설정에서 특히 발생합니다.

방금 main 브랜치에 수정 사항을 병합했습니다. 약 20분 내에 tests-passed에 반영될 것으로 예상됩니다.

tests-passed에 반영되면 전체 재빌드(full rebuild)를 수행해야 합니다. (nginx 설정 변경은 UI 기반 업그레이드 시 적용되지 않기 때문입니다.)

빠른 답변과 빠른 수정에 감사드립니다!

자, 업데이트 내용입니다.

./launcher rebuild app 명령어로 앱을 다시 빌드했습니다.

이 과정에서는 문제가 없었습니다.

HTTPS를 강제 적용하면 아래 이미지의 예외를 제외하고 오류가 사라졌습니다.

이제 403 거부 오류는 발생하지 않지만, 예를 들어 로그인할 때 로그인 상자가 표시되고 입력된 데이터를 받아들이는 것처럼 보이다가 다시 로그인되지 않은 사용자로 돌아갑니다.

페이지를 새로고침할 때도 위에서 설명한 동일한 오류가 발생합니다.

다시 HTTPS 강제를 비활성화하면 사이트는 정상적으로 작동합니다. 다만 명확히 하기 위해, 로고는 여전히 제공되지 않습니다.

해당 오류는 무관합니다. Cloudflare이 분석 스크립트를 주입하고, Discourse의 기본 콘텐츠 보안 정책(CSP)이 이 주입을 차단하기 때문에 발생하는 문제입니다.

사용자와 Discourse 사이에 존재하는 프록시에 대해 더 자세히 알려 주실 수 있나요? Cloudflare이 첫 번째 레이어라고 가정합니다. Cloudflare과 Discourse 앱 컨테이너 사이에还有什么 다른 것이 있나요?

네!

말씀하신 대로 맞습니다. Cloudflare가 첫 번째 히트이고, 그 다음에는 centminmod가 제공하는 nginx 리버스 프록시가 도커로 연결됩니다.

이 설정을 공유해 주실 수 있나요? 특히, X-Forwarded-Proto 헤더를 추가로 추가하는지 궁금합니다.

물론입니다!

# Centmin Mod 시작 가이드
# 반드시 https://centminmod.com/getstarted.html 을 읽어주세요
# HTTP/2 SSL 설정을 위해
# https://centminmod.com/nginx_configure_https_ssl_spdy.html 을 읽어주세요

# www에서 non-www로 리다이렉트 및 강제 SSL
# 활성화하려면 주석을 해제하고 파일을 저장한 후 Nginx를 재시작하세요
# 확실하지 않다면 return 301을 사용하기 전에 return 302를 사용하세요
 server {
       listen   80;
       server_name exiges.com www.exiges.com;
       return 302 https://$server_name$request_uri;
 }

server {
  listen 443 ssl http2;
  server_name exiges.com www.exiges.com;

  ssl_dhparam /usr/local/nginx/conf/ssl/exiges.com/dhparam.pem;
  ssl_certificate      /usr/local/nginx/conf/ssl/exiges.com/exiges.com.crt;
  ssl_certificate_key  /usr/local/nginx/conf/ssl/exiges.com/exiges.com.key;
  include /usr/local/nginx/conf/ssl_include.conf;

  # cloudflare 인증된 오리지인 풀 인증서 community.centminmod.com/threads/13847/
  #ssl_client_certificate /usr/local/nginx/conf/ssl/cloudflare/exiges.com/origin.crt;
  #ssl_verify_client on;
  
  
  
  # mozilla 권장 사항
  ssl_ciphers TLS13-AES-128-GCM-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS;
  ssl_prefer_server_ciphers   on;
  #add_header Alternate-Protocol  443:npn-spdy/3;

  # 아래 HSTS 라인을 활성화하기 전에 centminmod.com/nginx_domain_dns_setup.html#hsts 를 읽어주세요
  #add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
  #add_header X-Frame-Options SAMEORIGIN;
  add_header X-Xss-Protection "1; mode=block" always;
  add_header X-Content-Type-Options "nosniff" always;
  #add_header Referrer-Policy "strict-origin-when-cross-origin";
  #add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
  #spdy_headers_comp 5;
  ssl_buffer_size 1369;
  ssl_session_tickets on;
  
  # ocsp 스테이플링 활성화
  #resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 valid=10m;
  #resolver_timeout 10s;
  #ssl_stapling on;
  #ssl_stapling_verify on;
  #ssl_trusted_certificate /usr/local/nginx/conf/ssl/exiges.com/exiges.com-trusted.crt;

# ngx_pagespeed 및 ngx_pagespeed 핸들러
#include /usr/local/nginx/conf/pagespeed.conf;
#include /usr/local/nginx/conf/pagespeedhandler.conf;
#include /usr/local/nginx/conf/pagespeedstatslog.conf;

  # limit_conn limit_per_ip 16;
  # ssi  on;

  access_log /home/nginx/domains/exiges.com/log/access.log combined buffer=256k flush=5m;
  error_log /home/nginx/domains/exiges.com/log/error.log;

  include /usr/local/nginx/conf/autoprotect/exiges.com/autoprotect-exiges.com.conf;
  root /home/nginx/domains/exiges.com/public;
  # 서버 및/또는 vhost 사이트를 위해 cloudflare를 사용하는 경우
  # cloudflare.conf include 주석을 해제하세요
  #include /usr/local/nginx/conf/cloudflare.conf;
  include /usr/local/nginx/conf/503include-main.conf;

 
location / {
       proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
       proxy_set_header        X-Forwarded-Proto https;
       proxy_http_version 1.1;
       proxy_set_header X-Forwarded-Proto $scheme;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Real-IP $remote_addr;
   }   

  include /usr/local/nginx/conf/php.conf;
  
  #include /usr/local/nginx/conf/pre-staticfiles-local-exiges.com.conf;
  #include /usr/local/nginx/conf/pre-staticfiles-global.conf;
  #include /usr/local/nginx/conf/staticfiles.conf;
  #include /usr/local/nginx/conf/drop.conf;
  #include /usr/local/nginx/conf/errorpage.conf;
  #include /usr/local/nginx/conf/vts_server.conf;
}

x-proto 헤더에 대한 참조를 확인했습니다..

   proxy_set_header X-Forwarded-Proto $scheme;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Cloudflare가 원본 서버(Origin)에 어떻게 연결되고 있나요? SSL/TLS를 사용하고 있나요? 이상적으로는 여기서 “Full” 또는 "Full (Strict)"를 사용해야 합니다.

유연합니다. 게을러서 서버 인증서를 사용하지 않았습니다.

Cloudflare의 안전한 SSL 옵션 중 하나를 설정하는 것을 권장합니다. "Flexible"은 Cloudflare와 서버 간 트래픽이 암호화되지 않음을 의미합니다.

설정 파일을 다시 살펴보니:

헤더를 https로 설정한 후, 두 줄 아래에서 다시 $scheme으로 설정하고 있는 것으로 보입니다. (Cloudflare 설정에 따라 $scheme은 http가 됩니다.) 두 번째 설정을 제거하면, Discourse가 연결이 안전하다고 인식하게 됩니다(실제로는 안전하지 않지만).

흥미롭네요.

다만 이상한 점은, 이전에 잘 작동하고 있었다는 거예요.

의심(100% 확인은 안 했지만)컨데, Discourse 컨테이너의 NGINX가 이전에 ‘첫 번째’ X-Forwarded-Proto 헤더를 참조하고 있었던 것 같습니다. 이제 ‘마지막’ X-Forwarded-Proto 헤더를 참조하는데, 이는 ‘가장 최근’ 프록시에서 유래하며 해당 프록시가 수신된 요청에 대해 가장 많은 정보를 가지고 있기 때문에 논리적으로 더 안전한 방식입니다.

즉, 업데이트 전에는 Cloudflare가 도입한 X-Forwarded-Proto(https)를 참조하고 있었습니다. 하지만 이제는 ‘centminmod’ 리버스 프록시 설정이 도입한 X-Forwarded-Proto 헤더(http)를 참조하고 있습니다.

현재 정상적으로 작동하는 설정에서 proxy_set_header X-Forwarded-Proto $scheme;를 제거했습니다.

CentminMod를 설정하고 계신 분들을 위해, nginx 설정 파일은 두 개가 있습니다.

하나는 HTTP용이고, 다른 하나는 HTTPS용입니다.

HTTP 사이트 설정에서 두 번째 proxy_set_header X-Forwarded-Proto $scheme;를 제거했고, 이제 모든 것이 예상대로 작동합니다.

제 exiges.com.conf 파일입니다.

# Centmin Mod Getting Started Guide
# must read https://centminmod.com/getstarted.html

# redirect from non-www to www 
# uncomment, save file and restart Nginx to enable
# if unsure use return 302 before using return 301
#server {
#            listen   80;
#            server_name exiges.com;
#            return 301 $scheme://www.exiges.com$request_uri;
#       }

server {
  server_name exiges.com www.exiges.com;
#return 301 https://exiges.com.com$request_uri;
#return 301 $scheme://www.exiges.com$request_uri;

# ngx_pagespeed & ngx_pagespeed handler
#include /usr/local/nginx/conf/pagespeed.conf;
#include /usr/local/nginx/conf/pagespeedhandler.conf;
#include /usr/local/nginx/conf/pagespeedstatslog.conf;

  #add_header X-Frame-Options SAMEORIGIN;
  add_header X-Xss-Protection "1; mode=block" always;
  add_header X-Content-Type-Options "nosniff" always;
  #add_header Referrer-Policy "strict-origin-when-cross-origin";
  #add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";

  # limit_conn limit_per_ip 16;
  # ssi  on;

  access_log /home/nginx/domains/exiges.com/log/access.log combined buffer=256k flush=5m;
  error_log /home/nginx/domains/exiges.com/log/error.log;

  include /usr/local/nginx/conf/autoprotect/exiges.com/autoprotect-exiges.com.conf;
  root /home/nginx/domains/exiges.com/public;
  # uncomment cloudflare.conf include if using cloudflare for
  # server and/or vhost site
  #include /usr/local/nginx/conf/cloudflare.conf;
  include /usr/local/nginx/conf/503include-main.conf;

  # prevent access to ./directories and files
  #location ~ (?:^|/)\. {
  # deny all;
  #}

  location / {
 proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
		    proxy_set_header        X-Forwarded-Proto https;	
                #proxy_set_header Host $http_host;
                proxy_http_version 1.1;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                #proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-Real-IP $remote_addr;
  #include /usr/local/nginx/conf/503include-only.conf;

# block common exploits, sql injections etc
#include /usr/local/nginx/conf/block.conf;

  # Enables directory listings when index file not found
  #autoindex  on;

  # Shows file listing times as local time
  #autoindex_localtime on;

  # Wordpress Permalinks example
  #try_files $uri $uri/ /index.php?q=$uri&$args;

  }

  include /usr/local/nginx/conf/php.conf;
  
  #include /usr/local/nginx/conf/pre-staticfiles-local-exiges.com.conf;
  #include /usr/local/nginx/conf/pre-staticfiles-global.conf;
  #include /usr/local/nginx/conf/staticfiles.conf;
  #include /usr/local/nginx/conf/drop.conf;
  #include /usr/local/nginx/conf/errorpage.conf;
  #include /usr/local/nginx/conf/vts_server.conf;
}

제 exiges.com.conf.ssl 파일입니다.

# Centmin Mod Getting Started Guide
# must read https://centminmod.com/getstarted.html
# For HTTP/2 SSL Setup
# read https://centminmod.com/nginx_configure_https_ssl_spdy.html

# redirect from www to non-www  forced SSL
# uncomment, save file and restart Nginx to enable
# if unsure use return 302 before using return 301
 server {
       listen   80;
       server_name exiges.com www.exiges.com;
       return 302 https://$server_name$request_uri;
 }

server {
  listen 443 ssl http2;
  server_name exiges.com www.exiges.com;

  ssl_dhparam /usr/local/nginx/conf/ssl/exiges.com/dhparam.pem;
  ssl_certificate      /usr/local/nginx/conf/ssl/exiges.com/exiges.com.crt;
  ssl_certificate_key  /usr/local/nginx/conf/ssl/exiges.com/exiges.com.key;
  include /usr/local/nginx/conf/ssl_include.conf;

  # cloudflare authenticated origin pull cert community.centminmod.com/threads/13847/
  #ssl_client_certificate /usr/local/nginx/conf/ssl/cloudflare/exiges.com/origin.crt;
  #ssl_verify_client on;
  
  
  
  # mozilla recommended
  ssl_ciphers TLS13-AES-128-GCM-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS;
  ssl_prefer_server_ciphers   on;
  #add_header Alternate-Protocol  443:npn-spdy/3;

  # before enabling HSTS line below read centminmod.com/nginx_domain_dns_setup.html#hsts
  #add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
  #add_header X-Frame-Options SAMEORIGIN;
  add_header X-Xss-Protection "1; mode=block" always;
  add_header X-Content-Type-Options "nosniff" always;
  #add_header Referrer-Policy "strict-origin-when-cross-origin";
  #add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
  #spdy_headers_comp 5;
  ssl_buffer_size 1369;
  ssl_session_tickets on;
  
  # enable ocsp stapling
  #resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 valid=10m;
  #resolver_timeout 10s;
  #ssl_stapling on;
  #ssl_stapling_verify on;
  #ssl_trusted_certificate /usr/local/nginx/conf/ssl/exiges.com/exiges.com-trusted.crt;

# ngx_pagespeed & ngx_pagespeed handler
#include /usr/local/nginx/conf/pagespeed.conf;
#include /usr/local/nginx/conf/pagespeedhandler.conf;
#include /usr/local/nginx/conf/pagespeedstatslog.conf;

  # limit_conn limit_per_ip 16;
  # ssi  on;

  access_log /home/nginx/domains/exiges.com/log/access.log combined buffer=256k flush=5m;
  error_log /home/nginx/domains/exiges.com/log/error.log;

  include /usr/local/nginx/conf/autoprotect/exiges.com/autoprotect-exiges.com.conf;
  root /home/nginx/domains/exiges.com/public;
  # uncomment cloudflare.conf include if using cloudflare for
  # server and/or vhost site
  include /usr/local/nginx/conf/cloudflare.conf;
  include /usr/local/nginx/conf/503include-main.conf;

 
location / {
       proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
       proxy_set_header        X-Forwarded-Proto https;
       proxy_http_version 1.1;
       proxy_set_header X-Forwarded-Proto $scheme;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Real-IP $remote_addr;
   }   

  include /usr/local/nginx/conf/php.conf;
  
  #include /usr/local/nginx/conf/pre-staticfiles-local-exiges.com.conf;
  #include /usr/local/nginx/conf/pre-staticfiles-global.conf;
  #include /usr/local/nginx/conf/staticfiles.conf;
  #include /usr/local/nginx/conf/drop.conf;
  #include /usr/local/nginx/conf/errorpage.conf;
  #include /usr/local/nginx/conf/vts_server.conf;
}

@david 이 문제를 해결해 주셔서 정말 감사합니다.

참고로, 현재 저는 인증서 관련해서 유연하지 않고 완전히 고정된 상태입니다.