diff --git a/docs/source/dev/README.rst b/docs/source/dev/README.rst index 14d2de31f932a0aa50545643e30c679c36696e19..1a617e0bf8d753a1e64074b49aa9aae221274796 100644 --- a/docs/source/dev/README.rst +++ b/docs/source/dev/README.rst @@ -57,6 +57,20 @@ To update the channel, the script will need to be updated, along with the filenames that have the channel in them. +Gitlab Repositories +``````````````````` +To update the version of packages we import from gitlab, run: + +.. code: shell + + nix-shell --command 'tools/update-gitlab nixos/pkgs/<package>/repo.json' + +That will update the package to point at the latest version of the project.\ +The command uses branch and repository owner specified in the ``repo.json`` file, +but you can override them by passing the ``--branch`` or ``-owner`` arguments to the command. +A specific revision can also be pinned, by passing ``-rev``. + + Architecture overview --------------------- diff --git a/nixos/modules/packages.nix b/nixos/modules/packages.nix index d6518dcf290c27b95e3428434623a63cfbdb8e19..c4390dc00f3948e04e3e90ef270261cc0dd1cdbb 100644 --- a/nixos/modules/packages.nix +++ b/nixos/modules/packages.nix @@ -1,8 +1,13 @@ # A NixOS module which exposes custom packages to other modules. { pkgs, ...}: -{ +let + ourpkgs = pkgs.callPackage ../../nixos/pkgs {}; +in { config = { # Expose `nixos/pkgs` as a new module argument `ourpkgs`. - _module.args.ourpkgs = pkgs.callPackage ../../nixos/pkgs {}; + _module.args.ourpkgs = ourpkgs; + # Also expose it as a config setting, for usage by tests, + # since the `_module` config is not exposed in the result. + passthru.ourpkgs = ourpkgs; }; } diff --git a/nixos/modules/spending.nix b/nixos/modules/spending.nix new file mode 100644 index 0000000000000000000000000000000000000000..238fbe8f939c4ddb0c78b9a34e106dbea8e39921 --- /dev/null +++ b/nixos/modules/spending.nix @@ -0,0 +1,139 @@ +# A NixOS module which can run a Ristretto-based issuer for PrivateStorage +# ZKAPs. +{ lib, pkgs, config, ourpkgs, ... }@args: let + cfg = config.services.private-storage-spending; +in +{ + options = { + services.private-storage-spending = { + enable = lib.mkEnableOption "PrivateStorage Spending Service"; + package = lib.mkOption { + default = ourpkgs.zkap-spending-service; + type = lib.types.package; + example = lib.literalExample "ourpkgs.zkap-spending-service"; + description = '' + The package to use for the spending service. + ''; + }; + unixSocket = lib.mkOption { + default = "/run/zkap-spending-service/api.socket"; + type = lib.types.path; + description = '' + The unix socket that the spending service API listens on. + ''; + }; + }; + services.private-storage-spending.domain = lib.mkOption { + default = config.networking.fqdn; + type = lib.types.str; + example = lib.literalExample [ "spending.example.com" ]; + description = '' + The domain name at which the spending service is reachable. + ''; + }; + }; + + config = + lib.mkIf cfg.enable { + systemd.sockets.zkap-spending-service = { + enable = true; + wantedBy = [ "sockets.target" ]; + listenStreams = [ cfg.unixSocket ]; + }; + # Add a systemd service to run zkap-spending-service. + systemd.services.zkap-spending-service = { + enable = true; + description = "ZKAP Spending Service"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig.NonBlocking = true; + + # 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"; + + # Use a unnamed user. + serviceConfig.DynamicUser = true; + + serviceConfig = { + # Work around https://twistedmatrix.com/trac/ticket/10261 + # Create a runtime directory so that the service has permission + # to change the mode on the socket. + RuntimeDirectory = "zkap-spending-service"; + + # This set of restrictions is mostly dervied from + # - running `systemd-analyze security zkap-spending-service.service + # - Looking at the restrictions from the nixos nginx config. + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateNetwork = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = "AF_UNIX"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + # Lines starting with "~" are deny-list the others are allow-list + # Since the first line is allow, that bounds the set of allowed syscalls + # and the further lines restrict it. + SystemCallFilter = [ + # From systemd.exec(5), @system-service is "A reasonable set of + # system calls used by common system [...]" + "@system-service" + # This is from the nginx config, except that `@ipc` is not removed, + # since twisted uses a self-pipe. + "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" + ]; + Umask = "0077"; + }; + + script = let + httpArgs = "--http-endpoint systemd:domain=UNIX:index=0"; + in + "exec ${cfg.package}/bin/${cfg.package.meta.mainProgram} run ${httpArgs}"; + }; + + services.nginx = { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + virtualHosts."${cfg.domain}" = { + 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://unix:${cfg.unixSocket}"; + }; + locations."/" = { + # Return a 404 error for any paths not specified above. + extraConfig = '' + return 404; + ''; + }; + }; + }; + }; +} diff --git a/nixos/pkgs/default.nix b/nixos/pkgs/default.nix index 3d534430377cb5fbbf0739d60a8a7ca9bb0419f6..efcff08333a8c28e110e95f01c6c284c2411b594 100644 --- a/nixos/pkgs/default.nix +++ b/nixos/pkgs/default.nix @@ -20,5 +20,6 @@ let in { zkapissuer = callPackage ./zkapissuer {}; + zkap-spending-service = callPackage ./zkap-spending-service {}; inherit (ourpkgs) privatestorage leasereport; } diff --git a/nixos/pkgs/zkap-spending-service/default.nix b/nixos/pkgs/zkap-spending-service/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..4716109e7add7af74f032ee1668be1394cf05b17 --- /dev/null +++ b/nixos/pkgs/zkap-spending-service/default.nix @@ -0,0 +1,12 @@ +{ callPackage, fetchFromGitLab, lib }: +let + repo-data = lib.importJSON ./repo.json; + + repo = fetchFromGitLab (builtins.removeAttrs repo-data [ "branch" ]); +in +# We want to check the revision the service reports against the revsion +# that we install. The upsream derivation doesn't currently know its own +# version, but we do have it here. Thus, we add it as a meta attribute +# to the derviation provided from upstream. +lib.addMetaAttrs { inherit (repo-data) rev; } + (callPackage repo {}) diff --git a/nixos/pkgs/zkap-spending-service/repo.json b/nixos/pkgs/zkap-spending-service/repo.json new file mode 100644 index 0000000000000000000000000000000000000000..39aeb8404c890e4781ee77f2a93d85d68acee5c3 --- /dev/null +++ b/nixos/pkgs/zkap-spending-service/repo.json @@ -0,0 +1,9 @@ +{ + "owner": "privatestorage", + "repo": "zkap-spending-service", + "rev": "e0d63b79213d16f2de6629167ea8f1236ba22e14", + "branch": "main", + "domain": "whetstone.privatestorage.io", + "outputHash": "30abb0g9xxn4lp493kj5wmz8kj5q2iqvw40m8llqvb3zamx60gd8cy451ii7z15qbrbx9xmjdfw0k4gviij46fkx1s8nbich5c8qx57", + "outputHashAlgo": "sha512" +} diff --git a/nixos/system-tests.nix b/nixos/system-tests.nix index 73b6665ab91e4d9a8a2200fb0eec7ff596f79b39..7b6d382ada53c1121a1bc3d0edbf82964d644ad2 100644 --- a/nixos/system-tests.nix +++ b/nixos/system-tests.nix @@ -3,5 +3,6 @@ let pkgs = import ../nixpkgs-2105.nix { }; in { private-storage = pkgs.nixosTest ./tests/private-storage.nix; + spending = pkgs.nixosTest ./tests/spending.nix; tahoe = pkgs.nixosTest ./tests/tahoe.nix; } diff --git a/nixos/tests/spending.nix b/nixos/tests/spending.nix new file mode 100644 index 0000000000000000000000000000000000000000..c970157b9375e0d99e2be8d4f782992163a6c948 --- /dev/null +++ b/nixos/tests/spending.nix @@ -0,0 +1,32 @@ +{ pkgs, lib, ... }: +{ + name = "zkap-spending-service"; + nodes = { + spending = { config, pkgs, ourpkgs, modulesPath, ... }: { + imports = [ + ../modules/packages.nix + ../modules/spending.nix + ]; + + services.private-storage-spending.enable = true; + services.private-storage-spending.domain = "localhost"; + }; + }; + testScript = { nodes }: let + revision = nodes.spending.config.passthru.ourpkgs.zkap-spending-service.meta.rev; + curl = "${pkgs.curl}/bin/curl -sSf"; + in + '' + import json + + start_all() + + spending.wait_for_open_port(80) + with subtest("Ensure we can ping the spending service"): + output = spending.succeed("${curl} http://localhost/v1/_ping") + assert json.loads(output)["status"] == "ok", "Could not ping spending service." + with subtest("Ensure that the spending service version matches the expected version"): + output = spending.succeed("${curl} http://localhost/v1/_version") + assert json.loads(output)["revision"] == "${revision}", "Spending service revision does not match." + ''; +} diff --git a/tools/default.nix b/tools/default.nix index f9a0b1ff8d902f3072886939ad11e1e223ffbb7e..fb44c660e7d4a4a62cec6bb58a008a1bf00429dc 100644 --- a/tools/default.nix +++ b/tools/default.nix @@ -15,6 +15,7 @@ let }; python-commands = [ ./update-nixpkgs + ./update-gitlab-repo ]; in # This derivation creates a package that wraps our tools to setup an environment diff --git a/tools/update-gitlab-repo b/tools/update-gitlab-repo new file mode 100755 index 0000000000000000000000000000000000000000..ddc82cb7bfd943ed3b4b80f79cf9e47b447c8b7d --- /dev/null +++ b/tools/update-gitlab-repo @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +""" +Update a pinned gitlab repository. + +Pass this path to a JSON file and it will update it to the latest +version of the branch it specifies. You can also pass a different +branch or repository owner, which will update the file to point at +the new branch/repository, and update to the latest version. +""" + +import argparse +import json +from pathlib import Path + +import httpx +from ps_tools import get_url_hash + +HASH_TYPE = "sha512" + +ARCHIVE_TEMPLATE = "https://{domain}/api/v4/projects/{owner}%2F{repo}/repository/archive.tar.gz?sha={rev}" +BRANCH_TEMPLATE = ( + "https://{domain}/api/v4/projects/{owner}%2F{repo}/repository/branches/{branch}" +) + + +def get_gitlab_commit(config): + response = httpx.get(BRANCH_TEMPLATE.format(**config)) + response.raise_for_status() + return response.json()["commit"]["id"] + + +def get_gitlab_archive_url(config): + return ARCHIVE_TEMPLATE.format(**config) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "repo_file", + metavar="repo-file", + type=Path, + help="JSON file with pinned configuration.", + ) + parser.add_argument( + "--branch", + type=str, + help="Branch to update to.", + ) + parser.add_argument( + "--owner", + type=str, + help="Repository owner to update to.", + ) + parser.add_argument( + "--rev", + type=str, + help="Revision to pin.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + ) + args = parser.parse_args() + + repo_file = args.repo_file + config = json.loads(repo_file.read_text()) + + for key in ["owner", "branch"]: + if getattr(args, key) is not None: + config[key] = getattr(args, key) + + if args.rev is not None: + config["rev"] = args.rev + else: + config["rev"] = get_gitlab_commit(config) + + archive_url = get_gitlab_archive_url(config) + config.update(get_url_hash(HASH_TYPE, "source", archive_url)) + + output = json.dumps(config, indent=2) + if args.dry_run: + print(output) + else: + repo_file.write_text(output) + + +if __name__ == "__main__": + main()