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