Archived
Linode provisions and sizes /dev/sda (root) and /dev/sdb (swap) itself as whole, unpartitioned block devices before the OS ever boots. modules/disko/linode.nix declares them with destroy = false (skips Disko's wipe stage for these disks entirely) and a bare filesystem/swap content type matching that existing layout, so re-running it against an already-provisioned disk only mkfs/mkswaps if blkid shows it isn't formatted yet -- never repartitions or destroys data. With every host reachable through the installer menu now carrying a Disko config, auto-install.sh no longer needs to probe the flake and branch between `disko --mode destroy,format,mount` and a bind-mount fallback -- it just always runs Disko. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
241 lines
10 KiB
Nix
241 lines
10 KiB
Nix
{ pkgs, lib, vars, ... }:
|
|
|
|
{
|
|
imports = [
|
|
./host-keys.nix
|
|
];
|
|
|
|
networking.useDHCP = lib.mkDefault true;
|
|
|
|
# Recommended over the true default (bypasses ZFS's own import safeguards)
|
|
# per the option's own docs. This installer environment has no ZFS pools
|
|
# of its own to import, so this is a no-op here — just silences the
|
|
# eval-time warning, matching modules/common/configuration.nix.
|
|
boot.zfs.forceImportRoot = false;
|
|
|
|
time.timeZone = vars.timeZone;
|
|
|
|
# Without this, the installer only ever sees cache.nixos.org, which
|
|
# doesn't carry sops-install-secrets (it's built straight from the
|
|
# sops-nix flake's own Go source, not part of nixpkgs) — every install
|
|
# would otherwise compile it from scratch, which is what ran an 8GB LXC
|
|
# container's disk out of space. Push a built copy to nix-cache once
|
|
# (from a machine with real disk headroom) and every future install,
|
|
# of any type, fetches instead of rebuilding.
|
|
nix.settings = {
|
|
substituters = [
|
|
"http://nix-cache"
|
|
"https://cache.nixos.org/"
|
|
];
|
|
trusted-public-keys = [
|
|
"cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
|
];
|
|
};
|
|
|
|
environment = {
|
|
systemPackages = with pkgs; [
|
|
git
|
|
curl
|
|
jq
|
|
parted
|
|
e2fsprogs
|
|
btrfs-progs
|
|
util-linux
|
|
disko
|
|
];
|
|
|
|
# Write auto-install script to /root
|
|
etc."auto-install.sh" = {
|
|
text = ''
|
|
#!/run/current-system/sw/bin/bash
|
|
set -eux
|
|
|
|
set -euo pipefail
|
|
|
|
export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git"
|
|
|
|
echo "Fetching available NixOS hosts from flake..."
|
|
# Two categories deliberately excluded from the menu:
|
|
# lxc-* — these build a config.system.build.tarball meant for
|
|
# `pct restore` on Proxmox directly, not an install.
|
|
# Running nixos-install against one here would
|
|
# bind-mount / onto /mnt and then refuse to touch the
|
|
# filesystem it's currently running on — see
|
|
# docs/auto-installer.md.
|
|
# installer — this *is* the installer image's own flake target,
|
|
# not a deployable host; "installing" it means
|
|
# nixos-install-ing a copy of the installer into
|
|
# itself.
|
|
mapfile -t options < <(
|
|
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
|
"''${FLAKE_BASE_URL}#nixosConfigurations" \
|
|
--apply builtins.attrNames \
|
|
| jq -r '.[]
|
|
| select(startswith("lxc-") | not)
|
|
| select(. != "installer")'
|
|
)
|
|
|
|
if [[ ''${#options[@]} -eq 0 ]]; then
|
|
echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Note: lxc-* targets aren't installed this way — build them with"
|
|
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
|
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
|
|
|
echo "Choose the flake profile to install:"
|
|
select choice in "''${options[@]}"; do
|
|
if [[ -n "$choice" ]]; then
|
|
echo "You selected: $choice"
|
|
break
|
|
else
|
|
echo "Invalid selection. Try again."
|
|
fi
|
|
done
|
|
|
|
echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}"
|
|
|
|
# Optional: confirm before proceeding
|
|
read -rp "Proceed with installation? (y/N): " confirm
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
exit 1
|
|
fi
|
|
|
|
# A nix-cache host is *the* substituter/remote-builder for every other
|
|
# host once installed (its own config explicitly excludes itself from
|
|
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
|
# Installing one shouldn't depend on a nix-cache substituter either,
|
|
# for the same reason — plus in practice "nix-cache" only resolves over
|
|
# Tailscale, which a fresh installer environment was never connected to
|
|
# anyway, so it's dead weight even for non-nix-cache installs until
|
|
# that's sorted out. Override it away here specifically for nix-cache
|
|
# targets to keep install-time behaviour consistent with run-time.
|
|
nix_extra_opts=()
|
|
if [[ "''${choice}" == *-nix-cache ]]; then
|
|
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
|
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
|
fi
|
|
|
|
# Every host reachable through this menu has a Disko config (lxc-*
|
|
# is filtered out above, and is the only category that doesn't —
|
|
# see docs/auto-installer.md), so this can run unconditionally: no
|
|
# need to probe the flake first and branch on whether Disko applies.
|
|
disko --mode destroy,format,mount \
|
|
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
|
|
|
|
# sops-nix derives this host's decryption key from its own SSH host key
|
|
# at *activation* time, which runs before systemd would otherwise
|
|
# generate one on first boot. Without pre-seeding it here, secrets
|
|
# (including the login password) fail to decrypt on first boot.
|
|
# Generate the key with scripts/prepare-host-key.sh first.
|
|
#
|
|
# Two places a key can come from, checked in order:
|
|
# /etc/host-keys — baked into this image at build time (see
|
|
# modules/installer/host-keys.nix; only present
|
|
# if built with NIXOS_HOST_KEYS_DIR set)
|
|
# /root/host-keys — scp'd in manually after boot (older fallback,
|
|
# still supported for images built without keys)
|
|
mkdir -p /root/host-keys
|
|
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
|
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
|
|
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
|
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
|
|
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
else
|
|
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
|
|
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
|
echo "Run scripts/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
|
|
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
|
|
echo "/root/host-keys/ on this machine."
|
|
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
|
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
|
echo "Aborted."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
mkdir -p /mnt/install-tmp
|
|
export TMPDIR=/mnt/install-tmp
|
|
|
|
nixos-install \
|
|
--flake "''${FLAKE_BASE_URL}#''${choice}" \
|
|
"''${nix_extra_opts[@]}" \
|
|
--no-root-password
|
|
|
|
|
|
rm -rf /mnt/install-tmp
|
|
# Redundant copy of the host's private key — the real one is now at
|
|
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
|
# up on its own since it was written imperatively, not declaratively.
|
|
rm -rf /root/host-keys
|
|
sleep 10
|
|
reboot
|
|
'';
|
|
|
|
mode = "0755";
|
|
};
|
|
};
|
|
|
|
programs.git.enable = true;
|
|
|
|
# Run the installer on first login. Previously this copied an /etc file
|
|
# into the nixos user's ~/.bash_profile via an activation script that
|
|
# got dropped in a refactor (and only ever worked for that one user
|
|
# anyway) — loginShellInit is NixOS's native hook for this, applies to
|
|
# any user's login shell (root included), and needs no home-directory
|
|
# file-copying/chown.
|
|
programs.bash.loginShellInit = ''
|
|
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
|
|
sudo /etc/auto-install.sh
|
|
touch "$HOME/.auto_install_ran"
|
|
fi
|
|
'';
|
|
|
|
services.openssh.enable = true;
|
|
|
|
services.openssh.settings = {
|
|
PermitRootLogin = "yes";
|
|
PasswordAuthentication = true;
|
|
};
|
|
|
|
# nixpkgs' own installer profile (profiles/installation-device.nix, pulled
|
|
# in via installation-cd-minimal.nix) sets initialHashedPassword = "" for
|
|
# both users — its own passwordless-login convention for install media.
|
|
# That's a second, non-null password option alongside our hashedPassword
|
|
# below, which NixOS warns about as ambiguous precedence. Force it null
|
|
# rather than adopting passwordless login: this image now also boots over
|
|
# LAN PXE with PasswordAuthentication enabled, so passwordless root SSH
|
|
# would be reachable by anyone on the LAN, not just local console.
|
|
users.users.root = {
|
|
hashedPassword =
|
|
"$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
|
|
initialHashedPassword = lib.mkForce null;
|
|
};
|
|
|
|
users.users.${vars.primaryUser} = {
|
|
isNormalUser = true;
|
|
|
|
extraGroups = [
|
|
"wheel"
|
|
];
|
|
|
|
shell = pkgs.bashInteractive;
|
|
|
|
hashedPassword =
|
|
"$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
|
|
initialHashedPassword = lib.mkForce null;
|
|
|
|
openssh.authorizedKeys.keys = [
|
|
vars.adminSshKey
|
|
];
|
|
};
|
|
|
|
system.stateVersion = "26.05";
|
|
}
|