# A NixOS module which enables remotely-triggered deployment updates. { config, lib, pkgs, ... }: let # 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}"; in { options = { services.private-storage.deployment.authorizedKey = lib.mkOption { type = lib.types.str; example = '' 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. ''; }; }; config = { # 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" chown deployment "$KEY" fi ''; }; # 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% ''; # Configure the deployment user. users.users.deployment = { # 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. openssh.authorizedKeys.keys = [ (restrictedKey { inherit (cfg) authorizedKey gridName; command = ./update-deployment; }) ]; }; }; }