Skip to content
Snippets Groups Projects
issuer.nix 8.71 KiB
Newer Older
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed
# A NixOS module which can run a Ristretto-based issuer for PrivateStorage
# ZKAPs.
{ lib, pkgs, config, ... }: let
  cfg = config.services.private-storage-issuer;
  # Our own nixpkgs fork:
  ourpkgs = import ../../nixpkgs-ps.nix {};
  imports = [
    # Give it a good SSH configuration.
    ../../nixos/modules/ssh.nix
  ];

  options = {
    services.private-storage-issuer.enable = lib.mkEnableOption "PrivateStorage ZKAP Issuer Service";
    services.private-storage-issuer.package = lib.mkOption {
      default = ourpkgs.zkapissuer.components.exes."PaymentServer-exe";
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed
      example = lib.literalExample "pkgs.zkapissuer.components.exes.\"PaymentServer-exe\"";
      description = ''
        The package to use for the ZKAP issuer.
      '';
    };
    services.private-storage-issuer.domains = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      example = lib.literalExample [ "payments.example.com" ];
      description = ''
        The domain names 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";
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed
      type = lib.types.enum [ "Trivial" "Ristretto" ];
      example = lib.literalExample "Trivial";
      description = ''
        The issuer algorithm to use.  Either Trivial for a fake no-crypto
        algorithm or Ristretto for Ristretto-flavored PrivacyPass.
      '';
    };
    services.private-storage-issuer.ristrettoSigningKeyPath = lib.mkOption {
      type = lib.types.path;
        The path to a file containing the Ristretto signing key to use.
        Required if the issuer is ``Ristretto``.
    services.private-storage-issuer.stripeSecretKeyPath = lib.mkOption {
      type = lib.types.path;
      description = ''
        The path to a file containing a Stripe secret key to use for charge
        and payment management.
      '';
    };
    services.private-storage-issuer.stripeEndpointDomain = lib.mkOption {
      type = lib.types.str;
      description = ''
        The domain name for the Stripe API HTTP endpoint.
      '';
      default = "api.stripe.com";
    };
    services.private-storage-issuer.stripeEndpointScheme = lib.mkOption {
      type = lib.types.enum [ "HTTP" "HTTPS" ];
      description = ''
        Whether to use HTTP or HTTPS for the Stripe API.
      '';
      default = "HTTPS";
    };
    services.private-storage-issuer.stripeEndpointPort = lib.mkOption {
      type = lib.types.int;
      description = ''
        The port number for the Stripe API HTTP endpoint.
      '';
      default = 443;
    };
    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.
      '';
    };
    services.private-storage-issuer.allowedChargeOrigins = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      description = ''
        The CORS "Origin" values which are allowed to submit charges to the
        payment server.  Note this is not currently enforced by the
        PaymentServer.  It just controls the CORS headers served.
      '';
    };
      # We'll refer to this collection of domains by the first domain in the
      # list.
      domain = builtins.head cfg.domains;
      certServiceName = "acme-${domain}";
      # Payment server internal http port (arbitrary, non-priviledged):
      internalHttpPort = "1061";
    in lib.mkIf cfg.enable {
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed
    # Add a systemd service to run PaymentServer.
    systemd.services.zkapissuer = {
      enable = true;
      description = "ZKAP Issuer";
      wantedBy = [ "multi-user.target" ];

      # Make sure we have a certificate the first time, if we are running over
      # TLS and require a certificate.
      # 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
        # 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 "${certServiceName}.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";

      # Run w/o privileges
      serviceConfig = {
        DynamicUser = false;
        User = "zkapissuer";
        Group = "zkapissuer";
      };

      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-path ${cfg.ristrettoSigningKeyPath}";
          databaseArgs =
            if cfg.database == "Memory"
              then "--database Memory"
              else "--database SQLite3 --database-path ${cfg.databasePath}";
          httpArgs = "--http-port ${internalHttpPort}";
          prefixOption = s: "--cors-origin=" + s;
          originStrings = map prefixOption cfg.allowedChargeOrigins;
          originArgs = builtins.concatStringsSep " " originStrings;

          stripeArgs =
            "--stripe-key-path ${cfg.stripeSecretKeyPath} " +
            "--stripe-endpoint-domain ${cfg.stripeEndpointDomain} " +
            "--stripe-endpoint-scheme ${cfg.stripeEndpointScheme} " +
            "--stripe-endpoint-port ${toString cfg.stripeEndpointPort}";
          "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpArgs} ${stripeArgs}";
    # 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.
    networking.firewall.allowedTCPPorts = [
      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."/" = {
          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}";