Flask + Bokeh в аутентификации Docker

Мы используем Flask для маршрутизации пользователей на серверы Bokeh. Система работает внутри образа Docker. Все работает хорошо. Но теперь мы хотим добавить аутентификацию, что оказывается трудным, потому что мы не хотим сопоставлять порты сервера bokeh с клиентом.

Позвольте мне показать вам, как это работает в настоящее время (без аутентификации):

Flask app.py (маршрутизация):

...
@app.route('/folder/report_x')
def page_folder_report_x():
    ''' embedded bokeh server for report_x '''
    script = server_document('http://localhost:5001/report_x')
    resp = {
        'title': 'Report X',
        'script': script,
        'template': 'Flask', }
    return render_template('embed.html', **resp)
...
app.run(host='0.0.0.0', port=5000, use_reloader=False)

Flask embed.py (шаблон):

...
{% extends "base.html" %}
{% block content %}
  {{ script|safe }}
{% endblock %}
...

Сервер Bokeh запускается с помощью панели Python из командной строки (localhost: 5000 представляет собой сервер Flask):

panel serve report_x --port 5001 --allow-websocket-origin localhost:5000

Сервер Bokeh обслуживается с помощью файла main.ipynb:

import panel as pn
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import Button, DataTable, PreText
from bokeh.models.widgets import TableColumn, NumberFormatter, DateFormatter
...
gspec = pn.GridSpec(sizing_mode='stretch_both')
gspec[0:12, 0:12] = pn.WidgetBox(widgets)
...
gspec.servable()

Наш образ Docker показывает порты сервера flask и сервера (ов) боке:

...
RUN pip install -r /app/requirements.txt
EXPOSE 5000:5000
EXPOSE 5001:5001
...

Наконец, когда мы запускаем контейнер докеров, мы сопоставляем порты:

# success!
docker run -p 5000:5000 -p 5001:5001 report_server:0.1

Если мы запустим образ докера таким образом, все будет работать отлично.

Но если мы запустим его без сопоставления сервера боке, мы не сможем связаться с сервером боке (даже если он доступен внутри, как вы можете видеть в DockerFile):

# fail
docker run -p 5000:5000 report_server:0.1

В целях безопасности мы хотим сопоставить только один порт с внешним миром. Есть ли что-то, что нам не хватает о том, как встроить серверы Bokeh в Flask, что позволило бы только Flask взаимодействовать с сервером Bokeh?


person Legit Stack    schedule 08.07.2019    source источник
comment
Если у вас есть две службы в одном контейнере, что многие люди уже сочли бы плохой идеей, и вы хотите получить доступ к обоим, вам понадобится два открытых порта или что-то вроде обратного прокси.   -  person Klaus D.    schedule 09.07.2019


Ответы (1)


Есть ли что-то, что нам не хватает о том, как встроить серверы Bokeh в Flask, что позволило бы только Flask взаимодействовать с сервером Bokeh?

Клиент (браузер) должен иметь возможность взаимодействовать с сервером Bokeh, точка. Все функции сервера Bokeh выполняются через прямое соединение через веб-соединение между сервером Bokeh и браузером. Итак, краткий ответ на ваш вопрос - «вы не можете».

Однако вы можете настроить сервер Bokeh для:

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

Для этого вам необходимо сначала создать секрет для подписи идентификаторов сеансов с помощью команды bokeh secret, например

export BOKEH_SECRET_KEY=`bokeh secret` 

Затем также установите BOKEH_SIGN_SESSIONS и установите разрешенное происхождение веб-сокета:

BOKEH_SIGN_SESSIONS=yes bokeh serve --allow-websocket-origin=<app origin> app.py

Затем в своем приложении фляги вы явно указываете (подписанные) идентификаторы сеанса:

from bokeh.util.session_id import generate_session_id

script = server_session(url='http://localhost:5006/bkapp', 
                        session_id=generate_session_id())
return render_template("embed.html", script=script, template="Flask")

Обратите внимание, что переменная среды BOKEH_SECRET_KEY должна быть установлена ​​(и идентична) как для сервера Bokeh, так и для процессов Flask.

Теперь, если кто-то подключится к серверу Bokeh напрямую, он вернет ошибку 403, если URL-адрес подключения не содержит подписанный идентификатор сеанса, подписанный тем же секретом, с которым был запущен сервер Bokeh. Предположительно, только ваше приложение Flask знает этот секрет, поэтому только оно может успешно инициировать новые сеансы.

Этого достаточно, чтобы полностью обезопасить вещи? Технически любой, кто может получить доступ к строке подключения, отправленной в браузер (например, пользователь, просматривающий приложение, или изощренный злоумышленник MitM, особенно если вы не завершаете HTTPS перед приложением), может извлечь подписанный идентификатор сеанса. Но пока вы устанавливаете разрешенное происхождение веб-сокета, эту информацию нельзя использовать для инициирования нового соединения из любого места за пределами вашего приложения. Если бы кто-то попытался, сервер вернул бы 403:

ОШИБКА: bokeh.server.views.ws: отказ от подключения к веб-сокету от Origin 'http://localhost:5006'; используйте --allow-websocket-origin = localhost: 5006 или установите BOKEH_ALLOW_WS_ORIGIN = localhost: 5006, чтобы разрешить это; в настоящее время мы разрешаем происхождение {'localhost: 8000'}

Я не думаю, что вы можете подделать заголовок Origin из реального браузера, хотя, возможно, кто-то сможет создать модифицированный Chrome из исходного кода (это непросто, но не невозможно), чтобы подделать его. Если вам нужно принять меры против этого, лучше продолжить обсуждение на проекте Bokeh Project Discourse, поскольку он в некоторой степени открыт и может указывать на разработку новых функций (например, возможность указывать ограничение на количество подключений для сеансов или что идентификаторы сеансов никогда не будут использоваться повторно).

Для справки, здесь есть полный пример, который также встраивает сервер Bokeh непосредственно в процесс Flask (если вам нужно масштабировать или ожидать одновременного использования нескольких пользователей, это было бы слишком наивным развертыванием):

https://gist.github.com/bryevdv/481fc64c59620acb74c64bff0f4d47d0

В качестве последнего комментария вы, вероятно, также можете (дополнительно) поместить URL-адрес сервера bokeh за каким-либо аутентифицирующим прокси-сервером, чтобы предотвратить обновление WS в первую очередь без аутентификации. Хотя я не совсем уверен, как бы это выглядело навскидку. Это также было бы лучше обсудить в Дискурсе.

person bigreddot    schedule 09.07.2019
comment
спасибо, это большая помощь. Я сделал именно то, что вы сказали, но получаю, что идентификатор сеанса имеет недопустимую подпись: «0DT ... может быть, потому что я работаю в Windows? Я использовал SETX вместо export, который устанавливает переменную среды пользователя в Windows, но я также пробовал это как общесистемные переменные среды. А пока я попробую все это в образе докера, работающем под Linux ... - person Legit Stack; 09.07.2019
comment
Теперь это работает. Не уверен, почему он не работал с окнами. вероятно, имел какое-то отношение к переменным env. - person Legit Stack; 09.07.2019