From 79903605a57526d89ceb3bdf41f2ece4fa039e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phorcys=20=F0=9F=90=BE?= Date: Mon, 24 Aug 2026 20:10:10 +0000 Subject: [PATCH 1/2] refactor(installer): install-answers.json + initialUser option (#29) install.sh generated hosts//local.nix by copying local.nix.example and splicing values in with a two-stage Nix/sed escape. That was fragile (passwords with $, ", or \ could corrupt the generated Nix) and mixed credentials into the user-editable override file. Instead, write the install-time answers to hosts//install-answers.json with jq and read them from Nix via builtins.fromJSON. The generated default.nix maps them onto NixOS options at mkDefault, so local.nix stays credential-free and can override any value (e.g. from agenix/sops). Fix #29 by replacing the "set CODER_ADMIN_* on coder-init-admin.service via local.nix" pattern with a first-class option: services.coder-nixos.initialUser = { username; email; password; }; configuration.nix wires those onto coder-init-admin.service (the service that does the work) as INITIAL_USER_*, renamed to reflect that it only sets the initial user/password. Users no longer hand-edit a systemd service's environment. Also move the installer bootstrap assets (local.nix.example, disko-standard.nix) under installer/bootstrap/, and update the docs. --- README.md | 17 ++-- agents.md | 9 +- configuration.nix | 84 ++++++++++++------ flake.nix | 2 +- hosts/_appliance-disk/default.nix | 4 +- hosts/_appliance-iso/default.nix | 2 +- hosts/coder-thinkcentre/default.nix | 2 +- hosts/incus-vm/README.md | 16 ++-- install.sh | 83 +++++++++-------- .../bootstrap}/disko-standard.nix | 2 +- installer/bootstrap/local.nix.example | 78 ++++++++++++++++ local.nix.example | 88 ------------------- nixos/_images/box-turnkey.nix | 30 +++---- 13 files changed, 225 insertions(+), 192 deletions(-) rename {nixos => installer/bootstrap}/disko-standard.nix (98%) create mode 100644 installer/bootstrap/local.nix.example delete mode 100644 local.nix.example diff --git a/README.md b/README.md index d3da00e..f4c5cdf 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ default `networking.hostName = "coder-box"` (set in `configuration.nix`). Two community tools do the heavy lifting: -- [`disko`](https://github.com/nix-community/disko) declares partition layouts in Nix. `nixos/disko-standard.nix` is a single-disk UEFI layout (1 GB EFI / ZFS root pool; no on-disk swap — zram instead). `install.sh` picks the device at install time; the ZFS `networking.hostId` is derived in Nix from the hostname (sha256 substring), so each host gets a distinct id automatically. ZFS gives cheap on-demand snapshots (take one before a risky rebuild and roll back in seconds), zstd compression, and checksum/scrub integrity. +- [`disko`](https://github.com/nix-community/disko) declares partition layouts in Nix. `installer/bootstrap/disko-standard.nix` is a single-disk UEFI layout (1 GB EFI / ZFS root pool; no on-disk swap — zram instead). `install.sh` picks the device at install time; the ZFS `networking.hostId` is derived in Nix from the hostname (sha256 substring), so each host gets a distinct id automatically. ZFS gives cheap on-demand snapshots (take one before a risky rebuild and roll back in seconds), zstd compression, and checksum/scrub integrity. - [`nixos-facter`](https://github.com/nix-community/nixos-facter) writes a JSON hardware report (`facter.json`) that replaces `hardware-configuration.nix` on new hosts. The `nixos-facter-modules` module reads it to set kernel modules, microcode, GPU drivers, and so on. ## Installing on a new machine @@ -61,9 +61,9 @@ sudo ./install.sh \ `./install.sh --help` lists everything. `--coder-admin-password-file PATH` and `--nixos-password-file PATH` read passwords from a file so they don't end up in shell history. `--no-reboot` skips the automatic reboot at the end. -The installer generates `hosts//{default.nix,local.nix,facter.json}`, copies the repo into `/etc/nixos-repo` on the target, and symlinks `/etc/nixos/flake.nix`. After reboot, `nixos-rebuild switch` Just Works. Continue with [After install](#after-install). +The installer generates `hosts//{default.nix,local.nix,install-answers.json,facter.json}`, copies the repo into `/etc/nixos-repo` on the target, and symlinks `/etc/nixos/flake.nix`. After reboot, `nixos-rebuild switch` Just Works. Continue with [After install](#after-install). -> **Different partition layout?** Don't import `nixos/disko-standard.nix`; drop your own disko config into the host folder instead. See [disko examples](https://github.com/nix-community/disko/tree/master/example). +> **Different partition layout?** Don't import `installer/bootstrap/disko-standard.nix`; drop your own disko config into the host folder instead. See [disko examples](https://github.com/nix-community/disko/tree/master/example). > **BIOS hardware?** The shared config defaults to `systemd-boot` (UEFI). In your host's `default.nix`: > ```nix @@ -117,7 +117,7 @@ The turn-key login + Coder admin bootstrap shared by all image flavours live in desktop, and admin `admin@coder.com` / `PleaseChangeMe1234`. Coder comes up at `http://.local:3000` (or the `*.try.coder.app` tunnel URL in `/etc/motd`). Change these before sharing an image by dropping a gitignored -`hosts//local.nix` (same shape as `local.nix.example`). +`hosts//local.nix` (same shape as `installer/bootstrap/local.nix.example`). ### Appliance ISO (`_appliance-iso`) @@ -144,7 +144,7 @@ sudo dd if=out/appliance-iso/iso/coder-box-appliance-*.iso of=/dev/sdX bs=4M sta > a working build + boot. Built with [disko](https://github.com/nix-community/disko)'s image builder, so -it carries the real on-disk GPT layout from `nixos/disko-standard.nix` (1 GB +it carries the real on-disk GPT layout from `installer/bootstrap/disko-standard.nix` (1 GB ESP + ZFS root pool) and **state survives reboots**, exactly like a machine you ran `install.sh` on. `hosts/_appliance-disk/default.nix` imports `disko-standard.nix` + `box-turnkey.nix`. @@ -204,9 +204,10 @@ on every `sudo nixos-rebuild switch`. sudo nixos-rebuild switch # most changes sudo nixos-rebuild boot && sudo reboot # changes that touch the desktop stack -# Edited hosts//local.nix or facter.json? Re-mark intent-to-add: +# Edited hosts//local.nix, install-answers.json, or facter.json? Re-mark intent-to-add: sudo git -C /etc/nixos-repo add --intent-to-add -f \ hosts//local.nix \ + hosts//install-answers.json \ hosts//facter.json ``` @@ -265,14 +266,14 @@ Fully automated, no follow-up steps needed. The service: 3. Drops and recreates the PostgreSQL database 4. Wipes `/var/lib/coder` (data dir, sentinel, tokens, Podman volumes) 5. Starts Coder and waits for the API -6. Re-bootstraps the admin user from credentials in the host's `local.nix` +6. Re-bootstraps the initial user from `services.coder-nixos.initialUser` (set in the host's `install-answers.json`) 7. Mints a fresh long-lived session token → writes to `/etc/coder/session-token` 8. Restarts `coder-redirect` 9. Runs `nixos-rebuild switch` to push templates back via `coder-template-sync` ### Changing the admin password -1. Edit `hosts//local.nix`, update `CODER_ADMIN_PASSWORD`. +1. Edit `hosts//install-answers.json`, update `initialUser.password` (or override `services.coder-nixos.initialUser.password` in `local.nix`). 2. Run `sudo nixos-rebuild switch` to bake the new password into the service. 3. Run `sudo systemctl start coder-reset` to wipe and re-bootstrap with the new password. diff --git a/agents.md b/agents.md index 1715491..d93a9c2 100644 --- a/agents.md +++ b/agents.md @@ -198,7 +198,7 @@ sudo k3s kubectl describe pod -n coder-workspaces - **Template sync skips**, if `/etc/coder/session-token` is empty, the activation script exits cleanly. The token is auto-populated by `coder-init-admin.service`; if it's missing, check `journalctl -u coder-init-admin`. - **`coder` binary path** — the binary is in PATH via NixOS environment; don't hardcode nix store paths in scripts (they change with every package update). - **`--flake /etc/nixos` fails** — `/etc/nixos` is a plain dir holding only a `flake.nix` *symlink* into `/etc/nixos-repo`. Nix follows the symlink into the store but can't find the sibling files (configuration.nix, hosts/, nixos/), dying with `path '/nix/store/...-source/etc/nixos-repo/flake.nix' does not exist`. Always rebuild against the real tree: `--flake /etc/nixos-repo` (or `cd /etc/nixos-repo && nixos-rebuild switch --flake .`). -- **`Git tree '/etc/nixos-repo' is dirty` warning** — harmless. `hosts//{local.nix,facter.json}` are gitignored and intent-to-added by the installer, so the tree always reads "dirty". After editing them, re-mark intent-to-add so the flake sees them: `sudo git -C /etc/nixos-repo add --intent-to-add -f hosts//local.nix hosts//facter.json`. +- **`Git tree '/etc/nixos-repo' is dirty` warning** — harmless. `hosts//{local.nix,install-answers.json,facter.json}` are gitignored and intent-to-added by the installer, so the tree always reads "dirty". After editing them, re-mark intent-to-add so the flake sees them: `sudo git -C /etc/nixos-repo add --intent-to-add -f hosts//local.nix hosts//install-answers.json hosts//facter.json`. - **ScreenConnect blank screen** — the box now runs GNOME on Wayland (GDM), and GNOME 49 dropped the Xorg session, so there is no X11 desktop to fall back to. ScreenConnect reaches `DISPLAY=:0` through XWayland (see `nixos/screenconnect.nix`) but **cannot screen-capture the Wayland compositor** through it, so the remote view may be black/blank. Capturing the GNOME session needs a Wayland-aware path (PipeWire/portal, e.g. `gnome-remote-desktop`); the X11 agent will connect but not mirror the desktop. ## Wildcard App Access (TODO) @@ -216,9 +216,12 @@ sudo k3s kubectl describe pod -n coder-workspaces flake.nix # entry point: nixosConfigurations. per machine flake.lock # pinned nixpkgs / disko / nixos-facter-modules configuration.nix # shared NixOS config (edit here for services/packages) - local.nix.example # template for hosts//local.nix + install.sh # live-USB installer (writes hosts//{default.nix,local.nix,install-answers.json,facter.json}) + installer/ + bootstrap/ # assets install.sh consumes when generating a new host + disko-standard.nix # shared disko config: UEFI + single-disk layout for new hosts + local.nix.example # template copied to hosts//local.nix (optional overrides only) nixos/ - disko-standard.nix # shared disko config: UEFI + single-disk layout for new hosts modules/ # NixOS service modules (services.coder-nixos.*) k3s/ # base single-node k3s server podman/ # k3s + rootless Podman socket runtime diff --git a/configuration.nix b/configuration.nix index 3b4fe14..54b69fe 100644 --- a/configuration.nix +++ b/configuration.nix @@ -84,6 +84,29 @@ in description = "LAN IP of this box, used for CODER_AGENT_URL and k8s hostAliases so pods resolve the hostname without relying on mDNS. Set in the host's local.nix. Leave empty to fall back to hostname-based mDNS URL."; }; + # ── NixOS option: Coder initial user ─────────────────────────────────────── + # The owner account coder-init-admin.service creates on first boot. Set from + # hosts//install-answers.json by the generated default.nix, or overridden + # in local.nix (optionally wired to a secret via agenix/sops). Plain strings, + # not systemd env overrides, so credentials live in one obvious place. + options.services.coder-nixos.initialUser = { + username = lib.mkOption { + type = lib.types.str; + default = "admin"; + description = "Username of the Coder initial (owner) user created on first boot."; + }; + email = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Email of the Coder initial user. Empty skips the bootstrap and leaves the browser first-run wizard to create the user."; + }; + password = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Initial password for the Coder initial user. Change it after first login, or point it at a secret via agenix/sops."; + }; + }; + options.services.coder-sync-ssh-keys.githubUsers = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; @@ -128,7 +151,7 @@ in boot.loader.efi.canTouchEfiVariables = lib.mkDefault true; # ── Filesystem: ZFS root ─────────────────────────────────────────────────── - # The standard single-disk layout (nixos/disko-standard.nix) puts root on a + # The standard single-disk layout (installer/bootstrap/disko-standard.nix) puts root on a # ZFS pool ("rpool"). The kernel needs the ZFS module available at boot to # import it; declare it here so every host that follows the standard layout # (and the prebuilt appliance images) boots. Hosts that predate disko and @@ -152,7 +175,7 @@ in services.zfs.trim.enable = lib.mkDefault true; # ── Swap ────────────────────────────────────────────────────────────────── - # No on-disk swap partition (see nixos/disko-standard.nix). Use a + # No on-disk swap partition (see installer/bootstrap/disko-standard.nix). Use a # compressed in-RAM swap device instead, sized to half of RAM. zramSwap.enable = lib.mkDefault true; @@ -304,9 +327,10 @@ in services.printing.enable = true; # ── Users ───────────────────────────────────────────────────────────────── - # Desktop / SSH login user is declared per-host in local.nix (template - # in local.nix.example); username and password are install-time flags. - # The `coder` system user (uid 991) is shared and declared further down. + # The desktop / SSH login user is declared per-host by the generated + # hosts//default.nix from install-answers.json (username + initial + # password chosen at install time). The `coder` system user (uid 991) is + # shared and declared further down. security.sudo.wheelNeedsPassword = false; @@ -491,9 +515,9 @@ in # ── Coder server ────────────────────────────────────────────────────────── # Base env vars live here. Server secrets (e.g. OAuth) are merged in via # systemd.services.coder.environment in hosts//local.nix; no - # EnvironmentFile. Admin bootstrap creds (CODER_ADMIN_*) are NOT set here — - # they live on coder-init-admin.service so they stay off the long-running - # server's environment. + # EnvironmentFile. Initial-user credentials are NOT set here — they come from + # the services.coder-nixos.initialUser option and are wired onto + # coder-init-admin.service below, so they stay off the long-running server. systemd.services.coder = { description = "Coder Server"; wantedBy = [ "multi-user.target" ]; @@ -541,11 +565,10 @@ in }; # ── Admin user bootstrap ────────────────────────────────────────────────── - # Reads CODER_ADMIN_* from this service's own environment (set via - # local.nix as systemd.services.coder-init-admin.environment), keeping the - # admin credentials off the long-running coder.service. - # Creates a local admin account once; sentinel prevents re-running. - # If CODER_ADMIN_EMAIL is unset, skips and directs user to the browser wizard. + # Creates the Coder initial (owner) user once from the + # services.coder-nixos.initialUser option; a sentinel prevents re-running. + # If initialUser.email is unset, skips and directs the user to the browser + # wizard. systemd.services.coder-init-admin = { description = "Coder bootstrap: create admin, mint session token, deploy templates"; wantedBy = [ "multi-user.target" ]; @@ -553,10 +576,13 @@ in requires = [ "coder.service" ]; # Inherit the coder.service environment so CODER_PG_CONNECTION_URL (and the - # other server vars) are available without duplication. The CODER_ADMIN_* - # credentials are merged in on top via the coder-init-admin.environment - # definition in hosts//local.nix (NixOS merges attrset options). - inherit (config.systemd.services.coder) environment; + # other server vars) are available without duplication, then add the + # initial-user credentials from the services.coder-nixos.initialUser option. + environment = config.systemd.services.coder.environment // { + INITIAL_USER_USERNAME = config.services.coder-nixos.initialUser.username; + INITIAL_USER_EMAIL = config.services.coder-nixos.initialUser.email; + INITIAL_USER_PASSWORD = config.services.coder-nixos.initialUser.password; + }; serviceConfig = { Type = "oneshot"; @@ -573,8 +599,8 @@ in templates_sentinel=/var/lib/coder/.templates-deployed token_file=/etc/coder/session-token - if [ -z "''${CODER_ADMIN_EMAIL:-}" ]; then - echo "CODER_ADMIN_EMAIL not set, skipping bootstrap." + if [ -z "''${INITIAL_USER_EMAIL:-}" ]; then + echo "INITIAL_USER_EMAIL not set, skipping bootstrap." echo "Complete the first-run wizard at http://$(${pkgs.nettools}/bin/hostname -s).local:3000" exit 0 fi @@ -600,12 +626,12 @@ in if [ -f "$admin_sentinel" ]; then echo "Admin user already created." else - echo "Creating admin user $CODER_ADMIN_EMAIL..." + echo "Creating admin user $INITIAL_USER_EMAIL..." ${coder}/bin/coder server create-admin-user \ --postgres-url "$CODER_PG_CONNECTION_URL" \ - --username "$CODER_ADMIN_USERNAME" \ - --email "$CODER_ADMIN_EMAIL" \ - --password "$CODER_ADMIN_PASSWORD" + --username "$INITIAL_USER_USERNAME" \ + --email "$INITIAL_USER_EMAIL" \ + --password "$INITIAL_USER_PASSWORD" touch "$admin_sentinel" fi @@ -617,7 +643,7 @@ in echo "Logging in as admin to mint a long-lived token..." SESSION=$(${pkgs.curl}/bin/curl -sf -X POST http://localhost:3000/api/v2/users/login \ -H 'Content-Type: application/json' \ - -d "{\"email\":\"$CODER_ADMIN_EMAIL\",\"password\":\"$CODER_ADMIN_PASSWORD\"}" \ + -d "{\"email\":\"$INITIAL_USER_EMAIL\",\"password\":\"$INITIAL_USER_PASSWORD\"}" \ | ${pkgs.jq}/bin/jq -r '.session_token') [ -n "$SESSION" ] && [ "$SESSION" != "null" ] \ || { echo "Admin login failed." >&2; exit 1; } @@ -694,9 +720,9 @@ in ]; requires = [ "postgresql.service" ]; - # Step 8 mints a session token using CODER_ADMIN_EMAIL/PASSWORD, so pull in - # the coder-init-admin environment (which itself includes the coder.service - # vars plus the CODER_ADMIN_* credentials from local.nix). + # Step 8 mints a session token using the initial user's credentials, so pull + # in the coder-init-admin environment (which includes the coder.service vars + # plus the INITIAL_USER_* credentials from the initialUser option). inherit (config.systemd.services.coder-init-admin) environment; serviceConfig = { @@ -744,12 +770,12 @@ in echo "--- bootstrapping admin user" ${pkgs.systemd}/bin/systemctl start coder-init-admin.service - # 8. Mint a fresh long-lived session token using the admin creds from local.nix + # 8. Mint a fresh long-lived session token using the initial user's creds echo "--- minting session token" SESSION=$(${pkgs.curl}/bin/curl -sf \ -X POST http://localhost:3000/api/v2/users/login \ -H 'Content-Type: application/json' \ - -d "{\"email\":\"''${CODER_ADMIN_EMAIL}\",\"password\":\"''${CODER_ADMIN_PASSWORD}\"}" \ + -d "{\"email\":\"''${INITIAL_USER_EMAIL}\",\"password\":\"''${INITIAL_USER_PASSWORD}\"}" \ | ${pkgs.jq}/bin/jq -r '.session_token') LONG_TOKEN=$(CODER_URL=http://localhost:3000 CODER_SESSION_TOKEN="$SESSION" \ ${coder}/bin/coder tokens create --name nixos-sync --lifetime 8760h) diff --git a/flake.nix b/flake.nix index 4d449f9..6520c3e 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; # Declarative disk partitioning. The repo ships a single-disk UEFI - # layout under nixos/disko-standard.nix that hosts can import. + # layout under installer/bootstrap/disko-standard.nix that hosts can import. # install.sh runs `disko --mode disko` then `nixos-install`, # which builds the closure directly into /mnt/nix/store on the target # (avoids the tmpfs OOM that the `disko-install` one-shot hits on diff --git a/hosts/_appliance-disk/default.nix b/hosts/_appliance-disk/default.nix index b7dd19e..dc94691 100644 --- a/hosts/_appliance-disk/default.nix +++ b/hosts/_appliance-disk/default.nix @@ -4,7 +4,7 @@ # auto-discovery), so this host is exposed as `nixosConfigurations._appliance-disk`. # Unlike the appliance ISO (hosts/_appliance-iso), this builds a *persistent* disk # image (qcow2 or raw) using disko's image builder: it carries the real on-disk -# GPT layout (1 GB ESP + ZFS root pool from nixos/disko-standard.nix) and state +# GPT layout (1 GB ESP + ZFS root pool from installer/bootstrap/disko-standard.nix) and state # survives reboots, exactly like a machine you ran install.sh on. # # Build (the format is chosen at build time, see Makefile / README): @@ -28,7 +28,7 @@ { imports = [ - ../../nixos/disko-standard.nix # 1 GB ESP + ZFS root pool single-disk layout + ../../installer/bootstrap/disko-standard.nix # 1 GB ESP + ZFS root pool single-disk layout ../../nixos/_images/box-turnkey.nix # shared turn-key config (login + Coder bootstrap) ../../nixos/_images/base/disk.nix # bundles each disk image with its .sha256 (diskoImagesDir) ] diff --git a/hosts/_appliance-iso/default.nix b/hosts/_appliance-iso/default.nix index 1d90544..018356e 100644 --- a/hosts/_appliance-iso/default.nix +++ b/hosts/_appliance-iso/default.nix @@ -9,7 +9,7 @@ # nix build .#nixosConfigurations._appliance-iso.config.system.build.isoImage # # Unlike normal install hosts (hosts//), this host does NOT -# import nixos/disko-standard.nix, hardware-configuration.nix, or facter.json: +# import installer/bootstrap/disko-standard.nix, hardware-configuration.nix, or facter.json: # the appliance root filesystem is the squashfs + tmpfs overlay provided by # nixos/_images/appliance/iso.nix. All of the appliance-ISO wiring lives there. # diff --git a/hosts/coder-thinkcentre/default.nix b/hosts/coder-thinkcentre/default.nix index e43b156..cbd6830 100644 --- a/hosts/coder-thinkcentre/default.nix +++ b/hosts/coder-thinkcentre/default.nix @@ -8,7 +8,7 @@ # commit the result. # # Disk layout: this host stays on UUID-based mounts from hardware-configuration.nix. -# It does NOT import nixos/disko-standard.nix because doing so would conflict +# It does NOT import installer/bootstrap/disko-standard.nix because doing so would conflict # with the existing fileSystems entries. Fresh installs of new hosts use # disko-standard.nix and skip hardware-configuration.nix entirely. diff --git a/hosts/incus-vm/README.md b/hosts/incus-vm/README.md index 4bd7580..4411307 100644 --- a/hosts/incus-vm/README.md +++ b/hosts/incus-vm/README.md @@ -136,7 +136,7 @@ services.coder-nixos.podman.enable = true; gitignored and must be created manually: ```sh -cp /etc/nixos-repo/local.nix.example \ +cp /etc/nixos-repo/installer/bootstrap/local.nix.example \ /etc/nixos-repo/hosts/$HOSTNAME/local.nix # Mark it so the flake's builtins.readDir can see it without committing it. @@ -148,15 +148,15 @@ Edit `hosts/$HOSTNAME/local.nix` and at minimum set: ```nix services.coder-nixos.lanIp = "192.168.x.x"; # VM's primary IP -systemd.services.coder-init-admin.environment = { - CODER_ADMIN_EMAIL = "you@example.com"; - CODER_ADMIN_USERNAME = "admin"; - CODER_ADMIN_PASSWORD = "changeme"; +services.coder-nixos.initialUser = { + username = "admin"; + email = "you@example.com"; + password = "changeme"; }; ``` -These credentials are read by `coder-init-admin.service` on first boot to -automatically create the admin user and mint a long-lived session token for +These credentials feed `coder-init-admin.service` on first boot to +automatically create the initial user and mint a long-lived session token for template-sync. **Without this, templates won't be pushed on the first `nixos-rebuild switch`.** @@ -200,7 +200,7 @@ Once complete, the tunnel URL is in `/etc/motd`: cat /etc/motd ``` -**Fallback (no local.nix credentials):** If `CODER_ADMIN_EMAIL` was left empty, +**Fallback (no local.nix credentials):** If `initialUser.email` was left empty, `coder-init-admin` is skipped. Complete setup via the first-run wizard instead: ```sh diff --git a/install.sh b/install.sh index f77f85f..0311fff 100755 --- a/install.sh +++ b/install.sh @@ -216,7 +216,7 @@ fi # one is the clearest signal you're not running this where it's meant to run. # gum only drives the interactive prompts, so it's only required with # --interactive. -REQUIRED_TOOLS=(lsblk openssl git nix nixos-install) +REQUIRED_TOOLS=(lsblk openssl git jq nix nixos-install) [[ $INTERACTIVE -eq 1 ]] && REQUIRED_TOOLS+=(gum) for tool in "${REQUIRED_TOOLS[@]}"; do command -v "$tool" >/dev/null && continue @@ -311,16 +311,6 @@ validate_username() { } } -# Escape for a Nix "..." string literal. -nix_string_escape() { - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\$/\\$/g' -} - -# Escape for the REPLACEMENT in `sed s|...|REPL|`. -sed_replacement_escape() { - printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g' -} - list_disks() { # Whole-block-devices, non-removable, non-loop, non-rom. MODEL is last so an # empty model (e.g. virtio /dev/vda) can't shift the TYPE/RM columns. @@ -642,7 +632,8 @@ fi mkdir -p "$HOST_DIR" # default.nix: disko-standard layout, target disk override, conditional -# local.nix, facter when present. +# local.nix, facter when present, and the install-answers.json applied to the +# coder-nixos options. if [[ ! -f "$HOST_DIR/default.nix" ]]; then cat >"$HOST_DIR/default.nix" <"$HOST_DIR/install-answers.json" + chmod 600 "$HOST_DIR/install-answers.json" + echo " wrote hosts/$HOSTNAME_ARG/install-answers.json" +fi + # facter.json: hardware report. Use the flake's pinned nixos-facter so it # shares one nixpkgs source with the rest of the install (avoids parallel # tmpfs allocations of disko's and nixpkgs#'s own nixpkgs trees). @@ -701,13 +712,15 @@ if [[ ! -f "$HOST_DIR/facter.json" ]]; then fi # A git path flake ignores untracked files, so the freshly written host files -# must be intent-to-added for the flake to see them (local.nix is gitignored, so -# force-add it). Only meaningful when REPO_DIR is a git repo; the ISO writable -# copy may have no .git (a non-git path flake already sees every file), so skip. +# must be intent-to-added for the flake to see them (local.nix and +# install-answers.json are gitignored, so force-add them). Only meaningful when +# REPO_DIR is a git repo; the ISO writable copy may have no .git (a non-git path +# flake already sees every file), so skip. if git -C "$REPO_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$REPO_DIR" add --intent-to-add -f \ "hosts/$HOSTNAME_ARG/default.nix" \ "hosts/$HOSTNAME_ARG/facter.json" \ + "hosts/$HOSTNAME_ARG/install-answers.json" \ "hosts/$HOSTNAME_ARG/local.nix" >/dev/null fi diff --git a/nixos/disko-standard.nix b/installer/bootstrap/disko-standard.nix similarity index 98% rename from nixos/disko-standard.nix rename to installer/bootstrap/disko-standard.nix index 2917495..ce8ccff 100644 --- a/nixos/disko-standard.nix +++ b/installer/bootstrap/disko-standard.nix @@ -20,7 +20,7 @@ # device path if needed (the default is /dev/nvme0n1, the current demo box # is NVMe-only): # -# imports = [ ../../nixos/disko-standard.nix ]; +# imports = [ ../../installer/bootstrap/disko-standard.nix ]; # disko.devices.disk.main.device = "/dev/sda"; # SATA example # # install.sh handles the format + install on a fresh box by running diff --git a/installer/bootstrap/local.nix.example b/installer/bootstrap/local.nix.example new file mode 100644 index 0000000..7bafde6 --- /dev/null +++ b/installer/bootstrap/local.nix.example @@ -0,0 +1,78 @@ +# Per-host overrides for hosts//local.nix. +# +# Usage: +# cp installer/bootstrap/local.nix.example hosts//local.nix +# $EDITOR hosts//local.nix +# sudo git add --intent-to-add -f hosts//local.nix +# sudo nixos-rebuild switch +# +# install.sh writes the install-time answers (Coder initial user, OS login user, +# LAN IP) to hosts//install-answers.json and the generated default.nix +# applies them. This file is only for optional per-host tweaks and overrides; +# everything below is commented out by default. +# +# The credential options (services.coder-nixos.initialUser / loginUser) are plain +# NixOS strings, so you can override them here or point them at a secret manager +# (agenix/sops) instead of keeping plaintext in install-answers.json. +# +# Hostname comes from flake.nix's mkHost; override with `lib.mkForce` if needed. + +{ ... }: + +{ + # ── LAN IP (optional) ─────────────────────────────────────────────────────── + # Primary LAN IP so workspace pods reach the Coder server directly (bypasses + # mDNS). Leave unset to fall back to http://.local:3000. + # services.coder-nixos.lanIp = "192.168.x.x"; + + # ── Coder initial user (optional override) ────────────────────────────────── + # Set at install time from install-answers.json. Override the initial owner's + # credentials here, or wire them to a secret. An empty email skips the + # bootstrap and leaves the browser first-run wizard to create the user. + # services.coder-nixos.initialUser = { + # username = "admin"; + # email = "you@example.com"; + # password = "changeme"; + # }; + + # ── OS login user (optional override) ─────────────────────────────────────── + # Declared by the generated default.nix from install-answers.json. Change the + # password with `passwd ` after first boot; initialPassword only fires on + # user creation. + # users.users.coderbox.initialPassword = "changeme"; + + # ── GitHub OAuth (optional) ──────────────────────────────────────────────── + # Read by the long-running coder.service. Leave commented to use the built-in + # Coder GitHub App. + # systemd.services.coder.environment = { + # CODER_OAUTH2_GITHUB_CLIENT_ID = ""; + # CODER_OAUTH2_GITHUB_CLIENT_SECRET = ""; + # CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS = "true"; + # CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE = "false"; + # CODER_OAUTH2_GITHUB_ALLOW_EVERYONE = "true"; + # CODER_OAUTH2_GITHUB_ALLOWED_ORGS = "my-org"; + # }; + + # ── SSH key sync from GitHub ───────────────────────────────────────────── + # GitHub usernames whose public keys are fetched on each boot and written to + # /etc/ssh/authorized_keys.d/, granting SSH access to the OS login + # user. Leave commented to disable. + # services.coder-sync-ssh-keys.githubUsers = [ "bpmct" "colleague" ]; + + # ── ScreenConnect (optional) ──────────────────────────────────────────────── + # services.coder-nixos.screenconnect = { + # enable = true; + # installerUrl = "https://sc.example.com/Bin/ScreenConnect.ClientSetup.sh?e=Access&y=Guest&c=Coder&c=&c=&c=&c=&c=&c=&c="; + # }; + + # ── k3s + sysbox-runc ────────────────────────────────────────────────────── + # Enabled by default in configuration.nix because every shipped template + # needs k3s. Uncomment to opt this host out. + # services.coder-nixos.sysbox.enable = false; + + # ── Tailscale (optional) ──────────────────────────────────────────────────── + # services.coder-nixos.tailscale = { + # enable = true; + # authKey = "tskey-auth-…"; + # }; +} diff --git a/local.nix.example b/local.nix.example deleted file mode 100644 index e85fbbb..0000000 --- a/local.nix.example +++ /dev/null @@ -1,88 +0,0 @@ -# Per-host local.nix template. -# -# Usage: -# cp local.nix.example hosts//local.nix -# $EDITOR hosts//local.nix -# sudo git add --intent-to-add -f hosts//local.nix -# sudo nixos-rebuild switch -# -# Generated automatically for new hosts by nixos/install.sh, which splices -# in --coder-admin-email, --coder-admin-password, --nixos-username, -# --nixos-password, and --lan-ip when given. -# -# Hostname comes from flake.nix's mkHost; override with `lib.mkForce` if needed. - -{ lib, pkgs, ... }: - -let - # Desktop / SSH login user. install.sh splices --nixos-username here. - nixosUsername = "coderbox"; -in -{ - # ── LAN IP (optional) ─────────────────────────────────────────────────────── - # Set this to the machine's primary LAN IP so workspace pods can reach - # the Coder server directly (bypasses mDNS). Leave empty to fall back to - # http://.local:3000 (works if mDNS is reliable on your network). - # services.coder-nixos.lanIp = "192.168.x.x"; - - - # ── Coder admin bootstrap ─────────────────────────────────────────────────── - # If set, coder-init-admin.service creates this admin account on first boot. - # Use the email address linked to your GitHub account so that OAuth login - # merges into this admin account automatically. - # Skipped if CODER_ADMIN_EMAIL is empty (complete the wizard in the browser instead). - # - # These credentials live on coder-init-admin.service (the bootstrap service), - # NOT coder.service, so they stay out of the long-running server's environment. - systemd.services.coder-init-admin.environment = { - CODER_ADMIN_EMAIL = "you@example.com"; # change this - CODER_ADMIN_USERNAME = "admin"; - CODER_ADMIN_PASSWORD = "changeme"; # change this - }; - - # ── GitHub OAuth (optional) ──────────────────────────────────────────────── - # Read by the long-running coder.service. Leave this block commented out to - # use the built-in Coder GitHub App. - # systemd.services.coder.environment = { - # CODER_OAUTH2_GITHUB_CLIENT_ID = ""; - # CODER_OAUTH2_GITHUB_CLIENT_SECRET = ""; - # CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS = "true"; - # CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE = "false"; - # CODER_OAUTH2_GITHUB_ALLOW_EVERYONE = "true"; - # CODER_OAUTH2_GITHUB_ALLOWED_ORGS = "my-org"; - # }; - - # ── Desktop / OS login account ───────────────────────────────────────────── - # SDDM / SSH login. Change with `passwd ${nixosUsername}` after first boot; - # initialPassword only fires on user creation. - users.users.${nixosUsername} = { - isNormalUser = true; - description = nixosUsername; - extraGroups = [ "networkmanager" "wheel" ]; - packages = with pkgs; [ kdePackages.kate ]; - initialPassword = "changeme"; - }; - - # ── SSH key sync from GitHub ───────────────────────────────────────────── - # List GitHub usernames whose public keys are fetched on each boot and - # written to /etc/ssh/authorized_keys.d/, granting SSH access - # to the OS login user above. Leave commented to disable. - # services.coder-sync-ssh-keys.githubUsers = [ "bpmct" "colleague" ]; - - # ── ScreenConnect (optional) ──────────────────────────────────────────────── - # services.coder-nixos.screenconnect = { - # enable = true; - # installerUrl = "https://sc.example.com/Bin/ScreenConnect.ClientSetup.sh?e=Access&y=Guest&c=Coder&c=&c=&c=&c=&c=&c=&c="; - # }; - - # ── k3s + sysbox-runc ────────────────────────────────────────────────────── - # Enabled by default in configuration.nix because every shipped template - # needs k3s. Uncomment to opt this host out. - # services.coder-nixos.sysbox.enable = false; - - # ── Tailscale (optional) ──────────────────────────────────────────────────── - # services.coder-nixos.tailscale = { - # enable = true; - # authKey = "tskey-auth-…"; - # }; -} diff --git a/nixos/_images/box-turnkey.nix b/nixos/_images/box-turnkey.nix index 4e50f11..c539437 100644 --- a/nixos/_images/box-turnkey.nix +++ b/nixos/_images/box-turnkey.nix @@ -6,11 +6,12 @@ # - nixos/_images/installer/iso.nix (installer ISO: hosts/_installer-iso) # - hosts/_appliance-disk/ (persistent disk image: qcow2 / raw) # -# On real installs these settings come from install.sh + the gitignored -# hosts//local.nix it generates. The image flavours have no install step, -# so this module supplies the same turn-key defaults (same values the installer -# defaults to). Change them before handing an image to anyone untrusted, or -# override per-image via hosts//local.nix. +# On real installs these settings come from install.sh via the gitignored +# hosts//install-answers.json (and hosts//local.nix) it generates. +# The image flavours have no install step, so this module supplies the same +# turn-key defaults (same values the installer defaults to). Change them before +# handing an image to anyone untrusted, or override per-image via +# hosts//local.nix. { config, @@ -233,16 +234,15 @@ in initialPassword = "PleaseChangeMe1234"; }; - # coder-init-admin.service reads CODER_ADMIN_* from its own service - # environment and creates a local admin on first boot, then mints a session - # token and deploys the templates from /etc/nixos-repo/coderd. With these set - # the Coder instance is ready to use immediately. These admin credentials are - # only needed by the bootstrap service, so they are kept off the long-running - # coder.service environment. - systemd.services.coder-init-admin.environment = { - CODER_ADMIN_EMAIL = "admin@coder.com"; - CODER_ADMIN_USERNAME = "admin"; - CODER_ADMIN_PASSWORD = "PleaseChangeMe1234"; + # coder-init-admin.service reads services.coder-nixos.initialUser and creates + # the Coder owner on first boot, then mints a session token and deploys the + # templates from /etc/nixos-repo/coderd. With these set the Coder instance is + # ready to use immediately. The credentials are wired onto the bootstrap + # service internally, so they stay off the long-running coder.service. + services.coder-nixos.initialUser = { + username = "admin"; + email = "admin@coder.com"; + password = "PleaseChangeMe1234"; }; }; # end config From cc99e5effc8a1054d170b27e06b205bb9ee8848e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phorcys=20=F0=9F=90=BE?= Date: Mon, 24 Aug 2026 20:17:19 +0000 Subject: [PATCH 2/2] refactor(installer): apply install-answers.json from local.nix Move the install-answers.json read out of the generated default.nix and into local.nix (via local.nix.example). The generated default.nix goes back to hardware/disk only; local.nix now reads the answers file and sets services.coder-nixos.{lanIp,initialUser} and the OS login user, so the file the user owns is the one that applies (and can override) the values. --- agents.md | 2 +- configuration.nix | 8 ++-- install.sh | 24 ++--------- installer/bootstrap/local.nix.example | 62 +++++++++++++++------------ 4 files changed, 44 insertions(+), 52 deletions(-) diff --git a/agents.md b/agents.md index d93a9c2..07f3555 100644 --- a/agents.md +++ b/agents.md @@ -220,7 +220,7 @@ sudo k3s kubectl describe pod -n coder-workspaces installer/ bootstrap/ # assets install.sh consumes when generating a new host disko-standard.nix # shared disko config: UEFI + single-disk layout for new hosts - local.nix.example # template copied to hosts//local.nix (optional overrides only) + local.nix.example # template copied to hosts//local.nix; reads install-answers.json and applies the values nixos/ modules/ # NixOS service modules (services.coder-nixos.*) k3s/ # base single-node k3s server diff --git a/configuration.nix b/configuration.nix index 54b69fe..c227101 100644 --- a/configuration.nix +++ b/configuration.nix @@ -327,10 +327,10 @@ in services.printing.enable = true; # ── Users ───────────────────────────────────────────────────────────────── - # The desktop / SSH login user is declared per-host by the generated - # hosts//default.nix from install-answers.json (username + initial - # password chosen at install time). The `coder` system user (uid 991) is - # shared and declared further down. + # The desktop / SSH login user is declared per-host in local.nix from the + # install-answers.json values (username + initial password chosen at install + # time). The `coder` system user (uid 991) is shared and declared further + # down. security.sudo.wheelNeedsPassword = false; diff --git a/install.sh b/install.sh index 0311fff..62f3052 100755 --- a/install.sh +++ b/install.sh @@ -632,8 +632,8 @@ fi mkdir -p "$HOST_DIR" # default.nix: disko-standard layout, target disk override, conditional -# local.nix, facter when present, and the install-answers.json applied to the -# coder-nixos options. +# local.nix, facter when present. The coder-nixos options (initial user, OS +# login user, LAN IP) are applied by local.nix, which reads install-answers.json. if [[ ! -f "$HOST_DIR/default.nix" ]]; then cat >"$HOST_DIR/default.nix" </local.nix. +# Per-host config for hosts//local.nix. # -# Usage: -# cp installer/bootstrap/local.nix.example hosts//local.nix -# $EDITOR hosts//local.nix -# sudo git add --intent-to-add -f hosts//local.nix -# sudo nixos-rebuild switch +# install.sh copies this file to hosts//local.nix and writes the +# install-time answers (Coder initial user, OS login user, LAN IP) to +# hosts//install-answers.json. The block below reads that JSON and applies +# the values, so a freshly installed host is configured straight from its +# answers file. # -# install.sh writes the install-time answers (Coder initial user, OS login user, -# LAN IP) to hosts//install-answers.json and the generated default.nix -# applies them. This file is only for optional per-host tweaks and overrides; -# everything below is commented out by default. +# Everything is a plain NixOS option, so you can edit the values here, point them +# at a secret manager (agenix/sops), or drop the install-answers.json read +# entirely. The optional extras further down are commented out by default. # -# The credential options (services.coder-nixos.initialUser / loginUser) are plain -# NixOS strings, so you can override them here or point them at a secret manager -# (agenix/sops) instead of keeping plaintext in install-answers.json. +# After editing: +# sudo git -C /etc/nixos-repo add --intent-to-add -f hosts//local.nix +# sudo nixos-rebuild switch # # Hostname comes from flake.nix's mkHost; override with `lib.mkForce` if needed. -{ ... }: +{ lib, ... }: +let + # Install-time answers written by install.sh. Applied at mkDefault so any + # override below (or in another module) wins. + answers = builtins.fromJSON (builtins.readFile ./install-answers.json); +in { - # ── LAN IP (optional) ─────────────────────────────────────────────────────── - # Primary LAN IP so workspace pods reach the Coder server directly (bypasses - # mDNS). Leave unset to fall back to http://.local:3000. - # services.coder-nixos.lanIp = "192.168.x.x"; + # ── Values from install-answers.json ──────────────────────────────────────── + services.coder-nixos.lanIp = lib.mkDefault answers.lanIp; + services.coder-nixos.initialUser = lib.mkDefault answers.initialUser; + + # Desktop / SSH login user. Change the password with `passwd ` after + # first boot; initialPassword only fires on user creation. + users.users.${answers.loginUser.username} = { + isNormalUser = true; + description = answers.loginUser.username; + extraGroups = [ + "networkmanager" + "wheel" + ]; + initialPassword = lib.mkDefault answers.loginUser.password; + }; # ── Coder initial user (optional override) ────────────────────────────────── - # Set at install time from install-answers.json. Override the initial owner's - # credentials here, or wire them to a secret. An empty email skips the - # bootstrap and leaves the browser first-run wizard to create the user. + # Overrides the value read from install-answers.json above. An empty email + # skips the bootstrap and leaves the browser first-run wizard to create the user. # services.coder-nixos.initialUser = { # username = "admin"; # email = "you@example.com"; # password = "changeme"; # }; - # ── OS login user (optional override) ─────────────────────────────────────── - # Declared by the generated default.nix from install-answers.json. Change the - # password with `passwd ` after first boot; initialPassword only fires on - # user creation. - # users.users.coderbox.initialPassword = "changeme"; - # ── GitHub OAuth (optional) ──────────────────────────────────────────────── # Read by the long-running coder.service. Leave commented to use the built-in # Coder GitHub App.