Split into setup-local.sh + setup-remote.sh, add RDP configuration

Renames bootstrap.sh to setup-local.sh to match its actual scope (the
travel laptop only) and adds setup-remote.sh for the machine you
Remote-SSH/RDP into, since that's a different machine's one-time setup
and was already living outside setup-local.sh's reach (same reasoning
as the existing Tailscale-SSH prerequisite).

setup-remote.sh configures GNOME's system-level RDP (works from a cold
GDM login screen, not just an existing session) restricted to the
tailscale interface via a ufw rule, a self-signed TLS cert, and RDP
credentials that are deliberately separate from the account password
and never cached - only prompted if unset. Shares step-tracking helpers
with setup-local.sh via a new lib.sh rather than duplicating them.

Tested the branching logic (credentials-already-set, RDP-already-
enabled, ufw-already-active-with-rule, ufw-inactive-confirm/decline)
against realistic stubbed command output. Caught and fixed a real bug
in the process: the inactive/active ufw check used a bare `grep -qi
active`, which also matches the substring inside "inactive" - it was
silently skipping the enable-confirmation gate and going straight to
adding a firewall rule on a firewall that was never turned on. Fixed
by anchoring the match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tommy Rantti 2026-09-26 21:25:19 +03:00
parent 9999e1da41
commit ef28afb9b8
4 changed files with 263 additions and 24 deletions

View file

@ -1,10 +1,21 @@
# 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`,
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
@ -35,9 +46,10 @@ 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
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:
@ -76,8 +88,8 @@ 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:
One prerequisite for Remote-SSH that lives outside any script, run once on
whichever machine you Remote-SSH into:
```bash
sudo tailscale set --ssh
@ -85,10 +97,49 @@ 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
`~/.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.
## Gotchas hit while building this (so they don't get re-debugged)
- **`usermod`/`visudo`: command not found** after `su` - not missing, just
@ -99,6 +150,12 @@ separate trust relationship from logging into the home machine.)
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).