James_E

James_E

Managing SECRET_KEY_BASE without Kubernetes/Docker/etc?

I’m developing an application that’s going to be (for the forseeable future) installed and managed in a pretty boring, standard way — not using bespoke containerization, clustering, or anything like that.

I’ve currently got this code to manage SECRET_KEY_BASE, and it seems to work, but it also seems like a really messy hack:

# /config/runtime.exs

# …
if config_env() == :prod do
  # …
  secret_key_base =
    System.get_env("SECRET_KEY_BASE")
    || Foo.Application.Util.app_secret_key_auto()
  # …
end
# /lib/foo/util.ex
defmodule Foo.Application.Util do
  # …
  def app_secret_key_auto do
    p = Path.expand("secret_key_base.txt", :filename.basedir(:user_config, "foo-app"))

    # 1. Generate key if it doesn't exist
    :ok = mkdir_p(Path.expand("..", p))
    :ok = case File.open(p, [:write, :exclusive]) do # FIXME surely there must be some built-in tooling for this???
      {:error, :eexist} -> :ok # happy path
      {:ok, h} ->
        case File.chmod(p, 0o600) do
          :ok ->
            # https://github.com/phoenixframework/phoenix/blob/v1.7.17/lib/mix/tasks/phx.gen.secret.ex#L17
            data_ascii = (&:crypto.strong_rand_bytes(&1) |> Base.encode64(padding: false) |> binary_part(0, &1)).(64)
            result = IO.write(h, data_ascii)
            :ok = File.close(h)
            result
          {:error, e} ->
            :ok = File.close(h)
            {:error, e}
        end
      {:error, e} -> {:error, e}
    end

    # 2. load key
    {:ok, data_ascii} = File.open(p, [:read], &Enum.fetch!(IO.stream(&1, :line), 0))
    data_ascii
  end
end

Is there any existing utility function or simple pattern that I should be using instead of this pile of spaghetti, or is Phoenix really just not meant to be used outside of cloud containers?

The documentation didn’t say much about it.

First Post!

axelson

axelson

Scenic Core Team

Hmm, generally I wouldn’t generate the SECRET_KEY_BASE if it doesn’t exist, I’d instead always pass it in via Env variables. Specifically these are the situations I’ve dealt with before that didn’t involve Kubernetes/Docker:

  • App deployed on Heroku - Set an env variable in the UI
  • App deployed on Render - Set an env variable in the UI
  • App deployed on my own server and managed with systemd - Set EnvironmentFile=/path/to/some/.env file

That way the only snippet you need is something like:

# /config/runtime.exs

# …
if config_env() == :prod do
  # …
  secret_key_base =
    System.get_env("SECRET_KEY_BASE")
    || raise "SECRET_KEY_BASE is required and was not found"
  # …
end

Most Liked

LostKobrakai

LostKobrakai

Sessions are signed with the secret key. So all session cookies of user will become invalid, effectively logging them out if the were logged in and dropping any other stuff you might have stored in the session. Similarly csrf token given out (e.g. as part of forms) will become invalid, so users still having a form open won’t be able to successfully submit their forms. Phoenix.Tokens also reply on the secret if you happen to use those.

LostKobrakai

LostKobrakai

Adding one more option:

  • Release started manually
    env $(cat .env | xargs) ./bin/app remote

In the end system env is the complete opposite than catered to the cloud. You can provide those in a million ways and there’s likely one fitting your workflow – and it’s completely independent from the fact that you’re running elixir at all.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

This seems like a LOT of hoops to jump through just to avoid the actual good practice of doing a rolling deploy. And if you can’t rely on an environment variable being present on boot, how are you going to connect to the database anyway? Is that not also configured via an env var?

Zooming out: your app has preconditions to booting. The correct thing to do when those preconditions are not met is to not boot, which allows the currently running instance to continue to serve traffic. The boot failure goes to your logs, which gives you alerts, and you fix it.

None of that is docker or container specific, that’s been the normal rolling deploy pattern for 20+ years now.

Last Post!

James_E

James_E

Wow, I didn’t realize that! Thank you.

That ended up leading to this relatively generic solution to allow deferring arbitrary config:

defmodule Foo.Application do
  @moduledoc false
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      FooWeb.Telemetry,
      Foo.Repo,
      {Ecto.Migrator,
        repos: Application.fetch_env!(:fooApp, :ecto_repos),
        skip: skip_migrations?()},
      #{DNSCluster, query: Application.get_env(:foo, :dns_cluster_query) || :ignore},
      {Phoenix.PubSub, name: Foo.PubSub},
      {DynamicSupervisor, name: Foo.RoomSupervisor},
      FooWeb.Endpoint |> Foo.Util.defer_spec(
        secret_key_base: {Foo.Repo, Foo.Repo.Schemas.Secret, &FooWeb.Util.phx_gen_secret/0}
      )
    ]

    opts = [strategy: :one_for_one, name: Foo.Supervisor]
    Supervisor.start_link(children, opts)
  end

  …
end
Utility functions / heavy lifting
# lib/foo/util.ex
defmodule Foo.Util do
  require Ecto.Query

  …

  # https://github.com/elixir-ecto/ecto/blob/v3.12.5/lib/ecto/repo/queryable.ex#L153
  def one_or_insert_lazy!(repo, query, fun) do
    repo.transaction(fn ->
      case repo.all(query) do
        [value] -> value
        [] ->
          repo.insert!(fun.())
          repo.one!(query)
        other -> raise Ecto.MultipleResultsError, queryable: query, count: length(other)
      end
    end)
  end

  @spec defer_spec(Supervisor.module_spec(), deferred_opts :: keyword()) :: Supervisor.child_spec()
  def defer_spec(module_spec, deferred_opts \\ [])
  def defer_spec(module, deferred_opts) when is_atom(module), do: defer_spec({module, []}, deferred_opts)
  def defer_spec({module, opts}, deferred_opts)
      when is_atom(module) do
    %{
      id: module,
      start: {__MODULE__, :_start_link_helper, [module, opts, deferred_opts]}
    }
  end

  def _start_link_helper(module, opts, deferred_opts) do
    access = if is_list(opts), do: Keyword, else: Access

    opts = Enum.reduce(deferred_opts, opts, fn
      {key, {repo, schema, default_fun}}, acc when is_function(default_fun, 0) ->
        {_, acc} = access.get_and_update(acc, key, fn
          value when not is_nil(value) ->
            {value, value}

          _ ->
            key = Atom.to_string(key)
            value = one_or_insert_lazy!(
              repo,
              Ecto.Query.from(
                s in schema,
                where: s.name == ^key,
                select: s.value
              ),
              fn -> struct(
                schema,
                name: key,
                value: default_fun.()
              ) end
            )
            {nil, value}
        end)
        acc

      {key, fun}, acc when is_function(fun, 0) ->
        access.put(acc, key, fun.())

      {key, fun}, acc when is_function(fun, 1) ->
        {_, acc} = access.get_and_update(acc, key, &{&1, fun.(&1)})
        acc

      {key, {:get_and_update, fun}}, acc when is_function(fun, 1) ->
        {_, acc} = access.get_and_update(acc, key, fun)
        acc
    end)

    module.start_link(opts)
  end
end
# lib/foo_web/util.ex
defmodule FooWeb.Util do
  def phx_gen_secret(length \\ 64) do
    # https://github.com/phoenixframework/phoenix/blob/v1.7.17/lib/mix/tasks/phx.gen.secret.ex#L17
    :crypto.strong_rand_bytes(length) |> Base.encode64(padding: false) |> binary_part(0, length)
  end
end

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36820 110
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 44265 214
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement