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

Auto HTTPS in CE #4491

Merged
merged 11 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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
32 changes: 23 additions & 9 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"
zoldar marked this conversation as resolved.
Show resolved Hide resolved

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,13 @@ 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") ||
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP_PORT is added for consistency with the new HTTPS_PORT.

get_int_from_path_or_env(config_dir, "PORT", 8000)

https_port = get_int_from_path_or_env(config_dir, "HTTPS_PORT")
Copy link
Contributor Author

@ruslandoga ruslandoga Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting HTTPS_PORT enables site_encrypt so that auto-TLS is opt-in.


acme_directory_url = get_var_from_path_or_env(config_dir, "ACME_DIRECTORY_URL")

base_url = get_var_from_path_or_env(config_dir, "BASE_URL")

Expand Down Expand Up @@ -302,24 +310,30 @@ config :plausible,
custom_script_name: custom_script_name,
log_failed_login_attempts: log_failed_login_attempts,
license_key: license_key,
data_dir: data_dir
data_dir: data_dir,
acme_directory_url: acme_directory_url

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

if https_port do
config :plausible, PlausibleWeb.Endpoint,
https: [port: https_port, ip: listen_ip, cipher_suite: :compatible] ++ default_http_opts
Copy link
Contributor Author

@ruslandoga ruslandoga Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Compatible cipher suite supports tlsv1, tlsv1.1 and tlsv1.2.

From https://hexdocs.pm/plug/1.16.1/Plug.SSL.html#configure/1-cipher-suites

The reasoning for using this suite instead of a "strong" one is the same as in #1708 (comment)

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
21 changes: 20 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,20 @@ 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)
https_port = get_in(endpoint_config, [:https, :port])
https_enabled? = !!https_port

PlausibleWeb.Endpoint.enable_https(https_enabled?)

if https_enabled? do
{SiteEncrypt.Phoenix.Endpoint, endpoint: PlausibleWeb.Endpoint}
Copy link
Contributor Author

@ruslandoga ruslandoga Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be further limited to http_port==80 check since ACME issues challenges only on the default HTTP port.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else
PlausibleWeb.Endpoint
end
end
end
end
87 changes: 87 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 @@ -113,4 +118,86 @@ defmodule PlausibleWeb.Endpoint do
|> Application.fetch_env!(__MODULE__)
|> Keyword.fetch!(key)
end

on_ce do
require SiteEncrypt
@behaviour SiteEncrypt
@https_key {:plausible, :https}

@doc false
def enable_https(force?) when is_boolean(force?) do
# this function is called from application.ex during app start up
:persistent_term.put(@https_key, force?)
end

defp https?, do: :persistent_term.get(@https_key)

defp maybe_handle_acme_challenge(conn, _opts) do
if https?() do
SiteEncrypt.AcmeChallenge.call(conn, _endpoint = __MODULE__)
else
conn
end
end

defp maybe_force_ssl(conn, opts) do
if https?() 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 also being used by site_encrypt
Application.get_env(:plausible, _endpoint = __MODULE__, [])
end

@impl SiteEncrypt
def certification do
domain =
app_env_config()
|> Keyword.fetch!(:url)
|> Keyword.fetch!(:host)

domain_is_ip? =
case :inet.parse_address(to_charlist(domain)) do
{:ok, _address} -> true
_other -> false
end

domain_is_local? = domain == "localhost" or not String.contains?(domain, ".")

if domain_is_ip? or domain_is_local? do
raise ArgumentError, "Cannot generate TLS certificates for domain #{inspect(domain)}"
end

email =
case PlausibleWeb.Email.mailer_email_from() do
{_, email} -> email
email when is_binary(email) -> email
end

data_dir = Application.get_env(:plausible, :data_dir)
db_folder = Path.join(data_dir || System.tmp_dir!(), "site_encrypt")

directory_url =
Application.get_env(:plausible, :acme_directory_url) ||
"https://acme-v02.api.letsencrypt.org/directory"

SiteEncrypt.configure(
mode: :auto,
log_level: :notice,
client: :certbot,
domains: [domain],
Copy link
Contributor Author

@ruslandoga ruslandoga Sep 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it should try to issue "www.#{domain}" as well.

I think it's OK as is for now, and if someone complains I'll add extra logic to check if www should be issued too. Plausible CE is probably mostly served from a subdomain anyway.

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 @@ -146,7 +146,8 @@ defmodule Plausible.MixProject do
{:req, "~> 0.5.0"},
{:happy_tcp, github: "ruslandoga/happy_tcp", only: [:ce, :ce_dev, :ce_test]},
{:ex_json_schema, "~> 0.10.2"},
{:odgn_json_pointer, "~> 3.0.1"}
{:odgn_json_pointer, "~> 3.0.1"},
{:site_encrypt, github: "sasa1977/site_encrypt", only: [:ce, :de_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_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
Expand All @@ -135,6 +136,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 @@ -150,6 +152,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