Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(electric): Set cacerts on the Ecto repo when available #1293

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/electric/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ RUN make compile ${MAKE_RELEASE_TASK}
FROM ${RUNNER_IMAGE} AS runner_setup

RUN apt-get update -y && \
apt-get install -y libstdc++6 openssl libncurses5 locales && \
apt-get install -y libstdc++6 openssl ca-certificates libncurses5 locales && \
apt-get clean && \
rm -f /var/lib/apt/lists/*_*

Expand Down
2 changes: 1 addition & 1 deletion components/electric/lib/electric/postgres/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule Electric.Postgres.Repo do
username: conn_opts.username,
password: conn_opts.password,
database: conn_opts.database,
ssl: conn_opts.ssl == :required,
ssl: conn_opts[:ssl_opts] || false,
# Pass TCP options to the Postgrex adapter. This is used to let the adapter know to
# connect to the DB using IPv6, for example.
socket_options: Map.get(conn_opts, :tcp_opts, []),
Expand Down
88 changes: 34 additions & 54 deletions components/electric/lib/electric/replication/postgres_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,24 @@ defmodule Electric.Replication.PostgresConnectorMng do
case initialize_postgres(state) do
{:ok, ssl_used?} ->
state =
state
if ssl_used? do
# Connector config already has the right SSL configuration in this case.
state
else
# epgsql fell back to using an unencrypted connection. Modify the connector config
# accordingly, so that the correct SSL configuration is then used by
# `Electric.Postgres.Repo`.
fallback_to_nossl(state)
end
|> set_status(:establishing_repl_conn)
|> update_connector_config(&force_ssl_mode(&1, ssl_used?))

{:noreply, state, {:continue, :establish_repl_conn}}

{:error, {:ssl_negotiation_failed, _}} when state.conn_opts.ssl != :required ->
Logger.warning(
"Falling back to trying an unencrypted connection to Postgres, since DATABASE_REQUIRE_SSL=false."
)

state = fallback_to_nossl(state)
{:noreply, state, {:continue, :init}}

Expand Down Expand Up @@ -304,17 +315,10 @@ defmodule Electric.Replication.PostgresConnectorMng do
|> Keyword.put(:nulls, [nil, :null, :undefined])
|> Keyword.put(:ip_addr, ip_addr)
|> maybe_put_inet6(ip_addr)
|> maybe_put_sni()
|> maybe_verify_peer()
|> update_ssl_opts()
end)
end

def force_ssl_mode(connector_config, ssl_mode?) do
new_ssl_value = if ssl_mode?, do: :required, else: false

put_in(connector_config, [:connection, :ssl], new_ssl_value)
end

# Perform a DNS lookup for an IPv6 IP address, followed by a lookup for an IPv4 address in case the first one fails.
#
# This is done in order to obviate the need for specifying the exact protocol a given database is reachable over,
Expand All @@ -336,56 +340,32 @@ defmodule Electric.Replication.PostgresConnectorMng do

defp maybe_put_inet6(conn_opts, _), do: conn_opts

defp maybe_put_sni(conn_opts) do
defp update_ssl_opts(conn_opts) do
if conn_opts[:ssl] do
sni_opt = {:server_name_indication, String.to_charlist(conn_opts[:host])}
update_in(conn_opts, [:ssl_opts], &[sni_opt | List.wrap(&1)])
else
conn_opts
end
end

defp maybe_verify_peer(conn_opts) do
if conn_opts[:ssl] == :required do
ssl_opts = get_verify_peer_opts()
update_in(conn_opts, [:ssl_opts], &(ssl_opts ++ List.wrap(&1)))
ssl_opts =
conn_opts[:host]
|> String.to_charlist()
|> :tls_certificate_check.options()
|> Keyword.put(:signature_algs_cert, [
# https://github.com/erlang/otp/issues/8588
:ssl.signature_algs(:default, :"tlsv1.3") ++ [{:sha, :rsa}]
])
|> IO.inspect(label: :tls_certificate_check)

Keyword.put(conn_opts, :ssl_opts, ssl_opts)
else
conn_opts
end
end

defp get_verify_peer_opts do
case :public_key.cacerts_load() do
:ok ->
cacerts = :public_key.cacerts_get()
Logger.info("Successfully loaded #{length(cacerts)} cacerts from the OS")

[
verify: :verify_peer,
cacerts: cacerts,
customize_hostname_check: [
# Use a custom match function to support wildcard CN in server certificates.
# For example, CN = *.us-east-2.aws.neon.tech
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]

{:error, reason} ->
Logger.warning("Failed to load cacerts from the OS: #{inspect(reason)}")
# We're not sure how reliable OS certificate stores are in general, so keep going even
# if the loading of cacerts has failed. A warning will be logged every time a new
# database connection is established without the `verify_peer` option, so the issue will be
# visible to the developer.
[]
Keyword.delete(conn_opts, :ssl_opts)
end
end

defp fallback_to_nossl(state) do
Logger.warning(
"Falling back to trying an unencrypted connection to Postgres, since DATABASE_REQUIRE_SSL=false."
)

update_connector_config(state, &put_in(&1, [:connection, :ssl], false))
update_connector_config(state, fn connector_config ->
Keyword.update!(connector_config, :connection, fn conn_opts ->
conn_opts
|> Keyword.put(:ssl, false)
|> update_ssl_opts()
end)
end)
end

defp extra_error_description(:invalid_authorization_specification) do
Expand Down
1 change: 1 addition & 0 deletions components/electric/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ defmodule Electric.MixProject do
{:mox, "~> 1.1"},
{:mock, "~> 0.3.0", only: :test},
{:ssl_verify_fun, "~> 1.1.7", override: true},
{:tls_certificate_check, "~> 1.22"},
{:jason, "~> 1.4"},
{:dialyxir, "~> 1.4", only: [:dev], runtime: false},
{:excoveralls, "~> 0.18", only: :test, runtime: false},
Expand Down
1 change: 1 addition & 0 deletions components/electric/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.22.1", "0f450cc1568a67a65ce5e15df53c53f9a098c3da081c5f126199a72505858dc1", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3092be0babdc0e14c2e900542351e066c0fa5a9cf4b3597559ad1e67f07938c0"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
Expand Down
Loading