Docker 身份验证中的 Flask + Bokeh

Flask + Bokeh in Docker authentication

我们正在使用 Flask 将用户路由到 Bokeh 服务器。系统在 Docker 图像中 运行ning。一切正常。但是现在我们想添加身份验证,这被证明很困难,因为我们不想将散景服务器端口映射到客户端。

让我告诉你它目前是如何工作的(没有身份验证):

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)

烧瓶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 服务器和 bokeh 服务器的端口:

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

最后,当我们 运行 docker 容器时,我们映射端口:

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

如果我们运行 docker 图像以这种方式一切正常。

但是如果我们 运行 它没有映射 bokeh 服务器,我们就无法访问 bokeh 服务器(即使它是内部公开的,正如您在 Docker 文件中看到的那样):

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

为了安全起见,我们只想映射一个端口到外界。关于如何在 Flask 中嵌入 Bokeh 服务器,只允许 Flask 与 Bokeh 服务器通信,我们是否遗漏了什么?

Is there something we're missing about how to embed Bokeh servers in Flask that would allow only Flask to talk to the Bokeh server?

客户端(浏览器)必须能够与 Bokeh 服务器对话,句号。 Bokeh 服务器的所有功能都通过 Bokeh 服务器和浏览器之间的直接 websocket 连接进行。所以你的问题的简短答案是 "you can't"。

但是,您可以将 Bokeh 服务器配置为:

  • 不会在每个连接上自动创建新的 session
  • 只尊重 session 具有密码签名的 session id
  • 只接受来自白名单来源的连接

为此,您需要首先使用 bokeh secret 命令创建一个用于签署 session id 的密钥,例如

export BOKEH_SECRET_KEY=`bokeh secret` 

然后还要设置BOKEH_SIGN_SESSIONS并设置允许的websocket来源:

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

然后在你的烧瓶应用程序中,你明确提供(签名)session ids:

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 服务器和 Flask 进程设置(并且相同)环境变量 BOKEH_SECRET_KEY

现在,如果任何人直接连接到 Bokeh 服务器,他们将返回 403 错误,除非连接 URL 包含签名的 session id,使用与 Bokeh 服务器相同的秘密签名开始于。大概只有你的 Flask 应用程序知道这个秘密,所以只有它才能成功启动新的 sessions。

这足以完全保护事物吗?从技术上讲,任何可以访问发送到浏览器的连接字符串的人(例如查看应用程序的用户,或老练的 MitM 攻击者,特别是如果您不在应用程序前面终止 HTTPS 的话)都可以提取已签名的 session id .但是只要您设置了允许的 websocket 来源,那么此信息就不能用于从您的应用程序之外的任何地方启动新连接。如果有人尝试,服务器将 return 一个 403:

ERROR:bokeh.server.views.ws:Refusing websocket connection from Origin 'http://localhost:5006'; use --allow-websocket-origin=localhost:5006 or set BOKEH_ALLOW_WS_ORIGIN=localhost:5006 to permit this; currently we allow origins {'localhost:8000'}

我不认为你可以从一个真正的浏览器中伪造一个 Origin header,虽然也许有人可以从源代码构建一个修改过的 Chrome(这并不容易,但并非不可能) 欺骗一个。如果您需要防范这种情况,Bokeh Project Discourse 可能是继续讨论的更好的地方,因为它有点 open-ended,并且可能指向新功能开发(例如指定连接限制的能力sessions,或者说 session 永远不会 re-usable)。

作为参考,这里有一个完整的示例,它还将 Bokeh 服务器直接嵌入到 Flask 进程中(如果您需要横向扩展或期望同时有多个用户,那么这种部署就太天真了):

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

作为最后一条评论,您可能还(另外)将散景服务器 URL 置于某种身份验证代理之后,以防止 WS 升级首先发生,而无需身份验证。虽然我不确定那会是什么样子。这也将是 better-discussed 在 Discourse