Commit 9b23613b544f

Vincent Demeester <vincent@sbr.pm>
2026-07-27 18:05:37
feat(kyushu): add bootstrap script and README for Fedora CSB
Added imperative setup for kyushu migration from NixOS to Fedora CSB, following the aomi pattern. Includes Nix, SELinux policy, system-manager, native apps, TPM, unscd, WireGuard, and home-manager phases. No CRC or lid-close override (unlike aomi).
1 parent 95d11ab
Changed files (2)
imperative
imperative/kyushu/bootstrap.sh
@@ -0,0 +1,406 @@
+#!/usr/bin/env bash
+
+# Kyushu (Fedora CSB) Bootstrap Script
+# Description: Installs Nix (Determinate) and deploys system-manager config
+#
+# Prerequisites: Fedora CSB installed, user has sudo access
+# Usage: ./bootstrap.sh
+#   Or remotely: ssh vdemeest@kyushu.home 'bash -s' < imperative/kyushu/bootstrap.sh
+#
+# Can also be sourced to run individual phases without poisoning the calling
+# shell with errexit:
+#   bash -c 'source imperative/kyushu/bootstrap.sh && setup_tpm'
+
+# Only enable strict mode when executed directly, not when sourced — otherwise
+# a failing command would kill the interactive shell that sourced this file.
+if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
+	set -euo pipefail
+fi
+
+readonly GREEN='\033[0;32m'
+readonly YELLOW='\033[1;33m'
+readonly RED='\033[0;31m'
+readonly NC='\033[0m'
+
+REPO_URL="${REPO_URL:-https://git.sbr.pm/home.git}"
+REPO_PATH="${REPO_PATH:-$HOME/src/home}"
+SYSTEM_CONFIG="${SYSTEM_CONFIG:-kyushu}"
+
+log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
+
+check_root() {
+	if [[ $EUID -eq 0 ]]; then
+		log_error "Run as your regular user, not root (sudo is used when needed)"
+		exit 1
+	fi
+}
+
+# --- Phase 1: Nix ---
+
+install_nix() {
+	if command -v nix &>/dev/null; then
+		log_info "Nix already installed: $(nix --version)"
+		return 0
+	fi
+
+	log_info "Installing Nix (Determinate Systems installer)..."
+	curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
+
+	# Source nix for this session
+	if [[ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]]; then
+		# shellcheck disable=SC1091
+		. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+	fi
+
+	log_info "Nix installed: $(nix --version)"
+}
+
+configure_nix() {
+	local nix_conf="${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf"
+	mkdir -p "$(dirname "$nix_conf")"
+
+	# Determinate installer enables flakes by default, but ensure our preferences
+	if [[ ! -f "$nix_conf" ]] || ! grep -q 'use-xdg-base-directories' "$nix_conf" 2>/dev/null; then
+		cat >"$nix_conf" <<-'EOF'
+			experimental-features = nix-command flakes
+			use-xdg-base-directories = true
+			extra-substituters = http://okinawa.vpn:5000
+			extra-trusted-public-keys = cache.okinawa.home:gp+IG0OaO4L/J0drL8OwmDtMPmdUq4kfLwg3mR8BkCs=
+		EOF
+		log_info "Nix config written to $nix_conf"
+	else
+		log_info "Nix config already exists"
+	fi
+}
+
+# --- Phase 2: Repository ---
+
+clone_repo() {
+	if [[ -d "$REPO_PATH/.git" ]]; then
+		log_info "Repository exists at $REPO_PATH, pulling..."
+		git -C "$REPO_PATH" pull --ff-only || log_warn "Pull failed, continuing with existing"
+		return 0
+	fi
+
+	log_info "Cloning $REPO_URL → $REPO_PATH"
+	mkdir -p "$(dirname "$REPO_PATH")"
+	git clone "$REPO_URL" "$REPO_PATH"
+}
+
+# --- Phase 3: SELinux policy for Nix + systemd ---
+
+setup_selinux_policy() {
+	if ! command -v getenforce &>/dev/null || [[ "$(getenforce)" == "Disabled" ]]; then
+		log_info "SELinux not enforcing, skipping policy"
+		return 0
+	fi
+
+	if sudo semodule -l 2>/dev/null | grep -q nix-systemd; then
+		log_info "SELinux nix-systemd policy already installed"
+		return 0
+	fi
+
+	log_info "Installing SELinux policy for Nix + systemd integration..."
+
+	# Ensure build tools are available
+	sudo dnf install -y checkpolicy policycoreutils-python-utils
+
+	local tmpdir
+	tmpdir=$(mktemp -d)
+
+	cat >"${tmpdir}/nix-systemd.te" <<'POLICY'
+module nix-systemd 1.0;
+
+require {
+    type init_t;
+    type default_t;
+    class file { read open getattr execute execute_no_trans map };
+    class dir { search getattr open read };
+    class lnk_file { read getattr };
+}
+
+# Allow systemd (init_t) to read unit files with default_t context
+# This occurs when system-manager symlinks units into /etc/systemd/system/
+# pointing to /nix/store paths which have default_t SELinux context
+allow init_t default_t:dir { search getattr open read };
+allow init_t default_t:file { read open getattr execute execute_no_trans map };
+allow init_t default_t:lnk_file { read getattr };
+POLICY
+
+	checkmodule -M -m -o "${tmpdir}/nix-systemd.mod" "${tmpdir}/nix-systemd.te"
+	semodule_package -o "${tmpdir}/nix-systemd.pp" -m "${tmpdir}/nix-systemd.mod"
+	sudo semodule -i "${tmpdir}/nix-systemd.pp"
+
+	rm -rf "${tmpdir}"
+	log_info "SELinux policy installed"
+}
+
+# --- Phase 4: System-manager ---
+
+build_and_activate() {
+	log_info "Building system-manager config: $SYSTEM_CONFIG"
+
+	# Ensure nix is in PATH
+	if ! command -v nix &>/dev/null; then
+		# shellcheck disable=SC1091
+		. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+	fi
+
+	cd "$REPO_PATH"
+
+	local system_path
+	system_path=$(nix build ".#systemConfigs.${SYSTEM_CONFIG}" --no-link --print-out-paths)
+
+	log_info "Built: $system_path"
+	log_info "Activating system-manager..."
+	sudo "${system_path}/bin/activate"
+
+	log_info "System-manager activated!"
+}
+
+# --- Phase 5: Native apps (dnf/flatpak) ---
+
+install_native_apps() {
+	log_info "Installing native apps..."
+
+	# Niri compositor + Wayland essentials + terminal
+	log_info "Installing niri, Wayland support, and kitty..."
+	sudo dnf install -y \
+		podman podman-docker \
+		niri \
+		kitty \
+		xwayland-satellite \
+		xdg-desktop-portal-gnome \
+		xdg-desktop-portal-gtk \
+		libvirt
+
+	# Flatpak apps (sandboxed, auto-updating)
+	if ! command -v flatpak &>/dev/null; then
+		log_info "Installing flatpak..."
+		sudo dnf install -y flatpak
+		flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
+	fi
+
+	local flatpaks=(
+		com.slack.Slack
+		us.zoom.Zoom
+	)
+	for app in "${flatpaks[@]}"; do
+		if ! flatpak info "$app" &>/dev/null 2>&1; then
+			log_info "Installing $app..."
+			flatpak install -y flathub "$app"
+		else
+			log_info "$app already installed"
+		fi
+	done
+
+	# 1Password (dedicated RPM repo)
+	if ! rpm -q 1password &>/dev/null; then
+		log_info "Installing 1Password..."
+		sudo rpm --import https://downloads.1password.com/linux/keys/1password.asc
+		sudo tee /etc/yum.repos.d/1password.repo <<-'EOF'
+			[1password]
+			name=1Password Stable Channel
+			baseurl=https://downloads.1password.com/linux/rpm/stable/$basearch
+			enabled=1
+			gpgcheck=1
+			repo_gpgcheck=1
+			gpgkey=https://downloads.1password.com/linux/keys/1password.asc
+		EOF
+		sudo dnf install -y 1password 1password-cli
+	fi
+}
+
+# --- Phase 6: TPM 2.0 access ---
+
+setup_tpm() {
+	log_info "Configuring TPM 2.0 access..."
+
+	if [[ ! -e /dev/tpmrm0 && ! -e /dev/tpm0 ]]; then
+		log_warn "No TPM device found (/dev/tpm0, /dev/tpmrm0); skipping"
+		return 0
+	fi
+
+	# tpm2-tools gives tpm2_* utilities for inspection/testing
+	if ! command -v tpm2_pcrread &>/dev/null; then
+		sudo dnf install -y tpm2-tools
+	fi
+
+	# The tss group owns the TPM resource-manager device; membership is
+	# required for userspace access (age-plugin-tpm, ssh-tpm-agent, etc.)
+	if id -nG "$USER" | grep -qw tss; then
+		log_info "$USER already in tss group"
+	else
+		log_info "Adding $USER to tss group..."
+		sudo usermod -aG tss "$USER"
+		log_warn "Log out/in (or reboot) for tss group membership to take effect"
+	fi
+}
+
+# --- Phase 7: unscd for nix NSS resolution ---
+
+setup_unscd() {
+	# Nix glibc can't load system NSS modules (libnss_sss.so) so nix programs
+	# can't resolve LDAP/SSSD users. unscd creates /var/run/nscd/socket which
+	# nix glibc queries automatically. Must be built with system gcc/glibc
+	# so it can load system NSS modules.
+	if systemctl is-active --quiet unscd 2>/dev/null; then
+		log_info "unscd already running"
+		return 0
+	fi
+
+	log_info "Building and installing unscd for nix NSS resolution..."
+	sudo dnf install -y gcc make
+
+	local tmpdir
+	tmpdir=$(mktemp -d)
+	curl -sSL https://busybox.net/~vda/unscd/nscd-0.54.c -o "${tmpdir}/nscd.c"
+	gcc -O2 -o "${tmpdir}/unscd" "${tmpdir}/nscd.c"
+	sudo install -m 755 "${tmpdir}/unscd" /usr/local/sbin/unscd
+	rm -rf "${tmpdir}"
+
+	# Create systemd service
+	sudo tee /etc/systemd/system/unscd.service >/dev/null <<-'EOF'
+		[Unit]
+		Description=Name Service Cache Daemon (unscd for nix)
+		After=sssd.service network.target
+
+		[Service]
+		Type=forking
+		ExecStart=/usr/local/sbin/unscd
+		Restart=on-failure
+
+		[Install]
+		WantedBy=multi-user.target
+	EOF
+
+	sudo systemctl daemon-reload
+	sudo systemctl enable --now unscd
+	log_info "unscd installed and running"
+}
+
+# --- Phase 8: WireGuard ---
+
+setup_wireguard() {
+	# Use Fedora-native wg-quick service (proper SELinux context)
+	# Config file is managed by system-manager in /etc/wireguard/wg0.conf
+	log_info "Setting up WireGuard (native service)..."
+	sudo dnf install -y wireguard-tools
+	sudo systemctl enable wg-quick@wg0
+
+	# Trust wg0 interface in firewall (allows SSH and all traffic over VPN)
+	if command -v firewall-cmd &>/dev/null; then
+		log_info "Adding wg0 to firewalld trusted zone..."
+		sudo firewall-cmd --zone=trusted --add-interface=wg0 --permanent
+		sudo firewall-cmd --reload
+	fi
+
+	if [[ -f /etc/wireguard/private.key ]]; then
+		log_info "WireGuard private key exists, starting service..."
+		sudo systemctl start wg-quick@wg0
+		return 0
+	fi
+
+	log_warn "WireGuard private key not found at /etc/wireguard/private.key"
+	log_warn ""
+	log_warn "To set up WireGuard, either:"
+	log_warn "  1. Copy existing key:  sudo cp /path/to/backup/private.key /etc/wireguard/"
+	log_warn "  2. Generate new key:   wg genkey | sudo tee /etc/wireguard/private.key"
+	log_warn "     Then update globals.nix with new pubkey: sudo cat /etc/wireguard/private.key | wg pubkey"
+	log_warn "     (current VPN IP: 10.100.0.19)"
+	log_warn ""
+	log_warn "After placing the key:  sudo chmod 600 /etc/wireguard/private.key"
+	log_warn "Then re-run:  sudo systemctl restart wg-quick@wg0"
+}
+
+# --- Phase 9: Home-manager ---
+
+setup_home_manager() {
+	log_info "Setting up home-manager..."
+
+	# Ensure nix is in PATH
+	if ! command -v nix &>/dev/null; then
+		# shellcheck disable=SC1091
+		. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+	fi
+
+	cd "$REPO_PATH"
+
+	# Build and activate home-manager
+	nix run home-manager -- switch --flake ".#vdemeest@${SYSTEM_CONFIG}"
+
+	log_info "Home-manager activated!"
+}
+
+# --- Phase 10: Shell setup ---
+
+setup_shell() {
+	local zsh_path="$HOME/.local/state/nix/profile/bin/zsh"
+
+	if ! grep -q "$zsh_path" /etc/shells 2>/dev/null; then
+		log_info "Adding nix zsh to /etc/shells..."
+		echo "$zsh_path" | sudo tee -a /etc/shells
+	fi
+
+	# CSB uses LDAP/FreeIPA, chsh won't work — use bashrc exec instead
+	if ! grep -q 'exec.*zsh' ~/.bashrc 2>/dev/null; then
+		log_info "Configuring bash to exec into zsh..."
+		# shellcheck disable=SC2016
+		echo '[[ $- == *i* && -x "$HOME/.local/state/nix/profile/bin/zsh" ]] && exec "$HOME/.local/state/nix/profile/bin/zsh"' >>~/.bashrc
+	fi
+
+	log_info "Shell configured (zsh via bashrc exec)"
+}
+
+# --- Summary ---
+
+print_summary() {
+	log_info ""
+	log_info "╔═══════════════════════════════════════════╗"
+	log_info "║    Kyushu (Fedora CSB) Bootstrap Done     ║"
+	log_info "╠═══════════════════════════════════════════╣"
+	log_info "║  Nix:            ✓ installed              ║"
+	log_info "║  System-manager: ✓ activated              ║"
+	log_info "║  Home-manager:   ✓ activated              ║"
+	log_info "║  Native apps:    ✓ installed              ║"
+	log_info "╠═══════════════════════════════════════════╣"
+	log_info "║  Rebuild system:                          ║"
+	log_info "║    nix build .#systemConfigs.kyushu       ║"
+	log_info "║    sudo ./result/bin/activate              ║"
+	log_info "║  Rebuild home:                            ║"
+	log_info "║    home-manager switch --flake .#vdemeest@kyushu ║"
+	log_info "╚═══════════════════════════════════════════╝"
+}
+
+# --- Main ---
+
+main() {
+	log_info "Bootstrapping kyushu (Fedora CSB)..."
+	log_info "  Repo:   $REPO_URL → $REPO_PATH"
+	log_info "  Config: $SYSTEM_CONFIG"
+	echo
+
+	check_root
+	install_nix
+	configure_nix
+	clone_repo
+	setup_selinux_policy
+	build_and_activate
+	install_native_apps
+	setup_tpm
+	setup_unscd
+	setup_wireguard
+	setup_home_manager
+	setup_shell
+	print_summary
+}
+
+# Only run main when executed directly, not when sourced. This lets you run
+# individual phases, e.g. (use bash, the script targets bash):
+#   source imperative/kyushu/bootstrap.sh
+#   setup_tpm
+if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
+	main "$@"
+fi
imperative/kyushu/README.md
@@ -0,0 +1,95 @@
+# Kyushu — Fedora CSB + Nix
+
+ThinkPad X1 Carbon Gen 9 running Red Hat CSB (Fedora), managed with Nix
+system-manager and home-manager.
+
+## Architecture
+
+```
+Fedora CSB (base OS, IT-managed)
+  └── Nix (Determinate installer)
+       ├── system-manager  → WireGuard config, nix.custom.conf
+       └── home-manager    → shell, editors, dev tools, syncthing
+```
+
+## Bootstrap
+
+From another machine with SSH access to kyushu:
+
+```bash
+ssh vdemeest@kyushu.home 'bash -s' < imperative/kyushu/bootstrap.sh
+```
+
+Or on kyushu directly:
+
+```bash
+curl -sL https://git.sbr.pm/home/raw/branch/main/imperative/kyushu/bootstrap.sh | bash
+```
+
+Individual phases can be run by sourcing the script:
+
+```bash
+source imperative/kyushu/bootstrap.sh
+setup_tpm
+```
+
+## Rebuilding
+
+```bash
+cd ~/src/home
+
+# System-manager (WireGuard config, nix settings)
+nix build .#systemConfigs.kyushu && sudo ./result/bin/activate
+
+# Home-manager (dev tools, shell, emacs, syncthing)
+home-manager switch --flake .#vdemeest@kyushu
+```
+
+## WireGuard
+
+The system-manager config writes `/etc/wireguard/wg0.conf` but uses a
+placeholder for the private key. After first activation:
+
+```bash
+# Option 1: Generate a new keypair, then update globals.nix
+wg genkey | sudo tee /etc/wireguard/private.key
+sudo cat /etc/wireguard/private.key | wg pubkey
+# → update globals.nix machines.kyushu.net.vpn.pubkey
+#   (current VPN IP: 10.100.0.19)
+
+# Option 2: Restore backed-up key (if same pubkey)
+sudo cp /path/to/backup/private.key /etc/wireguard/
+
+sudo chmod 600 /etc/wireguard/private.key
+sudo systemctl restart wg-quick@wg0
+```
+
+## Syncthing
+
+After home-manager activation, Syncthing runs as a user service. The
+device ID needs to be updated in `globals.nix` after first run:
+
+```bash
+# Get the current device ID
+curl -s http://localhost:8384/rest/system/status | jq -r .myID
+```
+
+Update `globals.nix` with the new device ID, then deploy to other hosts
+so they accept the new kyushu.
+
+## Passage
+
+After WireGuard is up and the passage store is cloned:
+
+1. Generate a new TPM identity: `age-plugin-tpm --generate`
+2. Add the public key to `.age-recipients` in the passage store
+3. Remove the old kyushu TPM pubkey
+4. Run `passage reencrypt` from a machine with YubiKey #1
+   (needed for `redhat/ldap` which only has YubiKey + aomi TPM recipients)
+
+## Differences from aomi
+
+- **No CRC** — no local OpenShift cluster needed
+- **No lid-close override** — default suspend behavior is fine
+- Same shared work-Fedora profile (`home/common/profiles/work-fedora.nix`,
+  `systems/common/fedora-work/system.nix`)