# 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=\"sudo ${command}\" ${authorizedKey}"; update-script = pkgs.writeShellScript "update-deployment" '' set -euxo pipefail nix-env --profile /nix/var/nix/profiles/system --set -f ${lib.escapeShellArg ../top-level.nix} --argstr gridname ${lib.escapeShellArg cfg.gridName} --argstr hostname ${lib.escapeShellArg config.networking.hostName} /nix/var/nix/profiles/system/bin/switch-to-configuration switch ''; in { options = { services.private-storage.deployment.authorizedKey = lib.mkOption { type = lib.types.str; example = lib.literalExample '' 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 = lib.literalExample "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" ]; }; # This package is used by top-level.nix above. system.extraDependencies = [ pkgs.morph.lib ]; security.sudo.extraRules = [ { users = ["deployment"]; commands = [ "${update-script}" ]; runAs = "root"; } ]; # 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; # Authorize the supplied key to run the deployment update command. openssh.authorizedKeys.keys = [ (restrictedKey { inherit (cfg) authorizedKey gridName; command = update-script; }) ]; }; }; }