Newer
Older
# A NixOS module which enables remotely-triggered deployment updates.
{ config, lib, pkgs, ... }:
# 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 {
ssh-ed25519 AAAAC3N...
'';
description = ''
The SSH public key to authorize to trigger a deployment update.
'';
};
services.private-storage.deployment.gridName = lib.mkOption {
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;
})
];
};
};
}