cursa: build sd-card image with secret injection
`clan machines build cursa --format sd-card` failed in clan-cli's post-build secret injection: Expected directory /nix/store/...-nixos-image-sd-card-...img.zst/sd-card not found in build output clan-cli assumed $out/<format>/ (only true for iso; sd-card writes $out/sd-image/) and tried to inject into every format although only iso has an addFilesScript. Fixed upstream in patches/upstream-sd-card-secret-injection.patch (pending PR); until it lands: - shell.nix: apply patches/clan-cli-image-filepath.patch (the clan-cli part of the upstream change) to the devshell's clan-cli. - modules/nixos/sd-card-image.nix: local copy of the upstream clan.core.image.sd-card.addFilesScript module. Writes the vars tree directly into the ext4 root partition with debugfs (no boot-time hook), verifies with cmp + e2fsck. Delete once clan-core ships it. - cursa imports it and builds its sd-card image without disko.
This commit is contained in:
1 parent
6039edfac6
commit
b4f9ae3f94
6 files changed
+1182
-1
No files matched your search
@@ -0,0 +1,165 @@
|
||||
diff --git a/clan_lib/machines/build.py b/clan_lib/machines/build.py
|
||||
index 00e2b16..d9ed5a1 100644
|
||||
--- a/clan_lib/machines/build.py
|
||||
+++ b/clan_lib/machines/build.py
|
||||
@@ -98,32 +98,42 @@ def _hostplatform_unset_hint(machine: Machine, build_format: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
-def _is_image_format(build_format: str) -> bool:
|
||||
- """Check if the build format is an image format that supports secret injection."""
|
||||
- return build_format not in ("toplevel")
|
||||
+def _image_add_files_script(machine: Machine, build_format: str) -> str | None:
|
||||
+ """Return the format's addFilesScript store path.
|
||||
+
|
||||
+ None when clan has no file-injection support for this format
|
||||
+ (i.e. no `clan.core.image.<format>.addFilesScript` option), in which
|
||||
+ case secret injection must be skipped rather than fail.
|
||||
+ """
|
||||
+ result = machine.flake.select_machine(
|
||||
+ machine.name,
|
||||
+ f"config.clan.core.image.?{build_format}.addFilesScript",
|
||||
+ )
|
||||
+ return result.get(build_format)
|
||||
|
||||
|
||||
-def _find_image_file(build_dir: Path, build_format: str) -> Path:
|
||||
- """Find the actual image file within a build output directory.
|
||||
+def _find_image_file(machine: Machine, build_dir: Path, build_format: str) -> Path:
|
||||
+ """Resolve the image file within an image build output directory.
|
||||
|
||||
- Image builds output a directory (e.g., /nix/store/...-x86_64-linux.iso/)
|
||||
- containing the image file in a subdirectory named after the format
|
||||
- (e.g., iso/nixos-26.05-x86_64-linux.iso).
|
||||
+ Image builds output a directory whose layout is format-specific
|
||||
+ (iso/nixos-….iso, sd-image/nixos-….img.zst, …). nixpkgs exposes the
|
||||
+ relative location as `config.image.filePath`, attached to the image
|
||||
+ derivation as `passthru.filePath`.
|
||||
"""
|
||||
- format_dir = build_dir / build_format
|
||||
- if not format_dir.is_dir():
|
||||
- msg = f"Expected directory {format_dir} not found in build output"
|
||||
- raise ClanError(msg)
|
||||
- files = [f for f in format_dir.iterdir() if f.is_file()]
|
||||
- if len(files) != 1:
|
||||
- msg = f"Expected exactly one file in {format_dir}, found {len(files)}"
|
||||
+ file_path: str = machine.flake.select_machine(
|
||||
+ machine.name,
|
||||
+ f"config.system.build.images.{build_format}.passthru.filePath",
|
||||
+ )
|
||||
+ image_file = build_dir / file_path
|
||||
+ if not image_file.is_file():
|
||||
+ msg = f"Expected image file {image_file} not found in build output"
|
||||
raise ClanError(msg)
|
||||
- return files[0]
|
||||
+ return image_file
|
||||
|
||||
|
||||
def _inject_secrets_into_image(
|
||||
machine: Machine,
|
||||
- build_format: str,
|
||||
+ add_files_script: str,
|
||||
source_image_path: Path,
|
||||
output_path: Path,
|
||||
*,
|
||||
@@ -137,7 +147,9 @@ def _inject_secrets_into_image(
|
||||
secrets_target_dir = machine.secret_vars_store.get_upload_directory(machine.name)
|
||||
|
||||
with TemporaryDirectory(prefix="clan-build-secrets-") as tmpdir:
|
||||
- base_dir = Path(tmpdir).resolve()
|
||||
+ # The tree mapped to "/" inside the image lives in its own subdir so
|
||||
+ # the format script never sees its own output as input.
|
||||
+ base_dir = Path(tmpdir).resolve() / "root"
|
||||
upload_dir = base_dir / secrets_target_dir.lstrip("/")
|
||||
upload_dir.mkdir(parents=True)
|
||||
|
||||
@@ -153,12 +165,6 @@ def _inject_secrets_into_image(
|
||||
if not any(upload_dir.iterdir()):
|
||||
return False
|
||||
|
||||
- # Get the addFilesScript path from the NixOS config
|
||||
- script_path = machine.flake.select_machine(
|
||||
- machine.name,
|
||||
- f"config.clan.core.image.{build_format}.addFilesScript",
|
||||
- )
|
||||
-
|
||||
# Remove stale output from previous runs (xorriso refuses to
|
||||
# write to an existing non-empty file when indev != outdev)
|
||||
output = Path(output_path)
|
||||
@@ -167,12 +173,12 @@ def _inject_secrets_into_image(
|
||||
|
||||
# Write the image into the tmpdir so the sandbox only needs
|
||||
# write access to the temp directory, not the final destination.
|
||||
- sandbox_output = base_dir / "output" / output.name
|
||||
+ sandbox_output = base_dir.parent / "output" / output.name
|
||||
sandbox_output.parent.mkdir(parents=True)
|
||||
|
||||
# Execute the addFilesScript; clean up partial output on failure
|
||||
inject_cmd: list[str] = [
|
||||
- f"{script_path}/bin/add-files",
|
||||
+ f"{add_files_script}/bin/add-files",
|
||||
"--source",
|
||||
str(source_image_path),
|
||||
"--output",
|
||||
@@ -265,33 +271,40 @@ def build_machine(
|
||||
if build_path and (store := nix_test_store()):
|
||||
build_path = store / str(build_path).lstrip("/")
|
||||
|
||||
- # Post-build secret injection for image formats
|
||||
+ # Post-build secret injection for image formats that support it
|
||||
if (
|
||||
build_path
|
||||
- and _is_image_format(options.format)
|
||||
+ and options.format != "toplevel"
|
||||
and not options.no_secrets
|
||||
and not options.dry_run
|
||||
):
|
||||
- # build_path is a directory; resolve the actual image file inside
|
||||
- source_file = _find_image_file(build_path, options.format)
|
||||
- # Output to a file next to the symlink, named after the source file
|
||||
- output_file = Path.cwd() / source_file.name
|
||||
-
|
||||
- secrets_injected = _inject_secrets_into_image(
|
||||
- machine=machine,
|
||||
- build_format=options.format,
|
||||
- source_image_path=source_file,
|
||||
- output_path=output_file,
|
||||
- use_sandbox=options.use_sandbox,
|
||||
- )
|
||||
- if secrets_injected:
|
||||
- log.warning(
|
||||
- "This image contains secret key material "
|
||||
- "(age private key). Treat this image as a sensitive "
|
||||
- "artifact. Do not store it on shared or public media "
|
||||
- "without encryption. Securely erase after use."
|
||||
+ add_files_script = _image_add_files_script(machine, options.format)
|
||||
+ if add_files_script is None:
|
||||
+ log.info(
|
||||
+ f"Image format '{options.format}' has no addFilesScript; "
|
||||
+ "skipping secret injection"
|
||||
)
|
||||
- build_path = output_file
|
||||
+ else:
|
||||
+ # build_path is a directory; resolve the actual image file inside
|
||||
+ source_file = _find_image_file(machine, build_path, options.format)
|
||||
+ # Output to a file next to the symlink, named after the source file
|
||||
+ output_file = Path.cwd() / source_file.name
|
||||
+
|
||||
+ secrets_injected = _inject_secrets_into_image(
|
||||
+ machine=machine,
|
||||
+ add_files_script=add_files_script,
|
||||
+ source_image_path=source_file,
|
||||
+ output_path=output_file,
|
||||
+ use_sandbox=options.use_sandbox,
|
||||
+ )
|
||||
+ if secrets_injected:
|
||||
+ log.warning(
|
||||
+ "This image contains secret key material "
|
||||
+ "(age private key). Treat this image as a sensitive "
|
||||
+ "artifact. Do not store it on shared or public media "
|
||||
+ "without encryption. Securely erase after use."
|
||||
+ )
|
||||
+ build_path = output_file
|
||||
|
||||
return BuildResult(
|
||||
machine_name=machine.name,
|
||||
Reference in new issue
Block a user