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>
197 lines
10 KiB
Markdown
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.
|