Expose some http client ssl opts via HTTP_CLIENT_SSL_OPTS (#1221)

Expose the most commonly-used SSL client options to our OIDC and
ConnectivityChecks HTTP clients. Resolves some lingering issues some
users were facing with OIDC where they needed a custom TLS version
enforced or cacert file used to fetch the `discovery_document` and
resulting keys.

SSL misconfiguration can be a security concern, so we intentionally puke
when an unexpected key is passed. This should result in a new GitHub
issue being opened and dialog created to learn more about the use-case.

Fixes #996
This commit is contained in:
Jamil
2023-01-03 07:38:24 -08:00
committed by GitHub
parent 852fa6d779
commit 9a4103318d
6 changed files with 43 additions and 24 deletions

View File

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

View File

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

View File

@@ -29,4 +29,6 @@ defmodule FzHttp.Mocks.HttpClient do
@success_response
end
end
def post(url, _, _, _), do: post(url, nil)
end

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 | `[]` |