Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 24 additions & 19 deletions .github/workflows/build-ai-cluster-iso.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,7 @@ jobs:
# workflow-identity binding):
#
# cosign verify-blob \
# --certificate <iso>.pem \
# --signature <iso>.sig \
# --bundle <iso>.bundle \
# --certificate-identity 'https://github.com/Lucent-Financial-Group/Zeta/.github/workflows/build-ai-cluster-iso.yml@refs/heads/main' \
# --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
# <iso>
Expand All @@ -359,6 +358,7 @@ jobs:
# Per .claude/rules/dep-pin-search-first-authority.md

- name: Sign ISO with cosign (keyless OIDC + Fulcio + Rekor)
id: sign
env:
# Pass ISO path via env (not direct ${{ }} interpolation in run:)
# per the workflow's existing security discipline.
Expand All @@ -367,25 +367,32 @@ jobs:
run: |
set -euo pipefail
# --yes: skip interactive Fulcio confirmation (CI is non-interactive)
# Output files emitted alongside the ISO:
# <iso>.sig — signature (base64; small)
# <iso>.pem — short-lived Fulcio cert (the verifier's anchor)
# Output emitted to runner temp and uploaded alongside the ISO artifact:
# <runner-temp>/<iso>.bundle - cosign bundle containing signature,
# certificate, and transparency-log material.
COSIGN_BUNDLE="${RUNNER_TEMP}/${ISO_NAME}.bundle"
cosign sign-blob --yes \
--output-signature "${ISO_PATH}.sig" \
--output-certificate "${ISO_PATH}.pem" \
--bundle "${COSIGN_BUNDLE}" \
"${ISO_PATH}"
# Sanity: both artifacts present and non-empty.
test -s "${ISO_PATH}.sig" || { echo "::error::Empty signature"; exit 1; }
test -s "${ISO_PATH}.pem" || { echo "::error::Empty certificate"; exit 1; }
# Sanity: the verifier artifact must be present and non-empty.
test -s "${COSIGN_BUNDLE}" || { echo "::error::Empty cosign bundle"; exit 1; }
echo "bundle-path=${COSIGN_BUNDLE}" >> "$GITHUB_OUTPUT"
{
echo "## ISO signed via sigstore (cosign keyless OIDC)"
echo ""
echo "| Artifact | Path |"
echo "|---|---|"
echo "| Signature | \`${ISO_NAME}.sig\` |"
echo "| Certificate | \`${ISO_NAME}.pem\` |"
echo "| Bundle | \`${ISO_NAME}.bundle\` |"
echo ""
echo "Verification: see workflow run logs for the canonical \`cosign verify-blob\` command."
echo "Verification command after downloading the ISO artifact and cosign bundle artifact:"
echo ""
echo "\`\`\`bash"
echo "cosign verify-blob \\"
echo " --bundle '${ISO_NAME}.bundle' \\"
echo " --certificate-identity 'https://github.com/${GITHUB_WORKFLOW_REF}' \\"
echo " --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \\"
echo " '${ISO_NAME}'"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload ISO as workflow artifact
Expand All @@ -398,15 +405,13 @@ jobs:
retention-days: 90
compression-level: 0 # ISO is already compressed; re-zipping wastes time

- name: Upload cosign signature + certificate as workflow artifact
# B-0853.1 sibling upload — sig + pem alongside the ISO for consumers.
- name: Upload cosign bundle as workflow artifact
# B-0853.1 sibling upload — bundle alongside the ISO for consumers.
# Separate artifact (not bundled with ISO) so consumers downloading
# just the ISO don't pay the (small) cost; verifiers can grab both.
# just the ISO do not pay the small extra cost; verifiers can grab it.
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: ${{ steps.iso.outputs.name }}.cosign
path: |
${{ steps.iso.outputs.path }}.sig
${{ steps.iso.outputs.path }}.pem
path: ${{ steps.sign.outputs.bundle-path }}
if-no-files-found: error
retention-days: 90
1 change: 1 addition & 0 deletions full-ai-cluster/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
docker = ./nixos/modules/docker.nix;
local-storage = ./nixos/modules/local-storage.nix;
longhorn-disks = ./nixos/modules/longhorn-disks.nix;
zeta-self-register = ./nixos/modules/zeta-self-register.nix;
disko-shape-2nvme = ./nixos/modules/disko-shapes/2nvme.nix;
};

Expand Down
5 changes: 5 additions & 0 deletions full-ai-cluster/nixos/modules/common.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
# Per-vendor implementation lands via B-0850 Phase 3 sub-rows
# (3a-3h) that add install + login flows for each vendor's CLI.
./zeta-ai-agent.nix
# B-0855.1: post-install first-boot self-registration service.
# Disabled by default until host configs opt in after B-0855.2
# ships the TS implementation; imported here so every node type
# has the same module surface.
./zeta-self-register.nix
];

nix.settings = {
Expand Down
100 changes: 100 additions & 0 deletions full-ai-cluster/nixos/modules/zeta-self-register.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# full-ai-cluster/nixos/modules/zeta-self-register.nix
#
# B-0855.1: NixOS service surface for post-install self-registration.
# The service is intentionally disabled by default until B-0855.2 ships
# the TypeScript implementation at tools/installer/zeta-self-register.ts.
# Once enabled by a host config, it fires after the installed OS reaches
# network-online and credential-restore ordering, then keeps retrying
# until the local marker confirms registration intent was written.

{ config, lib, ... }:

let
cfg = config.zeta.selfRegister;
bunShimPath = "${cfg.home}/.local/share/mise/shims/bun";
in
{
options.zeta.selfRegister = {
enable = lib.mkEnableOption "Zeta post-install first-boot self-registration service";

user = lib.mkOption {
type = lib.types.str;
default = "zeta";
description = "User that runs zeta-self-register.service.";
};

group = lib.mkOption {
type = lib.types.str;
default = "users";
description = "Primary group for the self-registration service user.";
};

home = lib.mkOption {
type = lib.types.str;
default = "/home/zeta";
description = "Home directory used for credentials and local marker state.";
};
Comment thread
AceHack marked this conversation as resolved.

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-self-register.ts";
description = "Bun TypeScript entrypoint for composing self-registration intent.";
};

markerPath = lib.mkOption {
type = lib.types.str;
default = "${cfg.home}/.config/zeta/self-registered.marker";
description = "Fast-path local marker written after registration intent exists.";
};

intentDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.home}/.config/zeta/self-registration-intent";
description = "Directory where the service writes registration intent for the local agent steward.";
};
};

config = lib.mkIf cfg.enable {
systemd.services.zeta-self-register = {
description = "Zeta node self-registration intent writer (B-0855.1)";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
"zeta-creds-restore.service"
];

unitConfig = {
ConditionPathExists = [
"!${cfg.markerPath}"
cfg.scriptPath
bunShimPath
];
};

serviceConfig = {
Type = "oneshot";
User = cfg.user;
Group = cfg.group;
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"
"BUN_INSTALL=${cfg.home}/.bun"
"ZETA_SELF_REGISTER_MARKER=${cfg.markerPath}"
"ZETA_SELF_REGISTER_INTENT_DIR=${cfg.intentDir}"
"ZETA_SELF_REGISTER_REPO=${cfg.repoRoot}"
];
ExecStart = "${bunShimPath} ${cfg.scriptPath}";
Restart = "on-failure";
RestartSec = "30s";
};
};
};
}
25 changes: 25 additions & 0 deletions tools/ci/audit-installer-substrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const REQUIRED_FILES: readonly FileAssertion[] = [
{ path: "full-ai-cluster/nixos/modules/common.nix", minBytes: 500 },
{ path: "full-ai-cluster/nixos/modules/injected-hostname.nix" },
{ path: "full-ai-cluster/nixos/modules/login-banner.nix" },
// B-0855.1 post-install first-boot self-registration service surface
{ path: "full-ai-cluster/nixos/modules/zeta-self-register.nix", minBytes: 500 },
// operator-side flash tool (B-0789 + iter-5.x)
{ path: "full-ai-cluster/tools/zflash.ts", minBytes: 1000 },
];
Expand Down Expand Up @@ -126,9 +128,32 @@ const REQUIRED_SENTINELS: readonly SentinelAssertion[] = [
"./login-banner.nix", // iter-5.2.2 pre-login banner module
"services.avahi", // iter-5.1 mDNS publishing
"nssmdns4", // Avahi mDNS via nss
"./zeta-self-register.nix", // B-0855.1 first-boot self-registration module
],
rationale: "common.nix must import the iter-5.x modules so every host inherits them",
},
{
path: "full-ai-cluster/nixos/modules/zeta-self-register.nix",
mustContain: [
"systemd.services.zeta-self-register", // service unit exists
"Type = \"oneshot\"", // fires once, not a loop
"ConditionPathExists", // marker gate permits retries across failed first-boot attempts
"Restart = \"on-failure\"", // transient failures retry instead of losing first-boot opportunity
"RestartSec = \"30s\"", // bounded backoff before retrying registration intent
"network-online.target", // waits for network before registration intent
"zeta-creds-restore.service", // ordered after restored creds when that service exists
'default = "${cfg.home}/Zeta";', // repoRoot derives from home override
'default = "${cfg.repoRoot}/tools/installer/zeta-self-register.ts";', // scriptPath derives from repoRoot override
'default = "${cfg.home}/.config/zeta/self-registered.marker";', // markerPath derives from home override
'default = "${cfg.home}/.config/zeta/self-registration-intent";', // intentDir derives from home override
"tools/installer/zeta-self-register.ts", // delegates implementation to B-0855.2
".local/share/mise/shims/bun", // runtime follows the repo-wide mise-managed Bun substrate
"BUN_INSTALL", // keeps Bun global CLI state anchored under cfg.home
"ZETA_SELF_REGISTER_MARKER", // marker path exported to implementation
"ZETA_SELF_REGISTER_INTENT_DIR", // intent handoff dir exported to implementation
],
rationale: "B-0855.1 service must be a post-install marker-gated oneshot ordered after network and credential restore surfaces",
},
{
path: "full-ai-cluster/nixos/modules/injected-hostname.nix",
mustContain: [
Expand Down
Loading