什么是Erlang分布式?
“分布式协议”的意思是由多个Erlang节点组成的一个集群。当Erlang的节点被组成集群后,任何的进程都可以发送信息去任意节点去执行,并且可以在任意的节点上产生新的进程。 这样就形成了分布式应用的基础,比如Mnesia,数据库的实现分别由Erlang/OTP和消息代理RabbitMQ来进行。
Erlang分布式协议被设计为假定运行在一个可靠的网络。当节点互相联系是需要证明彼此是持有共享密钥的, 称为 "cookie",这样做的主要目的是保证不同的Erlang集群处于同一网络中是不会意外的合并;也不建议依靠cookie机制来防止攻击者。
此外,在一个集群中所有的Erlang节点完全信任彼此。集群中任何节点可以任意的其他节点运行代码,包括运行由os:cmd产生的任意命令。这就是为什么文章the Distribunomicon chapter of Learn You Some Erlang描述的Erlang安全模型时使用了这句话 * this space intentionally left blank *。
在这篇文章内,我将叙述如何通过TLS来进行Erlang分布式协议,以及能够或不能够解决的问题。
为什么是TLS?它能解决什么问题?
比如说在你的数据中心内你已经有了一个Erlang集群,并且你打算升级一下它以支持TLS。(你或许会想使用了TLS意味着你可以在公开的在网络上运行你的Erlang集群,那样的话我想说你是太勇敢了,我很想听听你是经历!)在最简单的可能的配置中,节点之间的通信是加密的,但是节点间并不去验证证书真实性。
这句话是什么意思?它表示当给定两个Erlang节点分别叫做Alice和Bob,如果Eve (一个窃听者)已经加入到你的网络,并且能够监听网络流量,它并不能看到你的两个Erlang节点间发送的内容,也就是你想保护的敏感信息。 这是一个纵深防御的例子:即使攻击者入侵了你的防火墙,在想得到他们想要的东西之前,仍然有很多的障碍。然而,如果另一攻击者Mallory加入到你的网络,他可以通过提供不同的证书来代理不同的节点间的通信,这样就能够执行中间人(MITM)攻击。
验证TLS证书的通常方法是检查它们是否由同一个信任的证书机构签发。这样能够保证Mallory不能够拦截双方的连接,除非他也拥有同一个CA的私钥。然而它依然引出了一个重要的问题:哪一个CA是你信任的? 当任何一个Erlang节点带有一个信任的CA颁发的证书并能访问你的节点的时候,你应该会想要信任尽可能少的CA,或是自己建立单独的CA来签发证书给你集群中的节点,并且只信任自建的那一个。
另一个减少风险的方法是创建信任证书的白名单。此功能并不是开箱即用的,但是你可以通过实现自己的验证功能来做到这一点。
如何使用基于TLS的分布式?
具体内容已经在官方文档中有描述(http://erlang.org/doc/apps/ssl/ssl_distribution.html),因此,这里我将会只给出一些提示和例子以便能够帮助你开始。
首先,由于这里需要输入很多较长的命令行参数到erl,我建议写一个恰当的shell脚本来启动erl,这样你就不需要每次都反复的在终端内鼓捣参数了。
文档写道你需要在你的启动脚本中包含SSL应用或是明确的在代码路径中包含SSL ebin的目录。 最终你可能会想要按照前一种方法来做,使用喜欢的工具直接生成,但是当你尝试后你会选择后一种方法。下面就是保存为正确目录的脚本参数的一个片断:
SSL_DIR=$(erl -noinput -eval 'io:format("~s~n",
[filename:dirname(code:which(inet_tls_dist))])' -s init stop
这涉及到额外调用Erlang虚拟机,会让启动变的有点慢,但从另一方面来说,你也不再需要担心手动的去查找对应的目录了。
在shell脚本的最后, 使用需要的参数来启动erl:
erl -pa $SSL_DIR -proto_dist inet_tls -ssl_dist_opt $SSL_DIST_OPT "$@"
至于还需要什么样的SSL_DIST_OPT变量呢?这取决于你想要使用什么样的验证。
只加密,不验证
在服务器端你至少需要一个证书和一个私有密钥。“客户端”并不需要提供证书。
注意“服务端”和“客户端” 在Erlang分布式中是外来的专有名词。在Erlang中,当两个节点连接后,它并不关心是哪个节点初始化的连接,但是当使用TLS连接时,我们设置不同的选项给两“边”。
证书和私有密钥会以PEM的格式存储。它们也可以串联成一个文件,在这种情况下你只需要server_certfile选项,或者存储为两个独立的文件,这时你也需要设置server_keyfile:
SSL_DIST_OPT="server_certfile erl-dist.pem server_keyfile erl-dist.key"
在这一步我们并不会去验证证书,因此自签名的证书是有效的。
由CA列表来验证服务器证书
如果你有一组可信任的证书机构,并要求“客户端”验证“服务端”的证书是否由他们签署,可通过选项client_cacertfile实现。 同时需要设置client_verify和verify_peer选项来让客户端执行验证:
SSL_DIST_OPT="server_certfile erl-dist.pem server_keyfile erl-dist.key \
client_cacertfile ca.pem client_verify verify_peer"
记住客户端也不会提供证书,所以把CA列表放到服务端是没有意义的。
通过CA列表校验客户端证书
不管怎么说,只验证服务端证书,而不验证客户端证书,是有点犯傻的。这是因为,原则上,一个随机节点最终都会链接到另一个节点上,并且两者之间可以互访,而不需要考虑方向(互为C/S)。因此,我们需要为每个节点配置相应的证书、密钥、CA列表等参数,同时通过server_fail_if_no_peer_cert选项,让服务端要求客户端提供证书。
SSL_DIST_OPT="server_certfile erl-dist.pem client_certfile erl-dist.pem \
server_keyfile erl-dist.key client_keyfile erl-dist.key \
server_cacertfile ca.pem client_cacertfile ca.pem \
server_verify verify_peer client_verify verify_peer \
server_fail_if_no_peer_cert true"
使用定制的校验函数(版本19.0)
为了在校验证书时获得更多的灵活性,例如:需要定制日志信息,或者需要实现一个证书白名单,我们需要实现一个定制的校验函数。在Erlang/OTP的19.0版本中,已经支持此功能。我们可以通过“客户端”和“服务端”的verify_fun选项来定义。此选项的使用,在官方的SSL模块文档中有类似的描述(http://erlang.org/doc/man/ssl.html):
SSL_DIST_OPT="server_certfile erl-dist.pem client_certfile erl-dist.pem \
server_keyfile erl-dist.key client_keyfile erl-dist.key \
server_cacertfile ca.pem client_cacertfile ca.pem \
server_verify verify_peer client_verify verify_peer \
server_verify_fun {my_module,my_function,my_state} \
client_verify_fun {my_module,my_function,my_state} \
server_fail_if_no_peer_cert true"
注意,当一个Erlang程序创建了一个TLS链接时,verify_fun选项是一个有两个元素的元组:一个函数,一个初始化状态术语。然而,从命令行中解析出来的Erlang术语,是无法创建函数对象的。因此,我们传递模块名和函数名用于原子替换,在这种情况下,functionmy_module:my_function/3将被调用。
这个校验回调函数将在多个验证环节中被调用:验证过程遇到错误时(所有错误)、验证未知的证书扩展、验证证书链中每个有效证书。针对每种情况,都要由该函数决定验证是成功或失败。函数还可以用于更新状态数据--元组的第三个元素是初始化状态。一个校验回调函数的实现,类似于下面的代码:
my_function(Cert, valid, State) -
|