Elixirのノード間通信をTLSで行う
Erlang/Elixirにおけるノード間通信は、特に何も指定しないと平文による通信になる。TLS通信もできるはずなのだが、どうやったらできるかを調べて、実際にやってみる。
やってみたいのは以下2つ。
- 同一ホスト内にノードをふたつ立ち上げて、TLS通信する
- Nerves内のノードと別のホストのノードとで、TLS通信する
参考資料
Erlang/ElixirでTLSクライアント認証する
HTTPでのTLSの話だが、Elixirにおけるそのあたりの事情についてよくまとめられている。
Erlang -- Using TLS for Erlang Distribution
Erlangのドキュメント。これをElixir側からやってあげればいけるはず。
Some CLI commands break for application release with TLS encrypted distributed erlang node configuration · Issue #640 · bitwalker/distillery
Distilleryのリリースでパラメタを渡す方法。ワークアラウンドが必要(上記のイシューが解決するまでは)。NervesでTLSでのノード間通信をしたい場合は、上記が必要か?
AWS ECS上にErlang(Elixir) Clusterを組むためのツールを作った - PartyIX
Erlangノードを楽にクラスタ化するためのツール。Erlangのsys.config
の話はErlang -- configにある。
Erlang ping node problem - Stack Overflow
nodeがつながらないときに。
ConnorRigby/elixir-distribution-ssl-example: Example of how to enable TLS distribution in an Elixir Releaseという名前の、それそのもののリポジトリがあった。説明の通り動かしてみたら、たしかに動く。
同一ホスト内にノードをふたつ立ち上げて、TLS通信する
というのについてはこれで実現できた。
しかし、↑のようなことをどうやってNerves上のノードからできるのかがわからない、つまり
Nerves内のノードと別のホストのノードとで、TLS通信する
を達成できないので、単刀直入にたずねてみた(Nervesプロジェクトのひとみたいだったので)。
TLS distribution using Nerves devices · Issue #1 · ConnorRigby/elixir-distribution-ssl-example
できた!
まずは、できてなかった理由から。
rel/vm.args.eex
の最後に以下のように書いた。
-proto_dist inet_tls -ssl_dist_optfile /etc/ssl.conf
(ssl.conf
の内容は省略)
これでいけるはずなのに、ホスト側のIExからつながらない。
で、mix firmware; mix upload
をしてssh nerves.local
して確かめてみたら、
> :init.get_plain_arguments()
['--no-halt', '--', '--dot-iex', '/etc/iex.exs', '-proto_dist', 'inet_tls',
'-ssl_dist_optfile', '/etc/ssl.conf', '--']
となっていて、オプションがVMをブートするプログラムにちゃんと渡されてなかった!
ref/vm.args.eex
をよくみてみると、
## Options added after -extra are interpreted as plain arguments and can be
## retrieved using :init.get_plain_arguments(). Options before the "--" are
## interpreted by Elixir and anything afterwards is left around for other IEx
## and user applications.
とある。ファイルの最後に書いちゃうと、--
以降の引数になって、VMの引数にならないのであった。
できた手順を書く。
まず、TLSを有効にしていない状態(生成されたファームウェアをデバイスにデプロイしただけの状態)。
iex(1)> :init.get_arguments()
[
root: ['/srv/erlang'],
progname: ['erlexec'],
home: ['/root'],
config: ['/srv/erlang/releases/0.1.0/sys.config'],
boot: ['/srv/erlang/releases/0.1.0/shoehorn'],
setcookie: ['nerves_tls_distribution_example_cookie'],
mode: ['embedded'],
kernel: ['shell_history', 'enabled'],
heart: [],
noshell: [],
user: ['Elixir.IEx.CLI'],
elixir: ['ansi_enabled', 'true'],
boot_var: ['RELEASE_LIB', '/srv/erlang/lib']
]
この状態だとノード間通信はプレーンなTCP通信(inet_tcp_dist
)で行われる。これをTLS通信(inet_tls_dist
)で行えるようにする。
rel/vm.args.eex
の、
## Options added after -extra are interpreted as plain arguments and can be
という行の前に、以下を追加する(他に証明書とか秘密鍵とか↓のファイルとかを追加するのだが、詳細はこのコミットを参照のこと)。
-proto_dist inet_tls -ssl_dist_optfile /etc/ssl.conf
この状態でアップロードしたら
iex(2)> :init.get_arguments()
[
root: ['/srv/erlang'],
progname: ['erlexec'],
home: ['/root'],
config: ['/srv/erlang/releases/0.1.0/sys.config'],
boot: ['/srv/erlang/releases/0.1.0/shoehorn'],
setcookie: ['nerves_tls_distribution_example_cookie'],
mode: ['embedded'],
kernel: ['shell_history', 'enabled'],
heart: [],
noshell: [],
user: ['Elixir.IEx.CLI'],
elixir: ['ansi_enabled', 'true'],
proto_dist: ['inet_tls'],
ssl_dist_optfile: ['/etc/ssl.conf'],
boot_var: ['RELEASE_LIB', '/srv/erlang/lib']
ssl_dist_optfile: ['/etc/ssl.conf'],
という行が現れた。
Nervesデバイス側でノードを起動する。
iex(9)> System.cmd("epmd", ["-daemon"])
{"", 0}
iex(10)> Node.start(:"device@nerves.local")
{:ok, #PID<0.1254.0>}
ホスト側で、以下のようにしてノードを起動する。
$ env RELEASE_COOKIE="nerves_tls_distribution_example_cookie" RELEASE_NODE=host@localhost RELEASE_DISTRIBUTION=name _build/rpi3_dev/rel/example/bin/example start_iex
で、IExからデバイス側につないでみると、うまくいった〜。
iex(host@localhost)1> Node.connect(:"device@nerves.local")
true
まとめたものをリポジトリにあげておいた。
TODO
- Mutual TLS(サーバ認証 + クライアント認証)したい!