A machine's role is now the interview's first question and the one answer Panama records. Servers get the same shell minus the screen: core packages, nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless ports from 80, firewalld, the nginx-bridge network, and a nightly image updater that replaced watchtower for cause. server/containers/ carries junior's 23 compose services -- secrets moved to per-machine .env files that never enter this public repo, every transformed compose proven to render byte-identical to what is live. 'panama server' enables, disables and relinks them; nothing here restarts a running service. 'boot --server' walks a fresh VPS from its root login to a normal install. Five new contracts pin the secrets rule, the catalog's shape, panama-server's behavior, the role plumbing, and the dotfile classification. Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
167 lines
7.1 KiB
Bash
Executable File
167 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Panama's front door: the one command a fresh Fedora machine needs.
|
|
#
|
|
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
|
# bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server
|
|
#
|
|
# Deliberately dumb, because a copy of this script leaves the repository the
|
|
# moment somebody curls it -- nothing here can be fixed by re-running
|
|
# ./install, so there is as little here as possible: get git, get the clone,
|
|
# hand off. Everything with judgment in it lives in `install`, which is also
|
|
# where re-runs and upgrades already work.
|
|
#
|
|
# The one exception to dumb is the root path below, which cannot live in
|
|
# `install`: a fresh VPS hands you a root login and nothing else, and the user
|
|
# that `install` needs to exist is exactly what has not been created yet.
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_URL="https://git.gbrown.org/gib/Panama.git"
|
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
|
export PANAMA_PATH
|
|
|
|
SERVER=0
|
|
INSTALL_ARGS=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--server) SERVER=1; INSTALL_ARGS+=(--server) ;;
|
|
*)
|
|
printf 'boot: unknown argument: %s\n' "$arg" >&2
|
|
printf 'usage: boot [--server]\n' >&2
|
|
exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# Panama assumes Fedora's repositories and package names.
|
|
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
|
echo "This looks like something other than Fedora; Panama only supports Fedora." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── Root ─────────────────────────────────────────────────────────────────────
|
|
#
|
|
# On a desktop, root is a mistake: the clone and every dotfile would land in
|
|
# root's home and configure the wrong user. On a fresh VPS it is the starting
|
|
# condition -- Hetzner hands over a root login and nothing else -- so with
|
|
# --server this walks the machine from that to a normal Panama install: a
|
|
# user with sudo, keys, an optionally hardened sshd, and `install --server`
|
|
# running as that user. Every step checks before acting, because the machine
|
|
# may be anywhere along this path already: a user half-created by hand, keys
|
|
# already copied, sshd already locked down.
|
|
if [[ "$(id -u)" -eq 0 ]]; then
|
|
if (( ! SERVER )); then
|
|
echo "Run this as your own user, not root: the install configures YOUR desktop." >&2
|
|
echo "Setting up a fresh server from its root login is: boot --server" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Everything here asks, and a root shell from `bash <(curl ...)` can have
|
|
# the pipe as stdin, so every prompt reads the terminal explicitly.
|
|
if ! (exec </dev/tty) 2>/dev/null; then
|
|
echo "No terminal to ask on; run this from an interactive root shell." >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'Username for this server [gib]: '
|
|
read -r username </dev/tty || username=""
|
|
username="${username:-gib}"
|
|
|
|
if id -u "$username" >/dev/null 2>&1; then
|
|
echo "User $username already exists"
|
|
# wheel is what makes sudo work on Fedora; a user created by hand may not
|
|
# have it, and everything after this depends on it.
|
|
id -nG "$username" | grep -qw wheel || usermod -aG wheel "$username"
|
|
else
|
|
echo "Creating $username with sudo (wheel)"
|
|
useradd -m -G wheel "$username"
|
|
fi
|
|
|
|
# useradd leaves the account locked, and sudo asks for this password -- a
|
|
# user who cannot sudo is a user the install cannot run as.
|
|
if ! passwd -S "$username" 2>/dev/null | awk '{exit $2 != "PS" && $2 != "P"}'; then
|
|
echo "Set a password for $username (sudo will ask for it):"
|
|
passwd "$username" </dev/tty
|
|
fi
|
|
|
|
# The key that reached root is the key that should reach the user, or the
|
|
# next SSH login has no way in once root logins are closed below.
|
|
user_home="$(getent passwd "$username" | cut -d: -f6)"
|
|
if [[ -s /root/.ssh/authorized_keys && ! -s "$user_home/.ssh/authorized_keys" ]]; then
|
|
echo "Copying root's authorized_keys to $username"
|
|
mkdir -p "$user_home/.ssh"
|
|
cp /root/.ssh/authorized_keys "$user_home/.ssh/authorized_keys"
|
|
chmod 700 "$user_home/.ssh"
|
|
chmod 600 "$user_home/.ssh/authorized_keys"
|
|
chown -R "$username:$username" "$user_home/.ssh"
|
|
fi
|
|
|
|
# Offered rather than imposed, defaulting to yes: a VPS keeps its provider's
|
|
# web console, so locking password and root logins out of sshd is
|
|
# recoverable even when it goes wrong. Written as a drop-in so it never
|
|
# fights the distribution's own sshd_config.
|
|
SSHD_DROPIN=/etc/ssh/sshd_config.d/90-panama.conf
|
|
if [[ -f "$SSHD_DROPIN" ]]; then
|
|
echo "sshd is already hardened ($SSHD_DROPIN)"
|
|
else
|
|
printf 'Harden sshd (disable root login and password auth)? [Y/n]: '
|
|
read -r harden </dev/tty || harden=""
|
|
if [[ ! "$harden" =~ ^[Nn] ]]; then
|
|
printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$SSHD_DROPIN"
|
|
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
|
|
echo "Wrote $SSHD_DROPIN; make sure your key works before logging out."
|
|
fi
|
|
fi
|
|
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
echo "Installing git, which the clone needs"
|
|
dnf install -y git
|
|
fi
|
|
|
|
# Cloned straight into the user's home and owned by them: this is the
|
|
# checkout `panama update` will pull from for the life of the machine, and
|
|
# a root-owned .git in a user's home is a wound that never heals.
|
|
PANAMA_PATH="$user_home/.local/share/Panama"
|
|
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
|
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
|
runuser -u "$username" -- git -C "$PANAMA_PATH" pull --ff-only \
|
|
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
|
else
|
|
runuser -u "$username" -- mkdir -p "$user_home/.local/share"
|
|
runuser -u "$username" -- git clone "$REPO_URL" "$PANAMA_PATH"
|
|
fi
|
|
|
|
echo "Handing off to install as $username"
|
|
exec runuser -u "$username" -- env PANAMA_PATH="$PANAMA_PATH" \
|
|
"$PANAMA_PATH/install" --server </dev/tty
|
|
fi
|
|
|
|
# git is the one dependency the clone itself needs. Everything else -- gum
|
|
# included -- is bootstrapped by `install`.
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
echo "Installing git, which the clone needs"
|
|
sudo dnf install -y git
|
|
fi
|
|
|
|
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
|
# An existing clone makes this the recovery command too. Only a fast-forward:
|
|
# local work is never rewritten, and a diverged clone still installs from
|
|
# what it has rather than stopping someone mid-repair.
|
|
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
|
git -C "$PANAMA_PATH" pull --ff-only \
|
|
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
|
else
|
|
git clone "$REPO_URL" "$PANAMA_PATH"
|
|
fi
|
|
|
|
# `curl | bash` and `bash <(curl ...)` can leave stdin as the pipe, and the
|
|
# first thing install runs is the interview, which has to be able to ask.
|
|
# Reattach the terminal when there is one; without one the interview will say
|
|
# so itself.
|
|
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
|
# controlling terminal passes -r and then fails the redirect.
|
|
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
|
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
|
fi
|
|
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|