molecule-core/plugins/browser-automation/host-bridge/install-host-bridge.sh
Hongming Wang 5ccceb92f9 fix(security): chmod 600 macOS launchd plist containing CDP token (#296)
One-liner oversight from #295: the macOS install path wrote the plist
with the default umask (~0644), leaving CDP_PROXY_TOKEN world-readable
to any local user account. The Linux path already writes to a chmod
600 env-file — this brings macOS to parity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 18:20:48 -07:00

156 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# install-host-bridge.sh — run ONCE on the host machine to keep cdp-proxy alive
# across reboots. Workspaces inside Docker then reach Chrome via the proxy.
#
# Supports macOS (launchd) and Linux (systemd --user). No root required.
#
# Usage:
# bash install-host-bridge.sh # install + start
# bash install-host-bridge.sh uninstall # stop + remove
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROXY_SCRIPT="${SCRIPT_DIR}/cdp-proxy.cjs"
LABEL="com.molecule.browser-automation.cdp-proxy"
NODE_BIN="$(command -v node || echo /usr/local/bin/node)"
TOKEN_FILE="${HOME}/.molecule-cdp-proxy-token"
if [[ ! -f "$PROXY_SCRIPT" ]]; then
echo "ERROR: $PROXY_SCRIPT not found" >&2
exit 1
fi
if [[ ! -x "$NODE_BIN" ]]; then
echo "ERROR: node not on PATH — install Node.js first" >&2
exit 1
fi
# #293: generate a per-install auth token so the proxy isn't exposed to the
# LAN without authentication. Written to ~/.molecule-cdp-proxy-token with
# 0600 perms. The proxy reads it at startup; workspace containers read it
# via the bundled connect() helper which mounts the token file over a bind.
ensure_token() {
if [[ -f "$TOKEN_FILE" ]] && [[ "$(wc -c < "$TOKEN_FILE")" -ge 17 ]]; then
echo "token: reusing existing $TOKEN_FILE"
return
fi
# 32 bytes of random, hex-encoded → 64 chars. openssl is available on
# every macOS + most Linux installs; fall back to /dev/urandom if not.
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32 > "$TOKEN_FILE"
else
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$TOKEN_FILE"
fi
chmod 600 "$TOKEN_FILE"
echo "token: generated new $TOKEN_FILE (0600)"
}
install_macos() {
local plist="$HOME/Library/LaunchAgents/${LABEL}.plist"
local token_val
token_val="$(cat "$TOKEN_FILE")"
cat > "$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>${NODE_BIN}</string>
<string>${PROXY_SCRIPT}</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>CDP_PROXY_TOKEN</key><string>${token_val}</string>
</dict>
<key>KeepAlive</key><true/>
<key>RunAtLoad</key><true/>
<key>StandardOutPath</key><string>${HOME}/.molecule-cdp-proxy.log</string>
<key>StandardErrorPath</key><string>${HOME}/.molecule-cdp-proxy.log</string>
</dict></plist>
EOF
# #296: the plist contains the CDP_PROXY_TOKEN in plaintext. Default
# umask leaves it world-readable (~0644) which leaks the token to any
# local user on a multi-account macOS host. Lock to owner-only. launchctl
# loads user agents as the owning UID so 0600 is safe.
chmod 600 "$plist"
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$plist"
launchctl kickstart -k "gui/$(id -u)/${LABEL}"
echo "installed macOS launchd agent: $plist"
echo "logs: ${HOME}/.molecule-cdp-proxy.log"
}
install_linux() {
local unit_dir="$HOME/.config/systemd/user"
mkdir -p "$unit_dir"
local unit="$unit_dir/${LABEL}.service"
# Read token from the file at service start instead of embedding it in
# the unit file — unit files are often world-readable, the token file
# is 0600. systemd EnvironmentFile reads key=value lines so we write a
# sidecar file containing CDP_PROXY_TOKEN=<value>.
local env_file="${HOME}/.molecule-cdp-proxy.env"
printf 'CDP_PROXY_TOKEN=%s\n' "$(cat "$TOKEN_FILE")" > "$env_file"
chmod 600 "$env_file"
cat > "$unit" <<EOF
[Unit]
Description=Molecule browser-automation CDP proxy (host → Chrome)
After=network-online.target
[Service]
Type=simple
EnvironmentFile=${env_file}
ExecStart=${NODE_BIN} ${PROXY_SCRIPT}
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now "${LABEL}.service"
echo "installed systemd user unit: $unit"
echo "logs: journalctl --user -u ${LABEL}.service -f"
}
uninstall() {
case "$(uname -s)" in
Darwin)
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
rm -f "$HOME/Library/LaunchAgents/${LABEL}.plist"
echo "uninstalled macOS launchd agent"
;;
Linux)
systemctl --user disable --now "${LABEL}.service" 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/${LABEL}.service"
systemctl --user daemon-reload
echo "uninstalled systemd user unit"
;;
esac
}
case "${1:-install}" in
install)
ensure_token
case "$(uname -s)" in
Darwin) install_macos ;;
Linux) install_linux ;;
*) echo "unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac
echo
echo "next step: launch your Chrome with --remote-debugging-port=9222 (once per reboot)"
echo " macOS: open -na 'Google Chrome' --args --remote-debugging-port=9222 --user-data-dir=\"\$HOME/.chrome-molecule\""
echo "verify: curl -H \"X-CDP-Proxy-Token: \$(cat $TOKEN_FILE)\" http://127.0.0.1:9223/json/version"
echo
echo "container side: mount $TOKEN_FILE into each workspace and the bundled"
echo "lib/connect.js helper will read it automatically. Bind:"
echo " -v $TOKEN_FILE:/run/secrets/cdp-proxy-token:ro"
;;
uninstall)
uninstall
rm -f "${HOME}/.molecule-cdp-proxy.env" 2>/dev/null || true
echo "note: ${TOKEN_FILE} preserved so a future reinstall keeps the same token."
echo " delete manually if you want to rotate."
;;
*) echo "usage: $0 [install|uninstall]"; exit 1 ;;
esac