From 4ad78138c8c28827b318cf114c0a8c9339113838 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Mon, 4 Nov 2019 13:50:09 -0500
Subject: [PATCH] Quick and dirty Let's Encrypt client service using certbot

---
 morph/grid.config.json         |  2 +
 morph/grid.nix                 |  1 +
 morph/issuer.nix               |  4 ++
 morph/make-grid.nix            |  4 +-
 morph/make-storage.nix         |  2 +-
 morph/testing-grid.config.json |  5 +++
 morph/testing-grid.nix         |  8 +++-
 morph/testing000.nix           |  2 +-
 nixos/modules/issuer.nix       | 67 +++++++++++++++++++---------------
 nixpkgs.rev                    |  2 +-
 10 files changed, 62 insertions(+), 35 deletions(-)
 create mode 100644 morph/testing-grid.config.json

diff --git a/morph/grid.config.json b/morph/grid.config.json
index 178f44d3..5b848d31 100644
--- a/morph/grid.config.json
+++ b/morph/grid.config.json
@@ -1,3 +1,5 @@
 { "publicStoragePort": 8898
 , "ristrettoSigningKeyPath": "../../PrivateStorageSecrets/ristretto.signing-key"
+, "issuerDomain": "payments.privatestorage.io"
+, "letsEncryptAdminEmail": "jean-paul@privatestorage.io"
 }
diff --git a/morph/grid.nix b/morph/grid.nix
index 466398b1..3d005f96 100644
--- a/morph/grid.nix
+++ b/morph/grid.nix
@@ -3,6 +3,7 @@
 # with the testing grid and have one fewer possible point of divergence.
 import ./make-grid.nix {
   name = "Production";
+  config = ./grid.config.json;
   nodes = cfg: {
     # Here are the hosts that are in this morph network.  This is sort of like
     # a server manifest.  We try to keep as many of the specific details as
diff --git a/morph/issuer.nix b/morph/issuer.nix
index aac47193..ddf01bdf 100644
--- a/morph/issuer.nix
+++ b/morph/issuer.nix
@@ -1,5 +1,7 @@
 { hardware
 , ristrettoSigningKeyPath
+, issuerDomain
+, letsEncryptAdminEmail
 , stateVersion
 , ...
 }: {
@@ -27,6 +29,8 @@
     ristrettoSigningKey = builtins.readFile (./.. + ristrettoSigningKeyPath);
     database = "SQLite3";
     databasePath = "/var/db/vouchers.sqlite3";
+    inherit letsEncryptAdminEmail;
+    domain = issuerDomain;
   };
 
   system.stateVersion = stateVersion;
diff --git a/morph/make-grid.nix b/morph/make-grid.nix
index 32e0b98e..de10df1e 100644
--- a/morph/make-grid.nix
+++ b/morph/make-grid.nix
@@ -3,11 +3,11 @@
 # and a function that takes the grid configuration as an argument and returns
 # a set of nodes specifying the addresses and NixOS configurations for each
 # server in the morph network.
-{ name, nodes }:
+{ name, config, nodes }:
 let
   pkgs = import <nixpkgs> { };
   # Load our JSON configuration for later use.
-  cfg = pkgs.lib.trivial.importJSON ./grid.config.json;
+  cfg = pkgs.lib.trivial.importJSON config;
 in
 {
   network =  {
diff --git a/morph/make-storage.nix b/morph/make-storage.nix
index 768cdb55..84a13be3 100644
--- a/morph/make-storage.nix
+++ b/morph/make-storage.nix
@@ -9,7 +9,7 @@
                              # to avoid breaking some software such as
                              # database servers. You should change this only
                              # after NixOS release notes say you should.
-
+, ...
 }: rec {
   deployment = {
     secrets = {
diff --git a/morph/testing-grid.config.json b/morph/testing-grid.config.json
new file mode 100644
index 00000000..018367db
--- /dev/null
+++ b/morph/testing-grid.config.json
@@ -0,0 +1,5 @@
+{ "publicStoragePort": 8898
+, "ristrettoSigningKeyPath": "../../PrivateStorageSecrets/ristretto.signing-key"
+, "issuerDomain": "payments.privatestorage-staging.com"
+, "letsEncryptAdminEmail": "jean-paul@privatestorage.io"
+}
diff --git a/morph/testing-grid.nix b/morph/testing-grid.nix
index 5591827e..b4b0649d 100644
--- a/morph/testing-grid.nix
+++ b/morph/testing-grid.nix
@@ -3,8 +3,14 @@
 # with the production grid and have one fewer possible point of divergence.
 import ./make-grid.nix {
   name = "Testing";
+  config = ./testing-grid.config.json;
   nodes = cfg: {
-    "testing000" = import ./testing000.nix (cfg // {
+    "payments.privatestorage-staging.com" = import ./issuer.nix ({
+      hardware = ./issuer-aws.nix;
+      stateVersion = "19.03";
+    } // cfg);
+
+    "35.157.216.200" = import ./testing000.nix (cfg // {
       publicIPv4 = "35.157.216.200";
     });
   };
diff --git a/morph/testing000.nix b/morph/testing000.nix
index e5f9c3f3..d45086ae 100644
--- a/morph/testing000.nix
+++ b/morph/testing000.nix
@@ -1,4 +1,4 @@
-{ publicIPv4, publicStoragePort, ristrettoSigningKeyPath }: rec {
+{ publicIPv4, publicStoragePort, ristrettoSigningKeyPath, ... }: rec {
 
   deployment = {
     secrets = {
diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix
index 2cd63cea..88413333 100644
--- a/nixos/modules/issuer.nix
+++ b/nixos/modules/issuer.nix
@@ -64,11 +64,18 @@ in {
         type is being used.
       '';
     };
+    services.private-storage-issuer.letsEncryptAdminEmail = lib.mkOption {
+      type = lib.types.str;
+      description = ''
+        An email address to give to Let's Encrypt as an operational contact
+        for the service's TLS certificate.
+      '';
+    };
   };
 
   config =
     let
-      acme = "/var/lib/acme";
+      certroot = "/var/lib/letsencrypt/live";
     in lib.mkIf cfg.enable {
     # Add a systemd service to run PaymentServer.
     systemd.services.zkapissuer = {
@@ -80,8 +87,8 @@ in {
         # interfaces.
         "network.target"
       ];
-      # Make sure we at least have a self-signed certificate.
-      requires = lib.optional cfg.tls "acme-selfsigned-${cfg.domain}.service";
+      # Make sure we at least have a certificate.
+      requires = lib.optional cfg.tls "cert-${cfg.domain}";
 
       serviceConfig = {
         ExecStart =
@@ -100,12 +107,9 @@ in {
               if cfg.tls
               then
                 "--https-port 443 " +
-                # acme has plugins to write the files in different ways but the
-                # self-signed certificate generator doesn't.  The files it
-                # writes are weirdly named and shaped but they work.
-                "--https-certificate-path ${acme}/${cfg.domain}/full.pem " +
-                "--https-certificate-chain-path ${acme}/${cfg.domain}/fullchain.pem " +
-                "--https-key-path ${acme}/${cfg.domain}/key.pem"
+                "--https-certificate-path ${certroot}/${cfg.domain}/cert.pem " +
+                "--https-certificate-chain-path ${certroot}/${cfg.domain}/chain.pem " +
+                "--https-key-path ${certroot}/${cfg.domain}/privkey.pem"
               else
                 # Only for automated testing.
                 "--http-port 80";
@@ -121,25 +125,30 @@ in {
 
     # Certificate renewal.  Note that preliminarySelfsigned only creates the
     # service.  We must declare that we *require* it in our service above.
-    security.acme = if cfg.tls
-      then {
-        production = false;
-        preliminarySelfsigned = true;
-        certs."${cfg.domain}" = {
-          email = "jean-paul@privatestorage.io";
-          postRun = "systemctl restart zkapissuer.service";
-          webroot = "${acme}/acme-challenges";
-          plugins = [ "account_key.json" "full.pem" "fullchain.pem" "key.pem" ];
-        };
-      }
-      else {};
-
-    services.nginx.virtualHosts = if cfg.tls
-      then {
-        "${cfg.domain}" = {
-          locations."/" = "${acme}/acme-challenges";
-        };
-      }
-      else {};
+    systemd.services."cert-${cfg.domain}" = {
+      enable = true;
+      description = "Issue/Renew certificate for ${cfg.domain}";
+      wantedBy = [ "zkapissuer.service" ];
+      serviceConfig = {
+        ExecStart =
+        let
+          configArgs = "--config-dir /var/lib/letsencrypt --work-dir /var/run/letsencrypt --logs-dir /var/run/log/letsencrypt";
+        in
+          pkgs.writeScript "cert-${cfg.domain}-start.sh" ''
+          #!${pkgs.runtimeShell} -e
+          # Register if necessary.
+          ${pkgs.certbot}/bin/certbot register ${configArgs} --agree-tos -m ${cfg.letsEncryptAdminEmail} || true
+          # Obtain the certificate.
+          ${pkgs.certbot}/bin/certbot certonly ${configArgs} -n --standalone --domains ${cfg.domain}
+          # Restart the server so the new certificate gets used.
+          systemctl restart zkapissuer.service
+          '';
+      };
+    };
+    # Open 80 and 443 for the certbot HTTP server and the PaymentServer HTTPS server.
+    networking.firewall.allowedTCPPorts = [
+      80
+      443
+    ];
   };
 }
diff --git a/nixpkgs.rev b/nixpkgs.rev
index 41802ea6..339195d5 100644
--- a/nixpkgs.rev
+++ b/nixpkgs.rev
@@ -1 +1 @@
-8bf142e001b6876b021c8ee90c2c7cec385fe8e9
\ No newline at end of file
+353333ef340952c05332e3c271dff953264cb017
\ No newline at end of file
-- 
GitLab