Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions full-ai-cluster/nixos/modules/common.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
# ships the TS implementation; imported here so every node type
# has the same module surface.
./zeta-self-register.nix
# B-0852.4a/d: boot-time credential restore from ESP. Disabled by
# default until host configs opt in via `zeta.credsRestore.enable = true;`
# AND operator pre-stages a passphrase source. Composes with B-0855.1
# zeta-self-register (which already declares
# `after = "zeta-creds-restore.service"`) so cred-restore fires before
# self-register on first boot. Imported here so every node type has
# the same module surface.
./zeta-creds-restore.nix
];

nix.settings = {
Expand Down
195 changes: 195 additions & 0 deletions full-ai-cluster/nixos/modules/zeta-creds-restore.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# full-ai-cluster/nixos/modules/zeta-creds-restore.nix
#
# B-0852.4a: NixOS service surface for boot-time credential restore from
# ESP. Consumes the encrypted blob written at install-time by the
# B-0852.3a picker (PR #5450); decrypts via the B-0852.2b restore CLI
# (PR #5425) using the USB UUID and operator passphrase; populates
# per-cred paths on the installed system before user-facing services
# (e.g., zeta-self-register.service) start.
#
# The service is intentionally disabled by default until a host config
# enables it AND a passphrase source is provided. Ordering: fires
# AFTER local-fs.target (ESP mounted), BEFORE zeta-self-register.service
# (which already declares `After = "zeta-creds-restore.service"` per
# B-0855.1 module).
#
# Two passphrase modes:
# - file (default; simpler; suitable for automated installs):
# Reads /run/zeta-creds-passphrase (operator pre-stages this).
# File is deleted on service stop.
# - interactive (operator-driven first boot; nicer UX):
# Uses systemd-ask-password on tty1 at boot. Operator types
# passphrase. (Implementation note: this mode currently writes
# a temporary file with the entered passphrase; B-0852.4b
# follow-on row may switch to stdin pipe to restore CLI for
# tighter handling.)
#
# Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
# over own creds; passphrase NEVER logged; required-cred restore
# failure surfaces (RestartSec retry) rather than silently degrading.
# Optional creds: the restore CLI itself reports skipped/error per
# cred; this module doesn't second-guess that policy.

{ config, lib, pkgs, ... }:

let
cfg = config.zeta.credsRestore;
bunShimPath = "${cfg.home}/.local/share/mise/shims/bun";
in
{
options.zeta.credsRestore = {
enable = lib.mkEnableOption "Zeta boot-time credential restore from ESP";

user = lib.mkOption {
type = lib.types.str;
default = "zeta";
description = "User that owns restored credential files.";
};

group = lib.mkOption {
type = lib.types.str;
default = "users";
description = "Primary group for restored credential files.";
};

home = lib.mkOption {
type = lib.types.str;
default = "/home/zeta";
description = "Home directory of the zeta user.";
};

repoRoot = lib.mkOption {
type = lib.types.str;
default = "${cfg.home}/Zeta";
description = "Path to the checked-out Zeta repository on the installed node.";
};

scriptPath = lib.mkOption {
type = lib.types.str;
default = "${cfg.repoRoot}/tools/installer/zeta-creds-restore.ts";
description = "Bun TypeScript entrypoint for restore CLI (B-0852.2b).";
};

blobPath = lib.mkOption {
type = lib.types.str;
default = "/esp/zeta-creds.enc";
description = "Path to encrypted cred-blob written by picker (B-0852.3a) at install time.";
};

usbUuidPath = lib.mkOption {
type = lib.types.str;
default = "/etc/zeta/usb-uuid";
description = "Path to file containing the USB UUID used as KDF binding (iter-4.2 ESP write).";
};

passphraseMode = lib.mkOption {
type = lib.types.enum [ "file" "interactive" ];
default = "file";
description = "How to obtain the passphrase: file (pre-staged at /run) or interactive (systemd-ask-password).";
};

passphraseFile = lib.mkOption {
type = lib.types.str;
default = "/run/zeta-creds-passphrase";
description = "When passphraseMode=file: read passphrase from this path (deleted on stop).";
};

persona = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional persona name; when set, restores persona-scoped credentials for that persona.";
};
};

config = lib.mkIf cfg.enable {
systemd.services.zeta-creds-restore = {
description = "Zeta credential restore from ESP at boot (B-0852.4a)";
wantedBy = [ "multi-user.target" ];
wants = [ "local-fs.target" ];
after = [
"local-fs.target"
];
# B-0855.1 zeta-self-register.service declares
# `after = "zeta-creds-restore.service"`; ordering enforced there.

unitConfig = {
ConditionPathExists = [
cfg.blobPath
cfg.usbUuidPath
cfg.scriptPath
bunShimPath
];
};

serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root"; # needs root to read /run passphrase + drop to zeta user via sudo
WorkingDirectory = cfg.repoRoot;
Environment = [
"HOME=${cfg.home}"
"PATH=${cfg.home}/.local/share/mise/shims:${cfg.home}/.bun/bin:/run/current-system/sw/bin:/usr/bin:/bin"
];
ExecStart = pkgs.writeShellScript "zeta-creds-restore-start" ''
set -euo pipefail

USB_UUID="$(cat ${cfg.usbUuidPath})"
if [ -z "$USB_UUID" ]; then
echo "zeta-creds-restore: empty USB UUID at ${cfg.usbUuidPath}; aborting"
exit 1
fi
Comment thread
AceHack marked this conversation as resolved.
Outdated

${
if cfg.passphraseMode == "file" then ''
PASSPHRASE_PATH="${cfg.passphraseFile}"
if [ ! -f "$PASSPHRASE_PATH" ]; then
echo "zeta-creds-restore: passphrase file $PASSPHRASE_PATH missing (passphraseMode=file)"
exit 1
fi
'' else ''
# interactive mode: prompt operator via systemd-ask-password
# Write to temp file (zeta-readable) so restore CLI can consume
PASSPHRASE_PATH="/run/zeta-creds-passphrase-temp"
PASSPHRASE="$(${pkgs.systemd}/bin/systemd-ask-password \
--timeout=300 \
"Zeta cred-blob passphrase: ")"
if [ -z "$PASSPHRASE" ]; then
echo "zeta-creds-restore: empty passphrase from systemd-ask-password"
exit 1
fi
umask 0177
echo -n "$PASSPHRASE" > "$PASSPHRASE_PATH"
chmod 0400 "$PASSPHRASE_PATH"
chown ${cfg.user}:${cfg.group} "$PASSPHRASE_PATH"
unset PASSPHRASE
''
}

PERSONA_ARGS=""
${lib.optionalString (cfg.persona != null) ''
PERSONA_ARGS="--persona ${cfg.persona}"
''}

sudo -u ${cfg.user} \
HOME="${cfg.home}" \
${bunShimPath} ${cfg.scriptPath} \
--usb-uuid "$USB_UUID" \
--input ${cfg.blobPath} \
--passphrase-file "$PASSPHRASE_PATH" \
--target-root / \
$PERSONA_ARGS
Comment thread
AceHack marked this conversation as resolved.
Outdated
'';
ExecStopPost = pkgs.writeShellScript "zeta-creds-restore-cleanup" ''
# Always clean up passphrase files on stop (success OR failure)
rm -f /run/zeta-creds-passphrase-temp
${lib.optionalString (cfg.passphraseMode == "file") ''
# File-mode: also delete the operator-staged passphrase
rm -f ${cfg.passphraseFile}
''}
'';
Comment thread
AceHack marked this conversation as resolved.
Restart = "on-failure";
RestartSec = "30s";
};
};
};
}
Loading