Skip to content

Commit

Permalink
Auto HTTPS in CE (plausible#4491)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
ruslandoga and zoldar authored Sep 10, 2024
1 parent 8ba5f7d commit 2180ab4
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 81 additions & 8 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion lib/plausible/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
]
Expand Down Expand Up @@ -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
68 changes: 68 additions & 0 deletions lib/plausible_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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"},
Expand All @@ -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", []},
Expand Down

0 comments on commit 2180ab4

Please sign in to comment.