James_E

James_E

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.

Showing Posts 1 to 10

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
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.

linusdm

linusdm

Just as a side note: there is a mix task to create a secret key.

James_E

James_E OP

The snippet you posted is actually the stock behavior of the phx.new template; I edited it away from that because I don’t want my server to crash on startup just because the technician attempted a perfectly reasonable ZIP installation.

The app isn’t being deployed in any kind of fancy cloud service, either. Heck, right now it’s targeting Windows server (though we hope to move over to Linux at some point.)

I guess that I could bundle extra instructions with the application demanding that the installing technician fiddle with the registry to get basic functionality… but is there really not any established pattern for making Phoenix “just work” in the absence of a fancy managed environment? (And even on Linux, putting a systemd service that references an external file just passes the buck of generating that file…)


That task seems to be hard-coded to send the result to standard output. I actually linked to that line in the comment at the top the bit of my code that does what that task would.

James_E

James_E OP

Or maybe I’m X-Y problemming this.

What, specifically, would go wrong if I simply set :secret_key_base randomly on startup whenever the environment doesn’t specify it? Would everything work perfectly so long as the app isn’t being clustered/distributed/whatever, or would I be laying the seeds of—for example—a catastrophic Ecto failure next time the server restarts?

If the former, it seems like it would be pretty simple to raise if and only if it’s unspecified and the app is being run in a way that actually requires a non-arbitrary value for it… am I missing something?

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.

James_E

James_E OP

I see… so if I just randomize it on startup, then users authenticated based on session cookies will be logged out, any CSRF-protected flows will be interrupted, and any Phoenix.tokens will be ungracefully invalidated. Definitely not great.

Since keeping it stable is apparently so crucial to so much of Phoenix’s operation, what are the risks/drawbacks to just storing it in Ecto after automatically generating it on the first run?

LostKobrakai

LostKobrakai

I’m wondering if you’re not better off figuring out system env on windows over figuring out how to work around it. Releases support being installed as windows services, which based on the docs allow you to define additional system env values.

erlsrv — OTP 29.0.2 (erts 17.0.2) (See Env parameter)

James_E

James_E OP

Hmm, one difficulty I’m running into there is that Ecto isn’t started during config-load time:

** (RuntimeError) could not lookup Ecto repo Foo.Repo because it was not started or it does not exist

import Config
require Ecto.Query

…

if config_env() == :prod do
  database_path =
    System.get_env("DATABASE_PATH")
    || Path.expand("foo.db", :filename.basedir(:user_data, "fooApp"))

  secret_key_base = # https://forum.elixirforum.com/t/managing-secret-key-base-without-kubernetes-docker-etc/67926?u=james_e
    case System.get_env("SECRET_KEY_BASE") do
      s when not is_nil(s) -> s
      nil -> Foo.Util.get_or_insert_one_lazy!(
        Foo.Repo,
        Ecto.Query.from(s in Foo.Repo.Schemas.Secret, where: s.type == "secret_key_base"),
        fn -> %Foo.Repo.Schemas.Secret{type: "secret_key_base", value: Foo.Util.phx_gen_secret} end
      ).value
    end

  …

  config :fooApp, FooWeb.Endpoint,
    …,
    secret_key_base: secret_key_base

  …
end

Is there any best practice for making parts of Endpoint config depend on values stored in Ecto, like that? If I were to start Ecto within this file, I’m pretty sure that’d break the supervisor structure, and I don’t see any obvious clean way to transform the phx.new template to do that. Maybe a separate :ignore process that runs immediately after Ecto.Migrator?

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.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews