From 2180ab4764131d7ea4f51e13b38426689f88d78d Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Tue, 10 Sep 2024 19:15:43 +0700 Subject: [PATCH] Auto HTTPS in CE (#4491) * auto https * changelog * enable auto-tls only when http_port is 80 * make erlang ssl less verbose * simplify config * simplify config * fix ce_dev typo * fix another typo --------- Co-authored-by: Adrian Gruntkowski --- CHANGELOG.md | 1 + Dockerfile | 3 +- config/runtime.exs | 89 +++++++++++++++++++++++++++++++---- lib/plausible/application.ex | 24 +++++++++- lib/plausible_web/endpoint.ex | 68 ++++++++++++++++++++++++++ mix.exs | 3 +- mix.lock | 3 ++ 7 files changed, 180 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eff6bf19d858..074ba7df9af1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. - New /debug/clickhouse route for super admins which shows information on clickhouse queries executed by user - Typescript support for `/assets` - Testing framework for `/assets` +- Automatic HTTPS plausible/analytics#4491 ### Removed - Deprecate `ECTO_IPV6` and `ECTO_CH_IPV6` env vars in CE plausible/analytics#4245 diff --git a/Dockerfile b/Dockerfile index 344a128228a4..2b72aad5ff81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,7 +66,8 @@ ENV MIX_ENV=$MIX_ENV RUN adduser -S -H -u 999 -G nogroup plausible RUN apk upgrade --no-cache -RUN apk add --no-cache openssl ncurses libstdc++ libgcc ca-certificates +RUN apk add --no-cache openssl ncurses libstdc++ libgcc ca-certificates \ + && if [ "$MIX_ENV" = "ce" ]; then apk add --no-cache certbot; fi COPY --from=buildcontainer --chmod=a+rX /app/_build/${MIX_ENV}/rel/plausible /app COPY --chmod=755 ./rel/docker-entrypoint.sh /entrypoint.sh diff --git a/config/runtime.exs b/config/runtime.exs index 16ac1f6310d9..296f50c435ee 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -19,9 +19,11 @@ config_dir = System.get_env("CONFIG_DIR", "/run/secrets") log_format = get_var_from_path_or_env(config_dir, "LOG_FORMAT", "standard") +default_log_level = if config_env() == :ce, do: "notice", else: "warning" + log_level = config_dir - |> get_var_from_path_or_env("LOG_LEVEL", "warning") + |> get_var_from_path_or_env("LOG_LEVEL", default_log_level) |> String.to_existing_atom() config :logger, @@ -61,7 +63,11 @@ listen_ip = ) # System.get_env does not accept a non string default -port = get_var_from_path_or_env(config_dir, "PORT") || 8000 +http_port = + get_int_from_path_or_env(config_dir, "HTTP_PORT") || + get_int_from_path_or_env(config_dir, "PORT", 8000) + +https_port = get_int_from_path_or_env(config_dir, "HTTPS_PORT") base_url = get_var_from_path_or_env(config_dir, "BASE_URL") @@ -308,18 +314,85 @@ config :plausible, :selfhost, enable_email_verification: enable_email_verification, disable_registration: disable_registration +default_http_opts = [ + transport_options: [max_connections: :infinity], + protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192] +] + config :plausible, PlausibleWeb.Endpoint, url: [scheme: base_url.scheme, host: base_url.host, path: base_url.path, port: base_url.port], - http: [ - port: port, - ip: listen_ip, - transport_options: [max_connections: :infinity], - protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192] - ], + http: [port: http_port, ip: listen_ip] ++ default_http_opts, secret_key_base: secret_key_base, websocket_url: websocket_url, secure_cookie: secure_cookie +# maybe enable HTTPS in CE +if config_env() in [:ce, :ce_dev, :ce_test] do + if https_port do + https_opts = [ + port: https_port, + ip: listen_ip, + cipher_suite: :compatible, + transport_options: [socket_opts: [log_level: :warning]] + ] + + https_opts = Config.Reader.merge(default_http_opts, https_opts) + config :plausible, PlausibleWeb.Endpoint, https: https_opts + + domain = base_url.host + + # do stricter checking in CE prod + if config_env() == :ce do + domain_is_ip? = + case :inet.parse_address(to_charlist(domain)) do + {:ok, _address} -> true + _other -> false + end + + if domain_is_ip? do + raise ArgumentError, "Cannot generate TLS certificates for IP address #{inspect(domain)}" + end + + domain_is_local? = domain == "localhost" or not String.contains?(domain, ".") + + if domain_is_local? do + raise ArgumentError, + "Cannot generate TLS certificates for local domain #{inspect(domain)}" + end + + unless http_port == 80 do + Logger.warning(""" + HTTPS is enabled but the HTTP port is not 80. \ + This will prevent automatic TLS certificate issuance as ACME validates the domain on port 80.\ + """) + end + end + + acme_directory_url = + get_var_from_path_or_env( + config_dir, + "ACME_DIRECTORY_URL", + "https://acme-v02.api.letsencrypt.org/directory" + ) + + db_folder = Path.join(data_dir || System.tmp_dir!(), "site_encrypt") + + email = + case mailer_email do + {_, email} -> email + email when is_binary(email) -> email + end + + config :plausible, :selfhost, + site_encrypt: [ + domain: domain, + email: email, + db_folder: db_folder, + directory_url: acme_directory_url + ] + end +end + db_maybe_ipv6 = if get_var_from_path_or_env(config_dir, "ECTO_IPV6") do if config_env() in [:ce, :ce_dev, :ce_test] do diff --git a/lib/plausible/application.ex b/lib/plausible/application.ex index 8296f9074bcb..c4cf81a2a5eb 100644 --- a/lib/plausible/application.ex +++ b/lib/plausible/application.ex @@ -10,6 +10,9 @@ defmodule Plausible.Application do on_ee(do: Plausible.License.ensure_valid_license()) on_ce(do: :inet_db.set_tcp_module(:happy_tcp)) + # in CE we start the endpoint under site_encrypt for automatic https + endpoint = on_ee(do: PlausibleWeb.Endpoint, else: maybe_https_endpoint()) + children = [ Plausible.Cache.Stats, Plausible.Repo, @@ -88,7 +91,7 @@ defmodule Plausible.Application do ] ), {Plausible.Auth.TOTP.Vault, key: totp_vault_key()}, - PlausibleWeb.Endpoint, + endpoint, {Oban, Application.get_env(:plausible, Oban)}, Plausible.PromEx ] @@ -252,4 +255,23 @@ defmodule Plausible.Application do [{impl_mod, Keyword.fetch!(opts, :adapter_opts)} | warmer_specs] end + + on_ce do + defp maybe_https_endpoint do + endpoint_config = Application.fetch_env!(:plausible, PlausibleWeb.Endpoint) + selfhost_config = Application.fetch_env!(:plausible, :selfhost) + site_encrypt_config = Keyword.get(selfhost_config, :site_encrypt) + + if get_in(endpoint_config, [:https, :port]) do + PlausibleWeb.Endpoint.force_https() + end + + if site_encrypt_config do + PlausibleWeb.Endpoint.allow_acme_challenges() + {SiteEncrypt.Phoenix.Endpoint, endpoint: PlausibleWeb.Endpoint} + else + PlausibleWeb.Endpoint + end + end + end end diff --git a/lib/plausible_web/endpoint.ex b/lib/plausible_web/endpoint.ex index c56e722e3559..91687cb51ab6 100644 --- a/lib/plausible_web/endpoint.ex +++ b/lib/plausible_web/endpoint.ex @@ -3,6 +3,11 @@ defmodule PlausibleWeb.Endpoint do use Sentry.PlugCapture use Phoenix.Endpoint, otp_app: :plausible + on_ce do + plug :maybe_handle_acme_challenge + plug :maybe_force_ssl, Plug.SSL.init(_no_opts = []) + end + @session_options [ # key to be patched key: "", @@ -125,4 +130,67 @@ defmodule PlausibleWeb.Endpoint do |> Application.fetch_env!(__MODULE__) |> Keyword.fetch!(key) end + + on_ce do + require SiteEncrypt + @behaviour SiteEncrypt + @force_https_key {:plausible, :force_https} + @allow_acme_challenges_key {:plausible, :allow_acme_challenges} + + @doc false + def force_https do + :persistent_term.put(@force_https_key, true) + end + + @doc false + def allow_acme_challenges do + :persistent_term.put(@allow_acme_challenges_key, true) + end + + defp maybe_handle_acme_challenge(conn, _opts) do + if :persistent_term.get(@allow_acme_challenges_key, false) do + SiteEncrypt.AcmeChallenge.call(conn, _endpoint = __MODULE__) + else + conn + end + end + + defp maybe_force_ssl(conn, opts) do + if :persistent_term.get(@force_https_key, false) do + Plug.SSL.call(conn, opts) + else + conn + end + end + + @impl SiteEncrypt + def handle_new_cert, do: :ok + + @doc false + def app_env_config do + # this function is being used by site_encrypt + Application.get_env(:plausible, _endpoint = __MODULE__, []) + end + + @impl SiteEncrypt + def certification do + selfhost_config = Application.fetch_env!(:plausible, :selfhost) + config = Keyword.fetch!(selfhost_config, :site_encrypt) + + domain = Keyword.fetch!(config, :domain) + email = Keyword.fetch!(config, :email) + db_folder = Keyword.fetch!(config, :db_folder) + directory_url = Keyword.fetch!(config, :directory_url) + + SiteEncrypt.configure( + mode: :auto, + log_level: :notice, + client: :certbot, + domains: [domain], + emails: [email], + db_folder: db_folder, + directory_url: directory_url + ) + end + end end diff --git a/mix.exs b/mix.exs index dba0786faf76..edfd125101a0 100644 --- a/mix.exs +++ b/mix.exs @@ -148,7 +148,8 @@ defmodule Plausible.MixProject do {:ex_json_schema, "~> 0.10.2"}, {:odgn_json_pointer, "~> 3.0.1"}, {:phoenix_bakery, "~> 0.1.2", only: [:ce, :ce_dev, :ce_test]}, - {:tzdata, github: "ruslandoga/tzdata", branch: "fix-for-2024b", override: true} + {:tzdata, github: "ruslandoga/tzdata", branch: "fix-for-2024b", override: true}, + {:site_encrypt, github: "sasa1977/site_encrypt", only: [:ce, :ce_dev, :ce_test]} ] end diff --git a/mix.lock b/mix.lock index beb805f0315c..3ee6535bcff1 100644 --- a/mix.lock +++ b/mix.lock @@ -109,6 +109,7 @@ "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.0.0", "d5982a319e725fcd2305b306b65c18a86afdcf7d96821473cf0649ff88877615", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.0", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "3401d13a1d4b7aa941a77e6b3ec074f0ae77f83b5b2206766ce630123a9291a9"}, "paginator": {:git, "https://github.com/duffelhq/paginator.git", "3508d6ad77a95ac1faf15d5fd7f959fab3e17da2", []}, + "parent": {:hex, :parent, "0.12.1", "495c4386f06de0df492e0a7a7199c10323a55e9e933b27222060dd86dccd6d62", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2ab589ef1f37bfcedbfb5ecfbab93354972fb7391201b8907a866dadd20b39d1"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, "phoenix_bakery": {:hex, :phoenix_bakery, "0.1.2", "ca57673caea1a98f1cc763f94032796a015774d27eaa3ce5feef172195470452", [:mix], [{:brotli, "~> 0.3.0", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "45cc8cecc5c3002b922447c16389761718c07c360432328b04680034e893ea5b"}, @@ -136,6 +137,7 @@ "scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"}, "sentry": {:hex, :sentry, "10.2.0", "046ca9fabfca3568b35ddb638d02c427e47380b2390416c4b26fee9c4ce56450", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4786fa2a08c2ddf48bfd79c54787a8e5b2f321368d781052fd20327318da8188"}, "siphash": {:hex, :siphash, "3.2.0", "ec03fd4066259218c85e2a4b8eec4bb9663bc02b127ea8a0836db376ba73f2ed", [:make, :mix], [], "hexpm", "ba3810701c6e95637a745e186e8a4899087c3b079ba88fb8f33df054c3b0b7c3"}, + "site_encrypt": {:git, "https://github.com/sasa1977/site_encrypt.git", "046fbeca11b889604dafd2df6a71001f8abe5e2c", []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, @@ -151,6 +153,7 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, + "x509": {:hex, :x509, "0.8.9", "03c47e507171507d3d3028d802f48dd575206af2ef00f764a900789dfbe17476", [:mix], [], "hexpm", "ea3fb16a870a199cb2c45908a2c3e89cc934f0434173dc0c828136f878f11661"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "zstream": {:hex, :zstream, "0.6.4", "169ce887a443d4163085ee682ab1b0ad38db8fa45e843927b9b431a92f4b7d9e", [:mix], [], "hexpm", "acc6c35b6db9eb2cfe8b85e972cb9dc1b730f8efeb76c5bbe871216fe639d9a1"}, "zxcvbn": {:git, "https://github.com/techgaun/zxcvbn-elixir.git", "aede1d49d39e89d7b3d1c381de5f04c9907d8171", []},