diff --git a/apps/fz_common/lib/fz_common.ex b/apps/fz_common/lib/fz_common.ex index 5f833d8f9..c3ae5e42a 100644 --- a/apps/fz_common/lib/fz_common.ex +++ b/apps/fz_common/lib/fz_common.ex @@ -2,4 +2,31 @@ defmodule FzCommon do @moduledoc """ Documentation for `FzCommon`. """ + + @doc ~S""" + Maps JSON-decoded ssl opts to pass to Erlang's ssl module. Most users + don't need to override many, if any, SSL opts. Most commonly this is + to use custom cacert files and TLS versions. + + ## Examples: + + iex> FzCommon.map_ssl_opts(%{"verify" => "verify_none", "versions" => ["tlsv1.3"]}) + [verify: :verify_none, versions: ['tlsv1.3']] + + iex> FzCommon.map_ssl_opts(%{"keep_secrets" => true}) + ** (ArgumentError) unsupported key keep_secrets in ssl opts + + iex> FzCommon.map_ssl_opts(%{"cacertfile" => "/tmp/cacerts.pem"}) + [cacertfile: '/tmp/cacerts.pem'] + """ + def map_ssl_opts(decoded_json) do + Keyword.new(decoded_json, fn {k, v} -> + {String.to_atom(k), map_values(k, v)} + end) + end + + defp map_values("verify", v), do: String.to_atom(v) + defp map_values("versions", v), do: Enum.map(v, &String.to_charlist/1) + defp map_values("cacertfile", v), do: String.to_charlist(v) + defp map_values(k, _v), do: raise(ArgumentError, message: "unsupported key #{k} in ssl opts") end diff --git a/apps/fz_http/lib/fz_http/connectivity_check_service.ex b/apps/fz_http/lib/fz_http/connectivity_check_service.ex index 6bd6a8a01..65693017d 100644 --- a/apps/fz_http/lib/fz_http/connectivity_check_service.ex +++ b/apps/fz_http/lib/fz_http/connectivity_check_service.ex @@ -34,7 +34,7 @@ defmodule FzHttp.ConnectivityCheckService do def post_request(request_url) do body = "" - case http_client().post(request_url, body) do + case http_client().post(request_url, body, [], http_client_options()) do {:ok, response} -> ConnectivityChecks.create_connectivity_check(%{ response_body: response.body, @@ -79,4 +79,8 @@ defmodule FzHttp.ConnectivityCheckService do defp enabled? do FzHttp.Config.fetch_env!(:fz_http, :connectivity_checks_enabled) end + + defp http_client_options do + Application.fetch_env!(:fz_http, :http_client_options) + end end diff --git a/apps/fz_http/test/support/mocks/http_client.ex b/apps/fz_http/test/support/mocks/http_client.ex index fa6b4517d..df8ae8478 100644 --- a/apps/fz_http/test/support/mocks/http_client.ex +++ b/apps/fz_http/test/support/mocks/http_client.ex @@ -29,4 +29,6 @@ defmodule FzHttp.Mocks.HttpClient do @success_response end end + + def post(url, _, _, _), do: post(url, nil) end diff --git a/config/config.exs b/config/config.exs index 91da016da..ea18bc16d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -23,6 +23,7 @@ config :fz_http, FzHttpWeb.Auth.JSON.Authentication, config :fz_http, FzHttp.Repo, migration_timestamps: [type: :timestamptz] config :fz_http, + http_client_options: [], external_trusted_proxies: [], private_clients: [], sandbox: true, diff --git a/config/runtime.exs b/config/runtime.exs index 3bdbf22ca..fa4288ce9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -56,6 +56,7 @@ if config_env() == :prod do database_ssl = FzString.to_boolean(System.get_env("DATABASE_SSL", "false")) database_ssl_opts = Jason.decode!(System.get_env("DATABASE_SSL_OPTS", "{}")) database_parameters = Jason.decode!(System.get_env("DATABASE_PARAMETERS", "{}")) + http_client_ssl_opts = Jason.decode!(System.get_env("HTTP_CLIENT_SSL_OPTS", "{}")) phoenix_listen_address = System.get_env("PHOENIX_LISTEN_ADDRESS", "0.0.0.0") phoenix_port = String.to_integer(System.get_env("PHOENIX_PORT", "13000")) external_trusted_proxies = Jason.decode!(System.get_env("EXTERNAL_TRUSTED_PROXIES", "[]")) @@ -118,28 +119,6 @@ if config_env() == :prod do # Password is not needed if using bundled PostgreSQL, so use nil if it's not set. database_password = System.get_env("DATABASE_PASSWORD") - # XXX: Using to_atom here because this is trusted input and to_existing_atom - # won't work because we won't know the keys ahead of time. Hardcoding supported - # ssl_opts as well. - map_ssl_opt_val = fn k, v -> - case k do - "verify" -> - # verify expects an atom - String.to_atom(v) - - "versions" -> - # versions expects a list of atoms - Enum.map(v, &String.to_atom(&1)) - - _ -> - # Everything else is usually a string - v - end - end - - ssl_opts = - Keyword.new(database_ssl_opts, fn {k, v} -> {String.to_atom(k), map_ssl_opt_val.(k, v)} end) - parameters = Keyword.new(database_parameters, fn {k, v} -> {String.to_atom(k), v} end) # Database configuration @@ -150,7 +129,7 @@ if config_env() == :prod do port: database_port, pool_size: database_pool, ssl: database_ssl, - ssl_opts: ssl_opts, + ssl_opts: FzCommon.map_ssl_opts(database_ssl_opts), parameters: parameters, queue_target: 500 ] @@ -213,6 +192,7 @@ if config_env() == :prod do secret_key: guardian_secret_key config :fz_http, + http_client_options: [ssl: FzCommon.map_ssl_opts(http_client_ssl_opts)], saml_entity_id: saml_entity_id, saml_certfile_path: saml_certfile_path, saml_keyfile_path: saml_keyfile_path, @@ -235,6 +215,10 @@ if config_env() == :prod do admin_email: admin_email, default_admin_password: default_admin_password + # Configure OpenID Connect + config :openid_connect, + http_client_options: [ssl: FzCommon.map_ssl_opts(http_client_ssl_opts)] + # Configure strategies identity_strategy = {:identity, diff --git a/docs/docs/reference/env-vars.mdx b/docs/docs/reference/env-vars.mdx index 1bb3d7db8..fec221539 100644 --- a/docs/docs/reference/env-vars.mdx +++ b/docs/docs/reference/env-vars.mdx @@ -43,6 +43,7 @@ default). Required fields in **bold**. | `DATABASE_SSL` | Whether to connect to the database over SSL | Boolean | `false` | | `DATABASE_SSL_OPTS` | Map of options to send to the `:ssl_opts` option when connecting over SSL. See [Ecto.Adapters.Postgres documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options) | JSON-encoded String | `{}` | | `DATABASE_PARAMETERS` | Map of parameters to send to the `:parameters` option when connecting to the database. See [Ecto.Adapters.Postgres documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options). | JSON-encoded String | `{}` | +| `HTTP_CLIENT_SSL_OPTS` | Map of options to use for outbound SSL connections for OIDC document retrieval and Connectivity Checks. | JSON-encoded String | `{}` | | `CONNECTIVITY_CHECKS_ENABLED` | Enable / disable periodic checking for egress connectivity. Determines the instance's public IP to populate `Endpoint` fields. | Boolean | `true` | | `CONNECTIVITY_CHECKS_INTERVAL` | Periodicity in seconds to check for egress connectivity. | Integer | `3600` | | `EXTERNAL_TRUSTED_PROXIES` | List of trusted reverse proxies. | JSON-encoded array | `[]` |