diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5bd30c23..7f2d3b29 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,22 @@ +2026-05-12 +fix(session): resolve session salts at runtime so prod release boots + The `@session_options` module attribute used `Application.compile_env`, + which baked compile-time placeholder values into the endpoint while + `config/runtime.exs` set the real values from env vars. Phoenix detected + the mismatch and refused to start (the failing migrate Job in k8s). + - config/config.exs: removed hardcoded session salts (no compile-time + binding for these keys now) + - config/dev.exs, config/test.exs: stable per-env salts so local + CI + don't need SESSION_*_SALT env vars + - lib/towerops_web/endpoint.ex: split static cookie opts (@static_session_options) + from runtime-resolved opts; added session_options/0 (used as MFA tuple + in socket connect_info) and runtime_session_opts/0 + - lib/towerops_web/plugs/runtime_session.ex: new wrapper that fetches + salts from app env on first request and caches the initialized + Plug.Session opts in :persistent_term + - SESSION_SIGNING_SALT / SESSION_ENCRYPTION_SALT in k8s/secrets.yaml are + now actually consumed at runtime in the release + 2026-05-10 feat(insights): AI now generates its own observations from full network state Previously the LLM was only used to enrich existing rule-based insights diff --git a/config/config.exs b/config/config.exs index 76a44007..a8d0afe8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -162,9 +162,6 @@ config :towerops, :scopes, test_setup_helper: :register_and_log_in_user ] -config :towerops, :session_encryption_salt, "vK3p8mNx" -config :towerops, :session_signing_salt, "hrDZxLhd" - # Agent Docker Image # Override this in runtime.exs or environment-specific config config :towerops, diff --git a/config/dev.exs b/config/dev.exs index 817a84a1..0a5748d1 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -152,12 +152,6 @@ config :towerops, Towerops.Vault, } ] -# For development, we disable any cache and enable -# debugging and code reloading. -# -# The watchers configuration can be used to run external -# watchers to your application. For example, we can use it -# to bundle .js and .css sources. config :towerops, ToweropsWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. @@ -227,6 +221,15 @@ config :towerops, :mib_dirs, [ # Disable rate limiting in development config :towerops, :rate_limiting_enabled, false +config :towerops, :session_encryption_salt, "dev-encryption-salt-stable" + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we can use it +# to bundle .js and .css sources. +config :towerops, :session_signing_salt, "dev-signing-salt-stable" # Redis is not configured in development - application will use stub Agent # If you need Redis for local development, set REDIS_HOST env var diff --git a/config/test.exs b/config/test.exs index cc6d85a7..60339355 100644 --- a/config/test.exs +++ b/config/test.exs @@ -110,6 +110,8 @@ config :towerops, :agent_webhook_secret, "test-webhook-secret" # Set environment identifier for runtime checks config :towerops, :env, :test +config :towerops, :session_encryption_salt, "test-encryption-salt-stable" +config :towerops, :session_signing_salt, "test-signing-salt-stable" config :towerops, ping_module: Towerops.Monitoring.PingMock, diff --git a/lib/towerops_web/endpoint.ex b/lib/towerops_web/endpoint.ex index c05a893e..64be5002 100644 --- a/lib/towerops_web/endpoint.ex +++ b/lib/towerops_web/endpoint.ex @@ -4,24 +4,43 @@ defmodule ToweropsWeb.Endpoint do # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. - # Set :encryption_salt if you would also like to encrypt it. # `http_only: true` blocks JavaScript access via `document.cookie` (XSS # defense). `secure` is enabled in :prod by `config/runtime.exs` so the # cookie is never sent over plain HTTP; left off here so local HTTP dev # still works. - @session_options [ + # + # Session salts (`signing_salt`, `encryption_salt`) come from env vars at + # runtime via `config/runtime.exs` (see `runtime_session_opts/0` below). + # They are merged in by `ToweropsWeb.Plugs.RuntimeSession` and by + # `session_options/0` (used by LiveView/socket `connect_info`). + @static_session_options [ store: :cookie, key: "_towerops_key", - signing_salt: Application.compile_env(:towerops, :session_signing_salt, "default-signing-salt-missing-env"), - encryption_salt: Application.compile_env(:towerops, :session_encryption_salt, "default-encryption-salt-missing-env"), same_site: "Lax", - http_only: true, - secure: Application.compile_env(:towerops, :secure_cookies, false) + http_only: true ] + @doc """ + Session options resolved at runtime. Used as an MFA tuple in socket + `connect_info` so LiveView decodes the session using the same salts + that `ToweropsWeb.Plugs.RuntimeSession` uses to encode it. + """ + def session_options do + @static_session_options ++ runtime_session_opts() + end + + @doc false + def runtime_session_opts do + [ + signing_salt: Application.fetch_env!(:towerops, :session_signing_salt), + encryption_salt: Application.fetch_env!(:towerops, :session_encryption_salt), + secure: Application.get_env(:towerops, :secure_cookies, false) + ] + end + socket "/live", Phoenix.LiveView.Socket, - websocket: [connect_info: [session: @session_options]], - longpoll: [connect_info: [session: @session_options]] + websocket: [connect_info: [session: {__MODULE__, :session_options, []}]], + longpoll: [connect_info: [session: {__MODULE__, :session_options, []}]] socket "/socket/agent", ToweropsWeb.AgentSocket, websocket: [connect_info: [:peer_data]], @@ -119,7 +138,7 @@ defmodule ToweropsWeb.Endpoint do plug Plug.MethodOverride plug Plug.Head - plug Plug.Session, @session_options + plug ToweropsWeb.Plugs.RuntimeSession, @static_session_options # Enable SQL Sandbox metadata for LiveView tests # This allows LiveView processes to access the test's sandbox connection diff --git a/lib/towerops_web/plugs/runtime_session.ex b/lib/towerops_web/plugs/runtime_session.ex new file mode 100644 index 00000000..90fedb5c --- /dev/null +++ b/lib/towerops_web/plugs/runtime_session.ex @@ -0,0 +1,33 @@ +defmodule ToweropsWeb.Plugs.RuntimeSession do + @moduledoc """ + Wrapper around `Plug.Session` that resolves session salts at runtime. + + The signing and encryption salts come from environment variables + (`SESSION_SIGNING_SALT` / `SESSION_ENCRYPTION_SALT`) via `config/runtime.exs`. + Because `Plug.Session` is compiled into the endpoint pipeline with its + options baked in, this plug defers `Plug.Session.init/1` until the first + request and caches the result in `:persistent_term`. + """ + + @behaviour Plug + + @impl true + def init(static_opts), do: static_opts + + @impl true + def call(conn, static_opts) do + Plug.Session.call(conn, session_opts(static_opts)) + end + + defp session_opts(static_opts) do + case :persistent_term.get({__MODULE__, :opts}, nil) do + nil -> + opts = Plug.Session.init(static_opts ++ ToweropsWeb.Endpoint.runtime_session_opts()) + :persistent_term.put({__MODULE__, :opts}, opts) + opts + + opts -> + opts + end + end +end