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 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
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"
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,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") ||
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.


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}
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
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],
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 @@ -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