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:
parent
9999e1da41
commit
ef28afb9b8
4 changed files with 263 additions and 24 deletions
71
README.md
71
README.md
|
|
@ -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).
|
||||
|
|
|
|||
28
lib.sh
Normal file
28
lib.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# Shared step-tracking helpers for setup-local.sh and setup-remote.sh
|
||||
|
||||
STEP_NAMES=()
|
||||
STEP_STATUS=()
|
||||
STEP_DETAIL=()
|
||||
|
||||
step_ok() { STEP_NAMES+=("$1"); STEP_STATUS+=("OK"); STEP_DETAIL+=("${2:-}"); echo "[OK] $1"; }
|
||||
step_skip() { STEP_NAMES+=("$1"); STEP_STATUS+=("SKIPPED"); STEP_DETAIL+=("${2:-}"); echo "[SKIPPED] $1 - ${2:-}"; }
|
||||
step_fail() { STEP_NAMES+=("$1"); STEP_STATUS+=("FAILED"); STEP_DETAIL+=("${2:-}"); echo "[FAILED] $1 - ${2:-}"; }
|
||||
|
||||
section() { echo; echo "== $1 =="; }
|
||||
|
||||
# Prints the OK/SKIPPED/FAILED table. Returns 1 if any step failed, 0 otherwise.
|
||||
# Does not exit and does not print anything past the table - callers add their
|
||||
# own pass/fail wording and next-steps text.
|
||||
print_summary_table() {
|
||||
echo
|
||||
echo "===================== summary ====================="
|
||||
local any_failed=0
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
printf '[%-8s] %s\n' "${STEP_STATUS[$i]}" "${STEP_NAMES[$i]}"
|
||||
[ -n "${STEP_DETAIL[$i]}" ] && printf ' %s\n' "${STEP_DETAIL[$i]}"
|
||||
[ "${STEP_STATUS[$i]}" = "FAILED" ] && any_failed=1
|
||||
done
|
||||
echo "====================================================="
|
||||
return "$any_failed"
|
||||
}
|
||||
|
|
@ -1,27 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
STEP_NAMES=()
|
||||
STEP_STATUS=()
|
||||
STEP_DETAIL=()
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "$SCRIPT_DIR/lib.sh"
|
||||
|
||||
KNOWN_HOSTS=()
|
||||
|
||||
step_ok() { STEP_NAMES+=("$1"); STEP_STATUS+=("OK"); STEP_DETAIL+=("${2:-}"); echo "[OK] $1"; }
|
||||
step_skip() { STEP_NAMES+=("$1"); STEP_STATUS+=("SKIPPED"); STEP_DETAIL+=("${2:-}"); echo "[SKIPPED] $1 - ${2:-}"; }
|
||||
step_fail() { STEP_NAMES+=("$1"); STEP_STATUS+=("FAILED"); STEP_DETAIL+=("${2:-}"); echo "[FAILED] $1 - ${2:-}"; }
|
||||
|
||||
section() { echo; echo "== $1 =="; }
|
||||
|
||||
print_summary() {
|
||||
echo
|
||||
echo "===================== summary ====================="
|
||||
local any_failed=0
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
printf '[%-8s] %s\n' "${STEP_STATUS[$i]}" "${STEP_NAMES[$i]}"
|
||||
[ -n "${STEP_DETAIL[$i]}" ] && printf ' %s\n' "${STEP_DETAIL[$i]}"
|
||||
[ "${STEP_STATUS[$i]}" = "FAILED" ] && any_failed=1
|
||||
done
|
||||
echo "====================================================="
|
||||
print_summary_table || any_failed=1
|
||||
if [ "$any_failed" = 1 ]; then
|
||||
echo "One or more steps failed. Fix the issue above and re-run this script -"
|
||||
echo "already-completed steps are safe to skip and will not be repeated."
|
||||
166
setup-remote.sh
Normal file
166
setup-remote.sh
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "$SCRIPT_DIR/lib.sh"
|
||||
|
||||
RDP_PORT=3389
|
||||
TLS_DIR="/etc/gnome-remote-desktop/tls"
|
||||
|
||||
print_summary() {
|
||||
local any_failed=0
|
||||
print_summary_table || any_failed=1
|
||||
if [ "$any_failed" = 1 ]; then
|
||||
echo "One or more steps failed. Fix the issue above and re-run this script -"
|
||||
echo "already-completed steps are safe to skip and will not be repeated."
|
||||
exit 1
|
||||
fi
|
||||
echo "All steps OK or already satisfied."
|
||||
echo
|
||||
echo "================== next steps ======================"
|
||||
echo "Connect from the travel laptop with GNOME Connections (or xfreerdp)"
|
||||
echo "to this machine's tailscale address, port $RDP_PORT. You'll be asked"
|
||||
echo "for the RDP username/password you just set - that gets you to the"
|
||||
echo "actual login screen, where you still log in with your real account"
|
||||
echo "password same as sitting at the machine."
|
||||
echo "====================================================="
|
||||
}
|
||||
|
||||
preflight_env() {
|
||||
section "preflight: environment"
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
step_fail "environment check" "do not run as root - script uses sudo for the specific commands that need it"
|
||||
print_summary
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v grdctl >/dev/null 2>&1; then
|
||||
step_fail "environment check" "grdctl not found - this script targets a machine with gnome-remote-desktop installed"
|
||||
print_summary
|
||||
exit 1
|
||||
fi
|
||||
step_ok "environment check"
|
||||
}
|
||||
|
||||
harden_firewall() {
|
||||
section "firewall (restrict RDP to tailscale)"
|
||||
|
||||
if ! command -v ufw >/dev/null 2>&1; then
|
||||
step_fail "firewall" "ufw not installed"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! sudo ufw status | head -1 | grep -qi "^Status: active"; then
|
||||
echo "ufw is currently inactive on this machine. Enabling it switches to a"
|
||||
echo "deny-incoming-by-default posture, which could affect any other LAN"
|
||||
echo "service here that doesn't already have an explicit allow rule."
|
||||
echo "Rules that will be in place once enabled:"
|
||||
sudo ufw show added
|
||||
read -rp "Type 'yes' to enable ufw now: " confirm
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
step_fail "firewall" "not confirmed - ufw left inactive, RDP rule not added"
|
||||
return
|
||||
fi
|
||||
sudo ufw --force enable
|
||||
fi
|
||||
|
||||
if sudo ufw status | grep -qF "${RDP_PORT}/tcp on tailscale0"; then
|
||||
step_skip "firewall" "RDP already restricted to tailscale0"
|
||||
return
|
||||
fi
|
||||
|
||||
if sudo ufw allow in on tailscale0 to any port "$RDP_PORT" proto tcp; then
|
||||
step_ok "firewall" "RDP (${RDP_PORT}/tcp) allowed on tailscale0 only"
|
||||
else
|
||||
step_fail "firewall" "ufw rule failed to apply"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_tls() {
|
||||
section "tls certificate"
|
||||
sudo mkdir -p "$TLS_DIR"
|
||||
|
||||
if sudo test -f "$TLS_DIR/cert.pem" && sudo test -f "$TLS_DIR/key.pem"; then
|
||||
step_skip "tls certificate" "already present at $TLS_DIR - not regenerating (would invalidate the fingerprint clients already trust)"
|
||||
else
|
||||
if sudo openssl req -x509 -newkey rsa:4096 -nodes \
|
||||
-keyout "$TLS_DIR/key.pem" -out "$TLS_DIR/cert.pem" \
|
||||
-days 3650 -subj "/CN=$(hostname)" >/tmp/grd-tls.log 2>&1; then
|
||||
sudo chown gnome-remote-desktop:gnome-remote-desktop "$TLS_DIR/key.pem" "$TLS_DIR/cert.pem"
|
||||
sudo chmod 600 "$TLS_DIR/key.pem"
|
||||
sudo chmod 644 "$TLS_DIR/cert.pem"
|
||||
step_ok "tls certificate" "self-signed cert generated at $TLS_DIR"
|
||||
else
|
||||
step_fail "tls certificate" "openssl req failed - see /tmp/grd-tls.log"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
sudo grdctl --system rdp set-tls-cert "$TLS_DIR/cert.pem"
|
||||
sudo grdctl --system rdp set-tls-key "$TLS_DIR/key.pem"
|
||||
}
|
||||
|
||||
configure_credentials() {
|
||||
section "rdp credentials"
|
||||
local rdp_status
|
||||
rdp_status=$(sudo grdctl --system rdp status --show-credentials 2>&1)
|
||||
|
||||
if ! echo "$rdp_status" | grep -q "Username: (null)"; then
|
||||
step_skip "rdp credentials" "already configured - not touching an existing password"
|
||||
return
|
||||
fi
|
||||
|
||||
local rdp_user
|
||||
read -rp "RDP username [$(whoami)]: " rdp_user
|
||||
rdp_user="${rdp_user:-$(whoami)}"
|
||||
|
||||
echo "RDP password - a separate secret from your account login password,"
|
||||
echo "gates only the initial RDP connection (you still log into the actual"
|
||||
echo "session with your real account password afterwards). Not stored by"
|
||||
echo "this script anywhere. Paste/type it, then press Enter and Ctrl-D:"
|
||||
local rdp_pass
|
||||
rdp_pass=$(cat)
|
||||
rdp_pass="${rdp_pass//[[:space:]]/}"
|
||||
|
||||
if [ -z "$rdp_pass" ]; then
|
||||
step_fail "rdp credentials" "empty password entered - not setting"
|
||||
return
|
||||
fi
|
||||
|
||||
if sudo grdctl --system rdp set-credentials "$rdp_user" "$rdp_pass"; then
|
||||
step_ok "rdp credentials" "set for user $rdp_user"
|
||||
else
|
||||
step_fail "rdp credentials" "grdctl set-credentials failed"
|
||||
fi
|
||||
unset rdp_pass
|
||||
}
|
||||
|
||||
enable_rdp() {
|
||||
section "enable rdp backend"
|
||||
sudo grdctl --system rdp set-port "$RDP_PORT"
|
||||
sudo grdctl --system rdp disable-port-negotiation
|
||||
|
||||
if sudo grdctl --system rdp status 2>&1 | grep -qi "Status: *enabled"; then
|
||||
step_skip "enable rdp backend" "already enabled"
|
||||
return
|
||||
fi
|
||||
|
||||
if sudo grdctl --system rdp enable; then
|
||||
step_ok "enable rdp backend"
|
||||
else
|
||||
step_fail "enable rdp backend" "grdctl --system rdp enable failed"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight_env
|
||||
harden_firewall
|
||||
configure_tls
|
||||
configure_credentials
|
||||
enable_rdp
|
||||
print_summary
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue