翻译自: Running Your Flask Application Over HTTPS

当你在写Flask应用的时候,你通常会使用web开发服务器,它提供了基本的WSGI HTTP服务器。但是当你准备在生产环境上部署应用的时候,需要考虑一件事:是否需要使用加密链接。

人们总是在问我,如何基于HTTPS运行一个Flask服务器。在这篇文章中,我将介绍几种在Flask应用上添加加密的方式,从5秒钟就可以应用的方法,到一个非常健壮的解决方案,从而在SSL Server Test上可以得到A+的分数

How Does HTTPS Work?

HTTP的加密和安全功能是通过Transport Layer Security(TLS 传输层安全协议) Protocol实现的。TLS定义了一种网络传输安全的标准方式。因为我不是一个安全专家,因此我不认为我能很好的给你解释TLS协议的详细内容,因此我只会给你一些我们对于配置一个安全和加密的Flask服务器中感兴趣的部分。

当客户端创建一个到服务器的连接并且请求一个加密连接,服务器会使用其SSL(Secure Sockets Layer)证书进行响应。证书是作为服务器的身份证明,其包括了服务器名和域名。为了保证服务器提供的信息是正确的,证书是通过一个证书认证机构签名的。如果客户端知道并且信任(Certificate authority)CA的话,它就可以确认这个签名是从这个CA签发的,这样这个客户端就可以确定这个连接的服务器是合法的。

在客户端验证证书之后,它为与服务器的通信创建了一个加密密钥。为了确保这个密钥被安全的送到服务器,它使用一个服务器证书上的公钥来加密。服务器拥有与证书上公钥对应的私钥,所以服务器是唯一能够解密的。这样从服务器接受到加密密钥之后,所有服务器和客户端之间的通信都是使用这个密钥加密的。

从这个总结中,你大概可以知道实现一个TLS加密我们需要两个东西:服务器证书,包含一个公钥并且是CA签名的,还有一个与公钥对应的私钥。

The Simplest Way To Do It

Flask,更确切的说是Werkzeug,支持on-the-fly证书的使用,可以快速在HTTPS上服务应用并且不用处理证书。所有你需要做的是,在你的app.run()调用里添加ssl_context='adhoc'。不幸的是,这个选项并不能通过Flask命令行接口使用。下面你可以看到官方文档上的一个hello world的Flask应用示例,添加了TLS加密。

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run(ssl_context='adhoc')

为了能够在Flask上使用ad hoc(点对点)证书,你需要在你的虚拟环境中安装一个额外的依赖:pip install pyopenssl

当你运行脚本的时候,你会注意到Flask正在运行一个https://服务器

1
2
$ python hello.py
* Running on https://127.0.0.1:5000/ (Press CTRL+C to quit)

很简单,是吧?问题是浏览器不喜欢这种类型的证书,因此它会在你可以访问网站之前显示一个很大的警告标志。一旦你允许浏览器连接之后,你将会得到一个加密的连接,就像你从一个服务器得到了有效证书一样,这样点对点的证书在快速测试中很方便,但是不会用于真实应用中。

Self-Signed Certificates

self-signed证书是签名通过私钥来生成的并且关联的证书。我上面提到了客户端需要知道并且信任CA签发的证书,因为正是信任关系才允许客户端去确认服务器证书。web浏览器和其他HTTP客户端会提前配置好一个已知的信任CAs列表,不过明显如果你使用一个self-signed证书,浏览器就不知道这个CA,因此验证也会失败。这也是我们之前使用的ad hoc证书时候发生的事情。如果浏览器不能验证服务器证书,它就会让你在存在问题的情况下运行和访问网站,但是它会确保你理解你正在有风险的访问。

但是风险是什么?对于Flask服务器,你当然可以信任,并且是没有风险的。问题是用户如果连接了他们不知道的或者无法控制的服务器出现的。在那种情况,用户就不知道服务器是否是认证过的,因为任何人都可以为任何域名生成证书。

self-signed证书有时会比较有用,Flask的ad hoc证书就不是这样了,因为每次服务器运行的时候,通过pyOpenSSL就会生成一个不同的运行中证书。当你在使用一个self-signed证书的时候,在每次运行服务器的时候使用同样的证书是比较好的,因为你可以配置浏览器去信任它,这样就可以消除安全报警了。

你可以很容易的从命令行中生成self-signed证书。所有你需要的是安装openssl

openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

命令生成了一个新的证书cert.pem以及对应的私钥在key.pem,有一个365天的有效周期。当你运行这个命令的时候,你将会被询问几个问题。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Generating a 4096 bit RSA private key
......................++
.............++
writing new private key to 'key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Oregon
Locality Name (eg, city) []:Portland
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Miguel Grinberg Blog
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:localhost
Email Address []:

我们现在就可以在Flask应用中使用这个新的self-signed证书了,通过在app.run()中设置ssl_context参数,这个参数形式就是证书的文件名和私钥的文件名

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run(ssl_context=('cert.pem', 'key.pem'))

浏览器仍然会继续就此证书进行询问,如果你检查这个证书,你就会发现当你创建这个证书时候填写的信息。

Using Production Web Servers

众所周知Flask的开发服务器只适合开发和测试,因此我们如何将SSL证书安装在一个生产服务器呢?

如果你使用的gunicorn,你可以加上下面的命令行参数

gunicorn --certfile cert.pem --keyfile key.pem -b 0.0.0.0:8000 hello:app

如果你使用nginx作为反向代理,你可以在nginx中配置证书,然后nginx就会终止加密连接,也就是说它会接受外部的加密连接,然后使用正常的非加密连接和你的应用通信。这是一个非常有用的配置,使得你的应用不需要处理证书和加密。在nginx进行如下配置:

1
2
3
4
5
6
7
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# ...
}

另外比较重要的是你需要考虑用户通过普通的HTTP连接的时候如何处理。最好的方式,在我看来,是给一个未加密请求一个基于HTTPS的相同url的重定向响应。对于Flask应用,你可以通过Flask-SSLify扩展来实现它。在nginx,你可以在另一个server块配置:

1
2
3
4
5
6
7
server {
listen 80;
server_name example.com;
location / {
return 301 https://$host$request_uri;
}
}

如果用的是另外的web服务器,你可以参考文档找到类似的配置方法。

Using “Real” Certificates

我们已经探索了关于self-signed证书所有的选项,但是在那些例子中,存在的限制是除非你告诉浏览器去信任这些证书,否则浏览器不会这么做。因此在生产环境中的网站的服务器证书最好是从那些浏览器自动信任的CAs获取。

当你向一个CA请求证书的时候,CA会检查你是否有服务器和域名的控制权,但是至于检查怎么完成的是要取决于CA。如果服务器通过检查,CA会发行一个自己签名的网站证书然后给你去安装。证书通常的有效期是一年。大多数CA会收费,但是仍然有一些是免费的。最流行的免费CA是Let's Encrypt

Let's Encrypt获取证书是非常容易的,整个的处理都是自动的。假设你使用的是Ubuntu服务器,你可以通过在你的服务器上安装certbot工具来开始

1
2
3
4
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install certbot

现在你就可以请求证书了。有几种核实网站的方法。webroot方法是最容易实现的。在这种方法下,certbot在你的web服务器的暴露出来的静态文件夹里添加一些文件,然后尝试通过HTTP使用你想来生成证书的域名来获取这些文件。如果测试成功,certbot就会知道服务器和域名是相对应的,然后它就会发行证书。请求证书的命令是这样的

$ sudo certbot certonly --webroot -w /var/www/example -d example.com

这个例子中,我们试图为example.com域名生成证书,使用了/var/www/example作为静态文件根目录。如果certbot可以核实域名,它就会写入证书文件/etc/letsencrypt/live/example.com/fullchain.pem和私钥文件/etc/letsencrypt/live/example.com/privkey.pem,有效期为90天。

为了使用这些新获取的证书,你可以将在self-signed使用的文件替换掉,并且要使用上面提到的配置。当然你需要你的域名可以访问你的应用,这是唯一可以使浏览器接受并且作为有效证书的方法。

如果你使用nginx作为反向代理,你可以充分使用它的映射功能,你可以在配置里创建并且给certbot一个私有目录,可以让它来写入核验文件。在下面的例子里,我扩展了HTTP server块来发送所有Let's Encrypt相关的请求到你指定的目录

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name example.com;
location ~ /.well-known {
root /path/to/letsencrypt/verification/directory;
}
location / {
return 301 https://$host$request_uri;
}
}

certbot也可以用在你更新证书的时候,你只需要下面这个命令

$ sudo certbot renew

如果系统中有任何即将过期的证书,上面的命令就可以更新它们,会在同样的地址留下新的证书。你只需要重启服务器就可以使用它们。