So you could make your own wrapper plug that obtains the config at runtime (i.e. inside call instead of init) and then calls Plug.Session.call(conn, config).
The thing is, wrapping the session plug would then read the configuration on every call and I’d like to avoid that. I’d rather have all the runtime configuration read once when the app boot.
I’m not sure, check the docs for it and possibly the source to see.
As for “reading the configuration”, you can do that once in runtime.exs and then set the configs there. At runtime (in call), you would just use Application.get_env which is a cheap operation and not something to worry about unless you have actually benchmarked it to be an issue.
Two years ago I worked in a proof of concept (PoC) where I used this type of approach. See the PoC here:
@session_options {NotesWeb.Endpoint.RuntimeSession, :options, []}
defmodule RuntimeSession do
def init(_opts) do
options()
|> Plug.Session.init()
end
def call(conn, opts) do
Plug.Session.call(conn, opts)
end
def options() do
Application.fetch_env!(:notes, NotesWeb.Endpoint)[:session_options]
end
end
config :phoenix360, Phoenix360Web.Endpoint,
# To be retrieved at runtime by `Phoenix360Web.PlugSession.options/0`.
session_options:
[
store: :cookie,
key: "_" <> System.fetch_env!("PHOENIX360_SESSION_COOKIE_KEY_NAME"),
signing_salt: System.fetch_env!("PHOENIX360_SESSION_SIGNING_SALT"),
encryption_salt: System.fetch_env!("PHOENIX360_SESSION_ENCRYPTION_SALT"),
],
A concrete example for the endpoint.ex on the app I am developing:
defmodule ApiBaasWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :api_baas
@session_options {ApiBaasWeb.Endpoint.PlugSessionRuntimeConfig, :options, []}
defmodule PlugSessionRuntimeConfig do
def init(_opts) do
IO.puts("PLUG SESSION INIT")
options()
|> Plug.Session.init()
end
def call(conn, opts) do
IO.puts("PLUG SESSION CALL")
Plug.Session.call(conn, opts)
end
def options() do
IO.puts("PLUG SESSION OPTIONS")
Application.fetch_env!(:api_baas, ApiBaasWeb.Endpoint)[:session_options]
end
end
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :api_baas,
gzip: false,
only: ~w(assets fonts images favicon favicon.ico site.webmanifest browserconfig.xml robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :api_baas
end
plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug ApiBaasWeb.Endpoint.PlugSessionRuntimeConfig
plug ApiBaasWeb.Router
end
This would set all the options at compile time since plug’s init callback is called at compile time (and the plug macro is also handled at compile time).
Here it goes development parity with production to the trash…
Thanks for the head-ups.
I keep being bitten and trapped by this unfortunate decisions, that privilege developer convenience over development environment consistence. Don’t get me wrong, because this is not a problem exclusive to Elixir.
What are the trade-offs of setting config :phoenix, :plug_init_mode, :runtime for production?
I really don’t want to have signing_salt and encryption_salt being set at compile time, because this is a serious security risk that cannot be overlooked from someone that works in Mobile and API security, like me.
# in the endpoint or session_options
plug Plug.Session, [
store: :cookie,
key: "blargh_session",
signing_salt: {SecretStuff, :signing_salt, []},
encryption_salt: {SecretStuff, :encryption_salt, []}
]
# elsewhere
defmodule SecretStuff do
def signing_salt, do: System.fetch_env!("PHOENIX360_SESSION_SIGNING_SALT")
def encryption_salt, do: System.fetch_env!("PHOENIX360_SESSION_ENCRYPTION_SALT")
end
EDIT: on further reflection, this may even be too much runtime behavior. Plug.Session.COOKIE will call the MFA every time, even though key generation is subsequently cached in Plug.Key.
$ mix compile
Compiling 1 file (.ex)
== Compilation error in file lib/api_baas_web/endpoint.ex ==
** (ArgumentError) could not fetch environment variable "SESSION_SIGNING_SALT" because it is not set
(elixir 1.13.4) lib/system.ex:698: System.fetch_env!/1
lib/api_baas_web/endpoint.ex:9: (module)
I can solve this by falling back to some dummy default values, but then I risk that a release may be run with them.
Another approach is to set the compile environment with some default dummy values for this env vars, but that is an hack that I was trying to avoid.
I am already using MFA, just in a slight different way:
In my understanding, despite my approach being a little different achieves the same result with the same drawbacks you mentioned:
In this case the MFA to retrieve the two env vars will be invoked literally for every request with a session, thus I wonder how much impact this will have at scale.
If my project succeeds this may become a bottleneck, but shipping a relase to each customer with the same signing and encryption salt is out of question, and building a release for each of them with different ones and be the one in charge of keeping them securely stored is also out of equation.
Also, building binaries with any kind of sensitive info on them is not a wise decision in terms of security. The CI systems are not trustworthy to handle sensitive info, because they may leak them into the logs, like Travis does, and they say its by design . Plus your release binary will now have sensitive info to be easily reverse engineered, and how do you secure this release binary against extraction of such sensitive info when it falls in the wrong hands. For example, as part of a data-breach/leak on whatever system used as part of the developer and devops workflow, like CI/CD pipelines (Travis, etc.), storage providers(S3 buckets, etc.), communication tools (Slack, Jira, email, etc.).
Now that I mention that I may need to revisit the architecture of my project and see if ditching Phoenix as much as possible and just use Elixir will be a wiser choice