Add README summarizing the design and known gotchas

Compacts the design discussion into a reference: the workflow model
(pure Remote-SSH by default, opportunistic local clones for
self-contained python+uv projects only), how to run the script, the
home-machine prerequisite (Tailscale SSH), and the gotchas hit while
building it (PATH issues after su, stale sudo group membership, the
token-paste-into-shell bug, the spurious-space host alias bug, and
the tailnet-wide MagicDNS requirement) so they don't need re-debugging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tommy Rantti 2026-09-26 20:43:32 +03:00
parent 20af6b67a8
commit 9999e1da41

128
README.md Normal file
View file

@ -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 <name>` 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 <user>` 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.