diff --git a/DEPLOYMENT-NOTES.rst b/DEPLOYMENT-NOTES.rst new file mode 100644 index 0000000000000000000000000000000000000000..5de83386dbb939cc3cfe2a8b68c198f218934933 --- /dev/null +++ b/DEPLOYMENT-NOTES.rst @@ -0,0 +1,14 @@ +Deployment notes +================ + +- 2021-09-07 `Manage access to payment metrics <https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/merge_requests/146>`_ requires moving and chown'ing the PaymentServer database on the ``payments`` host:: + + mkdir /var/lib/zkapissuer + + mv /var/db/vouchers.sqlite3 /var/lib/zkapissuer/vouchers.sqlite3 + + chown -R zkapissuer:zkapissuer /var/lib/zkapissuer + + chmod 750 /var/lib/zkapissuer + chmod 640 /var/lib/zkapissuer/vouchers.sqlite3 + diff --git a/morph/lib/issuer.nix b/morph/lib/issuer.nix index a14d70e0634ad150d3c076abcdfc36fb6a167513..d60af799888c97ec8f97a061d40b54d3f2db82a7 100644 --- a/morph/lib/issuer.nix +++ b/morph/lib/issuer.nix @@ -13,16 +13,16 @@ in { "ristretto-signing-key" = { destination = "/run/keys/ristretto.signing-key"; source = "${privateKeyPath}/ristretto.signing-key"; - owner.user = "root"; - owner.group = "root"; + owner.user = "zkapissuer"; + owner.group = "zkapissuer"; permissions = "0400"; action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; }; "stripe-secret-key" = { destination = "/run/keys/stripe.secret-key"; source = "${privateKeyPath}/stripe.secret"; - owner.user = "root"; - owner.group = "root"; + owner.user = "zkapissuer"; + owner.group = "zkapissuer"; permissions = "0400"; action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; }; @@ -56,6 +56,6 @@ in { ristrettoSigningKeyPath = config.deployment.secrets.ristretto-signing-key.destination; stripeSecretKeyPath = config.deployment.secrets.stripe-secret-key.destination; database = "SQLite3"; - databasePath = "/var/db/vouchers.sqlite3"; + databasePath = "/var/lib/zkapissuer/vouchers.sqlite3"; }; } diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix index e712ac0d3bbbcafcafd07552e69488e046e3e7e2..0433c4f011578bdecea023220c68d6d5047eae35 100644 --- a/nixos/modules/issuer.nix +++ b/nixos/modules/issuer.nix @@ -109,11 +109,13 @@ in { config = let - certroot = "/var/lib/letsencrypt/live"; # We'll refer to this collection of domains by the first domain in the # list. domain = builtins.head cfg.domains; - certServiceName = "cert-${domain}"; + certServiceName = "acme-${domain}"; + # Payment server internal http port (arbitrary, non-priviledged): + internalHttpPort = "1061"; + in lib.mkIf cfg.enable { # Add a systemd service to run PaymentServer. systemd.services.zkapissuer = { @@ -123,7 +125,14 @@ in { # Make sure we have a certificate the first time, if we are running over # TLS and require a certificate. - requires = lib.optional cfg.tls "${certServiceName}.service"; + # ACME will issue an interim self-signed certificate, which we want to + # use at least in the local dev network. But if ACME cannot get the + # created key signed by LE (probably because the host is not reachable + # from outside, or the domain is not a legit TLD) the ACME cert service + # will "fail". We still want to start our PaymentServer. Hence a weaker + # "wants" instead of a "requires" dependency. + # When ACME receives a fully signed cert from LE, it will reload NGINX. + wants = lib.optional cfg.tls "${certServiceName}.service"; after = [ # Make sure there is a network so we can bind to all of the @@ -140,6 +149,26 @@ in { serviceConfig.Restart = "always"; serviceConfig.Type = "simple"; + # Run w/o privileges + serviceConfig = { + DynamicUser = false; + User = "zkapissuer"; + Group = "zkapissuer"; + }; + + # Make systemd create a User/Group owned directory for PaymentServer + # state. According to the docs at + # https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RuntimeDirectory= + # "The specified directory names must be relative" ... this + # makes systemd create /var/lib/zkapissuer/ for us: + serviceConfig.StateDirectory = "zkapissuer"; + serviceConfig.StateDirectoryMode = "0750"; + + # Bail if there is still an old (root-owned) DB file on this system. + # If you hit this, and this /var/db/ file is indeed current, move it to + # /var/lib/zkapissuer/vouchers.sqlite3 and chown it to zkapissuer:zkapissuer. + unitConfig.AssertPathExists = "!/var/db/vouchers.sqlite3"; + script = let # Compute the right command line arguments to pass to it. The @@ -152,16 +181,7 @@ in { if cfg.database == "Memory" then "--database Memory" else "--database SQLite3 --database-path ${cfg.databasePath}"; - httpsArgs = - if cfg.tls - then - "--https-port 443 " + - "--https-certificate-path ${certroot}/${domain}/cert.pem " + - "--https-certificate-chain-path ${certroot}/${domain}/chain.pem " + - "--https-key-path ${certroot}/${domain}/privkey.pem" - else - # Only for automated testing. - "--http-port 80"; + httpArgs = "--http-port ${internalHttpPort}"; prefixOption = s: "--cors-origin=" + s; originStrings = map prefixOption cfg.allowedChargeOrigins; @@ -173,33 +193,21 @@ in { "--stripe-endpoint-scheme ${cfg.stripeEndpointScheme} " + "--stripe-endpoint-port ${toString cfg.stripeEndpointPort}"; in - "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpsArgs} ${stripeArgs}"; + "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpArgs} ${stripeArgs}"; }; - # Certificate renewal. A short-lived service meant to be repeatedly - # activated to request a new certificate be issued, if the current one is - # close to expiring. - systemd.services.${certServiceName} = { - enable = cfg.tls; - description = "Certificate ${domain}"; - # Activate this unit periodically so that certbot can determine if the - # certificate expiration time is close enough to warrant a renewal - # request. - startAt = "weekly"; - - serviceConfig = { - ExecStart = - let - configArgs = "--config-dir /var/lib/letsencrypt --work-dir /var/run/letsencrypt --logs-dir /var/run/log/letsencrypt"; - in - pkgs.writeScript "cert-${domain}-start.sh" '' - #!${pkgs.runtimeShell} -e - # Register if necessary. - ${pkgs.certbot}/bin/certbot register ${configArgs} --non-interactive --agree-tos -m ${cfg.letsEncryptAdminEmail} || true - # Obtain the certificate. - ${pkgs.certbot}/bin/certbot certonly ${configArgs} --non-interactive --standalone --expand --domains ${builtins.concatStringsSep "," cfg.domains} - ''; - }; + # PaymentServer runs as this user and group by default + # Mind the comments in nixpkgs/nixos/modules/misc/ids.nix: "When adding a uid, + # make sure it doesn't match an existing gid. And don't use uids above 399!" + ids.uids.zkapissuer = 397; + ids.gids.zkapissuer = 397; + users.extraGroups.zkapissuer.gid = config.ids.gids.zkapissuer; + users.extraUsers.zkapissuer = { + uid = config.ids.uids.zkapissuer; + isNormalUser = false; + group = "zkapissuer"; + # Let PaymentServer read from keys, if necessary. + extraGroups = [ "keys" ]; }; # Open 80 and 443 for the certbot HTTP server and the PaymentServer HTTPS server. @@ -207,5 +215,38 @@ in { 80 443 ]; + + # NGINX reverse proxy + security.acme.email = cfg.letsEncryptAdminEmail; + security.acme.acceptTerms = true; + services.nginx = { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + virtualHosts."${domain}" = { + serverAliases = builtins.tail cfg.domains; + enableACME = cfg.tls; + forceSSL = cfg.tls; + locations."/v1/" = { + # Only forward requests beginning with /v1/ so + # we pass less scanning spam on to our backend + # Want a regex instead? try locations."~ /v\d+/" + proxyPass = "http://127.0.0.1:${internalHttpPort}"; + }; + locations."/metrics" = { + # Only allow our monitoringvpn subnet + extraConfig = '' + allow 172.23.23.0/24; + deny all; + ''; + proxyPass = "http://127.0.0.1:${internalHttpPort}"; + }; + }; + }; + }; } diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix index 2687718bcba3f07cf9e229dadf071489445e4a54..3e8009b01d6c8a803909dcf08573029273c66bde 100644 --- a/nixos/modules/tests/private-storage.nix +++ b/nixos/modules/tests/private-storage.nix @@ -296,5 +296,14 @@ in { code, log = client.execute('cat /tmp/stdout /tmp/stderr') client.log(log) raise + + # The issuer metrics should be accessible from the monitoring network. + issuer.execute('ifconfig lo:fauxvpn 172.23.23.2/24') + issuer.wait_until_succeeds("nc -z 172.23.23.2 80") + issuer.succeed('curl --silent --insecure --fail --output /dev/null http://172.23.23.2/metrics') + # The issuer metrics should NOT be accessible from any other network. + issuer.fail('curl --silent --insecure --fail --output /dev/null http://localhost/metrics') + client.fail('curl --silent --insecure --fail --output /dev/null http://issuer/metrics') + issuer.execute('ifconfig lo:fauxvpn down') ''; }