#!/usr/bin/env bash set -eo pipefail # User variables config_location="git://git@git.mattmor.in/org/nix/os-config.git" target_hostname="" target_destination="" target_user="ta" ssh_key="" ssh_port="22" remote_passwd="nixos" persist_dir="" # Create a temp directory for generated host keys temp=$(mktemp -d) # Cleanup temporary directory on exit function cleanup() { rm -rf "$temp" } trap cleanup exit function red() { echo -e "\x1B[31m[!] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[31m[!] $($2) \x1B[0m" fi } function green() { echo -e "\x1B[32m[+] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[32m[+] $($2) \x1B[0m" fi } function yellow() { echo -e "\x1B[33m[*] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[33m[*] $($2) \x1B[0m" fi } function yes_or_no() { while true; do read -rp "$* [y/n] (default: y): " yn yn=${yn:-y} case $yn in [Yy]*) return 0 ;; [Nn]*) return 1 ;; esac done } function sync() { # $1 = user, $2 = source, $3 = destination rsync -av --filter=':- .gitignore' -e "ssh -l $1" "$2" "$1"@"${target_destination}": } function help_and_exit() { echo echo "Remotely installs NixOS on a target machine using this nix-config." echo echo "USAGE: $0 -n= -d= -k= [OPTIONS]" echo echo "ARGS:" echo " -n= specify target_hostname of the target host to deploy the nixos config on." echo " -d= specify ip or url to the target host." echo " -k= specify the full path to the ssh_key you'll use for remote access to the" echo " target during install process." echo " Example: -k=/home/${target_user}/.ssh/my_ssh_key" echo echo "OPTIONS:" echo " -u= specify target_user with sudo access. nix-config will be cloned to their home." echo " Default='${target_user}'." echo " -p= Specify a password for target machine user. This is temporary until install is complete." echo " Default='${remote_passwd}'." echo " --port= specify the ssh port to use for remote access. Default=${ssh_port}." echo " --impermanence Use this flag if the target machine has impermanence enabled. WARNING: Assumes /persist path." echo " --debug Enable debug mode." echo " -h | --help Print this help." exit 0 } # Handle command-line arguments while [[ $# -gt 0 ]]; do case "$1" in -n=*) target_hostname="${1#-n=}" ;; -d=*) target_destination="${1#-d=}" ;; -u=*) target_user="${1#-u=}" ;; -k=*) ssh_key="${1#-k=}" ;; -p=*) remote_passwd="${1#-p=}" ;; --port=*) ssh_port="${1#--port=}" ;; --temp-override=*) temp="${1#--temp-override=}" ;; --impermanence) persist_dir="/persist" ;; --debug) set -x ;; -h | --help) help_and_exit ;; *) echo "Invalid option detected." help_and_exit ;; esac shift done # SSH commands ssh_cmd="ssh -oport=${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i $ssh_key -t $target_user@$target_destination" ssh_root_cmd=$(echo "$ssh_cmd" | sed "s|${target_user}@|root@|") # uses @ in the sed switch to avoid it triggering on the $ssh_key value scp_cmd="scp -oport=${ssh_port} -o StrictHostKeyChecking=no -i $ssh_key" function nixos_anywhere() { green "Installing NixOS on remote host $target_hostname at $target_destination" ### # nixos-anywhere extra-files generation ### green "Preparing a new ssh_host_ed25519_key pair for $target_hostname." # Create the directory where sshd expects to find the host keys install -d -m755 "$temp/$persist_dir/etc/ssh" # Generate host keys without a passphrase ssh-keygen -t ed25519 -f "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key" -C root@"$target_hostname" -N "" # ssh-keygen -t ed25519 -f "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key" -C "$target_user"@"$target_hostname" -N "" # Set the correct permissions so sshd will accept the key chmod 600 "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key" echo "Adding ssh host fingerprint at $target_destination to ~/.ssh/known_hosts" # This will fail if we already know the host, but that's fine ssh-keyscan -p "$ssh_port" "$target_destination" >>~/.ssh/known_hosts || true ### # nixos-anywhere installation ### cd nixos-installer # disko expects a passphrase on /tmp/disko-password, so we set it for now and will update the passphrase later green "Preparing a temporary password for disko." $ssh_root_cmd "/bin/sh -c 'echo passphrase > /tmp/disko-password'" # copy our repo there via rsync for speed green "Syncing nix-config to $target_hostname" sync root "$PWD" $ssh_root_cmd "nixos-generate-config --no-filesystems --root /mnt" $scp_cmd root@"$target_destination":/mnt/etc/nixos/hardware-configuration.nix ../hosts/"$target_hostname"/hardware-configuration.nix sync root "$PWD" # --extra-files here picks up the ssh host key we generated earlier and puts it onto the target machine # FIXME: Double check that it will delete them? SHELL=/bin/sh nix run github:nix-community/nixos-anywhere -- --ssh-port "$ssh_port" --extra-files "$temp" --flake .#"$target_hostname" root@"$target_destination" echo "Updating ssh host fingerprint at $target_destination to ~/.ssh/known_hosts" # This will fail if we already know the host, but that's fine ssh-keyscan -p "$ssh_port" "$target_destination" >>~/.ssh/known_hosts || true # FIXME: Do we need this? I get errors: # /etc/tmpfiles.d/journal-nocow.conf:26: Failed to resolve specifier: uninitialized /etc/ detected, skipping. # And there is no /etc/machine-id after first rebuild... if [ -n "$persist_dir" ]; then $ssh_root_cmd "cp /etc/machine-id $persist_dir/etc/machine-id || true" $ssh_root_cmd "cp -R /etc/ssh/ $persist_dir/etc/ssh/ || true" fi cd - } function generate_age_keys() { green "Generating an age key based on the new ssh_host_ed25519_key." target_key=$(ssh-keyscan -p "$ssh_port" -t ssh-ed25519 "$target_destination" 2>&1 | rg ssh-ed25519 | cut -f2- -d" ") age_key=$(nix shell nixpkgs#ssh-to-age.out -c sh -c "echo $target_key | ssh-to-age") if grep -qv '^age1' <<<"$age_key"; then red "The result from generated age key does not match the expected format." yellow "Result: $age_key" yellow "Expected format: age10000000000000000000000000000000000000000000000000000000000" exit 1 else echo "$age_key" fi green "Updating nix-secrets/.sops.yaml" cd ../nix-secrets SOPS_FILE=".sops.yaml" sed -i "{ # Remove any * and & entries for this host /[*&]$target_hostname/ d; # Inject a new age: entry # n matches the first line following age: and p prints it, then we transform it while reusing the spacing /age:/{n; p; s/\(.*- \*\).*/\1$target_hostname/}; # Inject a new hosts: entry /&hosts:/{n; p; s/\(.*- &\).*/\1$target_hostname $age_key/} }" $SOPS_FILE green "Updating nix-secrets/.sops.yaml" cd - just rekey green "Updating flake lock on source machine with new .sops.yaml info" nix flake lock --update-input nix-secrets } # Validate required options if [ -z "${target_hostname}" ] || [ -z "${target_destination}" ] || [ -z "${ssh_key}" ]; then red "ERROR: -n, -d, and -k are all required" echo help_and_exit fi # Clear the keys, since they should be newly generated for the iso green "Wiping known_hosts of $target_destination" sed -i "/$target_hostname/d; /$target_destination/d" ~/.ssh/known_hosts if yes_or_no "Do you want to run nixos-anywhere installation?"; then nixos_anywhere fi if yes_or_no "Do you want to run age key generation?"; then generate_age_keys fi if yes_or_no "Do you want to add ssh host fingerprints for gitlab and github? If this is the first time running this script on $target_hostname, this will be required for the following steps?"; then if [ "$target_user" == "root" ]; then home_path="/root" else home_path="/home/$target_user" fi green "Adding ssh host fingerprints for gitlab and github" $ssh_cmd "mkdir -p $home_path/.ssh/; ssh-keyscan -t ssh-ed25519 gitlab.com github.com >>$home_path/.ssh/known_hosts" fi if yes_or_no "Do you want to copy your full nix-config and private keys to $target_hostname?"; then echo "Adding ssh host fingerprint at $target_destination to ~/.ssh/known_hosts" # This will fail if we already know the host, but that's fine ssh-keyscan -p "$ssh_port" "$target_destination" >>~/.ssh/known_hosts || true green "Copying full nix-config to $target_hostname" sync "$target_user" "$PWD" green "Copying full nix-secrets to $target_hostname" sync "$target_user" ../nix-secrets if yes_or_no "Do you want to rebuild immediately?"; then green "Rebuilding nix-config on $target_hostname" $ssh_cmd "cd nix-config && sudo nixos-rebuild --show-trace switch --flake .#$target_hostname" fi else echo green "NixOS was succcefully installed!" echo "Post-install config build instructions:" echo "To copy nix-config from this machine to the $target_hostname, run the following command from ~/nix-config" echo "just sync $target_user $target_destination" echo "To rebuild, sign into $target_hostname and run the following command from ~/nix-config" echo "cd nix-config" echo "just rebuild" echo fi if yes_or_no "You can now commit and push the nix-config, which includes the hardware-configuration.nix for $target_hostname?"; then (pre-commit run --all-files 2>/dev/null || true) && git add hosts/"$target_hostname"/hardware-configuration.nix && (git commit -m "feat: hardware-configuration.nix for $target_hostname" || true) && git push fi #TODO prune all previous generations? green "Success!"