plausible, nixos/plausible: Add listenAddress option.

This changes

* the plausible HTTP web server

to be listening on localhost only, explicitly.

This makes Plausible have an explicit safe default configuration,
like all other networked services in NixOS.

For background discussion, see: https://github.com/NixOS/nixpkgs/issues/130244

As per my upstream Plausible contribution
(https://github.com/plausible/analytics/pull/1190)
Plausible >= 1.5 also defaults to listening to localhost only;
nevertheless, this default should be stated explicitly in nixpkgs
for easier review and independence from upstream changes, and
a NixOS user must be able to configure the
`listenAddress`, as there are valid use cases for that.

Also, disable

* the Erlang Beam VM inter-node RPC port
* the Erlang EPMD port

because Plausible does not use them (see added comment).
This is done by setting `RELEASE_DISTRIBUTION=none`.

Thus, this commit also removes the NixOS setting `releaseCookiePath`,
because it now has no effect.
This commit is contained in:
Niklas Hambüchen 2022-01-13 03:21:32 +00:00
parent 85f1ba3e51
commit 65a471717c
2 changed files with 45 additions and 14 deletions

View File

@ -11,13 +11,6 @@ in {
package = mkPackageOptionMD pkgs "plausible" { };
releaseCookiePath = mkOption {
type = with types; either str path;
description = lib.mdDoc ''
The path to the file with release cookie. (used for remote connection to the running node).
'';
};
adminUser = {
name = mkOption {
default = "admin";
@ -92,6 +85,13 @@ in {
framework docs](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content).
'';
};
listenAddress = mkOption {
default = "127.0.0.1";
type = types.str;
description = lib.mdDoc ''
The IP address on which the server is listening.
'';
};
port = mkOption {
default = 8000;
type = types.port;
@ -162,6 +162,10 @@ in {
};
};
imports = [
(mkRemovedOptionModule [ "services" "plausible" "releaseCookiePath" ] "Plausible uses no distributed Erlang features, so this option is no longer necessary and was removed")
];
config = mkIf cfg.enable {
assertions = [
{ assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
@ -180,8 +184,6 @@ in {
enable = true;
};
services.epmd.enable = true;
environment.systemPackages = [ cfg.package ];
systemd.services = mkMerge [
@ -209,6 +211,32 @@ in {
# Configuration options from
# https://plausible.io/docs/self-hosting-configuration
PORT = toString cfg.server.port;
LISTEN_IP = cfg.server.listenAddress;
# Note [plausible-needs-no-erlang-distributed-features]:
# Plausible does not use, and does not plan to use, any of
# Erlang's distributed features, see:
# https://github.com/plausible/analytics/pull/1190#issuecomment-1018820934
# Thus, disable distribution for improved simplicity and security:
#
# When distribution is enabled,
# Elixir spwans the Erlang VM, which will listen by default on all
# interfaces for messages between Erlang nodes (capable of
# remote code execution); it can be protected by a cookie; see
# https://erlang.org/doc/reference_manual/distributed.html#security).
#
# It would be possible to restrict the interface to one of our choice
# (e.g. localhost or a VPN IP) similar to how we do it with `listenAddress`
# for the Plausible web server; if distribution is ever needed in the future,
# https://github.com/NixOS/nixpkgs/pull/130297 shows how to do it.
#
# But since Plausible does not use this feature in any way,
# we just disable it.
RELEASE_DISTRIBUTION = "none";
# Additional safeguard, in case `RELEASE_DISTRIBUTION=none` ever
# stops disabling the start of EPMD.
ERL_EPMD_ADDRESS = "127.0.0.1";
DISABLE_REGISTRATION = if isBool cfg.server.disableRegistration then boolToString cfg.server.disableRegistration else cfg.server.disableRegistration;
RELEASE_TMP = "/var/lib/plausible/tmp";
@ -238,7 +266,10 @@ in {
path = [ cfg.package ]
++ optional cfg.database.postgres.setup config.services.postgresql.package;
script = ''
export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )"
# Elixir does not start up if `RELEASE_COOKIE` is not set,
# even though we set `RELEASE_DISTRIBUTION=none` so the cookie should be unused.
# Thus, make a random one, which should then be ignored.
export RELEASE_COOKIE=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 20)
export ADMIN_USER_PWD="$(< $CREDENTIALS_DIRECTORY/ADMIN_USER_PWD )"
export SECRET_KEY_BASE="$(< $CREDENTIALS_DIRECTORY/SECRET_KEY_BASE )"
@ -265,7 +296,6 @@ in {
LoadCredential = [
"ADMIN_USER_PWD:${cfg.adminUser.passwordFile}"
"SECRET_KEY_BASE:${cfg.server.secretKeybaseFile}"
"RELEASE_COOKIE:${cfg.releaseCookiePath}"
] ++ lib.optionals (cfg.mail.smtp.passwordFile != null) [ "SMTP_USER_PWD:${cfg.mail.smtp.passwordFile}"];
};
};

View File

@ -8,9 +8,6 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
virtualisation.memorySize = 4096;
services.plausible = {
enable = true;
releaseCookiePath = "${pkgs.runCommand "cookie" { } ''
${pkgs.openssl}/bin/openssl rand -base64 64 >"$out"
''}";
adminUser = {
email = "admin@example.org";
passwordFile = "${pkgs.writeText "pwd" "foobar"}";
@ -28,6 +25,10 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
machine.wait_for_unit("plausible.service")
machine.wait_for_open_port(8000)
# Ensure that the software does not make not make the machine
# listen on any public interfaces by default.
machine.fail("ss -tlpn 'src = 0.0.0.0 or src = [::]' | grep LISTEN")
machine.succeed("curl -f localhost:8000 >&2")
machine.succeed("curl -f localhost:8000/js/script.js >&2")