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
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/.envfile
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
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
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
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
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
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









