Search code examples
securityelixiriptables

Elixir distributed security and whitelisting allowed nodes


TL;DR

  • How to whitelist nodes that can Node.connect to Elixir?
  • Any other security recommendations?

Setup

I've begun tinkering with distributing Elixir across (for now) two different servers.

For example, let's say the server's two IP addresses are:

  1. 198.51.100.0
  2. 203.0.113.0

First, I added new rules to the iptables firewall on both servers, opening up port 4369 (EPMD) and a range of 10 ports between 9000-9010 for nodes. I'm also only allowing incoming connections from the other server's exact IP address.

Example config for 198.51.100.0:

-A INPUT -p tcp -m state --state NEW --dport 4369 -s 203.0.113.0 -j ACCEPT
-A INPUT -p tcp -m state --state NEW --dport 9000:9010 -s 203.0.113.0 -j ACCEPT

Example config for 203.0.113.0:

-A INPUT -p tcp -m state --state NEW --dport 4369 -s 198.51.100.0 -j ACCEPT
-A INPUT -p tcp -m state --state NEW --dport 9000:9010 -s 198.51.100.0 -j ACCEPT

Now I can open up iex shells on each machine:

198.51.100.0:

$ iex --name one@198.51.100.0 --cookie secret --erl '-kernel inet_dist_listen_min 9000' --erl '-kernel inedist_listen_max 9010'

203.0.113.0:

$ iex --name two@203.0.113.0 --cookie secret --erl '-kernel inet_dist_listen_min 9000' --erl '-kernel inedist_listen_max 9010'

I can successfully connect to node two from node one:

iex(one@198.51.100.0)> Node.connect(:'two@203.0.113.0')
true

And list nodes from node two:

iex(two@203.0.113.0)> Node.list
[:"one@198.51.100.0"]

My Question:

I've read that :net_kernel.allow/1 can be used to whitelist an exact list of allowed connections. But I can't seem to get it working:

iex(one@198.51.100.0)> :net_kernel.allow([])
:ok
iex(one@198.51.100.0)> Node.connect(:'two@203.0.113.0')
true

I would expect that since I've allowed a list of none, no connection would be allowed. Any tips?

Update:

I discovered that if I pass at least one value to :net_kernel.allow, it seems to work:

iex(one@198.51.100.0)> :net_kernel.allow([:'127.0.0.0'])
:ok
iex(one@198.51.100.0)> Node.connect(:'two@203.0.113.0')
false
23:38:27.702 [error] ** Connection attempt with disallowed node :"two@203.0.113.0" **
iex(one@198.51.100.0)> :net_kernel.allow([:'two@203.0.113.0'])
:ok
iex(one@198.51.100.0)> Node.connect(:'two@203.0.113.0')
true

Is that the trick?


Solution

  • net_kernel is a module that creates a gen_server process. In that process state it has some parameters such as allowed which holds a list of allowed nodes and at startup was initiated by an empty list.

    There is an undocumented feature that if the given node for connection is not a member of allowed nodes but that list is empty, it lets the node to connect. This code snippet from net_kernel.erl module says this fact:

    setup(Node,Type,From,State) ->
        Allowed = State#state.allowed,
        case lists:member(Node, Allowed) of
            false when Allowed =/= [] ->
                error_msg("** Connection attempt with "
                          "disallowed node ~w ** ~n", [Node]),
                {error, bad_node};
            _ ->
                %% set up connection to given node
        end.
    

    Another important note is about net_kernel:allow/1 function which is an append-only function. You can check this fact in its source code when the new nodes are added to previous ones with ++ operator:

    handle_call({allow, Nodes}, From, State) ->
        case all_atoms(Nodes) of
            true ->
                Allowed = State#state.allowed,
                async_reply({reply,ok,State#state{allowed = Allowed ++ Nodes}},
                            From);
            false ->
                async_reply({reply,error,State}, From)
        end;