infra/setup-local.sh
Tommy Rantti b7ad3a9b8c Add remote-chromium.sh and install waypipe in setup-local.sh
Runs Chromium on the home machine with its window drawn on the travel
laptop, so logged-in browser accounts stay on the home machine. Closes
any running instance first, since Chromium's one-process-per-profile
lock would otherwise open the window on the home machine's own screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:37:54 +03:00

408 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "$SCRIPT_DIR/lib.sh"
KNOWN_HOSTS=()
print_summary() {
local any_failed=0
print_summary_table || any_failed=1
if [ "$any_failed" = 1 ]; then
echo "One or more steps failed. Fix the issue above and re-run this script -"
echo "already-completed steps are safe to skip and will not be repeated."
exit 1
fi
echo "All steps OK or already satisfied."
echo
echo "================== next steps ======================"
echo "1. Check you're actually on the tailnet:"
echo " tailscale status"
echo
if [ "${#KNOWN_HOSTS[@]}" -gt 0 ]; then
echo "2. Open VS Code, Ctrl+Shift+P -> 'Remote-SSH: Connect to Host...',"
echo " and pick one of the hosts this run found on your tailnet:"
for h in "${KNOWN_HOSTS[@]}"; do
echo " - $h"
done
echo " (a host you expect but don't see here just isn't on the tailnet"
echo " right now - check it's powered on and connected, then re-run"
echo " this script to refresh the list)"
else
echo "2. No other tailnet peers were found yet. Once your home machine is"
echo " online and joined to the tailnet, re-run this script to add it,"
echo " then use VS Code's 'Remote-SSH: Connect to Host...' to reach it."
fi
echo
echo "3. Once connected, File > Open Folder to browse that machine's"
echo " filesystem and get to your project - same as opening a folder"
echo " locally, just on the remote host."
echo "====================================================="
}
# ---------- phase 0: environment ----------
preflight_env() {
section "preflight: environment"
if [ "$(id -u)" = "0" ]; then
step_fail "environment check" "do not run as root - script uses sudo for the specific commands that need it"
print_summary
exit 1
fi
if ! grep -qi '^ID=debian' /etc/os-release 2>/dev/null; then
step_fail "environment check" "this script targets Debian; /etc/os-release did not report ID=debian"
print_summary
exit 1
fi
step_ok "environment check"
}
# ---------- phase 0.1: config ----------
gather_config() {
section "config"
local config_file="$HOME/.config/travel-bootstrap/config"
mkdir -p "$(dirname "$config_file")"
local DEFAULT_FORGEJO_URL="" DEFAULT_KEY_NAME=""
if [ -f "$config_file" ]; then
# shellcheck disable=SC1090
source "$config_file"
fi
if [ -z "${FORGEJO_URL:-}" ]; then
if [ -n "$DEFAULT_FORGEJO_URL" ]; then
read -rp "Forgejo instance URL [$DEFAULT_FORGEJO_URL]: " FORGEJO_URL
FORGEJO_URL="${FORGEJO_URL:-$DEFAULT_FORGEJO_URL}"
else
read -rp "Forgejo instance URL (e.g. https://git.example.com): " FORGEJO_URL
fi
fi
FORGEJO_URL="${FORGEJO_URL%/}"
if [ -z "${KEY_NAME:-}" ]; then
local key_default="${DEFAULT_KEY_NAME:-travel-laptop}"
read -rp "Label for this device's SSH key [$key_default]: " KEY_NAME
KEY_NAME="${KEY_NAME:-$key_default}"
fi
KEYFILE="$HOME/.ssh/id_ed25519_${KEY_NAME}"
cat > "$config_file" <<EOF
DEFAULT_FORGEJO_URL="$FORGEJO_URL"
DEFAULT_KEY_NAME="$KEY_NAME"
EOF
chmod 600 "$config_file"
step_ok "config gathered" "FORGEJO_URL=$FORGEJO_URL KEY_NAME=$KEY_NAME"
}
# ---------- phase 0.2: connectivity preflight ----------
check_url() {
local name="$1" url="$2"
if curl -fsS --max-time 5 -o /dev/null "$url"; then
step_ok "reachable: $name"
else
step_fail "reachable: $name" "curl could not reach $url"
fi
}
preflight_connectivity() {
section "preflight: connectivity"
check_url "Debian mirrors" "https://deb.debian.org"
check_url "Tailscale install" "https://pkgs.tailscale.com"
check_url "uv installer" "https://astral.sh"
check_url "Firefox extensions" "https://addons.mozilla.org"
check_url "VS Code repo" "https://packages.microsoft.com"
check_url "Forgejo instance" "$FORGEJO_URL"
for i in "${!STEP_NAMES[@]}"; do
if [ "${STEP_STATUS[$i]}" = "FAILED" ]; then
echo
echo "Connectivity check failed for one or more dependencies (see above)."
echo "Fix connectivity and re-run before continuing."
print_summary
exit 1
fi
done
}
# ---------- phase 1: base packages ----------
install_packages() {
section "base packages"
if command -v tailscale >/dev/null 2>&1; then
step_skip "tailscale package" "already installed"
else
if curl -fsSL https://tailscale.com/install.sh | sh >/tmp/tailscale-install.log 2>&1; then
step_ok "tailscale package"
else
step_fail "tailscale package" "see /tmp/tailscale-install.log"
fi
fi
if [ ! -f /etc/apt/keyrings/microsoft.gpg ]; then
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/microsoft.gpg >/dev/null
fi
local vscode_repo_line='deb [arch=amd64 signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/repos/code stable main'
if [ -f /etc/apt/sources.list.d/vscode.list ] && grep -qF "$vscode_repo_line" /etc/apt/sources.list.d/vscode.list; then
step_skip "vscode apt repo" "already configured"
else
echo "$vscode_repo_line" | sudo tee /etc/apt/sources.list.d/vscode.list >/dev/null
step_ok "vscode apt repo"
fi
if sudo apt-get update -qq && sudo apt-get install -y -qq \
vim tmux mosh git curl ca-certificates jq gnupg firefox-esr code waypipe >/tmp/apt-install.log 2>&1; then
step_ok "apt packages"
else
step_fail "apt packages" "see /tmp/apt-install.log"
fi
if command -v uv >/dev/null 2>&1; then
step_skip "uv" "already installed"
else
if curl -fsSL https://astral.sh/uv/install.sh | sh >/tmp/uv-install.log 2>&1; then
step_ok "uv"
else
step_fail "uv" "see /tmp/uv-install.log"
fi
fi
if code --list-extensions 2>/dev/null | grep -qx "ms-vscode-remote.remote-ssh"; then
step_skip "vscode remote-ssh extension" "already installed"
else
if code --install-extension ms-vscode-remote.remote-ssh >/tmp/vscode-ext.log 2>&1; then
step_ok "vscode remote-ssh extension"
else
step_fail "vscode remote-ssh extension" "see /tmp/vscode-ext.log"
fi
fi
}
# ---------- phase 2: tailscale join ----------
tailscale_join() {
section "tailscale"
if sudo tailscale status >/dev/null 2>&1; then
step_skip "tailscale up" "already connected"
else
if sudo tailscale up; then
step_ok "tailscale up"
else
step_fail "tailscale up" "tailscale up failed or was not approved"
fi
fi
}
# ---------- phase 3: ssh config from tailnet ----------
regen_ssh_config() {
section "ssh config (tailnet hosts)"
local sshconf="$HOME/.ssh/config"
local begin="# BEGIN travel-bootstrap"
local end="# END travel-bootstrap"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
touch "$sshconf"
chmod 600 "$sshconf"
local status_json
if ! status_json=$(tailscale status --json 2>/dev/null); then
step_fail "ssh config (tailnet hosts)" "tailscale status --json failed"
return
fi
local self_dns suffix user
self_dns=$(echo "$status_json" | jq -r '.Self.DNSName' | sed 's/\.$//')
suffix=${self_dns#*.}
user=$(whoami)
if [ -z "$suffix" ] || [ "$suffix" = "$self_dns" ]; then
step_fail "ssh config (tailnet hosts)" "could not determine tailnet suffix from $self_dns"
return
fi
local new_block
new_block=$(cat <<EOF
$begin
Host *.$suffix
User $user
IdentityFile ~/.ssh/id_ed25519_${KEY_NAME}
EOF
)
while read -r name dns; do
[ -z "$name" ] && continue
KNOWN_HOSTS+=("$name")
new_block+=$(cat <<EOF
Host $name
HostName $dns
User $user
IdentityFile ~/.ssh/id_ed25519_${KEY_NAME}
EOF
)
done < <(echo "$status_json" | jq -r '.Peer[] | (.DNSName | rtrimstr(".")) as $d | "\($d | split(".")[0]) \($d)"')
new_block+=$'\n'"$end"
local old_block
old_block=$(awk -v b="$begin" -v e="$end" '$0==b{f=1} f{print} $0==e{f=0}' "$sshconf")
if [ "$old_block" = "$new_block" ]; then
step_skip "ssh config (tailnet hosts)" "no change - tailnet peers unchanged"
return
fi
local tmp
tmp=$(mktemp)
awk -v b="$begin" -v e="$end" '$0==b{skip=1} !skip{print} $0==e{skip=0}' "$sshconf" > "$tmp"
printf '%s\n' "$new_block" >> "$tmp"
mv "$tmp" "$sshconf"
chmod 600 "$sshconf"
if [ -z "$old_block" ]; then
step_ok "ssh config (tailnet hosts)" "wrote wildcard + $(echo "$status_json" | jq '.Peer | length') peer entries"
else
step_ok "ssh config (tailnet hosts)" "updated - peer list changed"
fi
}
# ---------- phase 4: ssh keypair + forgejo registration ----------
forgejo_ssh_key() {
section "forgejo ssh key"
local keyfile="$KEYFILE"
if [ ! -f "$keyfile" ]; then
ssh-keygen -t ed25519 -f "$keyfile" -N "" -C "${KEY_NAME}-$(date +%Y%m%d)" >/dev/null
fi
local pubkey fingerprint
pubkey=$(cat "${keyfile}.pub")
fingerprint=$(ssh-keygen -lf "${keyfile}.pub" | awk '{print $2}')
echo "Need a token: log into $FORGEJO_URL -> avatar (top right) -> Settings ->"
echo "Applications tab -> Manage Access Tokens -> name it, grant 'write:user'"
echo "scope (or tick 'user' read+write) -> Generate Token -> copy it now, it's"
echo "only shown once."
echo "Paste the token below, then press Enter and then Ctrl-D to finish:"
local token
token=$(cat)
token="${token//[[:space:]]/}"
if [ -z "$token" ]; then
echo "Captured: empty (nothing came through - paste may not have worked in this terminal)"
else
echo "Captured: length=${#token} chars, ${token:0:4}...${token: -4}"
fi
local tmpbody status
tmpbody=$(mktemp)
if ! status=$(curl -sS -o "$tmpbody" -w '%{http_code}' -H "Authorization: token $token" "$FORGEJO_URL/api/v1/user/keys"); then
step_fail "forgejo ssh key" "GET /api/v1/user/keys - curl could not connect at all (DNS/TLS/network)"
unset token; rm -f "$tmpbody"
return
fi
if [ "$status" != "200" ]; then
step_fail "forgejo ssh key" "GET /api/v1/user/keys - HTTP $status: $(cat "$tmpbody") (401=bad token, 403=missing write:user scope, 404=check FORGEJO_URL)"
unset token; rm -f "$tmpbody"
return
fi
local existing
existing=$(cat "$tmpbody")
rm -f "$tmpbody"
if echo "$existing" | jq -e --arg fp "$fingerprint" '.[] | select(.fingerprint == $fp)' >/dev/null 2>&1; then
step_skip "forgejo ssh key" "already registered ($fingerprint)"
unset token
return
fi
local payload
payload=$(jq -n --arg title "${KEY_NAME}-$(hostname)-$(date +%Y%m%d)" --arg key "$pubkey" \
'{title: $title, key: $key}')
tmpbody=$(mktemp)
if ! status=$(curl -sS -o "$tmpbody" -w '%{http_code}' -X POST \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "$payload" \
"$FORGEJO_URL/api/v1/user/keys"); then
step_fail "forgejo ssh key" "POST /api/v1/user/keys - curl could not connect at all (DNS/TLS/network)"
unset token; rm -f "$tmpbody"
return
fi
if [ "$status" = "200" ] || [ "$status" = "201" ]; then
step_ok "forgejo ssh key" "registered ($fingerprint)"
else
step_fail "forgejo ssh key" "POST /api/v1/user/keys - HTTP $status: $(cat "$tmpbody") (401=bad token, 403=missing write:user scope, 404=check FORGEJO_URL)"
fi
rm -f "$tmpbody"
unset token
}
# ---------- phase 5: browser hardening ----------
harden_firefox() {
section "firefox hardening"
local policy_dir="/etc/firefox/policies"
local policy_file="$policy_dir/policies.json"
local desired
desired=$(cat <<'EOF'
{
"policies": {
"DisableTelemetry": true,
"DisableFirefoxAccounts": true,
"OfferToSaveLogins": false,
"NoDefaultBookmarks": true,
"DisableFormHistory": true,
"DisablePocket": true,
"Preferences": {
"browser.privatebrowsing.autostart": { "Value": true, "Status": "locked" },
"places.history.enabled": { "Value": false, "Status": "locked" },
"browser.cache.disk.enable": { "Value": false, "Status": "locked" }
},
"ExtensionSettings": {
"uBlock0@raymondhill.net": {
"installation_mode": "force_installed",
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"
}
}
}
}
EOF
)
if [ -f "$policy_file" ] && [ "$(cat "$policy_file")" = "$desired" ]; then
step_skip "firefox policy" "already up to date"
return
fi
sudo mkdir -p "$policy_dir"
echo "$desired" | sudo tee "$policy_file" >/dev/null
step_ok "firefox policy" "wrote $policy_file - VERIFY: private-browsing-autostart + forced uBlock Origin behave as expected on first launch"
}
main() {
preflight_env
gather_config
preflight_connectivity
install_packages
tailscale_join
regen_ssh_config
forgejo_ssh_key
harden_firefox
print_summary
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi