diff --git a/README.md b/README.md index 02cf7a7..77aa34a 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,15 @@ and spoofing surfaces. Internally, the builder employs a dedicated secret-handling pipeline backed by a tmpfs-only secrets directory (`/dev/shm/cdlb_secrets`). Sensitive material such as root passwords, SSH keys, and signing keys never appears on the command -line, is guarded by strict `0400 root:root` permissions, and any symlink inside the secret path is treated as a hard failure -that aborts the run. Critical code paths temporarily disable Bash xtrace so that credentials never leak into debug logs, and -transient secret files are shredded (`shred -fzu`) as soon as they are no longer needed. GNUPG homes used for signing are -wiped, unencrypted chroot artifacts and includes are removed after `lb build`, and the final artifact is reduced to the -encrypted SquashFS inside the LUKS2 container. At runtime, LUKS passphrases in the live ISO and installer are transported via -named pipes inside the initramfs instead of process arguments, further minimizing exposure in process listings. +line, is guarded by a `0700 root:root` secret root and single-link regular `0400` or `0600` root-owned files, and any symlink +inside the secret path is treated as a hard failure that aborts the run. Filename-only secret arguments reject slashes and +traversal. +Critical code paths temporarily disable Bash xtrace, and a final exact-value debug-log sanitisation pass provides additional +defence in depth. Transient secret files are shredded (`shred -fzu`) as soon as they are no longer needed, but this is only a +best-effort cleanup on SSD, NVMe, copy-on-write, journaled, and virtualised storage. Use tmpfs for secrets and encrypted storage +for build workspaces. Destructive build cleanup is restricted to the exact canonical directory carrying the +`.ciss-live-builder-owned` marker. This private operator workflow still requires strict local path validation; it does not +define public ISO release policy. Check out more leading world-class services powered by Centurion Intelligence Consulting Agency: * [CenturionDNS Resolver](https://eddns.eu/) @@ -493,10 +496,14 @@ To use **``CISS.debian.live.builder``** as intended, the following baseline is e 2. Preparation: 1. Ensure you are root. - 2. Create the build directory `mkdir /opt/cdlb` and the tmpfs secrets directory `mkdir /dev/shm/cdlb_secrets`. - 3. Place your desired SSH public key in the `authorized_keys` file, for example, in the `/dev/shm/cdlb_secrets` directory. - 4. Place your desired Password in the `password.txt` file, for example, in the `/dev/shm/cdlb_secrets` directory. - 5. Make any other changes you need to. + 2. Create the empty build directory with `install -d -m 0700 -o root -g root /opt/cdlb`. + 3. Create the tmpfs secret root with `install -d -m 0700 -o root -g root /dev/shm/cdlb_secrets`. + 4. Place required secret files in the secret root as single-link regular, non-symlink, root-owned files with mode `0400` + or `0600`. + 5. Place your desired SSH public key in `/dev/shm/cdlb_secrets/authorized_keys`. + 6. Place your desired root password in `/dev/shm/cdlb_secrets/password.txt`. + 7. Use filename-only values without slashes, `.` or `..` for `--key_age`, `--key_luks`, and signing-file arguments. + 8. Make any other changes you need to. 3. Run the config builder script `./ciss_live_builder.sh` and the integrated `lb build` command (example): @@ -538,6 +545,10 @@ To use **``CISS.debian.live.builder``** as intended, the following baseline is e both the newer Sigstore bundle asset, and the legacy-split certificate/signature assets before checking the downloaded SOPS binary with `sha256sum -c --ignore-missing`. + On the first run, the builder creates `.ciss-live-builder-owned` in a new or empty build directory whose canonical parent + already exists. A populated directory without that marker is rejected and is never adopted automatically. Cleanup remains + intentionally destructive inside the exact validated marker-owned directory. + 4. Locate your ISO in the `--build-directory`. 5. Boot from the ISO and login to the live image via the console, or the multi-layer secured **coresecret** SSH tunnel. 6. Type `sysp` for the final kernel hardening features. @@ -559,7 +570,8 @@ preview it or run it. 2. Preparation: 1. Ensure you are root. - 2. Create the build directory `mkdir /opt/cdlb` and the tmpfs secrets directory `mkdir /dev/shm/cdlb_secrets`. + 2. Create the empty build directory and tmpfs secret root with restrictive ownership and permissions: + `install -d -m 0700 -o root -g root /opt/cdlb /dev/shm/cdlb_secrets`. 3. Place your desired SSH public key in the `authorized_keys` file, for example, in the `/dev/shm/cdlb_secrets` directory. 4. Place your desired Password in the `password.txt` file, for example, in the `/dev/shm/cdlb_secrets` directory. 5. Copy and edit the sample and set your options (no spaces around commas in lists): @@ -656,10 +668,10 @@ The private directory is ignored by Git. The hooks fail if the CISS EFI image si #... - name: Preparing the build environment. run: | - mkdir -p /opt/config - mkdir -p /opt/livebuild - echo "${{ secrets.CHANGE_ME }}" >| /opt/config/password.txt - echo "${{ secrets.CHANGE_ME }}" >| /opt/config/authorized_keys + install -d -m 0700 -o root -g root /opt/livebuild /dev/shm/cdlb_secrets + umask 0077 + printf '%s\n' "${{ secrets.CHANGE_ME }}" >| /dev/shm/cdlb_secrets/password.txt + printf '%s\n' "${{ secrets.CHANGE_ME }}" >| /dev/shm/cdlb_secrets/authorized_keys #... - name: Starting CISS.debian.live.builder. This may take a while ... run: | @@ -672,9 +684,9 @@ The private directory is ignored by Git. The hooks fail if the CISS EFI image si --build-directory /opt/livebuild \ --control "${timestamp}" \ --jump-host "${{ secrets.CHANGE_ME }}" \ - --root-password-file /opt/config/password.txt \ + --root-password-file /dev/shm/cdlb_secrets/password.txt \ --ssh-port CHANGE_ME \ - --ssh-pubkey /opt/config + --ssh-pubkey /dev/shm/cdlb_secrets #... ### SKIP OR CHANGE ALL REMAINING STEPS ``` diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 05e1146..045b217 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -37,6 +37,10 @@ A lightweight Shell Wrapper for building a hardened Debian Live ISO Image. --build-directory Where the Debian Live Build Image should be generated. RECOMMENDED path: + The path MUST be canonical and dedicated to the builder; a new directory's canonical parent MUST already exist. + New or empty directories receive the + '.ciss-live-builder-owned' marker; populated unmarked directories are rejected. Cleanup is intentionally destructive + only inside the exact validated marker-owned directory. MUST be provided. --change-splash one of @@ -57,6 +61,7 @@ A lightweight Shell Wrapper for building a hardened Debian Live ISO Image. --debug, -d Enables debug logging for the main program routine. Detailed logging information are written to: + A final exact-value sanitisation pass is defence in depth and does not replace careful tracing discipline. --dhcp-centurion If a DHCP lease is provided, the provider's name server will be overridden and the hardened, privacy-focused @@ -86,11 +91,13 @@ A lightweight Shell Wrapper for building a hardened Debian Live ISO Image. --key_age=* The SOPS AGE private keyring for decryption operations. Change '*' to your desired SOPS AGE key file. + '*' MUST be a filename only without slashes, '.' or '..' traversal. File MUST be placed in: --key_luks=* The LUKS encryption / decryption passphrase for '/'-fs-encryption. Change '*' to your desired passphrase file. + '*' MUST be a filename only without slashes, '.' or '..' traversal. File MUST be placed in: @@ -140,7 +147,7 @@ A lightweight Shell Wrapper for building a hardened Debian Live ISO Image. --root-password-file > Password file for 'root', if given, MUST be a string of 42 to 64 characters. If the argument is omitted, no further login authentication is required for the local console. - MUST be placed in: + Safe absolute paths remain supported and are validated separately. RECOMMENDED path: --secure-boot-profile one of @@ -156,7 +163,8 @@ A lightweight Shell Wrapper for building a hardened Debian Live ISO Image. specified via '--signing_key=*'. If the keyring is protected, then provide the passphrase in its own file. Specify the fingerprint of the key to use via '--signing_key_fpr=*'. Optionally import an offline GPG CA signing public key via: '--signing_ca=*'. - Change '*' to your desired files / fingerprint. Files MUST be placed in: + Change '*' to your desired filename-only files / fingerprint. Filename-only values MUST NOT contain slashes or traversal. + Files MUST be placed in: --sshfp @@ -182,6 +190,9 @@ A lightweight Shell Wrapper for building a hardened Debian Live ISO Image. 💡 Notes: 🔵 You MUST be 'root' to run this script. +🔵 Private operator control does not remove the requirement for strict local secret path validation. +🔵 '/dev/shm/cdlb_secrets' MUST be tmpfs-backed, root-owned, mode 0700, and contain only single-link regular non-symlink files + with mode 0400 or 0600. Secure deletion with shred is best-effort only on modern storage. 💷 Please consider donating to my work at: 🌐 https://coresecret.eu/spenden/ diff --git a/lib/lib_usage.sh b/lib/lib_usage.sh index bef358a..00b1dd4 100644 --- a/lib/lib_usage.sh +++ b/lib/lib_usage.sh @@ -67,6 +67,10 @@ usage() { echo echo -e "\e[97m --build-directory \e[0m" echo " Where the Debian Live Build Image should be generated. RECOMMENDED path: " + echo " The path MUST be canonical and dedicated to the builder; a new directory's canonical parent MUST already exist." + echo " New or empty directories receive the" + echo " '.ciss-live-builder-owned' marker; populated unmarked directories are rejected. Cleanup is intentionally" + echo " destructive only inside the exact validated marker-owned directory." echo " MUST be provided." echo echo -e "\e[97m --change-splash one of \e[0m" @@ -87,6 +91,7 @@ usage() { echo -e "\e[97m --debug, -d \e[0m" echo " Enables debug logging for the main program routine. Detailed logging information are written to:" echo " " + echo " A final exact-value sanitisation pass is defence in depth and does not replace careful tracing discipline." echo echo -e "\e[97m --dhcp-centurion \e[0m" echo " If a DHCP lease is provided, the provider's name server will be overridden and the hardened, privacy-focused " @@ -108,11 +113,13 @@ usage() { echo echo -e "\e[97m --key_age=* \e[0m" echo " The SOPS AGE private keyring for decryption operations. Change '*' to your desired SOPS AGE key file." + echo " '*' MUST be a filename only without slashes, '.' or '..' traversal." echo " File MUST be placed in:" echo " " echo echo -e "\e[97m --key_luks=* \e[0m" echo " The LUKS encryption / decryption passphrase for '/'-fs-encryption. Change '*' to your desired passphrase file." + echo " '*' MUST be a filename only without slashes, '.' or '..' traversal." echo " File MUST be placed in:" echo " " echo @@ -162,7 +169,7 @@ usage() { echo -e "\e[97m --root-password-file > \e[0m" echo " Password file for 'root', if given, MUST be a string of 42 to 64 characters." echo " If the argument is omitted, no further login authentication is required for the local console." - echo " MUST be placed in:" + echo " Safe absolute paths remain supported and are validated separately. RECOMMENDED path:" echo " " echo echo -e "\e[97m --secure-boot-profile one of \e[0m" @@ -178,7 +185,8 @@ usage() { echo " specified via '--signing_key=*'. If the keyring is protected, then provide the passphrase in its own file." echo " Specify the fingerprint of the key to use via '--signing_key_fpr=*'." echo " Optionally import an offline GPG CA signing public key via: '--signing_ca=*'." - echo " Change '*' to your desired files / fingerprint. Files MUST be placed in:" + echo " Change '*' to your desired filename-only files / fingerprint. Filename-only values MUST NOT contain slashes" + echo " or traversal. Files MUST be placed in:" echo " " echo echo -e "\e[97m --sops-version \e[0m" @@ -212,6 +220,9 @@ usage() { echo echo -e "\e[93m💡 Notes: \e[0m" echo -e "\e[93m🔵 You MUST be 'root' to run this script. \e[0m" + echo -e "\e[93m🔵 Private operator control does not remove the requirement for strict local secret path validation. \e[0m" + echo -e "\e[93m🔵 '/dev/shm/cdlb_secrets' MUST be tmpfs-backed, root-owned, mode 0700, and contain only \e[0m" + echo -e "\e[93m single-link regular secret files with mode 0400 or 0600. Secure deletion with shred is best-effort only. \e[0m" echo echo -e "\e[95m💷 Please consider donating to my work at: \e[0m" echo -e "\e[95m🌐 https://coresecret.eu/spenden/ \e[0m" diff --git a/tests/test_secret_debug_cleanup.sh b/tests/test_secret_debug_cleanup.sh new file mode 100755 index 0000000..242c048 --- /dev/null +++ b/tests/test_secret_debug_cleanup.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# SPDX-Version: 3.0 +# SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; +# SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git +# SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency +# SPDX-FileCopyrightText: 2024-2026; WEIDNER, Marc S.; +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 +# SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. +# SPDX-PackageName: CISS.debian.live.builder +# SPDX-Security-Contact: security@coresecret.eu +set -Ceuo pipefail +# shellcheck disable=SC1091,SC2034 + +declare TEST_ROOT="" +declare TEST_TMP="" +TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_TMP="$(realpath "$(mktemp -d)")" +readonly TEST_ROOT TEST_TMP +declare -r ERR_BUILD_PATH=217 +declare -r ERR_GUARD_SRCE=131 +declare -r ERR_SANITIZING=133 +declare -r ERR_SECRET_PATH=216 +declare -r VAR_WORKDIR="${TEST_ROOT}" +declare VAR_TMP_SECRET="${TEST_TMP}/secret-root" +declare LOG_DEBUG="" +declare LOG_ERROR="" +declare LOG_VAR="" +declare VAR_EARLY_DEBUG="false" +declare ERRTRAP="true" + +cleanup_test() { + chmod -R u+rwX "${TEST_TMP}" 2>/dev/null || true + rm -rf "${TEST_TMP}" +} +trap cleanup_test EXIT + +guard_sourcing() { + return 0 +} + +# shellcheck source=../lib/lib_secret_validation.sh +. "${TEST_ROOT}/lib/lib_secret_validation.sh" +# shellcheck source=../lib/lib_build_directory.sh +. "${TEST_ROOT}/lib/lib_build_directory.sh" +# shellcheck source=../lib/lib_debug_sanitizer.sh +. "${TEST_ROOT}/lib/lib_debug_sanitizer.sh" +# shellcheck source=../lib/lib_trap_on_exit.sh +. "${TEST_ROOT}/lib/lib_trap_on_exit.sh" + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +expect_failure() { + declare description="$1" + shift + if "$@" >/dev/null 2>&1; then + fail "${description}" + fi +} + +mkdir -m 0700 "${VAR_TMP_SECRET}" +printf 'safe-private-value\n' > "${VAR_TMP_SECRET}/safe.txt" +chmod 0400 "${VAR_TMP_SECRET}/safe.txt" + +validate_secret_directory "${VAR_TMP_SECRET}" "test secret root" "false" || fail "safe secret root rejected" +validate_secret_filename "safe.txt" "test secret filename" || fail "safe secret filename rejected" +validate_secret_file "${VAR_TMP_SECRET}/safe.txt" "test secret file" || fail "safe secret file rejected" +validate_secret_absolute_file "${VAR_TMP_SECRET}/safe.txt" "test absolute secret file" \ + || fail "safe absolute secret file rejected" +expect_failure "relative external secret path accepted" \ + validate_secret_absolute_file "secret-root/safe.txt" "test absolute secret file" +mkdir -m 0700 "${TEST_TMP}/external-secret-dir" +printf 'external-private-value\n' > "${TEST_TMP}/external-secret-dir/external.txt" +chmod 0400 "${TEST_TMP}/external-secret-dir/external.txt" +ln -s "${TEST_TMP}/external-secret-dir" "${TEST_TMP}/external-secret-dir-link" +expect_failure "external secret file through a symlinked parent accepted" \ + validate_secret_absolute_file "${TEST_TMP}/external-secret-dir-link/external.txt" "test absolute secret file" +expect_failure "external secret directory through a symlinked parent accepted" \ + validate_secret_absolute_directory "${TEST_TMP}/external-secret-dir-link" "test absolute secret directory" +declare secret_root_fs="" +secret_root_fs="$(secure_stat -f -c '%T' "${VAR_TMP_SECRET}")" +if [[ "${secret_root_fs}" != "tmpfs" && "${secret_root_fs}" != "ramfs" ]]; then + expect_failure "persistent secret staging area accepted" validate_secret_staging_area +fi +expect_failure "secret filename traversal accepted" validate_secret_filename "../safe.txt" "test secret filename" +expect_failure "absolute filename-only secret accepted" validate_secret_filename "/tmp/safe.txt" "test secret filename" +expect_failure "slash in filename-only secret accepted" validate_secret_filename "subdir/safe.txt" "test secret filename" + +ln -s "${VAR_TMP_SECRET}" "${TEST_TMP}/secret-root-link" +expect_failure "secret-root symlink accepted" \ + validate_secret_directory "${TEST_TMP}/secret-root-link" "test secret root" "false" +mkdir -m 0700 "${TEST_TMP}/unsafe-secret-root-mode" +chmod 0755 "${TEST_TMP}/unsafe-secret-root-mode" +expect_failure "broad secret-root permissions accepted" \ + validate_secret_directory "${TEST_TMP}/unsafe-secret-root-mode" "test secret root" "false" +ln -s "${VAR_TMP_SECRET}/safe.txt" "${VAR_TMP_SECRET}/unsafe-link" +expect_failure "secret-file symlink accepted" validate_secret_file "${VAR_TMP_SECRET}/unsafe-link" "test secret file" +rm "${VAR_TMP_SECRET}/unsafe-link" +ln "${VAR_TMP_SECRET}/safe.txt" "${VAR_TMP_SECRET}/unsafe-hardlink" +expect_failure "hardlinked secret file accepted" validate_secret_file "${VAR_TMP_SECRET}/safe.txt" "test secret file" +rm "${VAR_TMP_SECRET}/unsafe-hardlink" + +declare fake_secret='CISS-CANARY-[exact]-value' +declare expected_redaction="" +declare sanitisation_status=0 +printf -v expected_redaction '%*s' "${#fake_secret}" '' +expected_redaction="${expected_redaction// /*}" +register_secret_value "${fake_secret}" + +LOG_DEBUG="${TEST_TMP}/debug.log" +LOG_VAR="${TEST_TMP}/var.log" +LOG_ERROR="${TEST_TMP}/error.log" +printf 'before %s after\nunrelated line\n' "${fake_secret}" > "${LOG_DEBUG}" +printf 'unrelated vars\n' > "${LOG_VAR}" +printf 'unrelated error\n' > "${LOG_ERROR}" +chmod 0600 "${LOG_DEBUG}" "${LOG_VAR}" "${LOG_ERROR}" + +sanitize_debug_logs || fail "debug-log sanitisation failed" +grep -Fq "${fake_secret}" "${LOG_DEBUG}" && fail "debug-log canary remained" +grep -Fq "${expected_redaction}" "${LOG_DEBUG}" || fail "expected exact-value redaction missing" +grep -Fq 'unrelated line' "${LOG_DEBUG}" || fail "unrelated debug content changed" + +ln -s "${LOG_DEBUG}" "${TEST_TMP}/unsafe-debug-link" +( + LOG_DEBUG="${TEST_TMP}/unsafe-debug-link" + trap_on_exit 73 "test" 1 "test" "false" +) 2>/dev/null || sanitisation_status=$? +[[ ${sanitisation_status} -eq 73 ]] || fail "sanitisation failure masked the original exit status" + +expect_failure "empty build-directory path accepted" validate_build_directory_path "" +expect_failure "root build-directory path accepted" validate_build_directory_path "/" +expect_failure "broad parent build-directory path accepted" validate_build_directory_path "/tmp" +expect_failure "secret root accepted as build directory" validate_build_directory_path "${VAR_TMP_SECRET}" +mkdir -m 0700 "${VAR_TMP_SECRET}/unsafe-build-child" +expect_failure "secret-root descendant accepted as build directory" \ + validate_build_directory_path "${VAR_TMP_SECRET}/unsafe-build-child" + +mkdir -m 0700 "${TEST_TMP}/unmarked" +expect_failure "build directory without marker accepted" validate_build_directory_marker "${TEST_TMP}/unmarked" +printf 'do-not-adopt\n' > "${TEST_TMP}/unmarked/content" +expect_failure "non-empty unmarked build directory adopted" initialize_build_directory "${TEST_TMP}/unmarked" unsafe_result + +mkdir -m 0700 "${TEST_TMP}/unsafe-build-mode" +chmod 0777 "${TEST_TMP}/unsafe-build-mode" +expect_failure "unsafe build-directory permissions accepted" \ + initialize_build_directory "${TEST_TMP}/unsafe-build-mode" unsafe_result + +mkdir -m 0700 "${TEST_TMP}/marker-link-dir" +printf '%s\n' "${TEST_TMP}/marker-link-dir" > "${TEST_TMP}/marker-target" +chmod 0400 "${TEST_TMP}/marker-target" +ln -s "${TEST_TMP}/marker-target" "${TEST_TMP}/marker-link-dir/.ciss-live-builder-owned" +expect_failure "symlinked builder-owned marker accepted" validate_build_directory_marker "${TEST_TMP}/marker-link-dir" + +mkdir -m 0700 "${TEST_TMP}/marker-hardlink-dir" +printf '%s\n' "${TEST_TMP}/marker-hardlink-dir" > "${TEST_TMP}/marker-hardlink-target" +chmod 0400 "${TEST_TMP}/marker-hardlink-target" +ln "${TEST_TMP}/marker-hardlink-target" "${TEST_TMP}/marker-hardlink-dir/.ciss-live-builder-owned" +expect_failure "hardlinked builder-owned marker accepted" validate_build_directory_marker "${TEST_TMP}/marker-hardlink-dir" + +mkdir -m 0700 "${TEST_TMP}/marker-extra-content-dir" +printf '%s\nunexpected\n' "${TEST_TMP}/marker-extra-content-dir" \ + > "${TEST_TMP}/marker-extra-content-dir/.ciss-live-builder-owned" +chmod 0400 "${TEST_TMP}/marker-extra-content-dir/.ciss-live-builder-owned" +expect_failure "builder-owned marker with extra content accepted" \ + validate_build_directory_marker "${TEST_TMP}/marker-extra-content-dir" + +ln -s "${TEST_TMP}/unmarked" "${TEST_TMP}/build-link" +expect_failure "build-directory symlink accepted" validate_build_directory_path "${TEST_TMP}/build-link" + +declare validated_build_dir="" +initialize_build_directory "${TEST_TMP}/owned-build" validated_build_dir || fail "safe builder-owned directory rejected" +validate_build_directory_marker "${validated_build_dir}" || fail "builder marker rejected" +mkdir "${validated_build_dir}/subdir" +printf 'remove me\n' > "${validated_build_dir}/artifact" +printf 'remove me too\n' > "${validated_build_dir}/.hidden-artifact" +printf 'nested\n' > "${validated_build_dir}/subdir/nested" +clean_build_directory_contents "${validated_build_dir}" || fail "safe builder-owned cleanup failed" +validate_build_directory_marker "${validated_build_dir}" || fail "builder marker removed by cleanup" +[[ -z "$(find "${validated_build_dir}" -mindepth 1 ! -name '.ciss-live-builder-owned' -print -quit)" ]] \ + || fail "builder-owned cleanup left unexpected content" + +mkdir -m 0700 "${TEST_TMP}/outside-build" +mkdir "${TEST_TMP}/outside-build/includes.chroot" +ln -s "${TEST_TMP}/outside-build" "${validated_build_dir}/config" +expect_failure "cleanup subpath through a symlinked parent accepted" \ + validate_build_directory_subpath "${validated_build_dir}" "config/includes.chroot" unsafe_subpath + +printf 'PASS: secret validation, debug sanitisation, and build cleanup guards\n'