From a43647e8acdd491d5a693e4bde5c22925f00524c Mon Sep 17 00:00:00 2001
From: Florian Sesser <florian@private.storage>
Date: Mon, 9 Aug 2021 14:26:47 +0000
Subject: [PATCH] Move NGINX proxy from morph/../customize-issuer to
 nixos/../issuer

As per @jcalderone and @florian videoconf on 2021-08-06.

- Remove custom handling of Let's Encrypt certs, use NixOs' ACME
  integration instead.

- Use NGINX for TLS termination.
---
 morph/lib/customize-issuer.nix | 36 +------------
 morph/lib/issuer.nix           |  2 -
 nixos/modules/issuer.nix       | 99 ++++++++++++++++------------------
 3 files changed, 48 insertions(+), 89 deletions(-)

diff --git a/morph/lib/customize-issuer.nix b/morph/lib/customize-issuer.nix
index 2cbc52f6..d195b06b 100644
--- a/morph/lib/customize-issuer.nix
+++ b/morph/lib/customize-issuer.nix
@@ -78,6 +78,7 @@
   networking.domain = domain;
 
   services.private-storage.sshUsers = sshUsers;
+
   services.private-storage.monitoring.vpn.client = {
     enable = true;
     ip = monitoringvpnIPv4;
@@ -86,41 +87,8 @@
   };
 
   services.private-storage-issuer = {
-    inherit allowedChargeOrigins;
+    inherit allowedChargeOrigins letsEncryptAdminEmail;
     domains = issuerDomains;
-    # Arbitrary non-priviledged port:
-    httpPort = 1061;
-  };
-
-  # nginx reverse proxy
-  security.acme.email = letsEncryptAdminEmail;
-  security.acme.acceptTerms = true;
-  services.nginx = {
-    enable = true;
-
-    recommendedGzipSettings = true;
-    recommendedOptimisation = true;
-    recommendedProxySettings = true;
-    recommendedTlsSettings = true;
-
-    # Only allow PFS-enabled ciphers with AES256:
-    sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";
-
-    virtualHosts."${config.networking.hostName}.${config.networking.domain}" = {
-      enableACME = true;
-      forceSSL = true;
-      locations."/" = {
-        proxyPass = "http://127.0.0.1:${toString config.services.private-storage-issuer.httpPort}";
-      };
-      locations."/metrics" = {
-        # Only allow our monitoringvpn subnet
-        extraConfig = ''
-          allow 172.23.23.0/24;
-          deny all;
-        '';
-        proxyPass = "http://127.0.0.1:${toString config.services.private-storage-issuer.httpPort}";
-      };
-    };
   };
 
   system.stateVersion = "19.03";
diff --git a/morph/lib/issuer.nix b/morph/lib/issuer.nix
index 716fe760..43306923 100644
--- a/morph/lib/issuer.nix
+++ b/morph/lib/issuer.nix
@@ -50,8 +50,6 @@ rec {
 
   services.private-storage-issuer = {
     enable = true;
-    # We'll let NGINX handle TLS termination:
-    tls = false;
     ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination;
     stripeSecretKeyPath = deployment.secrets.stripe-secret-key.destination;
     database = "SQLite3";
diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix
index 0db40f19..676b60bc 100644
--- a/nixos/modules/issuer.nix
+++ b/nixos/modules/issuer.nix
@@ -111,29 +111,15 @@ in {
         PaymentServer.  It just controls the CORS headers served.
       '';
     };
-    services.private-storage-issuer.httpPort = lib.mkOption {
-      type = lib.types.int;
-      description = ''
-        The port number for the HTTP endpoint.
-      '';
-      default = 80;
-    };
-    services.private-storage-issuer.httpsPort = lib.mkOption {
-      type = lib.types.int;
-      description = ''
-        The port number for the HTTPs endpoint.
-      '';
-      default = 443;
-    };
   };
 
   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}";
+
     in lib.mkIf cfg.enable {
     # Add a systemd service to run PaymentServer.
     systemd.services.zkapissuer = {
@@ -143,7 +129,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
@@ -172,15 +165,8 @@ in {
             if cfg.database == "Memory"
               then "--database Memory"
               else "--database SQLite3 --database-path ${cfg.databasePath}";
-          httpsArgs =
-            if cfg.tls
-            then
-              "--https-port ${toString cfg.httpsPort}" +
-              "--https-certificate-path ${certroot}/${domain}/cert.pem " +
-              "--https-certificate-chain-path ${certroot}/${domain}/chain.pem " +
-              "--https-key-path ${certroot}/${domain}/privkey.pem"
-            else
-              "--http-port ${toString cfg.httpPort}";
+          # Arbitrary non-priviledged port:
+          httpArgs = "--http-port 1061";
 
           prefixOption = s: "--cors-origin=" + s;
           originStrings = map prefixOption cfg.allowedChargeOrigins;
@@ -192,33 +178,7 @@ in {
             "--stripe-endpoint-scheme ${cfg.stripeEndpointScheme} " +
             "--stripe-endpoint-port ${toString cfg.stripeEndpointPort}";
         in
-          "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpsArgs} ${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}
-          '';
-      };
+          "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpArgs} ${stripeArgs}";
     };
 
     # Open 80 and 443 for the certbot HTTP server and the PaymentServer HTTPS server.
@@ -226,5 +186,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;
+
+      # Only allow PFS-enabled ciphers with AES256:
+      sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";
+
+      virtualHosts."${domain}" = {
+        serverAliases = builtins.tail cfg.domains;
+        enableACME = cfg.tls;
+        forceSSL = cfg.tls;
+        locations."/" = {
+          proxyPass = "http://127.0.0.1:1061";
+        };
+        locations."/metrics" = {
+          # Only allow our monitoringvpn subnet
+          extraConfig = ''
+            allow 172.23.23.0/24;
+            deny all;
+          '';
+          proxyPass = "http://127.0.0.1:1061";
+        };
+      };
+    };
+
   };
 }
-- 
GitLab