diff --git a/morph/grid.config.json b/morph/grid.config.json
index 178f44d39e5dd88ef709f92713d579c2cd32caff..5b848d31264fb84017752a76376467466c717f35 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 c29b8866ce5ac82bb06d26c468dcd12bd7fb6d6f..0de7007d3cc98ff90f8a1bc01461014c0149444e 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
@@ -16,6 +17,11 @@ import ./make-grid.nix {
     # doesn't specify one.
     #
     # The names must be unique!
+    "payments.privatestorage.io" = import ./issuer.nix ({
+      hardware = ./issuer-aws.nix;
+      stateVersion = "19.03";
+    } // cfg);
+
     "storage001" = import ./make-storage.nix ({
         cfg = import ./storage001-config.nix;
         hardware = ./storage001-hardware.nix;
diff --git a/morph/issuer-aws.nix b/morph/issuer-aws.nix
new file mode 100644
index 0000000000000000000000000000000000000000..b4d4757ad5597b69363ef12e4297aec80913f00e
--- /dev/null
+++ b/morph/issuer-aws.nix
@@ -0,0 +1,4 @@
+{
+  imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];
+  ec2.hvm = true;
+}
diff --git a/morph/issuer.nix b/morph/issuer.nix
new file mode 100644
index 0000000000000000000000000000000000000000..ddf01bdfc832a13aee357475ef639c539ea5bda3
--- /dev/null
+++ b/morph/issuer.nix
@@ -0,0 +1,37 @@
+{ hardware
+, ristrettoSigningKeyPath
+, issuerDomain
+, letsEncryptAdminEmail
+, stateVersion
+, ...
+}: {
+  deployment = {
+    secrets = {
+      "ristretto-signing-key" = {
+        source = ristrettoSigningKeyPath;
+        destination = "/var/secrets/ristretto.signing-key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "zkapissuer.service"];
+      };
+    };
+  };
+
+  imports = [
+    hardware
+    ../nixos/modules/issuer.nix
+  ];
+
+  services.private-storage-issuer = {
+    enable = true;
+    # XXX This should be passed as a path.
+    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 32e0b98ed3ffd5741522f9b0e3ef67037fa6a180..de10df1e9a62ee0ac7fde98070743ee4a9cf484b 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 768cdb55f304ef0708a2af434df772861572502d..84a13be3c4089194b0d1a1ff6218b1161c1537f0 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 0000000000000000000000000000000000000000..018367db9da09364c718a521dd28ef06a2642288
--- /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 5591827e3e02423abeeb31123c383bd42dfdf1c5..b4b0649d8af349cd08e2b147ffdd207f32e8d1c6 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 e5f9c3f32bf4c75fea438a309a92c372f44f8ff8..d45086ae90fb5dfd64b5181d1723c757d219e6bb 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 7cb7cde17b180dceb9c0f80d81bbf9325fbe56f8..fc0d2355f204c3192133e3d63c210aefce7a467b 100644
--- a/nixos/modules/issuer.nix
+++ b/nixos/modules/issuer.nix
@@ -15,9 +15,26 @@ in {
         The package to use for the ZKAP issuer.
       '';
     };
+    services.private-storage-issuer.domain = lib.mkOption {
+      default = "payments.privatestorage.io";
+      type = lib.types.str;
+      example = lib.literalExample "payments.example.com";
+      description = ''
+        The domain name at which the issuer is reachable.
+      '';
+    };
+    services.private-storage-issuer.tls = lib.mkOption {
+      default = true;
+      type = lib.types.bool;
+      description = ''
+        Whether or not to listen on TLS.  For real-world use you should always
+        listen on TLS.  This is provided as an aid to automated testing where
+        it might be difficult to obtain a real certificate.
+      '';
+    };
     services.private-storage-issuer.issuer = lib.mkOption {
       default = "Ristretto";
-      type = lib.types.str;
+      type = lib.types.enum [ "Trivial" "Ristretto" ];
       example = lib.literalExample "Trivial";
       description = ''
         The issuer algorithm to use.  Either Trivial for a fake no-crypto
@@ -32,33 +49,108 @@ in {
         ``Ristretto``.
       '';
     };
+    services.private-storage-issuer.database = lib.mkOption {
+      default = "Memory";
+      type = lib.types.enum [ "Memory" "SQLite3" ];
+      description = ''
+        The kind of voucher database to use.
+      '';
+    };
+    services.private-storage-issuer.databasePath = lib.mkOption {
+      default = null;
+      type = lib.types.str;
+      description = ''
+        The path to a database file in the filesystem, if the SQLite3 database
+        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 = lib.mkIf cfg.enable {
+  config =
+    let
+      certroot = "/var/lib/letsencrypt/live";
+    in lib.mkIf cfg.enable {
     # Add a systemd service to run PaymentServer.
     systemd.services.zkapissuer = {
       enable = true;
       description = "ZKAP Issuer";
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
 
+      # Make sure we have a certificate the first time, if we are running over
+      # TLS and require a certificate.
+      requires = lib.optional cfg.tls "cert-${cfg.domain}.service";
+
+      after = [
+        # Make sure there is a network so we can bind to all of the
+        # interfaces.
+        "network.target"
+      ] ++
+        # Make sure we run after the certificate is issued, if we are running
+        # over TLS and require a certificate.
+        lib.optional cfg.tls "cert-${cfg.domain}.service";
+
+      # It really shouldn't ever exit on its own!  If it does, it's a bug
+      # we'll have to fix.  Restart it and hope it doesn't happen too much
+      # before we can fix whatever the issue is.
+      serviceConfig.Restart = "always";
+      serviceConfig.Type = "simple";
+
+      script =
+        let
+          # Compute the right command line arguments to pass to it.  The
+          # signing key is only supplied when using the Ristretto issuer.
+          issuerArgs =
+            if cfg.issuer == "Trivial"
+              then "--issuer Trivial"
+              else "--issuer Ristretto --signing-key ${cfg.ristrettoSigningKey}";
+          databaseArgs =
+            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}/${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";
+        in
+          "${cfg.package}/bin/PaymentServer-exe ${issuerArgs} ${databaseArgs} ${httpsArgs}";
+    };
+
+    # Certificate renewal.  We must declare that we *require* it in our
+    # service above.
+    systemd.services."cert-${cfg.domain}" = {
+      enable = true;
+      description = "Issue/Renew certificate for ${cfg.domain}";
       serviceConfig = {
         ExecStart =
-          let
-            # Compute the right command line arguments to pass to it.  The
-            # signing key is only supplied when using the Ristretto issuer.
-            args =
-              if cfg.issuer == "Trivial"
-                then "--issuer Trivial"
-                else "--issuer Ristretto --signing-key ${cfg.ristrettoSigningKey}";
-          in
-            "${cfg.package}/bin/PaymentServer-exe ${args}";
-        Type = "simple";
-        # It really shouldn't ever exit on its own!  If it does, it's a bug
-        # we'll have to fix.  Restart it and hope it doesn't happen too much
-        # before we can fix whatever the issue is.
-        Restart = "always";
+        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} --non-interactive --agree-tos -m ${cfg.letsEncryptAdminEmail} || true
+          # Obtain the certificate.
+          ${pkgs.certbot}/bin/certbot certonly ${configArgs} --non-interactive --standalone --domains ${cfg.domain}
+          '';
       };
     };
+    # Open 80 and 443 for the certbot HTTP server and the PaymentServer HTTPS server.
+    networking.firewall.allowedTCPPorts = [
+      80
+      443
+    ];
   };
 }
diff --git a/nixos/modules/tests/get-passes.py b/nixos/modules/tests/get-passes.py
index ac5cf790fd95d8ce5295879a04d4c19026bf2dba..d76ce3ccbf703bfabd08e120f84ed39063339297 100755
--- a/nixos/modules/tests/get-passes.py
+++ b/nixos/modules/tests/get-passes.py
@@ -13,15 +13,19 @@ from time import sleep
 
 def main():
     clientAPIRoot, issuerAPIRoot = argv[1:]
+    if not clientAPIRoot.endswith("/"):
+        clientAPIRoot += "/"
+    if not issuerAPIRoot.endswith("/"):
+        issuerAPIRoot += "/"
 
     # Construct a voucher that's acceptable to various parts of the system.
     voucher = "a" * 44
 
-    zkapauthz = clientAPIRoot + "/storage-plugins/privatestorageio-zkapauthz-v1"
+    zkapauthz = clientAPIRoot + "storage-plugins/privatestorageio-zkapauthz-v1"
 
     # Simulate a payment for a voucher.
     post(
-        issuerAPIRoot + "/v1/stripe/webhook",
+        issuerAPIRoot + "v1/stripe/webhook",
         dumps(charge_succeeded_json(voucher)),
         headers={"content-type": "application/json"},
     )
diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix
index cc2fa798f3654a0d249c8c21a9c9499a0740fc43..b731404dcad8c6b0729778f9778e9fdbec311b26 100644
--- a/nixos/modules/tests/private-storage.nix
+++ b/nixos/modules/tests/private-storage.nix
@@ -10,7 +10,7 @@ let
   exercise-storage = ./exercise-storage.py;
 
   # The root URL of the Ristretto-flavored PrivacyPass issuer API.
-  issuerURL = "http://issuer:8081/";
+  issuerURL = "http://issuer/";
 
   # The issuer's signing key.  Notionally, this is a secret key.  This is only
   # the value for this system test though so I don't care if it leaks to the
@@ -81,6 +81,8 @@ import <nixpkgs/nixos/tests/make-test.nix> {
       ];
       services.private-storage-issuer = {
         enable = true;
+        domain = "issuer";
+        tls = false;
         issuer = "Ristretto";
         inherit ristrettoSigningKey;
       };
@@ -140,7 +142,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
 
       # Get some ZKAPs from the issuer.
       eval {
-        $client->succeed('set -eo pipefail; ${get-passes} http://127.0.0.1:3456 http://issuer:8081 | systemd-cat');
+        $client->succeed('set -eo pipefail; ${get-passes} http://127.0.0.1:3456 ${issuerURL} | systemd-cat');
         # succeed() is not success but 1 is.
         1;
       } or do {
diff --git a/nixos/pkgs/zkapissuer-repo.nix b/nixos/pkgs/zkapissuer-repo.nix
index 44182649362476fdc2b1b203d9f92a5e91f9af42..433ecfba94c162fd1ec4159cb34eedcf37b16b41 100644
--- a/nixos/pkgs/zkapissuer-repo.nix
+++ b/nixos/pkgs/zkapissuer-repo.nix
@@ -4,6 +4,6 @@ in
   pkgs.fetchFromGitHub {
     owner = "PrivateStorageio";
     repo = "PaymentServer";
-    rev = "028d26152eba4f034aba405caa17627a764c2bbe";
-    sha256 = "06hdln97r2ign7phf661wlzh3z06bk9906lvc0gm3lh1pa23d3gb";
+    rev = "6dfc02e395fbbec2c70a109874227ab21bddbb25";
+    sha256 = "1zc8cxc37zixsh8zcqasvg07rfsravlx0bhnx6zv9c5srm37iqap";
   }
\ No newline at end of file
diff --git a/nixpkgs.rev b/nixpkgs.rev
index 41802ea669e9d5bb48fff0fe62e5da506b741bf8..339195d572f2a1784ed2ed911834e0e571d639e2 100644
--- a/nixpkgs.rev
+++ b/nixpkgs.rev
@@ -1 +1 @@
-8bf142e001b6876b021c8ee90c2c7cec385fe8e9
\ No newline at end of file
+353333ef340952c05332e3c271dff953264cb017
\ No newline at end of file