main
  1#!/usr/bin/env bash
  2
  3# Kyushu (Fedora CSB) Bootstrap Script
  4# Description: Installs Nix (Determinate) and deploys system-manager config
  5#
  6# Prerequisites: Fedora CSB installed, user has sudo access
  7# Usage: ./bootstrap.sh
  8#   Or remotely: ssh vdemeest@kyushu.home 'bash -s' < imperative/kyushu/bootstrap.sh
  9#
 10# Can also be sourced to run individual phases without poisoning the calling
 11# shell with errexit:
 12#   bash -c 'source imperative/kyushu/bootstrap.sh && setup_tpm'
 13
 14# Only enable strict mode when executed directly, not when sourced — otherwise
 15# a failing command would kill the interactive shell that sourced this file.
 16if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
 17	set -euo pipefail
 18fi
 19
 20readonly GREEN='\033[0;32m'
 21readonly YELLOW='\033[1;33m'
 22readonly RED='\033[0;31m'
 23readonly NC='\033[0m'
 24
 25REPO_URL="${REPO_URL:-https://git.sbr.pm/home.git}"
 26REPO_PATH="${REPO_PATH:-$HOME/src/home}"
 27SYSTEM_CONFIG="${SYSTEM_CONFIG:-kyushu}"
 28
 29log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
 30log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
 31log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
 32
 33check_root() {
 34	if [[ $EUID -eq 0 ]]; then
 35		log_error "Run as your regular user, not root (sudo is used when needed)"
 36		exit 1
 37	fi
 38}
 39
 40# --- Phase 1: Nix ---
 41
 42install_nix() {
 43	if command -v nix &>/dev/null; then
 44		log_info "Nix already installed: $(nix --version)"
 45		return 0
 46	fi
 47
 48	log_info "Installing Nix (Determinate Systems installer)..."
 49	curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
 50
 51	# Source nix for this session
 52	if [[ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]]; then
 53		# shellcheck disable=SC1091
 54		. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
 55	fi
 56
 57	log_info "Nix installed: $(nix --version)"
 58}
 59
 60configure_nix() {
 61	local nix_conf="${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf"
 62	mkdir -p "$(dirname "$nix_conf")"
 63
 64	# Determinate installer enables flakes by default, but ensure our preferences
 65	if [[ ! -f "$nix_conf" ]] || ! grep -q 'use-xdg-base-directories' "$nix_conf" 2>/dev/null; then
 66		cat >"$nix_conf" <<-'EOF'
 67			experimental-features = nix-command flakes
 68			use-xdg-base-directories = true
 69			extra-substituters = http://okinawa.vpn:5000
 70			extra-trusted-public-keys = cache.okinawa.home:gp+IG0OaO4L/J0drL8OwmDtMPmdUq4kfLwg3mR8BkCs=
 71		EOF
 72		log_info "Nix config written to $nix_conf"
 73	else
 74		log_info "Nix config already exists"
 75	fi
 76}
 77
 78# --- Phase 2: Repository ---
 79
 80clone_repo() {
 81	if [[ -d "$REPO_PATH/.git" ]]; then
 82		log_info "Repository exists at $REPO_PATH, pulling..."
 83		git -C "$REPO_PATH" pull --ff-only || log_warn "Pull failed, continuing with existing"
 84		return 0
 85	fi
 86
 87	log_info "Cloning $REPO_URL$REPO_PATH"
 88	mkdir -p "$(dirname "$REPO_PATH")"
 89	git clone "$REPO_URL" "$REPO_PATH"
 90}
 91
 92# --- Phase 3: SELinux policy for Nix + systemd ---
 93
 94setup_selinux_policy() {
 95	if ! command -v getenforce &>/dev/null || [[ "$(getenforce)" == "Disabled" ]]; then
 96		log_info "SELinux not enforcing, skipping policy"
 97		return 0
 98	fi
 99
100	if sudo semodule -l 2>/dev/null | grep -q nix-systemd; then
101		log_info "SELinux nix-systemd policy already installed"
102		return 0
103	fi
104
105	log_info "Installing SELinux policy for Nix + systemd integration..."
106
107	# Ensure build tools are available
108	sudo dnf install -y checkpolicy policycoreutils-python-utils
109
110	local tmpdir
111	tmpdir=$(mktemp -d)
112
113	cat >"${tmpdir}/nix-systemd.te" <<'POLICY'
114module nix-systemd 1.0;
115
116require {
117    type init_t;
118    type default_t;
119    class file { read open getattr execute execute_no_trans map };
120    class dir { search getattr open read };
121    class lnk_file { read getattr };
122}
123
124# Allow systemd (init_t) to read unit files with default_t context
125# This occurs when system-manager symlinks units into /etc/systemd/system/
126# pointing to /nix/store paths which have default_t SELinux context
127allow init_t default_t:dir { search getattr open read };
128allow init_t default_t:file { read open getattr execute execute_no_trans map };
129allow init_t default_t:lnk_file { read getattr };
130POLICY
131
132	checkmodule -M -m -o "${tmpdir}/nix-systemd.mod" "${tmpdir}/nix-systemd.te"
133	semodule_package -o "${tmpdir}/nix-systemd.pp" -m "${tmpdir}/nix-systemd.mod"
134	sudo semodule -i "${tmpdir}/nix-systemd.pp"
135
136	rm -rf "${tmpdir}"
137	log_info "SELinux policy installed"
138}
139
140# --- Phase 4: System config files ---
141# Generated by nix from globals.nix, deployed without system-manager.
142# system-manager's activation clobbers /etc/profile.d, /etc/systemd/system,
143# and /etc/tmpfiles.d with nix-store symlinks, breaking Fedora's shell/NSS.
144
145setup_system_configs() {
146	log_info "Building and deploying system config files..."
147
148	# Ensure nix is in PATH
149	if ! command -v nix &>/dev/null; then
150		# shellcheck disable=SC1091
151		. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
152	fi
153
154	cd "$REPO_PATH"
155
156	local config_path
157	config_path=$(nix build ".#fedoraConfigs.${SYSTEM_CONFIG}" --no-link --print-out-paths)
158
159	log_info "Built: $config_path"
160	log_info "Deploying config files..."
161	sudo "${config_path}/deploy"
162
163	log_info "System config files deployed!"
164}
165
166# --- Phase 5: Native apps (dnf/flatpak) ---
167
168install_native_apps() {
169	log_info "Installing native apps..."
170
171	# Niri compositor + Wayland essentials + terminal
172	log_info "Installing niri, Wayland support, and kitty..."
173	sudo dnf install -y \
174		acpi \
175		brightnessctl \
176		powertop \
177		podman podman-docker \
178		niri \
179		kitty \
180		xwayland-satellite \
181		xdg-desktop-portal-gnome \
182		xdg-desktop-portal-gtk \
183		libvirt
184
185	# Flatpak apps (sandboxed, auto-updating)
186	if ! command -v flatpak &>/dev/null; then
187		log_info "Installing flatpak..."
188		sudo dnf install -y flatpak
189		flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
190	fi
191
192	local flatpaks=(
193		com.slack.Slack
194		us.zoom.Zoom
195	)
196	for app in "${flatpaks[@]}"; do
197		if ! flatpak info "$app" &>/dev/null 2>&1; then
198			log_info "Installing $app..."
199			flatpak install -y flathub "$app"
200		else
201			log_info "$app already installed"
202		fi
203	done
204
205	# 1Password (dedicated RPM repo)
206	if ! rpm -q 1password &>/dev/null; then
207		log_info "Installing 1Password..."
208		sudo rpm --import https://downloads.1password.com/linux/keys/1password.asc
209		sudo tee /etc/yum.repos.d/1password.repo <<-'EOF'
210			[1password]
211			name=1Password Stable Channel
212			baseurl=https://downloads.1password.com/linux/rpm/stable/$basearch
213			enabled=1
214			gpgcheck=1
215			repo_gpgcheck=1
216			gpgkey=https://downloads.1password.com/linux/keys/1password.asc
217		EOF
218		sudo dnf install -y 1password 1password-cli
219	fi
220}
221
222# --- Phase 6: TPM 2.0 access ---
223
224setup_tpm() {
225	log_info "Configuring TPM 2.0 access..."
226
227	if [[ ! -e /dev/tpmrm0 && ! -e /dev/tpm0 ]]; then
228		log_warn "No TPM device found (/dev/tpm0, /dev/tpmrm0); skipping"
229		return 0
230	fi
231
232	# tpm2-tools gives tpm2_* utilities for inspection/testing
233	if ! command -v tpm2_pcrread &>/dev/null; then
234		sudo dnf install -y tpm2-tools
235	fi
236
237	# The tss group owns the TPM resource-manager device; membership is
238	# required for userspace access (age-plugin-tpm, ssh-tpm-agent, etc.)
239	if id -nG "$USER" | grep -qw tss; then
240		log_info "$USER already in tss group"
241	else
242		log_info "Adding $USER to tss group..."
243		sudo usermod -aG tss "$USER"
244		log_warn "Log out/in (or reboot) for tss group membership to take effect"
245	fi
246}
247
248# --- Phase 7: LUKS FIDO2 (YubiKey) ---
249
250setup_luks_fido2() {
251	local luks_dev
252	luks_dev=$(blkid -t TYPE=crypto_LUKS -o device 2>/dev/null | head -1)
253
254	if [[ -z "$luks_dev" ]]; then
255		log_warn "No LUKS device found; skipping FIDO2 enrollment"
256		return 0
257	fi
258
259	log_info "Found LUKS device: $luks_dev"
260
261	# Check if FIDO2 is already enrolled
262	if sudo systemd-cryptenroll "$luks_dev" --fido2-device=list 2>/dev/null | grep -q 'fido2'; then
263		log_info "FIDO2 already available"
264	else
265		log_warn "No FIDO2 device detected. Plug in YubiKey, then run:"
266		log_warn "  sudo systemd-cryptenroll $luks_dev --fido2-device=auto --fido2-with-client-pin=no"
267	fi
268
269	# Ensure dracut includes fido2 support in initramfs
270	if ! lsinitrd 2>/dev/null | grep -q fido2; then
271		log_info "Adding fido2 support to initramfs..."
272		sudo dracut --add fido2 -f
273		log_info "Initramfs rebuilt with fido2 support"
274	else
275		log_info "Initramfs already has fido2 support"
276	fi
277}
278
279# --- Phase 7b: YubiKey sudo (pam-u2f) ---
280
281setup_yubikey_sudo() {
282	if ! rpm -q pam-u2f &>/dev/null; then
283		log_info "Installing pam-u2f..."
284		sudo dnf install -y pam-u2f pamu2fcfg
285	else
286		log_info "pam-u2f already installed"
287	fi
288
289	local u2f_keys="${HOME}/.config/Yubico/u2f_keys"
290	if [[ ! -f "$u2f_keys" ]]; then
291		log_info "Registering YubiKey for PAM U2F — touch the key when it blinks..."
292		mkdir -p "$(dirname "$u2f_keys")"
293		pamu2fcfg >"$u2f_keys"
294		log_info "YubiKey registered at $u2f_keys"
295		log_warn "To add a backup key later: pamu2fcfg -n >> $u2f_keys"
296	else
297		log_info "YubiKey already registered at $u2f_keys"
298	fi
299
300	# Configure sudo to accept YubiKey touch (sufficient = no password needed)
301	local sudo_pam="/etc/pam.d/sudo"
302	if ! grep -q pam_u2f.so "$sudo_pam" 2>/dev/null; then
303		log_info "Adding pam_u2f to $sudo_pam (sufficient — falls back to password)..."
304		sudo sed -i '1a auth       sufficient   pam_u2f.so' "$sudo_pam"
305		log_info "YubiKey sudo configured"
306	else
307		log_info "pam_u2f already in $sudo_pam"
308	fi
309}
310
311# --- Phase 8: unscd for nix NSS resolution ---
312
313setup_unscd() {
314	# Nix glibc can't load system NSS modules (libnss_sss.so) so nix programs
315	# can't resolve LDAP/SSSD users. unscd creates /var/run/nscd/socket which
316	# nix glibc queries automatically. Must be built with system gcc/glibc
317	# so it can load system NSS modules.
318	if systemctl is-active --quiet unscd 2>/dev/null; then
319		log_info "unscd already running"
320		return 0
321	fi
322
323	log_info "Building and installing unscd for nix NSS resolution..."
324	sudo dnf install -y gcc make
325
326	local tmpdir
327	tmpdir=$(mktemp -d)
328	curl -sSL https://busybox.net/~vda/unscd/nscd-0.54.c -o "${tmpdir}/nscd.c"
329	# Newer glibc removed __nss_disable_nscd from headers; provide a stub
330	sed -i '/^void __nss_disable_nscd(/c\void __nss_disable_nscd(void (*hell)(size_t, struct traced_file*)) {}' "${tmpdir}/nscd.c"
331	PATH=/usr/bin:/usr/sbin /usr/bin/gcc -O2 -o "${tmpdir}/unscd" "${tmpdir}/nscd.c"
332	sudo install -m 755 "${tmpdir}/unscd" /usr/local/sbin/unscd
333	rm -rf "${tmpdir}"
334
335	# Create systemd service
336	sudo tee /etc/systemd/system/unscd.service >/dev/null <<-'EOF'
337		[Unit]
338		Description=Name Service Cache Daemon (unscd for nix)
339		After=sssd.service network.target
340
341		[Service]
342		Type=forking
343		ExecStart=/usr/local/sbin/unscd
344		Restart=on-failure
345
346		[Install]
347		WantedBy=multi-user.target
348	EOF
349
350	sudo systemctl daemon-reload
351	sudo systemctl enable --now unscd
352	log_info "unscd installed and running"
353}
354
355# --- Phase 8: WireGuard ---
356
357setup_power() {
358	log_info "Configuring power management..."
359
360	# Powertop auto-tune service
361	if [[ ! -f /etc/systemd/system/powertop.service ]]; then
362		log_info "Creating powertop auto-tune service..."
363		sudo tee /etc/systemd/system/powertop.service >/dev/null <<-'EOF'
364			[Unit]
365			Description=PowerTOP auto-tune
366			After=multi-user.target
367
368			[Service]
369			Type=oneshot
370			ExecStart=/usr/sbin/powertop --auto-tune
371
372			[Install]
373			WantedBy=multi-user.target
374		EOF
375		sudo systemctl daemon-reload
376		sudo systemctl enable --now powertop.service
377	else
378		log_info "powertop service already exists"
379	fi
380
381	# Configure tuned-ppd mapping (PPD profiles → tuned profiles)
382	# All values must be unique per map (injective) for reversibility.
383	# AC:      power-saver→powersave, balanced→balanced, performance→latency-performance
384	# Battery: power-saver→powersave, balanced→balanced-battery, performance→balanced
385	if command -v tuned-adm &>/dev/null; then
386		log_info "Configuring tuned-ppd..."
387		sudo tee /etc/tuned/ppd.conf >/dev/null <<-'EOF'
388			[main]
389			default=balanced
390			battery_detection=true
391			sysfs_acpi_monitor=true
392
393			[profiles]
394			power-saver=powersave
395			balanced=balanced
396			performance=latency-performance
397
398			[battery]
399			balanced=balanced-battery
400			performance=balanced
401		EOF
402		sudo systemctl restart tuned tuned-ppd
403		log_info "tuned-ppd configured"
404	fi
405}
406
407setup_wireguard() {
408	# Use Fedora-native wg-quick service (proper SELinux context)
409	# Config file is managed by system-manager in /etc/wireguard/wg0.conf
410	log_info "Setting up WireGuard (native service)..."
411	sudo dnf install -y wireguard-tools
412	sudo systemctl enable wg-quick@wg0
413
414	# Trust wg0 interface in firewall (allows SSH and all traffic over VPN)
415	if command -v firewall-cmd &>/dev/null; then
416		log_info "Adding wg0 to firewalld trusted zone..."
417		sudo firewall-cmd --zone=trusted --add-interface=wg0 --permanent
418		sudo firewall-cmd --reload
419	fi
420
421	if [[ -f /etc/wireguard/private.key ]]; then
422		log_info "WireGuard private key exists, starting service..."
423		sudo systemctl start wg-quick@wg0
424		return 0
425	fi
426
427	log_warn "WireGuard private key not found at /etc/wireguard/private.key"
428	log_warn ""
429	log_warn "To set up WireGuard, either:"
430	log_warn "  1. Copy existing key:  sudo cp /path/to/backup/private.key /etc/wireguard/"
431	log_warn "  2. Generate new key:   wg genkey | sudo tee /etc/wireguard/private.key"
432	log_warn "     Then update globals.nix with new pubkey: sudo cat /etc/wireguard/private.key | wg pubkey"
433	log_warn "     (current VPN IP: 10.100.0.19)"
434	log_warn ""
435	log_warn "After placing the key:  sudo chmod 600 /etc/wireguard/private.key"
436	log_warn "Then re-run:  sudo systemctl restart wg-quick@wg0"
437}
438
439# --- Phase 9: Home-manager ---
440
441setup_home_manager() {
442	log_info "Setting up home-manager..."
443
444	# Ensure nix is in PATH
445	if ! command -v nix &>/dev/null; then
446		# shellcheck disable=SC1091
447		. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
448	fi
449
450	cd "$REPO_PATH"
451
452	# Build and activate home-manager
453	nix run home-manager -- switch --flake ".#vdemeest@${SYSTEM_CONFIG}"
454
455	log_info "Home-manager activated!"
456}
457
458# --- Phase 10: Shell setup ---
459
460setup_shell() {
461	local zsh_path="$HOME/.local/state/nix/profile/bin/zsh"
462
463	if ! grep -q "$zsh_path" /etc/shells 2>/dev/null; then
464		log_info "Adding nix zsh to /etc/shells..."
465		echo "$zsh_path" | sudo tee -a /etc/shells
466	fi
467
468	# CSB uses LDAP/FreeIPA, chsh won't work — use bashrc exec instead
469	if ! grep -q 'exec.*zsh' ~/.bashrc 2>/dev/null; then
470		log_info "Configuring bash to exec into zsh..."
471		# shellcheck disable=SC2016
472		echo '[[ $- == *i* && -x "$HOME/.local/state/nix/profile/bin/zsh" ]] && exec "$HOME/.local/state/nix/profile/bin/zsh"' >>~/.bashrc
473	fi
474
475	log_info "Shell configured (zsh via bashrc exec)"
476}
477
478# --- Summary ---
479
480print_summary() {
481	log_info ""
482	log_info "╔═══════════════════════════════════════════╗"
483	log_info "║    Kyushu (Fedora CSB) Bootstrap Done     ║"
484	log_info "╠═══════════════════════════════════════════╣"
485	log_info "║  Nix:            ✓ installed              ║"
486	log_info "║  System configs: ✓ written               ║"
487	log_info "║  Home-manager:   ✓ activated              ║"
488	log_info "║  Native apps:    ✓ installed              ║"
489	log_info "╠═══════════════════════════════════════════╣"
490	log_info "║  Rebuild home:                            ║"
491	log_info "║  Rebuild home:                            ║"
492	log_info "║    home-manager switch --flake .#vdemeest@kyushu ║"
493	log_info "╚═══════════════════════════════════════════╝"
494}
495
496# --- Main ---
497
498main() {
499	log_info "Bootstrapping kyushu (Fedora CSB)..."
500	log_info "  Repo:   $REPO_URL$REPO_PATH"
501	log_info "  Config: $SYSTEM_CONFIG"
502	echo
503
504	check_root
505	install_nix
506	configure_nix
507	clone_repo
508	setup_selinux_policy
509	install_native_apps
510	setup_tpm
511	setup_luks_fido2
512	setup_unscd
513	setup_system_configs
514	setup_power
515	setup_wireguard
516	setup_yubikey_sudo
517	setup_shell
518	setup_home_manager
519	print_summary
520}
521
522# Only run main when executed directly, not when sourced. This lets you run
523# individual phases, e.g. (use bash, the script targets bash):
524#   source imperative/kyushu/bootstrap.sh
525#   setup_tpm
526if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
527	main "$@"
528fi