infra/README.md
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

197 lines
10 KiB
Markdown

# 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 `setup-local.sh`,
run it, give it a couple of credentials, and you're back to full working
order in minutes.
Two scripts, one per machine role:
- **`setup-local.sh`** - run on the disposable travel laptop. Everything in
this README that isn't explicitly about the home machine refers to this.
- **`setup-remote.sh`** - run once on the machine you Remote-SSH/RDP *into*
(e.g. your home desktop). Configures RDP access for occasional full-desktop
use (browser sessions already logged into Gmail etc.) - see "Remote
desktop access" below.
Both share `lib.sh` (step-tracking/summary helpers) - all three files need
to stay together when you fetch this repo.
## 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/setup-local.sh -o setup-local.sh
curl -fsSL https://git.brainpaingames.com/tommy/infra/raw/branch/main/lib.sh -o lib.sh
chmod +x setup-local.sh
./setup-local.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 for Remote-SSH that lives outside any script, run once 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 `setup-local.sh` registers to
Forgejo is for a *different* purpose - git operations against Forgejo, a
separate trust relationship from logging into the home machine.)
## Remote desktop access (occasional, full-desktop use)
Remote-SSH covers development, but not things like an already-logged-in
Gmail session - for that, `setup-remote.sh` configures GNOME's built-in RDP
(`gnome-remote-desktop`) on the home machine. Run it there once:
```bash
./setup-remote.sh
```
It restricts the RDP port to the tailscale interface only (via a `ufw`
rule - if `ufw` isn't active yet, it asks before turning it on, since that
changes this machine's default network posture more broadly), sets up a
self-signed TLS cert, and prompts for RDP credentials **only if none are
set yet** - these are deliberately never cached anywhere.
**Two authentication layers, not one**: the RDP username/password you set
here just gates the RDP connection itself and gets you to the login
screen - it is *not* your account password and doesn't grant a session by
itself. You still log in with your real account password once connected,
same as sitting at the machine. Keep both: relying on tailnet-reachability
alone as the only gate would collapse this to a single point of failure,
the same reasoning that already justified keeping SSH keys behind Tailscale
SSH rather than trusting tailnet membership alone.
From the travel laptop: GNOME Connections (ships by default with a GNOME
desktop, nothing extra to install) or `xfreerdp`, pointed at the home
machine's tailscale address, port 3389.
**Connecting to a fresh boot with nobody logged in locally** (e.g. a power
outage and the machine restarts unattended) works *if* the disk isn't
encrypted, since Tailscale and the RDP daemon are both system services that
start before any login - `setup-remote.sh` targets exactly this
"system/GDM-level" RDP mode rather than the simpler per-session one, which
only shares a session that already exists. If this machine is ever
LUKS-encrypted later, this recovery path breaks (nothing starts until
someone types the disk passphrase locally) unless something like
`dropbear-initramfs` is added for remote unlock.
## Single remote app: Chromium with your logged-in accounts
`remote-chromium.sh` (run on the travel laptop) closes any Chromium you have
running on the home machine, then launches it there with its window drawn
locally via `waypipe` (`--x11` for classic X11 forwarding instead). The close
step matters: Chromium allows one process per profile, so a still-running
instance would swallow the new window onto the home machine's own screen.
Needs `waypipe` on both ends (`setup-local.sh` installs it locally; on the
home machine: `sudo apt install waypipe`). Untested against Tailscale SSH,
which may not support the socket/X11 forwarding these rely on - if the
connection fails, plain OpenSSH is the fix.
## 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.
- **`grep -qi active` also matches "in`active`"** - a substring check for
whether ufw is active matched the *inactive* case too, silently skipping
the enable-confirmation step and going straight to adding a firewall rule
on a firewall that was never actually turned on. Caught by testing the
branch directly rather than assuming; fixed by anchoring the match
(`^Status: active`).
- **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.