Обсуждение обратного прокси и HTTPS

Я пытаюсь настроить Discourse за моим обратным прокси Apache, но не могу заставить его корректно работать с HTTPS.

У меня возникло множество проблем на этом этапе. Сейчас у меня есть сервер с Discourse и сервер Apache перед ним, выступающий в роли обратного прокси. Сначала у меня было много проблем с запуском за обратным прокси, поскольку Discourse всегда пытался перенаправлять на хостнейм, указанный в app.yaml.

Как-то мне удалось это настроить, но теперь в браузере появляются предупреждения о смешанном содержимом.
В Apache у меня настроено перенаправление с HTTP на HTTPS, и это работает нормально. Однако Discourse всё ещё отдаёт некоторые ресурсы по HTTP, и я не могу понять, как заставить его использовать HTTPS.

Например, фавикон отдаётся по HTTP, и я не могу понять, как это исправить.

Могу ли я заставить Discourse изменить все ссылки на HTTPS, не заставляя сам Discourse обрабатывать HTTPS-трафик?

Я пытался установить в Apache:

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

Но это, похоже, не помогает.

Включение флага force https в Discourse тоже не помогает: это просто ломает сайт, так как Discourse начинает игнорировать всё, что приходит по HTTP.

Что мне нужно сделать, чтобы избавиться от смешанного содержимого?

Apache2 доставит вам много проблем. Подумайте о переходе на nginx, caddy, traefik или haproxy.

У меня Apache2 заработал «без проблем» в тестовой среде: Apache2 выступает в роли обратного прокси к unix-сокету внутри контейнера.

Единственное различие, которое я обнаружил (примечание: тестирование длилось всего несколько часов, это не полное исследование), заключалось в следующем:

  • Apache2 не работает с символической ссылкой на unix-сокет в общем томе внутри контейнера;
  • Apache2 был немного медленнее в грубом тесте, но незначительно.

Лично я не сторонник религиозных войн вокруг технологий; поэтому я не согласен с утверждением, что «Apache2 создаст вам много проблем». Во время моих тестов я не столкнулся с какими-либо негативными проблемами с Apache2.

Ниже приведена основная конфигурация, которую я использовал с Apache2 (HTTP, кстати, отлично работает с LETSENCRYPT):

# 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>

Примечание: Единственный раз, когда мы сталкивались с проблемой выдачи HTTP-контента даже при включённом force_https, это было связано с отсутствием файлов в директории /uploads, но это (разумеется) не связано с выбором между Apache2 и nginx в качестве обратного прокси.

Спасибо за ответы, но у меня Apache не работает на том же сервере, что и Discourse. Возможно, я не очень ясно выразился.
У меня уже есть сервер Apache с множеством веб-сайтов, и мне нужно настроить его как обратный прокси для Discourse, который находится на другом сервере, поэтому я не могу использовать сокеты.

Спасибо.

Я не пробовал этот метод, но вы можете рассмотреть возможность монтирования удаленной файловой системы и проверить, сможете ли вы таким образом получить доступ к unix-сокету, особенно если серверы находятся в одном дата-центре и проблемы с производительностью сети в широкой сети не возникают.

Без предоставления информации об архитектуре, операционной системе, конфигурации сети и т. д., трудно ответить, и, возможно, это даже выходит за рамки обсуждения здесь, на meta discourse.

Будьте креативны!

В последний раз, когда я пробовал использовать эту конфигурацию с Apache2, у меня возникали сбои подключения к шине сообщений Discourse. Это было уже больше года назад. Сайт, похоже, загружается нормально, но в консоли разработчика (F12) явно указывалось, что соединения WSS завершались по тайм-ауту после нескольких первых попыток.

Как видно из приведённого мной примера конфигурации, мы не использовали wss.

wss !== обратный прокси apache2 (это лишь один из способов, а мы wss не используем).

На самом деле мы используем только конфигурации обратного прокси nginx и apache2 с применением unix-сокетов, потому что:

  • Я ленив и предпочитаю простые конфигурации, которые легко отлаживать.
  • unix-сокеты просты и удобны для отладки.
  • В nginx мы можем переключаться между обратным прокси и любым контейнером с помощью символической ссылки.
  • apache2 (обратный прокси к контейнеру) не работает с символическими ссылками, поэтому требуется перезапуск веб-сервера.

Однако @Grunskin задал вопрос о настройке, которую мы ещё не реализовали: обратный прокси на одном хосте и запуск контейнера на другом.

Когда у меня появится время, я протестирую это для обоих nginx и apache2 в одном дата-центре и посмотрю, получится ли запустить это с монтированием удалённой файловой системы и использованием unix-сокета.

До тех пор…

Примечание: на мой взгляд, эта проблема не имеет отношения ни к nginx, ни к apache2, которые выступают лишь в роли обратных прокси (но, как уже упоминалось, конфигурация удалённого доступа ещё не тестировалась, поэтому я не могу комментировать это детальнее).

Почему это необходимо?

Discourse — это приложение, а не веб-сайт. После того как начальная нагрузка JavaScript доставлена в ваш браузер, многие функции зависят от быстрого соединения с сервером Discourse. Проксирование через другую систему добавит задержку и серьёзно ухудшит пользовательский опыт.

Можете ли вы объяснить причины вашей необходимости?

Есть множество причин для использования обратного прокси.
Например, если у меня есть только один публичный IP-адрес, но нужно сделать доступными несколько веб-серверов через порты 80/443, и я не могу запустить Discourse на этом конкретном сервере.
Используйте обратный прокси для SSL-оффлоадинга, чтобы конечный сервер не занимался шифрованием.
Поверхность атаки на конечном сервере уменьшается, если разместить его за обратным прокси.
Существует множество законных причин для этого, и я не могу понять, почему это не должно быть возможно.

Вскоре я вспомнил, что у меня уже есть другой сервер с Discourse, работающий за моим сервером Apache, и он стабильно функционирует уже как минимум два года. Я настроил новый Discourse таким же образом, но не могу заставить его перестать отдавать некоторые ресурсы по протоколу HTTP. Единственное различие между серверами Discourse заключается в том, что на старом запущена версия 2.4, а на новом — 2.5, поэтому я не знаю, есть ли здесь какие-либо отличия.

Как я уже писал в первом сообщении, параметр force_https ломает сайт, делая невозможным вход в систему, принятие приглашений и т. д. Похоже, что некоторые JavaScript-скрипты не работают, поскольку, вероятно, они отдаются по HTTP.
Разве не логичнее было бы заставить force_https переписывать все ссылки с http на https, а не отбрасывать их? По крайней мере, сделать это опциональным.

Какой является рекомендуемый способ публичной настройки Discourse? Развернуть сервер в DMZ с его собственным внешним IP-адресом?

Я рекомендую обратить внимание на Traefik.
Он отлично работает и автоматически управляет SSL-сертификатами.

Причин для использования обратного прокси-сервера множество. Я почти уверен, что инфраструктура Discourse.org работает за HAproxy.

Я использую Traefik и Caddy, а в прошлом настраивал nginx, HAproxy и даже Apache (для установки WordPress в подпапке).

Это и есть ваша проблема. Вам нужно включить force_https и выяснить, почему он вызывает сбои. Отключение его — не вариант. Вы запросили бесплатную поддержку, но те, кто откликнулся, не имеют решения для Apache, поэтому вам придётся самому возглавить «оркестр» Apache.

Да, мы полностью согласны.

Теперь мы используем конфигурацию «два контейнера с обратным прокси» на всех наших сайтах — в продакшене, при тестировании и на стадии подготовки, за исключением сервера, который мы используем для подготовки основного миграционного процесса.

Это связано с тем, что запускать различные скрипты миграции проще, когда PostgreSQL, MySQL и код приложения Discourse находятся в одном контейнере. Это не продакшен-окружение, и отладка в нём проще. Но как только мы убеждаемся, что всё работает, мы переносим резервную копию в продакшен и выполняем восстановление.

Одна из проблем такого подхода заключается в том, что даже самая продвинутая конфигурация с двумя контейнерами и обратным прокси не может компенсировать простои во время восстановления базы данных, поскольку база данных всего одна.

Возможно, в будущем мы попробуем конфигурацию с двумя контейнерами базы данных, чтобы иметь возможность восстанавливать одну из них и переключаться между ними — и тоже на уровне данных.

Или ещё лучше: мы будем восстанавливать данные в базу данных с другим именем и переключаться в реальном времени, но пока не знаем, как это сделать. Если вы знаете, где в коде можно изменить имя продакшен-базы данных с discourse на discourse2 без полной пересборки приложения, это было бы замечательно :slight_smile: Может быть, наш креативный и инновационный супер-консультант @pfaffman знает, как это сделать?

Обновление: Это уже есть в файле templates/postgres.template.yml :slight_smile:

(Я вижу здесь действительно интересные операции mv с папкой PostgreSQL :slight_smile: :). )

И автономная, и многоконтейнерная конфигурации имеют свои преимущества и недостатки; но для продакшена мы полностью перешли на конфигурацию «два контейнера с обратным прокси». Для тестирования миграций и подготовки лучше всего подходит автономный вариант.

Что касается Apache2, я бы хотел, но у меня пока нет времени настроить и протестировать это с обратным прокси на одном сервере и контейнерами на другом. Извините за это…

Также мне нужно найти способ поддерживать работу сайта в конфигурации с двумя контейнерами во время восстановления базы данных. Я вижу (теперь), что шаблон templates/postgres.template.yml ориентирован на конфигурацию с одним автономным контейнером.

Хочу добавить, что Discourse не использует WSS. Message Bus использует Long Polling с кодированием по частям (потоковая передача).

Вы должны установить это имя хоста на то, по которому будет осуществляться доступ к Discourse. Если доменное имя в app.yml не совпадает с тем, который пользователи вводят в браузере для доступа к вашему сайту, это не сработает.

У вас в app.yml нет шаблонов ssl или letsencrypt, верно?

Вам действительно нужно включить force_https.

Кроме того, вам придётся пройти через некоторые сложности, чтобы заставить работать долгие опросы (long polling). Я не помню, какие именно это сложности.

Привет, @Grunskin

Мы понимаем ваше разочарование. Однако, когда вы устанавливаете:

force_https = true

Это не является проблемой. Как отметил выше @pfaffman (по крайней мере дважды, один из ведущих экспертов по миграции на Meta):

Вы “должны” оставить force_https = true и затем найти “истинную проблему”.

Если бы я был на вашем месте, основываясь на том, что я прочитал:

Сначала я бы настроил свой обратный прокси на том же сервере, что и контейнер(ы) Discourse, чтобы упростить задачу — только для тестирования и устранения неполадок. Убедитесь, что этот простой тестовый случай работает идеально. Затем, когда всё заработает на одном хосте, отключите этот тестовый обратный прокси и переходите к вашей желаемой двухсерверной конфигурации.

Это интересная проблема. Вы сможете её решить, если оставите force_https = true и будете придерживаться структурированного, пошагового метода устранения неполадок. Подобные проблемы — это просто ИТ-головоломки, ждущие своего решения.

У вас всё получится.


PS: Если вы устанете от этой головоломки или потеряете интерес, вы всегда можете обратиться к @pfaffman или другому опытному участнику Meta и оплатить их услуги, чтобы преодолеть это препятствие и двигаться дальше к более светлым горизонтам.


Примечание: У нас не возникло никаких проблем с настройкой Apache2 в качестве обратного прокси с использованием Unix-доменного сокета на одном сервере. Искренне извиняюсь, что у меня нет личного времени, чтобы настроить двухсерверную конфигурацию и заставить метод обратного прокси Apache2 работать на двух разных серверах. Мы заняты финальными задачами миграции: исправлением “безумного злоупотребления bbcode” и проблемами с Markdown, и это занимает больше времени, чем мы изначально предполагали.

Я решил настроить Discourse на localhost в порту 8443 с использованием SSL, заблокировать порт 8443 с помощью UFW (фаервол) и проксировать порт 443 в 8443 через apache2. Сейчас пробую.

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

Я перенёс наш Discourse на новую конфигурацию, и теперь при входе возникает ошибка HTTP 403 на POST-запрос сессии. Я предполагаю, что это проблема 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, поэтому я добавил

set_real_ip_from 10.10.10.21/24;

в файл yml для discourse.conf в контейнере web_only. Я также пробовал с адресом по умолчанию 127.0.0.1/24, но в обоих случаях при входе возникает одна и та же ошибка 403.