diff --git a/config/runtime.exs b/config/runtime.exs index a981ddae80..ea15378846 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -476,13 +476,48 @@ if db_socket_dir? do password: password end else - config :plausible, Plausible.Repo, - url: db_url, - socket_options: db_maybe_ipv6 + config :plausible, Plausible.Repo, url: db_url - if db_cacertfile do - config :plausible, Plausible.Repo, ssl: [cacertfile: db_cacertfile] + unless Enum.empty?(db_maybe_ipv6) do + config :plausible, Plausible.Repo, socket_options: db_maybe_ipv6 end + + db_query = URI.decode_query(db_uri.query || "") + # https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS + pg_sslmode = db_query["sslmode"] + + pg_ssl = + cond do + db_cacertfile -> + [cacertfile: db_cacertfile, verify: :verify_peer] + + pg_sslmode == "verify-full" -> + if pg_sslrootcert = db_query["sslrootcert"] do + [cacertfile: pg_sslrootcert, verify: :verify_peer] + else + raise ArgumentError, + "PostgreSQL SSL mode `sslmode=#{pg_sslmode}` requires a certificate, set it in `sslrootcert`" + end + + pg_sslmode == "verify-ca" -> + [cacerts: :public_key.cacerts_get(), verify: :verify_peer] + + pg_sslmode == "require" -> + [verify: :verify_none] + + pg_sslmode == "disable" -> + false + + pg_sslmode -> + raise ArgumentError, + "PostgreSQL SSL mode `sslmode=#{pg_sslmode}` is not supported, use `disable`, `require`, `verify-ca` or `verify-full` instead" + + true -> + # tls is disabled by default, because in self-hosted docker compose postgres is co-located + false + end + + config :plausible, Plausible.Repo, ssl: pg_ssl end sentry_app_version = runtime_metadata[:version] || app_version diff --git a/test/plausible/config_test.exs b/test/plausible/config_test.exs index de11bb66ec..666ca5782e 100644 --- a/test/plausible/config_test.exs +++ b/test/plausible/config_test.exs @@ -458,7 +458,7 @@ defmodule Plausible.ConfigTest do assert get_in(config, [:plausible, Plausible.Repo]) == [ url: "postgres://postgres:postgres@plausible_db:5432/plausible_db", - socket_options: [] + ssl: false ] end @@ -500,7 +500,7 @@ defmodule Plausible.ConfigTest do assert get_in(config, [:plausible, Plausible.Repo]) == [ url: "postgresql://your_username:your_password@cluster-do-user-1234567-0.db.ondigitalocean.com:25060/defaultdb", - socket_options: [] + ssl: false ] end @@ -516,8 +516,123 @@ defmodule Plausible.ConfigTest do assert get_in(config, [:plausible, Plausible.Repo]) == [ url: "postgresql://your_username:your_password@cluster-do-user-1234567-0.db.ondigitalocean.com:25060/defaultdb", - socket_options: [], - ssl: [cacertfile: "/path/to/cacert.pem"] + ssl: [cacertfile: "/path/to/cacert.pem", verify: :verify_peer] + ] + end + + test "sslmode=require disables peer verification" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=require"} + ] + + config = runtime_config(env) + + assert get_in(config, [:plausible, Plausible.Repo]) == [ + url: + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=require", + ssl: [verify: :verify_none] + ] + end + + test "sslmode=disable explicitly disables SSL" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=disable"} + ] + + config = runtime_config(env) + + assert get_in(config, [:plausible, Plausible.Repo]) == [ + url: + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=disable", + ssl: false + ] + end + + test "sslmode=verify-ca uses system certificates" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=verify-ca"} + ] + + config = runtime_config(env) + + assert get_in(config, [:plausible, Plausible.Repo]) == [ + url: + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=verify-ca", + ssl: [ + cacerts: :public_key.cacerts_get(), + verify: :verify_peer + ] + ] + end + + test "sslmode=verify-full raises error without certificate" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=verify-full"} + ] + + assert_raise ArgumentError, + ~r/PostgreSQL SSL mode `sslmode=verify-full` requires a certificate/, + fn -> runtime_config(env) end + end + + test "unsupported sslmode raises error" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=prefer"} + ] + + assert_raise ArgumentError, ~r/PostgreSQL SSL mode `sslmode=prefer` is not supported/, fn -> + runtime_config(env) + end + end + + test "verify-full with sslrootcert enables peer verification" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=verify-full&sslrootcert=/path/to/cert.pem"} + ] + + config = runtime_config(env) + + assert get_in(config, [:plausible, Plausible.Repo]) == [ + url: + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslmode=verify-full&sslrootcert=/path/to/cert.pem", + ssl: [cacertfile: "/path/to/cert.pem", verify: :verify_peer] + ] + end + + test "sslrootcert alone is not enough for peer verification" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslrootcert=/path/to/cert.pem"} + ] + + config = runtime_config(env) + + assert get_in(config, [:plausible, Plausible.Repo]) == [ + url: + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslrootcert=/path/to/cert.pem", + ssl: false + ] + end + + test "DATABASE_CACERTFILE takes precedence over sslrootcert" do + env = [ + {"DATABASE_URL", + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslrootcert=/url/cert.pem"}, + {"DATABASE_CACERTFILE", "/env/cacert.pem"} + ] + + config = runtime_config(env) + + assert get_in(config, [:plausible, Plausible.Repo]) == [ + url: + "postgresql://username:password@company.postgres.database.azure.com:5432/prod_plausible?sslrootcert=/url/cert.pem", + ssl: [cacertfile: "/env/cacert.pem", verify: :verify_peer] ] end end