diff --git a/README.md b/README.md new file mode 100644 index 0000000..d82a817 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# travel laptop bootstrap + +A disposable, extremely light Debian laptop for working on the road. If it's +lost, stolen, or dropped in the sea: install Debian, fetch `bootstrap.sh`, +run it, give it a couple of credentials, and you're back to full working +order in minutes. + +## The idea + +- The travel laptop stays minimal: vim, tmux, mosh, tailscale, VS Code + (+ Remote-SSH extension), `uv`, and a locked-down browser. No language + runtimes, no Docker, no databases. +- **Default workflow is pure Remote-SSH, nothing local.** VS Code connects + over Tailscale to your home machine (or another tailnet box) and edits + that machine's checkout directly. Actually running/building/testing code + always happens there too - never on the travel laptop - so a project that + generates tens of GB of logs/DB state on a "heavy" run never becomes the + travel laptop's problem, regardless of whether that particular project + is big or small. +- **Local clones are opportunistic, not default.** Only migrate a repo to + the laptop itself if it's genuinely self-contained (Python + `uv`, no + services/DB/heavy runtime state) and you specifically want it editable + offline. Everything else (e.g. anything needing a distrobox container, + a full toolchain, Docker) stays remote-only, always. +- Offline mode = edit + commit only. Building/running resumes once you're + back online and reconnect via Remote-SSH. +- Idempotent, not transactional: every step checks its own state before + acting, so the script is safe to run any number of times. If a step + fails, fix the underlying issue and re-run - already-done steps skip, + nothing needs to be undone. + +## Running it + +On a fresh Debian install, once you're on the network: + +```bash +sudo apt update && sudo apt install -y curl +curl -fsSL https://git.brainpaingames.com/tommy/infra/raw/branch/main/bootstrap.sh -o bootstrap.sh +chmod +x bootstrap.sh +./bootstrap.sh +``` + +It prompts for: +- **Forgejo instance URL** and a **device/key label** (remembered across + runs in `~/.config/travel-bootstrap/config` and offered as the default + next time - just hit Enter to reuse them). +- **Tailscale login** (interactive, opens a URL to approve the device). +- A **Forgejo access token**, used once to register a fresh SSH keypair via + the API, never stored. Get one at: your Forgejo instance -> avatar (top + right) -> Settings -> Applications -> Manage Access Tokens -> grant + `write:user` scope -> Generate Token -> copy it immediately, it's shown + once. When prompted, paste it and press **Ctrl-D** (not Enter) to submit. + +What it does, in order: installs packages, joins Tailscale, regenerates +`~/.ssh/config` from the live tailnet peer list (a wildcard entry covering +the whole tailnet's DNS suffix, plus one named `Host` alias per peer - +regenerated fresh every run, so new peers just show up next time), registers +the new SSH key to Forgejo, and hardens Firefox (forced incognito, no +history, forced uBlock Origin). Ends with a summary and, if everything +succeeded, a concrete "here's what to do next" block listing the tailnet +hosts it found. + +## Connecting to a project + +Open VS Code -> `Ctrl+Shift+P` -> **Remote-SSH: Connect to Host...** -> pick +the home machine's alias from the list -> `File > Open Folder`, same as +opening a folder locally, just on the remote filesystem. + +For a project living inside a **distrobox** container on the remote: +Remote-SSH gets you the host's shell, not the container - run +`distrobox enter ` in the terminal same as you would locally. +Nothing about arriving over SSH changes that. Tighter integration (VS Code +extensions actually running inside the container) is possible later via +the Dev Containers extension's "Attach to Running Container," launched +from within an already-open Remote-SSH window - not set up yet. + +## On the home machine side + +One prerequisite that lives outside this script, on whichever machine you +Remote-SSH into: + +```bash +sudo tailscale set --ssh +``` + +This lets Tailscale itself authorize SSH logins via your tailnet identity, +so the travel laptop's key never needs to be manually added to +`~/.ssh/authorized_keys` there. (The SSH key this script registers to +Forgejo is for a *different* purpose - git operations against Forgejo, a +separate trust relationship from logging into the home machine.) + +## Gotchas hit while building this (so they don't get re-debugged) + +- **`usermod`/`visudo`: command not found** after `su` - not missing, just + not on `PATH`. Use `su -` (with the dash) for a proper root login shell, + or call by full path (`/usr/sbin/usermod`, `/usr/sbin/visudo`). +- **Group membership looks correct but `sudo` still refuses** - group + changes only apply to *new* login sessions. Check with plain `id` (no + args) in the actual session you're testing in; if `sudo` isn't listed + there even though `id ` shows it, the session is stale - reboot or + fully re-login. +- **Forgejo API call fails with a vague error** - the script reports the + real HTTP status now (401 = bad/expired token, 403 = missing + `write:user` scope, 404 = check the instance URL). +- **A pasted token ends up executed as a shell command** - some "copy + token" buttons include a trailing newline. A plain `read` stops at that + newline and leaves the rest of the paste sitting in the terminal's input + buffer, which the shell then runs once the script exits. Fixed by + reading raw stdin until EOF (paste, then Ctrl-D) instead of stopping at + the first newline. +- **A generated `Host` alias had a space in it** - Tailscale's raw + `HostName` field is whatever the OS reports (an Android phone's name can + be e.g. `"Pixel 9"`), which breaks unquoted in an SSH config `Host` line. + Fixed by deriving the alias from the already-sanitized `DNSName` instead. +- **`.ts.net` names don't resolve, even a device resolving its own name** + - MagicDNS has to be switched on tailnet-wide (Tailscale admin console -> + DNS tab), separately from any per-device setting. Each device always + gets a `.ts.net` name assigned regardless of whether this is on - it + just won't actually resolve until it is. + +## Deliberately deferred / manual + +- **Disk encryption (LUKS)** - a Debian-installer-time choice, not + something this script can retrofit. Worth doing given this machine holds + an SSH key, even if the disk otherwise stays close to empty. +- **Browser choice**: Firefox, for its declarative `policies.json`. +- **Window manager**: left as whatever Debian's installer gives you; a + minimal WM (sway/i3) would cut RAM/disk further if that becomes worth it.