diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f0a4d..ae6ce99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Versioning. - Align the desktop-server device class with macos-ubuntu-bootstrap: Docker may be none, rootful, or rootless. The bootstrap orchestrator forwards the declared docker_mode and server-baseline hardening flags. +- Keep `append_hardening_flags` `set -e` safe when ssh/ufw/fail2ban are unset, + so desktop-server apply does not exit before phase 0. ## [0.9.2] - 2026-09-12 diff --git a/scripts/bootstrap-device.sh b/scripts/bootstrap-device.sh index 18764ca..376c0a1 100755 --- a/scripts/bootstrap-device.sh +++ b/scripts/bootstrap-device.sh @@ -283,9 +283,9 @@ append_hardening_flags() { HARDEN_SSH=$(yaml_get "$DEVICE_PATH" "device.class.hardening.ssh" || true) HARDEN_UFW=$(yaml_get "$DEVICE_PATH" "device.class.hardening.ufw" || true) HARDEN_F2B=$(yaml_get "$DEVICE_PATH" "device.class.hardening.fail2ban" || true) - [ "${HARDEN_SSH:-}" = "true" ] && OS_ARGS+=("--harden-ssh") - [ "${HARDEN_UFW:-}" = "true" ] && OS_ARGS+=("--enable-ufw") - [ "${HARDEN_F2B:-}" = "true" ] && OS_ARGS+=("--with-fail2ban") + if [ "${HARDEN_SSH:-}" = "true" ]; then OS_ARGS+=("--harden-ssh"); fi + if [ "${HARDEN_UFW:-}" = "true" ]; then OS_ARGS+=("--enable-ufw"); fi + if [ "${HARDEN_F2B:-}" = "true" ]; then OS_ARGS+=("--with-fail2ban"); fi } case "$PROFILE" in desktop) diff --git a/tests/test_bootstrap_device.py b/tests/test_bootstrap_device.py index e2b424a..4adc171 100644 --- a/tests/test_bootstrap_device.py +++ b/tests/test_bootstrap_device.py @@ -53,6 +53,9 @@ def test_orchestrator_forwards_class_docker_mode() -> None: assert 'OS_ARGS+=("--docker-mode" "${CLASS_DOCKER:-none}")' in script assert 'OS_ARGS+=("--docker-mode" "${CLASS_DOCKER:-rootful}")' in script assert "append_hardening_flags" in script + # `cmd && append` is not `set -e` safe as the last statement of a case arm. + assert 'if [ "${HARDEN_SSH:-}" = "true" ]; then OS_ARGS+=("--harden-ssh"); fi' in script + assert '] && OS_ARGS+=("--harden-ssh")' not in script def test_seed_go_verifies_the_physical_toolchain_without_auto_selection() -> None: @@ -256,6 +259,28 @@ def git(path: Path, *args: str) -> str: assert "OS installer present" in result.stdout assert git(estate, "status", "--porcelain") == before == "" + # desktop-server with no hardening must not trip `set -e` on the flag + # appender. Phase 2 --plan prints the derived installer argv. + device.write_text( + "device:\n id: example-device\n name: example\n os: linux\n" + " architecture: x86_64\n class:\n profile: desktop-server\n" + " gui: enabled\n docker_mode: rootful\n" + " execution_policy: interactive-desktop-server\n" + ) + desktop_server = subprocess.run( + [str(embedded), "--estate-root", str(estate), "--device", + "estate/devices/example.yaml", "--phase", "2", "--plan"], + env=env, capture_output=True, text=True, + ) + assert desktop_server.returncode == 0, desktop_server.stdout + desktop_server.stderr + assert "--profile desktop-server" in desktop_server.stdout + assert "--docker-mode rootful" in desktop_server.stdout + device.write_text( + "device:\n id: example-device\n name: example\n os: linux\n" + " architecture: x86_64\n class:\n profile: desktop-builds\n" + " gui: enabled\n docker_mode: rootful\n" + ) + # Source identity comes from the engine, not the consuming estate commit. version = subprocess.check_output( [str(embedded), "--estate-root", str(estate), "--source-build-version"],