diff --git a/.secrets.baseline b/.secrets.baseline index 874acf9..d1e81f4 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -133,9 +133,9 @@ "filename": "scripts/init_system.sh", "hashed_secret": "96183ea4ff07d786ed3233777364ddbf14eb74cc", "is_verified": false, - "line_number": 19 + "line_number": 23 } ] }, - "generated_at": "2026-02-24T14:14:56Z" + "generated_at": "2026-03-13T14:03:33Z" } diff --git a/scripts/init_system.sh b/scripts/init_system.sh index f35bbed..fc45f18 100755 --- a/scripts/init_system.sh +++ b/scripts/init_system.sh @@ -3,6 +3,10 @@ # Initialize host prerequisites for OpenCode + A2A (idempotent). set -euo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./init_system_uv_release_manifest.sh +source "${SCRIPT_DIR}/init_system_uv_release_manifest.sh" + OPENCODE_CORE_DIR="/opt/.opencode" SHARED_WRAPPER_DIR="/opt/opencode-a2a" OPENCODE_A2A_DIR="${SHARED_WRAPPER_DIR}/opencode-a2a-server" @@ -165,10 +169,10 @@ install_packages() { log_done "Package installation completed." } -download_script() { +download_file() { local url="$1" local dest="$2" - if ! curl -fL "$url" -o "$dest"; then + if ! curl -fsSL "$url" -o "$dest"; then return 1 fi if [[ ! -s "$dest" ]]; then @@ -177,15 +181,6 @@ download_script() { return 0 } -validate_script_contains() { - local path="$1" - local pattern="$2" - if ! grep -Eqi "$pattern" "$path"; then - return 1 - fi - return 0 -} - verify_file_checksum() { local path="$1" local expected="$2" @@ -208,6 +203,151 @@ verify_file_checksum() { return 0 } +detect_linux_libc() { + local output="" + if command -v ldd >/dev/null 2>&1; then + output="$(ldd --version 2>&1 || true)" + fi + if printf '%s' "$output" | grep -qi "musl"; then + printf 'musl\n' + return 0 + fi + if getconf GNU_LIBC_VERSION >/dev/null 2>&1; then + printf 'gnu\n' + return 0 + fi + if printf '%s' "$output" | grep -Eqi "glibc|gnu libc"; then + printf 'gnu\n' + return 0 + fi + return 1 +} + +resolve_uv_release_artifact() { + local arch="$1" + local libc="$2" + + case "${arch}:${libc}" in + x86_64:gnu) + printf '%s|%s\n' "$UV_TARBALL_X86_64_GNU" "$UV_TARBALL_X86_64_GNU_SHA256" + ;; + x86_64:musl) + printf '%s|%s\n' "$UV_TARBALL_X86_64_MUSL" "$UV_TARBALL_X86_64_MUSL_SHA256" + ;; + aarch64:gnu) + printf '%s|%s\n' "$UV_TARBALL_AARCH64_GNU" "$UV_TARBALL_AARCH64_GNU_SHA256" + ;; + aarch64:musl) + printf '%s|%s\n' "$UV_TARBALL_AARCH64_MUSL" "$UV_TARBALL_AARCH64_MUSL_SHA256" + ;; + *) + return 1 + ;; + esac +} + +node_manual_install_hint() { + warn "Install Node.js >= ${NODE_MAJOR} manually from a trusted distro repository or internal mirror, then rerun init_system.sh with INSTALL_NODE=false." +} + +ensure_node_from_package_manager() { + case "$PKG_MANAGER" in + apt) + $SUDO apt-get update + if ! $SUDO apt-get install -y nodejs npm; then + return 1 + fi + ;; + dnf) + if ! $SUDO dnf install -y nodejs npm && ! $SUDO dnf install -y nodejs; then + return 1 + fi + ;; + yum) + if ! $SUDO yum install -y nodejs npm && ! $SUDO yum install -y nodejs; then + return 1 + fi + ;; + pacman) + if ! $SUDO pacman -Syu --noconfirm nodejs npm; then + return 1 + fi + ;; + *) + return 1 + ;; + esac +} + +install_uv_from_release() { + local os_name + local arch + local libc + local resolved + local asset + local checksum + local tmpdir + local archive + local extract_dir + + os_name="$(uname -s)" + if [[ "$os_name" != "Linux" ]]; then + warn "Unsupported OS for pinned uv bootstrap: $os_name" + return 1 + fi + + arch="$(uname -m)" + case "$arch" in + x86_64|amd64) arch="x86_64" ;; + aarch64|arm64) arch="aarch64" ;; + *) + warn "Unsupported architecture for pinned uv bootstrap: $arch" + return 1 + ;; + esac + + if ! libc="$(detect_linux_libc)"; then + warn "Unable to detect Linux libc for uv bootstrap." + return 1 + fi + + if ! resolved="$(resolve_uv_release_artifact "$arch" "$libc")"; then + warn "No pinned uv release artifact configured for ${arch}/${libc}." + return 1 + fi + + IFS='|' read -r asset checksum <<< "$resolved" + tmpdir="$(mktemp -d)" + archive="${tmpdir}/${asset}" + extract_dir="${tmpdir}/${asset%.tar.gz}" + + if ! download_file "${UV_RELEASE_BASE_URL}/${asset}" "$archive"; then + rm -rf "$tmpdir" + return 1 + fi + if ! verify_file_checksum "$archive" "$checksum"; then + rm -rf "$tmpdir" + return 1 + fi + if ! tar -xzf "$archive" -C "$tmpdir"; then + rm -rf "$tmpdir" + return 1 + fi + if [[ ! -x "${extract_dir}/uv" ]]; then + warn "Pinned uv archive is missing the uv binary: ${extract_dir}/uv" + rm -rf "$tmpdir" + return 1 + fi + + $SUDO install -d -m 755 /usr/local/bin + $SUDO install -m 755 "${extract_dir}/uv" /usr/local/bin/uv + if [[ -x "${extract_dir}/uvx" ]]; then + $SUDO install -m 755 "${extract_dir}/uvx" /usr/local/bin/uvx + fi + rm -rf "$tmpdir" + return 0 +} + ensure_group() { local group="$1" if [[ -z "$group" ]]; then @@ -335,81 +475,28 @@ if is_truthy "$INSTALL_NODE"; then if [[ -n "$node_major" && "$node_major" -ge "$NODE_MAJOR" ]]; then log_done "Node.js already installed: ${node_version}" else - log_start "Installing Node.js ${NODE_MAJOR}.x..." - case "$PKG_MANAGER" in - apt) - nodesource_script="$(mktemp)" - log_start "Downloading NodeSource setup script..." - if ! download_script "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" "$nodesource_script"; then - warn "Failed to download NodeSource setup script." - INCOMPLETE=1 - else - log_done "NodeSource setup script downloaded." - log_start "Validating NodeSource setup script..." - if ! validate_script_contains "$nodesource_script" "nodesource|NodeSource"; then - warn "NodeSource setup script validation failed." - INCOMPLETE=1 - else - log_done "NodeSource setup script validated." - $SUDO -E bash "$nodesource_script" - fi - fi - rm -f "$nodesource_script" - $SUDO apt-get install -y nodejs - ;; - dnf) - nodesource_script="$(mktemp)" - log_start "Downloading NodeSource setup script..." - if ! download_script "https://rpm.nodesource.com/setup_${NODE_MAJOR}.x" "$nodesource_script"; then - warn "Failed to download NodeSource setup script." - INCOMPLETE=1 - else - log_done "NodeSource setup script downloaded." - log_start "Validating NodeSource setup script..." - if ! validate_script_contains "$nodesource_script" "nodesource|NodeSource"; then - warn "NodeSource setup script validation failed." - INCOMPLETE=1 - else - log_done "NodeSource setup script validated." - $SUDO -E bash "$nodesource_script" - fi - fi - rm -f "$nodesource_script" - $SUDO dnf install -y nodejs - ;; - yum) - nodesource_script="$(mktemp)" - log_start "Downloading NodeSource setup script..." - if ! download_script "https://rpm.nodesource.com/setup_${NODE_MAJOR}.x" "$nodesource_script"; then - warn "Failed to download NodeSource setup script." - INCOMPLETE=1 - else - log_done "NodeSource setup script downloaded." - log_start "Validating NodeSource setup script..." - if ! validate_script_contains "$nodesource_script" "nodesource|NodeSource"; then - warn "NodeSource setup script validation failed." - INCOMPLETE=1 - else - log_done "NodeSource setup script validated." - $SUDO -E bash "$nodesource_script" - fi - fi - rm -f "$nodesource_script" - $SUDO yum install -y nodejs - ;; - pacman) - $SUDO pacman -Syu --noconfirm nodejs npm - ;; - *) - warn "Unsupported package manager for Node.js install: $PKG_MANAGER" - INCOMPLETE=1 - ;; - esac + log_start "Installing Node.js from trusted package manager repositories..." + if ! ensure_node_from_package_manager; then + warn "Node.js install via package manager failed." + node_manual_install_hint + INCOMPLETE=1 + fi if command -v node >/dev/null 2>&1; then node_version="$(node -v 2>/dev/null || true)" - log_done "Node.js installed: ${node_version}" + node_major="" + if [[ "$node_version" =~ ^v([0-9]+) ]]; then + node_major="${BASH_REMATCH[1]}" + fi + if [[ -n "$node_major" && "$node_major" -ge "$NODE_MAJOR" ]]; then + log_done "Node.js installed: ${node_version}" + else + warn "Installed Node.js version does not satisfy NODE_MAJOR=${NODE_MAJOR}: ${node_version:-unknown}" + node_manual_install_hint + INCOMPLETE=1 + fi else warn "Node.js installation failed." + node_manual_install_hint INCOMPLETE=1 fi fi @@ -465,23 +552,12 @@ if is_truthy "$INSTALL_UV"; then INCOMPLETE=1 else log_start "Installing uv..." - uv_script="$(mktemp)" - log_start "Downloading uv install script..." - if ! download_script "https://astral.sh/uv/install.sh" "$uv_script"; then - warn "Failed to download uv install script." + log_start "Downloading pinned uv release tarball..." + if ! install_uv_from_release; then + warn "Pinned uv release installation failed." + warn "Install uv ${UV_VERSION} manually from a trusted release artifact, then rerun init_system.sh with INSTALL_UV=false." INCOMPLETE=1 - else - log_done "uv install script downloaded." - log_start "Validating uv install script..." - if ! validate_script_contains "$uv_script" "astral|uv"; then - warn "uv install script validation failed." - INCOMPLETE=1 - else - log_done "uv install script validated." - UV_PYTHON_INSTALL_DIR="$UV_PYTHON_INSTALL_DIR" sh "$uv_script" - fi fi - rm -f "$uv_script" if ! command -v uv >/dev/null 2>&1; then if [[ -x "$HOME/.local/bin/uv" ]]; then log_start "Moving uv into /usr/local/bin." @@ -618,7 +694,7 @@ else if [[ -n "$OPENCODE_INSTALL_CMD" ]]; then opencode_install_script="$(mktemp)" log_start "Downloading OpenCode installer..." - if ! download_script "$OPENCODE_INSTALLER_URL" "$opencode_install_script"; then + if ! download_file "$OPENCODE_INSTALLER_URL" "$opencode_install_script"; then warn "Failed to download OpenCode installer." INCOMPLETE=1 elif ! verify_file_checksum "$opencode_install_script" "$OPENCODE_INSTALLER_SHA256"; then diff --git a/scripts/init_system_readme.md b/scripts/init_system_readme.md index bec1c08..de0c3dc 100644 --- a/scripts/init_system_readme.md +++ b/scripts/init_system_readme.md @@ -15,8 +15,8 @@ The script does not accept runtime arguments. Adjust defaults by editing constan ## What It Does - installs base tooling and `gh` -- installs Node.js >= 20 (`npm`/`npx`) -- installs `uv` and pre-downloads Python `3.10/3.11/3.12/3.13` (if missing) +- installs Node.js >= 20 (`npm`/`npx`) from the distro package manager only +- installs pinned `uv` release binaries and pre-downloads Python `3.10/3.11/3.12/3.13` (if missing) - creates shared directories and applies permissions - clones this repository to shared path (HTTPS by default) - creates A2A virtualenv via `uv sync --all-extras` @@ -30,13 +30,30 @@ The script does not accept runtime arguments. Adjust defaults by editing constan - Toggles: `INSTALL_PACKAGES`, `INSTALL_UV`, `INSTALL_GH`, `INSTALL_NODE` - Versions: `NODE_MAJOR`, `UV_PYTHON_VERSIONS` - Installer pinning: `OPENCODE_INSTALLER_URL`, `OPENCODE_INSTALLER_VERSION`, `OPENCODE_INSTALLER_SHA256`, `OPENCODE_INSTALL_CMD` +- uv release pinning: [`init_system_uv_release_manifest.sh`](./init_system_uv_release_manifest.sh) + +## Current Trust Model + +- `gh`: installed from distro package manager or package-manager-backed vendor repo configuration +- Node.js: installed only from the distro package manager; the script no longer executes NodeSource setup scripts +- `uv`: installed from a pinned upstream GitHub release tarball, with static asset/checksum data sourced from `init_system_uv_release_manifest.sh` +- OpenCode: installer is still a remote script, but it is version-pinned and checksum-verified before execution +- repo bootstrap: clone still trusts the configured git remote + branch head ## Recommended Secure Mode - keep installer pinning/checksum verification enabled +- keep `init_system_uv_release_manifest.sh` aligned with the exact `uv` release assets you intend to trust +- prefer distro package mirrors or internal mirrors for Node.js instead of adding ad-hoc third-party setup scripts - keep `/opt/uv-python` in controlled group mode (`770` -> `750` hardening flow) - set `UV_PYTHON_DIR_GROUP` to a controlled group and add runtime users intentionally +## Failure Fallback + +- If `uv` bootstrap reports an unsupported Linux architecture/libc, install the pinned `uv` release manually and rerun `./scripts/init_system.sh` with `INSTALL_UV=false`. +- If the distro package manager cannot provide `Node.js >= NODE_MAJOR`, install Node.js manually from a trusted distro or internal mirror and rerun with `INSTALL_NODE=false`. +- If OpenCode installer checksum validation fails, stop and refresh the pinned installer metadata before retrying. + ## Next Step After bootstrap, deploy a project instance with [`deploy_readme.md`](./deploy_readme.md). diff --git a/scripts/init_system_uv_release_manifest.sh b/scripts/init_system_uv_release_manifest.sh new file mode 100644 index 0000000..b964b02 --- /dev/null +++ b/scripts/init_system_uv_release_manifest.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Pinned uv release assets used by scripts/init_system.sh. + +UV_VERSION="0.10.7" +UV_RELEASE_BASE_URL="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}" + +UV_TARBALL_X86_64_GNU="uv-x86_64-unknown-linux-gnu.tar.gz" +UV_TARBALL_X86_64_GNU_SHA256="9ac6cee4e379a5abfca06e78a777b26b7ba1f81cb7935b97054d80d85ac00774" # pragma: allowlist secret + +UV_TARBALL_X86_64_MUSL="uv-x86_64-unknown-linux-musl.tar.gz" +UV_TARBALL_X86_64_MUSL_SHA256="992529add6024e67135b1c80617abd2eca7be2cf0b99b3911f923de815bd8dc1" # pragma: allowlist secret + +UV_TARBALL_AARCH64_GNU="uv-aarch64-unknown-linux-gnu.tar.gz" +UV_TARBALL_AARCH64_GNU_SHA256="20efc27d946860093650bcf26096a016b10fdaf03b13c33b75fbde02962beea9" # pragma: allowlist secret + +UV_TARBALL_AARCH64_MUSL="uv-aarch64-unknown-linux-musl.tar.gz" +UV_TARBALL_AARCH64_MUSL_SHA256="115291f9943531a3b63db3a2eabda8b74b8da4831551679382cb309c9debd9f7" # pragma: allowlist secret diff --git a/tests/test_init_system_security.py b/tests/test_init_system_security.py index 427531e..f891e3a 100644 --- a/tests/test_init_system_security.py +++ b/tests/test_init_system_security.py @@ -3,21 +3,23 @@ INIT_SYSTEM_PATH = Path("scripts/init_system.sh") INIT_SYSTEM_TEXT = INIT_SYSTEM_PATH.read_text() +UV_MANIFEST_PATH = Path("scripts/init_system_uv_release_manifest.sh") +UV_MANIFEST_TEXT = UV_MANIFEST_PATH.read_text() -def _extract_var(var_name: str) -> str: - match = re.search(rf"^{var_name}=\"([^\"]*)\"", INIT_SYSTEM_TEXT, re.MULTILINE) +def _extract_var(text: str, var_name: str) -> str: + match = re.search(rf"^{var_name}=\"([^\"]*)\"", text, re.MULTILINE) if not match: - msg = f"Missing constant {var_name} in scripts/init_system.sh" + msg = f"Missing constant {var_name}" raise AssertionError(msg) return match.group(1) def test_opencode_install_flow_is_pinned_and_verified() -> None: - url = _extract_var("OPENCODE_INSTALLER_URL") - version = _extract_var("OPENCODE_INSTALLER_VERSION") - checksum = _extract_var("OPENCODE_INSTALLER_SHA256") - install_cmd = _extract_var("OPENCODE_INSTALL_CMD") + url = _extract_var(INIT_SYSTEM_TEXT, "OPENCODE_INSTALLER_URL") + version = _extract_var(INIT_SYSTEM_TEXT, "OPENCODE_INSTALLER_VERSION") + checksum = _extract_var(INIT_SYSTEM_TEXT, "OPENCODE_INSTALLER_SHA256") + install_cmd = _extract_var(INIT_SYSTEM_TEXT, "OPENCODE_INSTALL_CMD") assert url == "https://opencode.ai/install" assert version @@ -26,20 +28,47 @@ def test_opencode_install_flow_is_pinned_and_verified() -> None: assert "bash -" not in install_cmd assert "--version" in install_cmd assert "curl -fsSL https://opencode.ai/install | bash" not in INIT_SYSTEM_TEXT - assert 'download_script "$OPENCODE_INSTALLER_URL"' in INIT_SYSTEM_TEXT + assert 'source "${SCRIPT_DIR}/init_system_uv_release_manifest.sh"' in INIT_SYSTEM_TEXT + assert 'download_file "$OPENCODE_INSTALLER_URL"' in INIT_SYSTEM_TEXT assert ( 'verify_file_checksum "$opencode_install_script" "$OPENCODE_INSTALLER_SHA256"' in INIT_SYSTEM_TEXT ) +def test_uv_install_flow_is_pinned_to_release_tarballs() -> None: + version = _extract_var(UV_MANIFEST_TEXT, "UV_VERSION") + base_url = _extract_var(UV_MANIFEST_TEXT, "UV_RELEASE_BASE_URL") + + assert re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version) + assert base_url == "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}" + assert "astral.sh/uv/install.sh" not in INIT_SYSTEM_TEXT + assert "install_uv_from_release" in INIT_SYSTEM_TEXT + assert "resolve_uv_release_artifact" in INIT_SYSTEM_TEXT + for checksum_var in ( + "UV_TARBALL_X86_64_GNU_SHA256", + "UV_TARBALL_X86_64_MUSL_SHA256", + "UV_TARBALL_AARCH64_GNU_SHA256", + "UV_TARBALL_AARCH64_MUSL_SHA256", + ): + assert re.fullmatch(r"[0-9a-f]{64}", _extract_var(UV_MANIFEST_TEXT, checksum_var)) + + +def test_node_install_flow_avoids_remote_setup_scripts() -> None: + assert "deb.nodesource.com/setup_" not in INIT_SYSTEM_TEXT + assert "rpm.nodesource.com/setup_" not in INIT_SYSTEM_TEXT + assert "NodeSource setup script" not in INIT_SYSTEM_TEXT + assert "Installing Node.js from trusted package manager repositories" in INIT_SYSTEM_TEXT + assert "Install Node.js >= ${NODE_MAJOR} manually" in INIT_SYSTEM_TEXT + + def _parse_octal_mode(mode: str) -> int: return int(mode, 8) def test_uv_python_default_permissions_are_not_world_writable() -> None: - initial_mode = _extract_var("UV_PYTHON_DIR_MODE") - final_mode = _extract_var("UV_PYTHON_DIR_FINAL_MODE") + initial_mode = _extract_var(INIT_SYSTEM_TEXT, "UV_PYTHON_DIR_MODE") + final_mode = _extract_var(INIT_SYSTEM_TEXT, "UV_PYTHON_DIR_FINAL_MODE") mode_groups = _parse_octal_mode(initial_mode), _parse_octal_mode(final_mode) for mode in mode_groups: assert mode & 0o002 == 0