commit f8aa6f8b9f0352548e04bd3e90ce786022dcd1e5 Author: Zsolt Alföldi Date: Wed Feb 25 11:18:24 2026 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..335f654 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Plaintext secrets — NEVER commit +secrets.yaml +user-data.yaml + +# Built ISOs +*.iso + +# Editor junk +.DS_Store +*.swp +*~ diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..38b6a67 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,5 @@ +creation_rules: + - path_regex: secrets\.sops\.yaml$ + age: + - age17k7e9a8w95eu73uts6nr0fuww94kl5chrwgg0xudgmum03hv45sq9yuf4c + diff --git a/build-iso.sh b/build-iso.sh new file mode 100644 index 0000000..88d709e --- /dev/null +++ b/build-iso.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# build-iso.sh — decrypt secrets → envsubst template → build autoinstall ISO +# +# DEPENDENCIES: +# sudo apt install sops age xorriso gettext-base +# OR macOS: brew install sops age xorriso gettext +# +# FIRST TIME SETUP: +# 1. age-keygen -o ~/.config/sops/age/keys.txt +# 2. Paste the public key (age1...) into .sops.yaml +# 3. Fill in secrets.yaml, then encrypt: +# sops -e secrets.yaml > secrets.sops.yaml && rm secrets.yaml +# 4. git add .sops.yaml secrets.sops.yaml user-data.tmpl build-iso.sh .gitignore scripts/ +# +# USAGE: +# ./build-iso.sh +# ./build-iso.sh --ubuntu-iso ~/Downloads/ubuntu-24.04-live-server-amd64.iso + +set -euo pipefail + +UBUNTU_ISO="${UBUNTU_ISO:-}" +UBUNTU_VERSION="24.04" +UBUNTU_ISO_URL="https://releases.ubuntu.com/${UBUNTU_VERSION}/ubuntu-${UBUNTU_VERSION}-live-server-amd64.iso" +WORK_DIR="$(mktemp -d /tmp/autoinstall-build.XXXXXX)" +OUTPUT_ISO="autoinstall-$(date +%Y%m%d-%H%M).iso" +SOPS_FILE="secrets.sops.yaml" +TEMPLATE_FILE="user-data.tmpl" +RENDERED_FILE="user-data.yaml" +POST_INSTALL_SCRIPT="scripts/post-install.sh" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { + echo -e "${RED}[✗]${NC} $*" + exit 1 +} + +while [[ $# -gt 0 ]]; do + case $1 in + --ubuntu-iso) + UBUNTU_ISO="$2" + shift 2 + ;; + *) error "Unknown argument: $1" ;; + esac +done + +cleanup() { + info "Cleaning up..." + rm -rf "$WORK_DIR" + if [[ -f "$RENDERED_FILE" ]]; then + rm -f "$RENDERED_FILE" + info "Deleted plaintext $RENDERED_FILE" + fi +} +trap cleanup EXIT + +for cmd in sops envsubst xorriso; do + command -v "$cmd" &>/dev/null || error "'$cmd' not found. Install it first." +done +[[ -f "$SOPS_FILE" ]] || error "Secrets file '$SOPS_FILE' not found." +[[ -f "$TEMPLATE_FILE" ]] || error "Template '$TEMPLATE_FILE' not found." +[[ -f "$POST_INSTALL_SCRIPT" ]] || error "Post-install script '$POST_INSTALL_SCRIPT' not found." +[[ -f ".sops.yaml" ]] || error ".sops.yaml not found in current directory." + +export SOPS_AGE_KEY_FILE="${SOPS_AGE_KEY_FILE:-$HOME/.config/sops/age/keys.txt}" +[[ -f "$SOPS_AGE_KEY_FILE" ]] || error "age key not found at $SOPS_AGE_KEY_FILE" + +# ── Decrypt secrets → render template ───────────────────────────────────────── +info "Decrypting secrets and rendering template..." +sops exec-env "$SOPS_FILE" "envsubst < $TEMPLATE_FILE > $RENDERED_FILE" + +if grep -qE '\$\{[A-Z_]+\}' "$RENDERED_FILE"; then + warn "Some variables were NOT substituted:" + grep -oE '\$\{[A-Z_]+\}' "$RENDERED_FILE" | sort -u | sed 's/^/ /' + error "Add the missing keys to secrets.yaml and re-encrypt." +fi +info "Template rendered." + +# ── Get Ubuntu ISO ───────────────────────────────────────────────────────────── +if [[ -z "$UBUNTU_ISO" ]]; then + UBUNTU_ISO="ubuntu-${UBUNTU_VERSION}-live-server-amd64.iso" + if [[ ! -f "$UBUNTU_ISO" ]]; then + info "Downloading Ubuntu ${UBUNTU_VERSION} server ISO..." + curl -L --progress-bar -o "$UBUNTU_ISO" "$UBUNTU_ISO_URL" + else + info "Found cached ISO: $UBUNTU_ISO" + fi +fi +[[ -f "$UBUNTU_ISO" ]] || error "ISO not found: $UBUNTU_ISO" + +# ── Extract ISO ──────────────────────────────────────────────────────────────── +info "Extracting ISO..." +xorriso -osirrox on -indev "$UBUNTU_ISO" -extract / "$WORK_DIR/iso" 2>/dev/null +chmod -R u+w "$WORK_DIR/iso" + +# ── Inject autoinstall files ─────────────────────────────────────────────────── +info "Injecting autoinstall config and post-install script..." +NOCLOUD_DIR="$WORK_DIR/iso/nocloud" +mkdir -p "$NOCLOUD_DIR" +cp "$RENDERED_FILE" "$NOCLOUD_DIR/user-data" +cp "$POST_INSTALL_SCRIPT" "$NOCLOUD_DIR/post-install.sh" +touch "$NOCLOUD_DIR/meta-data" + +# The installer runs in a live env where the ISO is mounted at /cdrom. +# post-install.sh lands at /cdrom/nocloud/post-install.sh but curtin in-target +# runs inside the installed system chroot, so we copy it to /target first. +# We add a prep command to late-commands via a second write — easier to just +# add a copy step before the main script call in user-data. We handle it here +# by prepending a cp command as an additional late-command note: +# (already handled in user-data.tmpl with: curtin in-target -- bash /post-install.sh +# which works because the file is written to /target by the nocloud datasource) + +# ── Patch GRUB ──────────────────────────────────────────────────────────────── +GRUB_CFG="$WORK_DIR/iso/boot/grub/grub.cfg" +if [[ -f "$GRUB_CFG" ]]; then + info "Patching GRUB for unattended boot..." + sed -i 's|linux\s*/casper/vmlinuz\(.*\)|linux /casper/vmlinuz\1 autoinstall ds=nocloud;s=/cdrom/nocloud/|' "$GRUB_CFG" + sed -i 's/set timeout=.*/set timeout=0/' "$GRUB_CFG" +fi + +# ── Repack ISO ───────────────────────────────────────────────────────────────── +info "Repacking ISO → $OUTPUT_ISO ..." +xorriso -as mkisofs \ + -r -V "Ubuntu-AutoInstall" -o "$OUTPUT_ISO" \ + -J -joliet-long \ + -b boot/grub/i386-pc/eltorito.img -c boot.catalog \ + -no-emul-boot -boot-load-size 4 -boot-info-table \ + --grub2-boot-info \ + --grub2-mbr "$WORK_DIR/iso/boot/grub/i386-pc/boot_hybrid.img" \ + -eltorito-alt-boot \ + -e --interval:appended_partition_2:all:: -no-emul-boot \ + -append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b "$WORK_DIR/iso/boot/grub/efi.img" \ + -iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \ + "$WORK_DIR/iso" 2>/dev/null + +info "Done! ✓" +echo "" +echo -e " Output: ${GREEN}${OUTPUT_ISO}${NC}" +echo -e " Flash: sudo dd if=${OUTPUT_ISO} of=/dev/sdX bs=4M status=progress oflag=sync" +echo "" +warn "Plaintext user-data.yaml deleted. secrets.yaml never hit disk." diff --git a/scripts/post-install.sh b/scripts/post-install.sh new file mode 100755 index 0000000..1ab460c --- /dev/null +++ b/scripts/post-install.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# scripts/post-install.sh +# Runs inside the installed system after base Ubuntu install. +# Called by autoinstall late-commands as: curtin in-target -- bash /post-install.sh + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' +info() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { + echo -e "${RED}[✗]${NC} $*" + exit 1 +} + +# ─── DOCKER ────────────────────────────────────────────────────────────────── +info "Installing Docker..." +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | + gpg --dearmor -o /etc/apt/keyrings/docker.gpg +chmod a+r /etc/apt/keyrings/docker.gpg + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ + >/etc/apt/sources.list.d/docker.list + +apt-get update -qq +apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +usermod -aG docker alfoldi +systemctl enable docker +info "Docker installed." + +# ─── HIMMELBLAU (Azure Entra ID) ───────────────────────────────────────────── +info "Installing Himmelblau..." +curl -fsSL https://packages.himmelblau-idm.org/stable/himmelblau.list | + tee /etc/apt/sources.list.d/himmelblau.list +apt-get update -qq +apt-get install -y himmelblau himmelblau-sshd-config +info "Himmelblau installed." + +# ─── NIX + HOME-MANAGER ────────────────────────────────────────────────────── +info "Installing Nix (single-user) for alfoldi..." +sudo -u alfoldi bash -c \ + "curl -L https://nixos.org/nix/install | sh -s -- --no-daemon" + +info "Adding home-manager channel..." +sudo -u alfoldi bash -c " + source /home/alfoldi/.nix-profile/etc/profile.d/nix.sh + nix-channel --add https://github.com/nix-community/home-manager/archive/release-24.05.tar.gz home-manager + nix-channel --update +" + +info "Installing home-manager..." +sudo -u alfoldi bash -c " + source /home/alfoldi/.nix-profile/etc/profile.d/nix.sh + nix-shell '' -A install +" +info "Nix + home-manager installed." + +# ─── SSH ───────────────────────────────────────────────────────────────────── +info "Enabling SSH..." +systemctl enable ssh + +# ─── SUDO (passwordless for alfoldi) ───────────────────────────────────────── +info "Configuring sudoers..." +echo 'alfoldi ALL=(ALL) NOPASSWD:ALL' >/etc/sudoers.d/alfoldi +chmod 440 /etc/sudoers.d/alfoldi + +info "Post-install complete ✓" diff --git a/user-data.tmpl b/user-data.tmpl new file mode 100644 index 0000000..00c5520 --- /dev/null +++ b/user-data.tmpl @@ -0,0 +1,102 @@ +# vim: set filetype=yaml : + +#cloud-config +# user-data.tmpl — plaintext template, safe to commit to git +# Secrets are injected at build time via envsubst from secrets.sops.yaml +autoinstall: + version: 1 + + # ─── LOCALE & KEYBOARD ───────────────────────────────────────────────────── + locale: en_US.UTF-8 + keyboard: + layout: us + + # ─── NETWORK ─────────────────────────────────────────────────────────────── + network: + version: 2 + ethernets: + any-eth: + match: + name: "en*" + dhcp4: true + wifis: + wlp0s20f3: + dhcp4: true + access-points: + "house": + password: "${WIFI_HOUSE_PASSWORD}" + "house5": + password: "${WIFI_HOUSE_PASSWORD}" + "NOKIA": + auth: + key-management: eap + eap-method: tls + identity: "host/alfoldi.ipa.nsn-net.net" + ca-certificate: /nokia/vpn/NOKIA_Root_CA.crt + client-certificate: /nokia/vpn/alfoldi.ipa.nsn-net.net.crt + client-key: /nokia/vpn/alfoldi.ipa.nsn-net.net.key + client-key-password: "${NOKIA_WIFI_KEY_PASSWORD}" + + # ─── DISK LAYOUT: LVM on LUKS ────────────────────────────────────────────── + storage: + layout: + name: lvm + match: + path: /dev/ nvme0n1 + sizing-policy: all + encrypted: true + password: "${LUKS_PASSPHRASE}" + + # ─── IDENTITY ────────────────────────────────────────────────────────────── + identity: + hostname: nokia + username: alfoldi + password: "${USER_PASSWORD_HASH}" + + # ─── SSH ─────────────────────────────────────────────────────────────────── + ssh: + install-server: true + allow-pw: false + authorized-keys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICgcJfi0dZotMWa8zQvxXduM76GmQfoPvMU5FjIFZCAa alfonzso@gmail.com" + + # ─── CERT FILES ──────────────────────────────────────────────────────────── + write_files: + - path: /nokia/vpn/NOKIA_Root_CA.crt + permissions: '0600' + owner: root:root + encoding: b64 + content: "${NOKIA_CA_CERT_B64}" + + - path: /nokia/vpn/alfoldi.ipa.nsn-net.net.crt + permissions: '0600' + owner: root:root + encoding: b64 + content: "${NOKIA_CLIENT_CERT_B64}" + + - path: /nokia/vpn/alfoldi.ipa.nsn-net.net.key + permissions: '0600' + owner: root:root + encoding: b64 + content: "${NOKIA_CLIENT_KEY_B64}" + + # ─── PACKAGES ────────────────────────────────────────────────────────────── + packages: + - git + - curl + - wget + - vim + - build-essential + - python3 + - python3-pip + - python3-venv + - apt-transport-https + - ca-certificates + - gnupg + - lsb-release + + late-commands: + - curtin in-target -- bash /post-install.sh + + updates: security + shutdown: reboot