freerdp2-x11 assumed the FreeRDP 2.x package/binary naming, but newer Debian/Ubuntu releases have moved to a 3.x package where the binary may be xfreerdp3 instead of xfreerdp. Try both, and use whichever binary actually ends up available. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| lib.sh | ||
| rdp-test.sh | ||
| README.md | ||
| setup-local.sh | ||
| setup-remote.sh | ||
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:
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/configand 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:userscope -> 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:
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:
./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.
Gotchas hit while building this (so they don't get re-debugged)
usermod/visudo: command not found aftersu- not missing, just not onPATH. Usesu -(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
sudostill refuses - group changes only apply to new login sessions. Check with plainid(no args) in the actual session you're testing in; ifsudoisn't listed there even thoughid <user>shows it, the session is stale - reboot or fully re-login. grep -qi activealso matches "inactive" - 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:userscope, 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
readstops 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
Hostalias had a space in it - Tailscale's rawHostNamefield is whatever the OS reports (an Android phone's name can be e.g."Pixel 9"), which breaks unquoted in an SSH configHostline. Fixed by deriving the alias from the already-sanitizedDNSNameinstead. .ts.netnames 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.netname assigned regardless of whether this is on - it just won't actually resolve until it is.
- MagicDNS has to be switched on tailnet-wide (Tailscale admin console ->
DNS tab), separately from any per-device setting. Each device always
gets a
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.