Skip to content
Snippets Groups Projects
deployment.nix 4.22 KiB
Newer Older
# A NixOS module which enables remotely-triggered deployment updates.
  # A handy alias for our part of the configuration.
  cfg = config.services.private-storage.deployment;

  # Compute an authorized_keys line that allows the holder of a certain key to
  # execute a certain command *only*.
  restrictedKey =
    { authorizedKey, command, gridName }:
    # `restrict` means "disable all the things" then `command` means "but
    # enable running this one command" (the client does not have to supply the
    # command; if they authenticate, this is the command that will run).
    "restrict,command=\"${command} ${gridName}\" ${authorizedKey}";
    services.private-storage.deployment.authorizedKey = lib.mkOption {
      type = lib.types.str;
        ssh-ed25519 AAAAC3N...
      '';
      description = ''
        The SSH public key to authorize to trigger a deployment update.
      '';
    };
    services.private-storage.deployment.gridName = lib.mkOption {
      type = lib.types.str;
      example = "staging";
      description = ''
        The name of the grid configuration to use to update this deployment.
      '';
    };
    # Configure the system to use our binary cache so that deployment updates
    # only require downloading pre-built software, not building it ourselves.
    nix = {
      binaryCachePublicKeys = [
        "saxtons.private.storage:MplOcEH8G/6mRlhlKkbA8GdeFR3dhCFsSszrspE/ZwY="
      ];
      binaryCaches = [
        "http://saxtons.private.storage"
      ];
    };

    # Create a one-time service that will set up an ssh key that allows the
    # deployment user to authorize as root to perform the system update with
    # `morph deploy`.
    systemd.services.authorize-morph-as-root = {
      enable = true;
      serviceConfig = {
        # Tell systemd that the service is a process that runs and then exits.
        # By being "oneshot" instead of "simple" any dependencies are not
        # started until after the process exits.  We have no dependencies yet
        # but if we did it would be more correct for them to wait until we are
        # done.
        #
        # It is not clear that "oneshot" means "run once" though (maybe it
        # does, I can't tell) so the script is robust in the face of repeated
        # runs even though it should only ever need to be run once.
        Type = "oneshot";
      };
      wantedBy = [
        # Run this to reach the multi-user target, a good target that is
        # reached in the typical course of system startup.
        "multi-user.target"
      ];
      # Here's the program to run for this unit.  It's a shell script that
      # creates an ssh key that authorized root access via ssh and give it to
      # the deployment user.  If such a key appears to exist already, do
      # nothing.
      script = ''
        KEY=~deployment/.ssh/morph_key
        TMP="$KEY"_tmp
        if [ ! -e "$KEY" ]; then
          mkdir -p ~deployment/.ssh ~root/.ssh
          chown deployment ~deployment/.ssh
          ${pkgs.openssh}/bin/ssh-keygen -f "$TMP"
          cat "$TMP".pub >> ~root/.ssh/authorized_keys
          mv "$TMP".pub "$KEY".pub
          mv "$TMP" "$KEY"
    # Raise the hard-limit on the size of $XDG_RUNTIME_DIR (ie
    # /run/user/<uid>).  The default of 10% is too small on some systems for
    # the temporary state morph creates to do the self-update.
    services.logind.extraConfig = ''
      RuntimeDirectorySize=50%
    '';

      # A user must be either normal or system.  A normal user uses the
      # default shell, has a home directory created for it at the usual
      # location, and is in the "users" group.  That's pretty much what we
      # want for the deployment user.
      isNormalUser = true;
      packages = [
        # update-deployment dependencies
        pkgs.morph
        pkgs.git
      ];

      # Authorize the supplied key to run the deployment update command.
        (restrictedKey {
          inherit (cfg) authorizedKey gridName;
          command = ./update-deployment;
        })