-{ config, lib, pkgs, ... }:
+# auto-upgrade-with-pinch: Secure managed NixOS updates
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, version 3.
+
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
with lib;
let
+ local-pkgs = import ../. { inherit pkgs; };
cfg = config.system.autoUpgradeWithPinch;
- pull-repo-script =
- pkgs.writeShellScript "pull-repo" ''
- set -eo pipefail
-
- path=$1
- config=$2
-
- prop() {
- ${pkgs.jq}/bin/jq -r ".$1" <<< "$config"
- }
-
- echo Pulling in "$path" >&2
+ pull-repo-script = pkgs.writeShellScript "pull-repo" ''
+ set -eo pipefail
- if [[ ! -e "$path" ]];then
- d=$(mktemp -d)
- ${pkgs.git}/bin/git init "$d"
- ${pkgs.git}/bin/git -C "$d" checkout -b "$(prop localBranch)"
- ${pkgs.git}/bin/git -C "$d" remote add "$(prop remoteName)" "$(prop url)"
- ${pkgs.git}/bin/git -C "$d" branch -u "$(prop remoteBranch)"
- mkdir -p "$(${pkgs.coreutils}/bin/dirname "$path")"
- mv "$d" "$path"
- fi
+ path=$1
+ config=$2
- cd "$path"
-
- if [[ "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" != "$(prop url)" ]]; then
- echo Expected git remote "$(prop remoteName)" to point at "$(prop url)" \
- but it points at "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" >&2
- case "$(prop onRemoteURLMismatch)" in
- abort) exit 1;;
- update) echo Updating it >&2
- ${pkgs.git}/bin/git -C "$d" remote set-url "$(prop remoteName)" "$(prop url)";;
- esac
- fi
+ prop() {
+ ${pkgs.jq}/bin/jq -r ".$1" <<< "$config"
+ }
- ${pkgs.git}/bin/git fetch "$(prop remoteName)" "$(prop remoteBranch)"
+ echo Pulling in "$path" >&2
+
+ if [[ ! -e "$path" ]];then
+ d=$(mktemp -d)
+ ${pkgs.git}/bin/git init "$d"
+ ${pkgs.git}/bin/git -C "$d" checkout -b "$(prop localBranch)"
+ ${pkgs.git}/bin/git -C "$d" remote add "$(prop remoteName)" "$(prop url)"
+ ${pkgs.git}/bin/git -C "$d" branch -u "$(prop remoteBranch)"
+ mkdir -p "$(${pkgs.coreutils}/bin/dirname "$path")"
+ mv "$d" "$path"
+ fi
+
+ cd "$path"
+
+ if [[ "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" != "$(prop url)" ]]; then
+ echo Expected git remote "$(prop remoteName)" to point at "$(prop url)" \
+ but it points at "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" >&2
+ case "$(prop onRemoteURLMismatch)" in
+ abort) exit 1;;
+ update) echo Updating it >&2
+ ${pkgs.git}/bin/git -C "$d" remote set-url "$(prop remoteName)" "$(prop url)";;
+ esac
+ fi
+
+ ${pkgs.git}/bin/git fetch "$(prop remoteName)" "$(prop remoteBranch)"
+
+ if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != "$(prop localBranch)" ]];then
+ echo Could not merge because currently-checked-out \
+ \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \
+ \""$(prop localBranch)"\"
+ case "$(prop onBranchMismatch)" in
+ abort) exit 1;;
+ continue) exit 0;;
+ esac
+ fi
+
+ if [[ "$(prop requireSignature)" == true ]]; then
+ ${pkgs.polite-merge}/bin/polite-merge \
+ -c gpg.program=${escapeShellArg (local-pkgs.keyed-gpg cfg.signingKeys)} \
+ merge --ff-only --verify-signatures
+ else
+ ${pkgs.polite-merge}/bin/polite-merge merge --ff-only
+ fi
+ '';
- if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != "$(prop localBranch)" ]];then
- echo Could not merge because currently-checked-out \
- \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \
- \""$(prop localBranch)"\"
- case "$(prop onBranchMismatch)" in
- abort) exit 1;;
- continue) exit 0;;
- esac
- fi
+ auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
+ ${pkgs.coreutils}/bin/nice -n 17 \
+ ${pkgs.util-linux}/bin/ionice -c 3 \
+ ${pkgs.util-linux}/bin/flock /run/auto-upgrade-with-pinch ${pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
+ set -eo pipefail
- if [[ "$(prop requireSignature)" == true ]]; then
- ${pkgs.polite-merge}/bin/polite-merge \
- -c gpg.program='${pkgs.keyedgpg} '"$(prop 'signingKeys[]' | tr \\n ' ')"' --' \
- merge --ff-only --verify-signatures
- else
- ${pkgs.polite-merge}/bin/polite-merge merge --ff-only
+ dry_run=false
+ pinch_args=()
+ if [[ "$1" == --dry-run ]];then
+ dry_run=true
+ pinch_args=( --dry-run )
fi
- '';
-
- auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
- ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${
- pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
- set -eo pipefail
- dry_run=false
- pinch_args=()
- if [[ "$1" == --dry-run ]];then
- dry_run=true
- pinch_args=( --dry-run )
+ hydrate() {
+ if "$dry_run";then
+ echo "Would run: $*"
+ else
+ "$@"
fi
+ }
- hydrate() {
- if "$dry_run";then
- echo "Would run: $*"
- else
- "$@"
- fi
- }
-
- die() {
- echo "$*" >&2
- exit 1
- }
+ die() {
+ echo "$*" >&2
+ exit 1
+ }
- in_tmpdir() {
- d=$(${pkgs.coreutils}/bin/mktemp -d)
- pushd "$d"
- "$@"
- popd
- ${pkgs.coreutils}/bin/rm -r "$d"
- }
+ in_tmpdir() {
+ d=$(${pkgs.coreutils}/bin/mktemp -d)
+ pushd "$d"
+ "$@"
+ popd
+ ${pkgs.coreutils}/bin/rm -r "$d"
+ }
- ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
+ ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (
+ ''
verify_ownership() {
if [[ "$1" != /* ]];then
die "Unexpected relative path: $1"
fi
fi
'';
- }."${cfg.upgradeConfigOwnershipPolicy}"
+ }
+ ."${cfg.upgradeConfigOwnershipPolicy}"
}
}
''
- + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
- cfg.upgradeConfig)}
-
- config=$(${pkgs.nix}/bin/nix eval --json -f ${../upgrade-config.nix} \
- --arg upgradeConfig ${
- escapeShellArg ("["
- + lib.concatMapStringsSep " " lib.strings.escapeNixString
- cfg.upgradeConfig + "]")
- } config)
-
- config_query() {
- ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
- }
+ + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}") cfg.upgradeConfig
+ )}
+
+ config=$(${pkgs.nix}/bin/nix-instantiate --eval --strict --json -A config \
+ --arg upgradeConfig ${
+ escapeShellArg (
+ "[" + lib.concatMapStringsSep " " lib.strings.escapeNixString cfg.upgradeConfig + "]"
+ )
+ } ${../upgrade-config.nix})
+
+ config_query() {
+ ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
+ }
- repo_query() {
- config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
- }
+ repo_query() {
+ config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
+ }
- userenv_query() {
- config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
- }
+ userenv_query() {
+ config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
+ }
- # Pull updates
- while read path;do
- hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
- ${pull-repo-script} "$path" "$(repo_query "$path" "")"
- done < <( config_query '.repos | keys []' )
-
- # Update channels
- config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
-
- # Build
- in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
- while read user;do
- hydrate /run/wrappers/bin/sudo -u "$user" \
- ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
- done < <( config_query '.userEnvironments | keys []' )
-
- # Install
- hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
- while read user;do
- remove_arg=-r
- if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
- remove_arg=
- fi
- hydrate /run/wrappers/bin/sudo -u "$user" \
- ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
- done < <( config_query '.userEnvironments | keys []' )
- ''
- }
+ # Pull updates
+ while read path;do
+ hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
+ ${pull-repo-script} "$path" "$(repo_query "$path" "")"
+ done < <( config_query '.repos | keys []' )
+
+ # Update channels
+ config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
+
+ # Build
+ in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
+ while read user;do
+ pushd /
+ hydrate /run/wrappers/bin/sudo -u "$user" \
+ ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
+ popd
+ done < <( config_query '.userEnvironments | keys []' )
+ sync
+
+ # Install
+ hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
+ sync
+ while read user;do
+ remove_arg=-r
+ if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
+ remove_arg=
+ fi
+ hydrate /run/wrappers/bin/sudo -u "$user" \
+ ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
+ sync
+ done < <( config_query '.userEnvironments | keys []' )
+ ''}
'';
-in {
+in
+{
options = {
system.autoUpgradeWithPinch = {
'';
};
+ signingKeys = mkOption {
+ type = types.listOf types.path;
+ description = ''
+ Files containing GPG keys that are authorized to sign updates.
+ Updates are only merged if the commit at the tip of the remote
+ ref is signed with one of these keys.
+ '';
+ };
+
upgradeConfig = mkOption {
type = types.listOf types.path;
description = ''
};
upgradeConfigOwnershipPolicy = mkOption {
- type = types.enum [ "root" "wheel" "any" ];
+ type = types.enum [
+ "root"
+ "wheel"
+ "any"
+ ];
default = "root";
description = ''
Verify ownership of upgrade config files before using them for
config = lib.mkIf cfg.enable {
- security.sudo.extraRules = lib.mkAfter [{
- groups = [ "users" ];
- commands = [{
- command = "${auto-upgrade-script}";
- options = [ "NOPASSWD" "NOSETENV" ];
- }];
- }];
+ security.sudo.extraRules = lib.mkAfter [
+ {
+ groups = [ "users" ];
+ runAs = "root";
+ commands = [
+ {
+ command = "${auto-upgrade-script}";
+ options = [
+ "NOPASSWD"
+ "NOSETENV"
+ ];
+ }
+ ];
+ }
+ ];
# NOSETENV above still allows through ~17 vars, including PATH. Block those
# as well:
security.sudo.extraConfig = ''
'';
nixpkgs.overlays = [
- (import ../overlays/keyedgpg.nix)
(import ../overlays/pinch.nix)
(import ../overlays/polite-merge.nix)
(self: super: {
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig.Type = "oneshot";
- environment = config.nix.envVars // {
- inherit (config.environment.sessionVariables) NIX_PATH;
- HOME = "/root";
- } // config.networking.proxy.envVars;
+ environment =
+ config.nix.envVars
+ // {
+ inherit (config.environment.sessionVariables) NIX_PATH;
+ HOME = "/root";
+ }
+ // config.networking.proxy.envVars;
path = with pkgs; [
config.nix.package.out