Long 运行 Asyncio 服务器,重新加载证书

Long running Asyncio server, reload certificate

我有一个使用 Python 3.5 的 asyncio 库实现的自定义 TCP 服务器。我还使用 Lets Encrypt 证书对与服务器的通信进行 SSL 加密。 Lets Encrypt 只颁发有效期为 90 天的证书,我的服务器很可能不会在比这更长的时间内重新启动。

我正在生成这样的 SSLContext:

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_context.load_cert_chain(certfile='path/to/cert', keyfile='path/to/key')

服务器是这样的:

server = asyncio.streams.start_server(self._accept_client, ip, port, loop=self.loop, ssl=ssl_context)

当证书过期时,现有连接将继续运行,但新连接将(正确地)失败。显然 SSLContext 或服务器本身将证书和密钥文件保存在内存中,因为更新磁盘上的文件并不能解决问题。

我已经阅读了很多 Python 文档,但没有找到任何解决方案。我的一个想法是尝试定期调用 ssl_context.load_cert_chain(),希望这会更新证书和密钥。我无法查看 load_cert_chain 函数的源代码来确定它的行为,因为它显然是用 C 语言编写的,并且文档没有指定在将上下文传递到服务器后调用此函数的行为.

有没有办法在运行时更新 SSLContext 中加载的证书和密钥文件,而无需完全停止并重新启动服务器?

据我所知,asyncio.Server 中没有 API 在服务器上线时更新 SSL 证书,您有两个解决方案:

您可以在 SSL 握手期间使用 SNI 回调来更新证书,但它只适用于支持 SNI 的客户端。 这意味着对于支持此功能的每个客户端,将调用您选择的回调,并且可能 return 将用于与客户端建立连接的 SSLContext 对象。您可以进行此回调,以便它在每次调用时读取 certificate/key 文件,或者在您选择的触发器需要时重新加载。

要设置回调,请参阅:https://docs.python.org/3/library/ssl.html#ssl.SSLContext.set_servername_callback

另一个解决方案更复杂,要正确处理 asyncio 的当前状态 API:如果 asyncio 的内部实现发生变化,它可能会中断,我没有检查它如何与前导循环。但是,它适用于任何类型的客户端。

您可以使用新上下文创建新服务器,但使用第一个服务器的套接字:

# This will create a new server with the current socket, and setup what's required to use the new callback.
new_server = await asyncio.create_server(callback, None, None,
                   sock=old_server.sockets[0], ssl=new_context)

# Ensure that when cleaning the old_server, it doesn't try to close the socket we kept
old_server.sockets = []
old_server.close()
await old_server.wait_closed()

在 create_server() 期间的某一时刻,asyncio 将开始使用新的 ssl 上下文响应新连接。请注意,一旦连接启动并传递给您的回调 (_accept_client),它实际上独立于调用它的服务器,因此它不会影响任何现有连接。

我相当确定,保留您使用的上下文的副本并调用重新加载证书链的方法可以正常工作。至少在 C openssl API 级别,这是一件合理的事情,而且看起来 python 没有做任何事情来打破它。