nixos-infect/nixos-infect
Jairo Llopis 318fc516d1
feat: respect old machine ssh host keys (#107)
When lustrating another machine, you usually are already connected by SSH. Thus, you usually already trusted that machine's keys.

With this patch, those keys are kept, so after booting into nixos, you still connect to the same trusted IP+Keys combination.

@moduon MT-904
2022-07-11 10:25:21 -07:00

342 lines
11 KiB
Bash
Executable file

#! /usr/bin/env bash
# More info at: https://github.com/elitak/nixos-infect
set -e -o pipefail
makeConf() {
# Skip everything if main config already present
[[ -e /etc/nixos/configuration.nix ]] && return 0
# NB <<"EOF" quotes / $ ` in heredocs, <<EOF does not
mkdir -p /etc/nixos
# Prevent grep for sending error code 1 (and halting execution) when no lines are selected : https://www.unix.com/man-page/posix/1P/grep
local IFS=$'\n'
for trypath in /root/.ssh/authorized_keys /home/$SUDO_USER/.ssh/authorized_keys $HOME/.ssh/authorized_keys; do
[[ -r "$trypath" ]] \
&& keys=$(sed -E 's/^.*((ssh|ecdsa)-[^[:space:]]+)[[:space:]]+([^[:space:]]+)([[:space:]]*.*)$/\1 \3\4/' "$trypath") \
&& break
done
local network_import=""
[[ -n "$doNetConf" ]] && network_import="./networking.nix # generated at runtime by nixos-infect"
cat > /etc/nixos/configuration.nix << EOF
{ ... }: {
imports = [
./hardware-configuration.nix
$network_import
$NIXOS_IMPORT
];
boot.cleanTmpDir = true;
zramSwap.enable = ${zramswap};
networking.hostName = "$(hostname)";
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [$(while read -r line; do echo -n "
\"$line\" "; done <<< "$keys")
];
}
EOF
if isEFI; then
bootcfg=$(cat << EOF
boot.loader.grub = {
efiSupport = true;
efiInstallAsRemovable = true;
device = "nodev";
};
fileSystems."/boot" = { device = "$esp"; fsType = "vfat"; };
EOF
)
else
bootcfg=$(cat << EOF
boot.loader.grub.device = "$grubdev";
EOF
)
fi
# If you rerun this later, be sure to prune the filesSystems attr
cat > /etc/nixos/hardware-configuration.nix << EOF
{ modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
$bootcfg
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" ];
boot.initrd.kernelModules = [ "nvme" ];
fileSystems."/" = { device = "$rootfsdev"; fsType = "$rootfstype"; };
$swapcfg
}
EOF
[[ -n "$doNetConf" ]] && makeNetworkingConf || true
}
makeNetworkingConf() {
# XXX It'd be better if we used procfs for all this...
local IFS=$'\n'
eth0_name=$(ip address show | grep '^2:' | awk -F': ' '{print $2}')
eth0_ip4s=$(ip address show dev "$eth0_name" | grep 'inet ' | sed -r 's|.*inet ([0-9.]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|')
eth0_ip6s=$(ip address show dev "$eth0_name" | grep 'inet6 ' | sed -r 's|.*inet6 ([0-9a-f:]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|' || '')
gateway=$(ip route show dev "$eth0_name" | grep default | sed -r 's|default via ([0-9.]+).*|\1|')
gateway6=$(ip -6 route show dev "$eth0_name" | grep default | sed -r 's|default via ([0-9a-f:]+).*|\1|' || true)
ether0=$(ip address show dev "$eth0_name" | grep link/ether | sed -r 's|.*link/ether ([0-9a-f:]+) .*|\1|')
eth1_name=$(ip address show | grep '^3:' | awk -F': ' '{print $2}')||true
if [ -n "$eth1_name" ];then
eth1_ip4s=$(ip address show dev "$eth1_name" | grep 'inet ' | sed -r 's|.*inet ([0-9.]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|')
eth1_ip6s=$(ip address show dev "$eth1_name" | grep 'inet6 ' | sed -r 's|.*inet6 ([0-9a-f:]+)/([0-9]+).*|{ address="\1"; prefixLength=\2; }|' || '')
ether1=$(ip address show dev "$eth1_name" | grep link/ether | sed -r 's|.*link/ether ([0-9a-f:]+) .*|\1|')
interfaces1=<< EOF
$eth1_name = {
ipv4.addresses = [$(for a in "${eth1_ip4s[@]}"; do echo -n "
$a"; done)
];
ipv6.addresses = [$(for a in "${eth1_ip6s[@]}"; do echo -n "
$a"; done)
];
EOF
extraRules1="ATTR{address}==\"${ether1}\", NAME=\"${eth1_name}\""
else
interfaces1=""
extraRules1=""
fi
readarray nameservers < <(grep ^nameserver /etc/resolv.conf | sed -r \
-e 's/^nameserver[[:space:]]+([0-9.a-fA-F:]+).*/"\1"/' \
-e 's/127[0-9.]+/8.8.8.8/' \
-e 's/::1/8.8.8.8/' )
if [[ "$eth0_name" = eth* ]]; then
predictable_inames="usePredictableInterfaceNames = lib.mkForce false;"
else
predictable_inames="usePredictableInterfaceNames = lib.mkForce true;"
fi
cat > /etc/nixos/networking.nix << EOF
{ lib, ... }: {
# This file was populated at runtime with the networking
# details gathered from the active system.
networking = {
nameservers = [ ${nameservers[@]} ];
defaultGateway = "${gateway}";
defaultGateway6 = "${gateway6}";
dhcpcd.enable = false;
$predictable_inames
interfaces = {
$eth0_name = {
ipv4.addresses = [$(for a in "${eth0_ip4s[@]}"; do echo -n "
$a"; done)
];
ipv6.addresses = [$(for a in "${eth0_ip6s[@]}"; do echo -n "
$a"; done)
];
ipv4.routes = [ { address = "${gateway}"; prefixLength = 32; } ];
ipv6.routes = [ { address = "${gateway6}"; prefixLength = 128; } ];
};
$interfaces1
};
};
services.udev.extraRules = ''
ATTR{address}=="${ether0}", NAME="${eth0_name}"
$extraRules1
'';
}
EOF
}
checkExistingSwap() {
SWAPSHOW=$(swapon --show --noheadings --raw)
zramswap=true
swapcfg=""
if [[ -n "$SWAPSHOW" ]]; then
SWAP_DEVICE="${SWAPSHOW%% *}"
if [[ "$SWAP_DEVICE" == "/dev/"* ]]; then
zramswap=false
swapcfg="swapDevices = [ { device = \"${SWAP_DEVICE}\"; } ];"
NO_SWAP=true
fi
fi
}
makeSwap() {
swapFile=$(mktemp /tmp/nixos-infect.XXXXX.swp)
dd if=/dev/zero "of=$swapFile" bs=1M count=$((1*1024))
chmod 0600 "$swapFile"
mkswap "$swapFile"
swapon -v "$swapFile"
}
removeSwap() {
swapoff -a
rm -vf /tmp/nixos-infect.*.swp
}
isEFI() {
[ -d /sys/firmware/efi ]
}
findESP() {
esp=""
for d in /boot/EFI /boot/efi /boot; do
[[ ! -d "$d" ]] && continue
[[ "$d" == "$(df "$d" --output=target | sed 1d)" ]] \
&& esp="$(df "$d" --output=source | sed 1d)" \
&& break
done
[[ -z "$esp" ]] && { echo "ERROR: No ESP mount point found"; return 1; }
for uuid in /dev/disk/by-uuid/*; do
[[ $(readlink -f "$uuid") == "$esp" ]] && echo $uuid && return 0
done
}
prepareEnv() {
# $esp and $grubdev are used in makeConf()
if isEFI; then
esp="$(findESP)"
else
for grubdev in /dev/vda /dev/sda /dev/xvda /dev/nvme0n1 ; do [[ -e $grubdev ]] && break; done
fi
# Retrieve root fs block device
# (get root mount) (get partition or logical volume)
rootfsdev=$(mount | grep "on / type" | awk '{print $1;}')
rootfstype=$(df $rootfsdev --output=fstype | sed 1d)
# DigitalOcean doesn't seem to set USER while running user data
export USER="root"
export HOME="/root"
# Nix installer tries to use sudo regardless of whether we're already uid 0
#which sudo || { sudo() { eval "$@"; }; export -f sudo; }
# shellcheck disable=SC2174
mkdir -p -m 0755 /nix
}
fakeCurlUsingWget() {
# Use adapted wget if curl is missing
which wget && { \
curl() {
eval "wget $(
(local isStdout=1
for arg in "$@"; do
case "$arg" in
"-o")
echo "-O";
isStdout=0
;;
"-O")
isStdout=0
;;
"-L")
;;
*)
echo "$arg"
;;
esac
done;
[[ $isStdout -eq 1 ]] && echo "-O-"
)| tr '\n' ' '
)"
}; export -f curl; }
}
req() {
type "$1" > /dev/null 2>&1 || which "$1" > /dev/null 2>&1
}
checkEnv() {
[[ "$(whoami)" == "root" ]] || { echo "ERROR: Must run as root"; return 1; }
# Perform some easy fixups before checking
# TODO prevent multiple calls to apt-get update
(which dnf && dnf install -y perl-Digest-SHA) || true # Fedora 24
which bzcat || (which yum && yum install -y bzip2) \
|| (which apt-get && apt-get update && apt-get install -y bzip2) \
|| true
which xzcat || (which yum && yum install -y xz-utils) \
|| (which apt-get && apt-get update && apt-get install -y xz-utils) \
|| true
which curl || fakeCurlUsingWget \
|| (which apt-get && apt-get update && apt-get install -y curl) \
|| true
req curl || req wget || { echo "ERROR: Missing both curl and wget"; return 1; }
req bzcat || { echo "ERROR: Missing bzcat"; return 1; }
req xzcat || { echo "ERROR: Missing xzcat"; return 1; }
req groupadd || { echo "ERROR: Missing groupadd"; return 1; }
req useradd || { echo "ERROR: Missing useradd"; return 1; }
req ip || { echo "ERROR: Missing ip"; return 1; }
req awk || { echo "ERROR: Missing awk"; return 1; }
req cut || req df || { echo "ERROR: Missing coreutils (cut, df)"; return 1; }
}
infect() {
# Add nix build users
# FIXME run only if necessary, rather than defaulting true
groupadd nixbld -g 30000 || true
for i in {1..10}; do
useradd -c "Nix build user $i" -d /var/empty -g nixbld -G nixbld -M -N -r -s "$(which nologin)" "nixbld$i" || true
done
# TODO use addgroup and adduser as fallbacks
#addgroup nixbld -g 30000 || true
#for i in {1..10}; do adduser -DH -G nixbld nixbld$i || true; done
curl -L https://nixos.org/nix/install | sh
# shellcheck disable=SC1090
source ~/.nix-profile/etc/profile.d/nix.sh
[[ -z "$NIX_CHANNEL" ]] && NIX_CHANNEL="nixos-22.05"
nix-channel --remove nixpkgs
nix-channel --add "https://nixos.org/channels/$NIX_CHANNEL" nixos
nix-channel --update
export NIXOS_CONFIG="${NIXOS_CONFIG:-/etc/nixos/configuration.nix}"
nix-env --set \
-I nixpkgs=$HOME/.nix-defexpr/channels/nixos \
-f '<nixpkgs/nixos>' \
-p /nix/var/nix/profiles/system \
-A system
# Remove nix installed with curl | bash
rm -fv /nix/var/nix/profiles/default*
/nix/var/nix/profiles/system/sw/bin/nix-collect-garbage
# Reify resolv.conf
[[ -L /etc/resolv.conf ]] && mv -v /etc/resolv.conf /etc/resolv.conf.lnk && cat /etc/resolv.conf.lnk > /etc/resolv.conf
# Stage the Nix coup d'état
touch /etc/NIXOS
echo etc/nixos >> /etc/NIXOS_LUSTRATE
echo etc/resolv.conf >> /etc/NIXOS_LUSTRATE
echo root/.nix-defexpr/channels >> /etc/NIXOS_LUSTRATE
(cd / && ls etc/ssh/ssh_host_*_key* || true) >> /etc/NIXOS_LUSTRATE
rm -rf /boot.bak
isEFI && umount "$esp"
mv -v /boot /boot.bak || { cp -a /boot /book.bak ; rm -rf /boot/* ; umount /boot ; }
if isEFI; then
mkdir -p /boot
mount "$esp" /boot
find /boot -depth ! -path /boot -exec rm -rf {} +
fi
/nix/var/nix/profiles/system/bin/switch-to-configuration boot
}
[ "$PROVIDER" = "digitalocean" ] && doNetConf=y # digitalocean requires detailed network config to be generated
checkEnv
prepareEnv
checkExistingSwap
if [[ -z "$NO_SWAP" ]]; then
makeSwap # smallest (512MB) droplet needs extra memory!
fi
makeConf
infect
if [[ -z "$NO_SWAP" ]]; then
removeSwap
fi
if [[ -z "$NO_REBOOT" ]]; then
reboot
fi