From ee094eec2ffb7ea1a10fe4f2d1878957358ff2f5 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 3 Dec 2025 12:36:13 +0100 Subject: [PATCH] feat: add file env support for secrets --- config/runtime.exs | 99 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index c50356c..bd48cc9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,6 +7,75 @@ import Config # any compile-time configuration in here, as it won't be applied. # The block below contains prod specific runtime configuration. +# Helper function to read environment variables with Docker secrets support. +# Supports the _FILE suffix pattern: if VAR_FILE is set, reads the value from +# that file path. Otherwise falls back to VAR directly. +# VAR_FILE takes priority and must contain the full absolute path to the secret file. +get_env_or_file = fn var_name, default -> + file_var = "#{var_name}_FILE" + + case System.get_env(file_var) do + nil -> + System.get_env(var_name, default) + + file_path -> + case File.read(file_path) do + {:ok, content} -> + String.trim(content) + + {:error, reason} -> + raise """ + Failed to read secret from file specified in #{file_var}="#{file_path}". + Error: #{inspect(reason)} + """ + end + end +end + +# Same as get_env_or_file but raises if the value is not set +get_env_or_file! = fn var_name, error_message -> + case get_env_or_file.(var_name, nil) do + nil -> raise error_message + value -> value + end +end + +# Build database URL from individual components or use DATABASE_URL directly. +# Supports both approaches: +# 1. DATABASE_URL (or DATABASE_URL_FILE) - full connection URL +# 2. Separate vars: DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD (or _FILE), DATABASE_NAME, DATABASE_PORT +build_database_url = fn -> + case get_env_or_file.("DATABASE_URL", nil) do + nil -> + # Build URL from separate components + host = + System.get_env("DATABASE_HOST") || + raise "DATABASE_HOST is required when DATABASE_URL is not set" + + user = + System.get_env("DATABASE_USER") || + raise "DATABASE_USER is required when DATABASE_URL is not set" + + password = + get_env_or_file!.("DATABASE_PASSWORD", """ + DATABASE_PASSWORD or DATABASE_PASSWORD_FILE is required when DATABASE_URL is not set. + """) + + database = + System.get_env("DATABASE_NAME") || + raise "DATABASE_NAME is required when DATABASE_URL is not set" + + port = System.get_env("DATABASE_PORT", "5432") + + # URL-encode the password to handle special characters + encoded_password = URI.encode_www_form(password) + "ecto://#{user}:#{encoded_password}@#{host}:#{port}/#{database}" + + url -> + url + end +end + # ## Using releases # # If you use `mix release`, you need to explicitly enable the server @@ -21,12 +90,7 @@ if System.get_env("PHX_SERVER") do end if config_env() == :prod do - database_url = - System.get_env("DATABASE_URL") || - raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE - """ + database_url = build_database_url.() maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] @@ -41,12 +105,12 @@ if config_env() == :prod do # want to use a different value for prod and you most likely don't want # to check this value into version control, so we use an environment # variable instead. + # Supports SECRET_KEY_BASE or SECRET_KEY_BASE_FILE for Docker secrets. secret_key_base = - System.get_env("SECRET_KEY_BASE") || - raise """ - environment variable SECRET_KEY_BASE is missing. - You can generate one by calling: mix phx.gen.secret - """ + get_env_or_file!.("SECRET_KEY_BASE", """ + environment variable SECRET_KEY_BASE (or SECRET_KEY_BASE_FILE) is missing. + You can generate one by calling: mix phx.gen.secret + """) host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable." port = String.to_integer(System.get_env("PORT") || "4000") @@ -54,21 +118,22 @@ if config_env() == :prod do config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") # Rauthy OIDC configuration + # Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets. config :mv, :rauthy, client_id: System.get_env("OIDC_CLIENT_ID") || "mv", base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1", - client_secret: System.get_env("OIDC_CLIENT_SECRET"), + client_secret: get_env_or_file.("OIDC_CLIENT_SECRET", nil), redirect_uri: System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" # Token signing secret from environment variable # This overrides the placeholder value set in prod.exs + # Supports TOKEN_SIGNING_SECRET or TOKEN_SIGNING_SECRET_FILE for Docker secrets. token_signing_secret = - System.get_env("TOKEN_SIGNING_SECRET") || - raise """ - environment variable TOKEN_SIGNING_SECRET is missing. - You can generate one by calling: mix phx.gen.secret - """ + get_env_or_file!.("TOKEN_SIGNING_SECRET", """ + environment variable TOKEN_SIGNING_SECRET (or TOKEN_SIGNING_SECRET_FILE) is missing. + You can generate one by calling: mix phx.gen.secret + """) config :mv, :token_signing_secret, token_signing_secret