diff --git a/oci/remote/install_oci.sh b/oci/remote/install_oci.sh index e1b8651d..5735cef8 100755 --- a/oci/remote/install_oci.sh +++ b/oci/remote/install_oci.sh @@ -250,6 +250,7 @@ VOLUME_SEED_STAGE_ROOT="" VOLUME_SEED_TARGETS=() VOLUME_SEED_STAGES=() VOLUME_SEED_MOUNT_TYPES=() +CANONICAL_INPUT_DIR="" cleanup_runtime_console_log() { # The console log is the container's own log from here on, and its line in @@ -259,6 +260,14 @@ cleanup_runtime_console_log() { RUNTIME_CONSOLE_LOG="" } +cleanup_canonical_inputs() { + [[ -n ${CANONICAL_INPUT_DIR:-} ]] || return 0 + [[ $CANONICAL_INPUT_DIR == "/var/tmp/proxmenux-oci-inputs-${VMID}."* && -d $CANONICAL_INPUT_DIR && ! -L $CANONICAL_INPUT_DIR ]] \ + || { oci_log "Refusing to remove an unexpected canonical input directory: ${CANONICAL_INPUT_DIR}"; return 1; } + rm -rf -- "$CANONICAL_INPUT_DIR" || return 1 + CANONICAL_INPUT_DIR="" +} + # A derived check accepts any HTTP answer below 500: the application is up, # even when its first page is a redirect or asks for a login. healthcheck_probe() { @@ -275,6 +284,7 @@ cleanup_failed_install() { local status=$? stop_spinner cleanup_volume_seed_staging || true + cleanup_canonical_inputs || true if [[ -n ${PROXMENUX_OCI_TRANSACTION:-} ]]; then # The transaction owns recovery. Destroying this CT could destroy reused data. cleanup_runtime_console_log || true @@ -1282,8 +1292,13 @@ INSTANCE_ID=$(python3 "${SCRIPT_DIR}/oci_instances.py" prepare "$VMID" \ --template "$TEMPLATE_FILE" --deployment "$DEPLOYMENT_FILE") INSTANCE_CONTRACT="$INSTANCE_ROOT/$VMID/oci-compose.json" # The persisted contract is the source for the actual installation inputs. -jq '.template' "$INSTANCE_CONTRACT" >"$TEMPLATE_FILE" -jq '.deployment' "$INSTANCE_CONTRACT" >"$DEPLOYMENT_FILE" +# Do not rewrite caller-owned files, notably a regular user's file in sticky /tmp. +CANONICAL_INPUT_DIR=$(mktemp -d "/var/tmp/proxmenux-oci-inputs-${VMID}.XXXXXX") \ + || die "$(translate "Could not prepare canonical installation inputs")" +jq '.template' "$INSTANCE_CONTRACT" >"${CANONICAL_INPUT_DIR}/template.json" +jq '.deployment' "$INSTANCE_CONTRACT" >"${CANONICAL_INPUT_DIR}/deployment.json" +TEMPLATE_FILE="${CANONICAL_INPUT_DIR}/template.json" +DEPLOYMENT_FILE="${CANONICAL_INPUT_DIR}/deployment.json" fi skopeo_transport_reference() { diff --git a/oci/tests/test_installer_canonical_inputs.py b/oci/tests/test_installer_canonical_inputs.py new file mode 100644 index 00000000..c9665b51 --- /dev/null +++ b/oci/tests/test_installer_canonical_inputs.py @@ -0,0 +1,20 @@ +"""The installer must not need to rewrite files supplied by its caller.""" + +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +INSTALLER = ROOT / "remote" / "install_oci.sh" + + +class CanonicalInstallerInputTests(unittest.TestCase): + def test_persisted_contract_uses_private_copies_not_caller_files(self): + installer = INSTALLER.read_text(encoding="utf-8") + + self.assertIn('mktemp -d "/var/tmp/proxmenux-oci-inputs-${VMID}.XXXXXX"', installer) + self.assertIn('TEMPLATE_FILE="${CANONICAL_INPUT_DIR}/template.json"', installer) + self.assertIn('DEPLOYMENT_FILE="${CANONICAL_INPUT_DIR}/deployment.json"', installer) + self.assertIn('cleanup_canonical_inputs || true', installer) + self.assertNotIn('jq \'.template\' "$INSTANCE_CONTRACT" >"$TEMPLATE_FILE"', installer) + self.assertNotIn('jq \'.deployment\' "$INSTANCE_CONTRACT" >"$DEPLOYMENT_FILE"', installer)