Erlang/Elixir Cookies and Kubernetes

9 Jan 2022 10:27 erlang elixir kubernetes

Distributed Erlang and Elixir applications use a shared secret called a “cookie”. It’s just a string of alphanumeric characters. All nodes in the cluster must use the same cookie. Let’s take a look at what that means in a Kubernetes context.

tl;dr: Use Kubernetes secrets to set the RELEASE_COOKIE environment variable. For Erlang, also edit config/vm.args.src.

The ~/.erlang.cookie file

By default, Erlang reads the cookie from ~/.erlang.cookie:

$ cat ~/.erlang.cookie
MQATXXBUYSOQAYUNJPWZ

$ erl -sname foo
(foo@rpi401)1> erlang:get_cookie().
'MQATXXBUYSOQAYUNJPWZ'

$ iex --sname foo
iex(foo@rpi401)1> Node.get_cookie()
:MQATXXBUYSOQAYUNJPWZ

Note that you need to enable distribution by using -sname or -name. Otherwise erlang:get_cookie() returns nocookie.

If the ~/.erlang.cookie file doesn’t exist, it will be created and a randomly-generated string will be written to it.

Alternatively, you can specify the cookie on the command-line:

$ erl -sname foo -setcookie KMZWIWWTBVPEBURCLHVQ
(foo@rpi401)1> erlang:get_cookie().
'KMZWIWWTBVPEBURCLHVQ'

$ iex --sname foo --cookie KMZWIWWTBVPEBURCLHVQ
iex(foo@rpi401)1> Node.get_cookie()
:KMZWIWWTBVPEBURCLHVQ
By today’s standards, Erlang cookies would not be considered good security. They’re relatively-easily guessable and sniffable. If you want a properly-secure cluster, you need to use TLS with mutual authentication, and you should set up your network policy to restrict which pods can communicate with each other.

Erlang releases

If you’re using relx to build your Erlang releases, you’re probably specifying the cookie in vm.args; here’s the relevant snippet from the template:

## Cookie for distributed erlang
-setcookie {{name}}_cookie

…which, when rebar creates your app, expands to (e.g.):

## Cookie for distributed erlang
-setcookie myapp_cookie

Because vm.args is part of the release, every node will be using the same cookie, which is what you want. Yes, it’s predictable; you can change it.

Rotating cookies is hard. I’m not going to cover it here.

Unfortunately, vm.args is usually checked into source control, which means that your cookie is checked into source control.

Instead, rename vm.args to vm.args.src, and change it as follows:

## Cookie for distributed erlang
-setcookie ${RELEASE_COOKIE}

You’ll also have to update your relx.config (or the relx section in rebar.config) to include the following:

{vm_args_src, "./config/vm.args.src"}
This is not needed since rebar 3.14.0. It automatically uses the presence of config/vm.args.src to enable this mode.

If you do the above, then the startup script will automatically expand environment variables at runtime, meaning that you can set control the cookie by setting the RELEASE_COOKIE environment variable.

Elixir releases

If you’re using mix release, a random cookie is created at build time and written to _build/prod/rel/myapp/releases/COOKIE. This means that the cookie changes every time you build from clean, which will break your cluster.

You can specify a cookie with the :cookie option in mix.exs. I would avoid doing this because it means that the cookie is now visible in source control history.

The other way to set the cookie is to set the RELEASE_COOKIE environment variable before starting the release. You can do this in rel/env.sh.eex, or from a Kubernetes secret.

Secrets

ERLANG_COOKIE=$(head -c 40 < /dev/random | base64 | tr '/+' '_-')
kubectl --namespace myapp \
    create secret generic erlang-cookie \
        --from-literal=cookie="$ERLANG_COOKIE"

Remember:

  • Secrets are scoped to the namespace, so you might want to put your app name as a prefix, unless you’re using a dedicated namespace.
  • A secret can contain multiple items. The example above uses cookie as the key.
  • Secrets aren’t actually that secret. Fortunately, Erlang cookies aren’t actually that secret either.

There don’t appear to be any widespread conventions for this yet. RabbitMQ recommends a dedicated namespace and uses erlang-cookie as the secret name and cookie as the key; see this blog post.

Environment variables

# kind: Pod, StatefulSet, ReplicaSet or Deployment
containers:
- name: myapp
  env:
  - name: RELEASE_COOKIE
    valueFrom:
      secretKeyRef:
        name: erlang-cookie
        key: cookie

Why not use ConfigMaps?

Because they’re basically the same as Secrets.

The code to generate a new Erlang cookie is in lib/kernel/src/auth.erl. It generates 20 random characters from [A-Z].

It doesn’t actually use the standard RNG; it uses one taken from Knuth: The Art of Computer Programming, Volume II, Seminumerical Algorithms. I can only assume that it does this because the rest of the runtime (including the standard RNG) isn’t available yet.

It’s broadly the equivalent of this:

ERLANG_COOKIE="$(env LC_CTYPE=C tr -dc 'A-Z' < /dev/random | head -c 20)"

Elixir does this:

iex(1)> Base.url_encode64(:crypto.strong_rand_bytes(40))
"FW9RYfQpVbuycMxSodrXIKAzuLgsaR5gyArGeap8WTHNfJj3vfltYQ=="