Erlang 通过 TLS 的分布式

Posted OSC开源社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Erlang 通过 TLS 的分布式相关的知识,希望对你有一定的参考价值。

什么是Erlang分布式?

“分布式协议”的意思是由多个Erlang节点组成的一个集群。当Erlang的节点被组成集群后,任何的进程都可以发送信息去任意节点去执行,并且可以在任意的节点上产生新的进程。 这样就形成了分布式应用的基础,比如Mnesia,数据库的实现分别由Erlang/OTP和消息代理RabbitMQ来进行。

Erlang分布式协议被设计为假定运行在一个可靠的网络。当节点互相联系是需要证明彼此是持有共享密钥的, 称为 "cookie",这样做的主要目的是保证不同的Erlang集群处于同一网络中是不会意外的合并;也不建议依靠cookie机制来防止攻击者。

此外,在一个集群中所有的Erlang节点完全信任彼此。集群中任何节点可以任意的其他节点运行代码,包括运行由os:cmd产生的任意命令。这就是为什么文章 描述的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的分布式?

具体内容已经在官方文档中有描述(),因此,这里我将会只给出一些提示和例子以便能够帮助你开始。

首先,由于这里需要输入很多较长的命令行参数到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模块文档中有类似的描述():

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) ->
  ...
my_function(Cert, valid_peer, State) ->  ...
my_function(Cert, {bad_cert, Reason}, State) ->  ...
my_function(Cert, {extension, Extension}, State) ->  ...

 查看官方文档,可以了解更多的细节。 

检查证书是否被吊销(版本19.0

在理想环境中,一个特定证书正如它所声称的那样:能够被证明是拥有唯一实体的。然而,在真实环境中,私钥管理常常出现被错放、被偷、不恰当的分发等问题。在这种情况下,证书可能在到期前,就被吊销了。

当CA吊销一个证书时,它会将证书序列号加入证书吊销列表(CRL)中。但一个实体需要进行证书有效性校验时,可以从下载CRL并检查证书是否在其中。

如果我们想要在Erlang集群中使用证书有效性检查,我们需要指定从哪里获取CRLs。如果我们从一个“适当”的CA获取证书,证书中可能会有一个“分布点”扩展,其中包含一个可以下载CRL的URL信息。实际上,Erlang的ssl应用可以为我们下载它。否则,我们只能通过其它方法获取CRL,并手动地把它传递给ssl应用。

我们也可以选择如何进行广泛的检查,这是由crl_check设置的。默认设置是false,不进行检查。如果设置为true,在证书链中所有的证书,都将执行依赖于CRLs的检查。如果所有CRL都不存在,将证书当作“已吊销”进行处理。我们还可以为对端设置crl_check,仅用于检查对端的证书(不是CA颁发的),或者设置best_effort,当找不到与证书关联的CRL时,认定证书是有效的。

在Erlang/OTP 19.0环境中启动时,我们可以添加与下面类似的代码到启动脚本中:

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_crl_check  true         client_crl_check  true         \              server_crl_cache  {ssl_crl_cache,{internal,[{http,5000}]}}  \              client_crl_cache  {ssl_crl_cache,{internal,[{http,5000}]}}  \              server_fail_if_no_peer_cert true"

指定ssl_crl_cache模块通过HTTP获取CRLs,超时时间为5秒(设置为5000毫秒)。

我应该使用哪个 Erlang/OTP 版本?

虽然基于TLS的分布式已经存在很长时间了,我建议使用18.3以后的版本,在此版本中修复了一些很重要的问题:

  • 所有的socket默认使用nodelay(无延迟)选项。对于Linux,默认情况下,socket使用Nagle算法来减少开销,包头将被延迟发送,直到整个数据包准备好。或者直到超时,超时时间为40毫秒。不幸的是,这种交互方式对Erlang分布式来说是非常糟糕的:每次交互都存在40毫秒的延迟。

  • 有多个选项用于分配侦听端口,特别是inet_dist_listen_min和inet_dist_listen_max用于指定端口范围,它支持TLS分布以及非加密分布。(更多选项说明,参见官方文档 .)

  • 在早期版本中,存在一个竞争条件:如果一个节点已启动,而另一个节点在完全错误的时间点上试图去连接它。这第一个节点将会删除连接,并停止侦听后续的链接请求。这个问题在18.3版本中被修复。

  • TLS分布式支持 IPv6协议。通过在命令行中设置 -proto_dist inet6_tls来替代默认的IPv4协议。

如上所述,定制化的校验功能以及CRL检查,是从19.0版本开始支持的。

epmd是什么?

在你为将Erlang分布式部署到互联网上,而去开放你的防火墙之前,你应该先了解一下最后一块拼图:epmd,Erlang端口隐身守护进程。

因为可以在一个主机上运行多个Erlang节点,节点就不能使用相同的端口用于侦听。因此,你需要运行一个小程序来告诉你,你的Erlang节点正在使用哪个端口用于侦听。输入epmd -names命令,就可以了解相关信息:

$ epmd -names
epmd: up and running on port 4369 with data:
name foo at port 53668

一个Erlang节点要连接到“foo”节点上(无论它是在同一台主机还是在另一台主机上),首先要通过4369端口连接到epmd上,并获取“foo”用于侦听的端口号。获得的结果是53668。然后,它会通过该端口与“foo”建立连接,并完成分布式协议握手。

然而,甚至是你使用了 TLS 分布式协议,epmd 也是未加密的连接,因此还是有窃听担忧的,并且 MITM 对上面提到的依然适用。Michael Santos 在博客上刊登了  ,尽管有些过时,它还是描述了各种各样的有趣的事情,如果网络是可以访问的,就可以 epmd。

Erlang/OTP 19.0 中有一个新功能。有两个新的命令行参数被介绍:-start_epmd,你可以从 Erlang 节点启动就自动关闭 epmd,和 -epmd_module,你可以自定义一个“端口映射”模块来替换标准的那个来查询 epmd 。如果你总是监听一个端口,你可以锁定你的节点,您可以编写一个自定义模块,只返回端口,不查询任何东西,从而完全回避这个问题。

结 论

Erlang开发者都知道,分布式系统是非常难的,即使是使用 Erlang !Erlang 仅仅给了你一个工具去管理,复杂性的取舍在于你接受权衡。这同样适用于安全:这是很难的!在 TLS 内包装 Erlang 分布式协议,可能不会满足你对应用程序安全上的要求。


 了解详情请点击阅读原文


开源中国|ID:oschina2013


每天为你送上精选资讯早点

还有每天的 OSChina 乱弹哦


以上是关于Erlang 通过 TLS 的分布式的主要内容,如果未能解决你的问题,请参考以下文章

获取 badarith,[erlang,'+',[error,0],[],同时使用 Erlang 片段在 TSUNG 中执行算术运算

Erlang分布了反向字符串

消息队列集群配置

通过 REST 访问 Erlang 业务层

EMQ --集成搭建

使用 Elixir 和 Erlang ssl 模块获取 SSL 证书