diff --git a/CLAUDE.md b/CLAUDE.md index 8e2c0c2..078501f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,11 +28,11 @@ This means `./setup_env.sh my-vm` on macOS does everything end-to-end. - **Target environment**: OrbStack Ubuntu VM on macOS Apple Silicon (ARM64) - **Version manager**: mise (manages Node.js, Erlang, Elixir) -- **Language versions**: Configured at the top of `setup_env.sh` as variables (`ERLANG_VERSION`, `ELIXIR_VERSION`) -- **PostgreSQL auth**: Peer for local socket, trust for localhost TCP (127.0.0.1), scram-sha-256 for host network (192.168.0.0/16) +- **Versions**: All tools use latest by default. Erlang/Elixir versions can be configured at the top of `setup_env.sh` (`ERLANG_VERSION`, `ELIXIR_VERSION`) - set to "latest" or pin to specific versions +- **PostgreSQL auth**: Peer for local socket, scram-sha-256 for all TCP connections (localhost and network) - **Browser**: Chromium (no Chrome ARM64 Linux builds exist), symlinked to `google-chrome` - **VNC**: TigerVNC + XFCE on display :1 (port 5901), controlled via `vnc-start`/`vnc-stop` helpers in `~/bin` -- **Shared credentials**: `config.env` is created once and reused for all VMs +- **Shared credentials**: `config.env` stores git name/email only; VNC password is prompted each time (never stored) ## Working on This Project @@ -61,6 +61,16 @@ This means `./setup_env.sh my-vm` on macOS does everything end-to-end. - `--yes`/`-y` accepts all components without prompting but still allows interactive credential entry - `MISE_GLOBAL_CONFIG_FILE` and `MISE_CONFIG_DIR` are set to prevent OrbStack host-mount config pollution +### Security Patterns + +- **Input validation**: Use `validate_vm_name()`, `validate_vnc_password()`, `validate_safe_input()` for all user inputs +- **Safe config loading**: Use `load_config_safely()` which parses key=value pairs without shell execution (never `source`) +- **Credential passing**: Base64-encode values before passing to VM via `orb run` to prevent shell injection +- **Download helper**: Use `download_verified_binary()` for binary downloads (supports optional checksum verification) +- **Temp files**: Always use `mktemp` with restrictive permissions (`chmod 600`/`700`) +- **Symlinks**: Check with `[ ! -e path ]` (not `[ ! -L path ]`) to avoid overwriting existing files +- **Architecture detection**: Use `detect_architecture()` for portable binary downloads + ### Testing There are no automated tests. To test changes: @@ -73,8 +83,11 @@ There are no automated tests. To test changes: ### Security Considerations -- `config.env` is chmod 600 and gitignored -- PostgreSQL remote access uses scram-sha-256 (not trust) +- `config.env` is chmod 600 and gitignored (stores git name/email only, never VNC password) +- PostgreSQL uses scram-sha-256 for all TCP connections (peer auth for local socket) - The script refuses to run as root inside the VM -- VNC password is required (min 6 chars) -- VNC binds to all interfaces (`-localhost no`) to allow connections from the macOS host — this is intentional for the OrbStack use case +- VNC password is required (min 6 chars), validated to block shell metacharacters, never stored +- VNC binds to all interfaces (`-localhost no`) to allow connections from the macOS host — this is intentional for the OrbStack use case and documented in the vnc-start script +- All user inputs are validated before use to prevent command injection +- External scripts (mise, ollama) are downloaded to temp files and validated before execution +- Host filesystem access is disabled inside the VM for isolation diff --git a/README.md b/README.md index f5fa24a..7fad4d0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OrbStack Development Sandbox -Disposable, isolated Linux VMs for running Claude Code with `--dangerously-skip-permissions`. One command creates a fully provisioned environment. Blow it away and recreate it in minutes. +sDisposable, isolated Linux VMs for running Claude Code with `--dangerously-skip-permissions`. One command creates a fully provisioned environment. Blow it away and recreate it in minutes. The VM is a real Linux machine with its own filesystem, network, and process space — but you edit files from your Mac, access services on `*.orb.local`, and SSH in without any key setup. All the isolation of a container with none of the friction. @@ -32,7 +32,7 @@ If anything goes wrong: `orb delete my-project && ./setup_env.sh my-project` ./setup_env.sh my-sandbox ``` -On first run, you'll be prompted for git name, email, and a VNC password. These are saved to `config.env` and reused for all future VMs. You'll also get an interactive checklist to select which components to install. +On first run, you'll be prompted for git commit author name and email. These are saved to `config.env` and reused for all future VMs. You'll also get an interactive checklist to select which components to install. If you select VNC, you'll be prompted for a VNC password (this is never stored and must be entered each time). ```bash # Create additional VMs — reuses config.env, shows component picker @@ -64,13 +64,15 @@ All components are optional — deselect what you don't need in the interactive |------|---------|---------| | mise | latest | Version manager for runtimes | | Node.js | LTS | JavaScript runtime | -| Erlang | 28.3.1 | BEAM VM | -| Elixir | 1.19.5-otp-28 | Elixir language | +| Erlang | latest | BEAM VM | +| Elixir | latest | Elixir language | | Chromium | system | Browser automation target | | Playwright | latest | Browser testing framework | | PostgreSQL | system default | Database | | Ollama | latest | Local LLM inference | | Claude Code | latest | AI coding assistant | +| yq | latest | YAML processor | +| watchexec | latest | File watcher (via cargo) | | TigerVNC + XFCE | system | VNC access for browser login flows | ## Connecting from macOS @@ -177,7 +179,7 @@ A superuser matching your Linux username and a `dev` database are created automa **Auth model:** - Local socket (`psql dev`): peer auth (OS username must match PG role) -- Localhost TCP (127.0.0.1): trust (passwordless) +- Localhost TCP (127.0.0.1): scram-sha-256 (password required) - Host network (192.168.0.0/16, i.e., from macOS): scram-sha-256 ### From inside the VM @@ -246,16 +248,18 @@ The script installs these plugins at user scope: ## Configuration -Edit the version variables at the top of `setup_env.sh`: +All tools are configured to use the latest versions by default. Erlang and Elixir versions can be customized in `setup_env.sh`: ```bash -ERLANG_VERSION="28.3.1" -ELIXIR_VERSION="1.19.5-otp-28" +ERLANG_VERSION="latest" # or pin to specific version like "28.3.1" +ELIXIR_VERSION="latest" # or pin to specific version like "1.19.5-otp-28" ``` ### Shared credentials -Credentials are stored in `config.env` (gitignored). To reset: +Git credentials (name, email) are stored in `config.env` (gitignored). VNC passwords are never stored and must be entered each time you create a VM with VNC enabled. + +To reset git credentials: ```bash rm config.env @@ -294,4 +298,16 @@ The VM provisioning script is safe to run multiple times. It checks for existing ## Logs -Each provisioning run creates a log file at `/tmp/setup_env_.log` inside the VM with detailed output from package installations and any errors. +Each provisioning run creates a log file at `/tmp/setup_env_.log` inside the VM with detailed output from package installations and any errors. Log files are created with mode 600 (owner-only access). + +## Security + +The script includes several security hardening measures: + +- **Input validation**: VM names, VNC passwords, and git credentials are validated against strict patterns +- **No credential storage**: VNC passwords are prompted each time and never written to disk +- **Safe credential passing**: Values are base64-encoded when passed to the VM to prevent shell injection +- **Config file security**: `config.env` is created with mode 600 and parsed safely (not sourced) +- **Secure temp files**: Uses `mktemp` with restrictive permissions for all temporary files +- **Host filesystem isolation**: macOS home directory access is disabled inside the VM +- **PostgreSQL hardening**: Uses peer auth for local sockets, scram-sha-256 for network connections diff --git a/setup_env.sh b/setup_env.sh index 50a3842..9ad01c3 100755 --- a/setup_env.sh +++ b/setup_env.sh @@ -25,17 +25,11 @@ set -euo pipefail # ============================================================================ -# Configuration - edit versions here +# Configuration # ============================================================================ -ERLANG_VERSION="28.3.1" -ELIXIR_VERSION="1.19.5-otp-28" -PLAYWRIGHT_VERSION="1.50.1" -# Claude Code version - check https://www.npmjs.com/package/@anthropic-ai/claude-code for latest -CLAUDE_CODE_VERSION="1.0.16" -# yq version - check https://github.com/mikefarah/yq/releases for latest -YQ_VERSION="4.45.1" -# watchexec version - check https://github.com/watchexec/watchexec/releases for latest -WATCHEXEC_VERSION="2.3.2" +# Use 'latest' for mise-managed tools to always get the newest version +ERLANG_VERSION="latest" +ELIXIR_VERSION="latest" # ============================================================================ # Helpers @@ -1046,14 +1040,12 @@ install_ollama() { install_claude() { export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH" - # Security: Pin to specific version to prevent supply chain attacks - npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" + npm install -g @anthropic-ai/claude-code } install_playwright() { export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH" - # Security: Install specific version globally, then use it (avoids npx auto-download) - npm install -g "playwright@${PLAYWRIGHT_VERSION}" + npm install -g playwright playwright install --with-deps chromium # Symlink Playwright's Chromium if no system one (check for existing files) @@ -1105,7 +1097,7 @@ install_base_deps() { sudo ln -sf "$(which fdfind)" /usr/local/bin/fd fi - # yq - with architecture detection, version pinning, and checksum verification + # yq - with architecture detection (always latest) if ! command_exists yq; then local yq_arch local arch @@ -1114,65 +1106,20 @@ install_base_deps() { amd64) yq_arch="amd64" ;; arm64) yq_arch="arm64" ;; esac - local yq_binary="yq_linux_${yq_arch}" - local yq_url="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/${yq_binary}" - local yq_checksum_url="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/checksums" - - # Fetch and verify checksum - local temp_dir - temp_dir=$(mktemp -d) - curl -fsSL "$yq_checksum_url" -o "$temp_dir/checksums" - local expected_checksum - expected_checksum=$(grep "${yq_binary}$" "$temp_dir/checksums" | awk '{print $1}') - - if [ -n "$expected_checksum" ]; then - download_verified_binary "$yq_url" "/usr/local/bin/yq" "$expected_checksum" - else - log_error "Could not find checksum for yq, downloading without verification" - download_verified_binary "$yq_url" "/usr/local/bin/yq" - fi - rm -rf "$temp_dir" + local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${yq_arch}" + download_verified_binary "$yq_url" "/usr/local/bin/yq" fi - # watchexec - with architecture detection, version pinning, and checksum verification + # watchexec - install via cargo (always latest) if ! command_exists watchexec; then - local watchexec_arch - local arch - arch=$(detect_architecture) - case "$arch" in - amd64) watchexec_arch="x86_64-unknown-linux-gnu" ;; - arm64) watchexec_arch="aarch64-unknown-linux-gnu" ;; - esac - local watchexec_tarball="watchexec-${WATCHEXEC_VERSION}-${watchexec_arch}.tar.xz" - local watchexec_url="https://github.com/watchexec/watchexec/releases/download/v${WATCHEXEC_VERSION}/${watchexec_tarball}" - local watchexec_checksum_url="https://github.com/watchexec/watchexec/releases/download/v${WATCHEXEC_VERSION}/SHA512SUMS" - - local temp_dir - temp_dir=$(mktemp -d) - - # Fetch checksum file - curl -fsSL "$watchexec_checksum_url" -o "$temp_dir/SHA512SUMS" 2>/dev/null || true - local expected_checksum - expected_checksum=$(grep "${watchexec_tarball}$" "$temp_dir/SHA512SUMS" 2>/dev/null | awk '{print $1}') - - if curl -fsSL "$watchexec_url" -o "$temp_dir/watchexec.tar.xz"; then - # Verify checksum if available (SHA512 for watchexec) - if [ -n "$expected_checksum" ]; then - local actual_checksum - actual_checksum=$(sha512sum "$temp_dir/watchexec.tar.xz" | awk '{print $1}') - if [ "$actual_checksum" != "$expected_checksum" ]; then - log_error "Checksum mismatch for watchexec" - rm -rf "$temp_dir" - return 1 - fi - fi - tar -xJf "$temp_dir/watchexec.tar.xz" -C "$temp_dir" --strip-components=1 - sudo mv "$temp_dir/watchexec" /usr/local/bin/watchexec - sudo chmod +x /usr/local/bin/watchexec + if command_exists cargo; then + cargo install watchexec-cli else - log_error "Failed to download watchexec" + # Fallback: install cargo first, then watchexec + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source "$HOME/.cargo/env" + cargo install watchexec-cli fi - rm -rf "$temp_dir" fi # direnv hook