diff --git a/.gitignore b/.gitignore index 615a741..7cdd3c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ # Forgejo runner registration file — contains a secret, never commit. -# Bare `.runner` (no slash) matches it in any directory, incl. the repo root. -.runner runner/.runner *.env - -# Local secret notes — backup of credentials kept off git. NEVER commit. -secrets.local.md -*.local.secret diff --git a/README.md b/README.md index 9fb9ac2..1eb7c1e 100644 --- a/README.md +++ b/README.md @@ -27,212 +27,86 @@ declares routing labels. --- -## The VPS (provisioned 2026-06-02) - -Vultr shared-CPU **2 GB**, **Debian 13 (trixie)**, kernel 6.12, ~5.4 GB swap -already present (no need to add any). Desktop SSH key installed; Vultr firewall -groups applied on both IPv4 and IPv6. - -| | | -|---|---| -| IPv4 | `45.32.242.27` | -| IPv6 | `2001:19f0:5801:1313:5400:6ff:fe35:c0a7` | -| SSH | `root@45.32.242.27` (port 22) | - -## Pre-flight check - -Before and after each bootstrap step, run the read-only checker from your -desktop to confirm reachability, ports, the box's facts, and DNS: - -```sh -./scripts/check-vps.sh --ip6 2001:19f0:5801:1313:5400:6ff:fe35:c0a7 -``` - -It exits 0 when SSH + port 22 are good; closed app ports and unset DNS show as -warnings (expected until DNS is updated and the stack is up). - ## Bootstrap (one-time, in order) Order matters — TLS issuance needs DNS + open ports first, and the `edge` network must exist before any stack references it. -### 1. DNS — managed at **Cloudflare**, not VentraIP -VentraIP is only the registrar; the domain's nameservers are delegated to -Cloudflare (`grace/keenan.ns.cloudflare.com`), so create the records **in the -Cloudflare dashboard**. Point them at the VPS and **wait for propagation**: -- `A joshbairstow.com → 45.32.242.27` -- `A *.joshbairstow.com → 45.32.242.27` (wildcard: covers blog/art/code/git + future) -- `AAAA joshbairstow.com → 2001:19f0:5801:1313:5400:6ff:fe35:c0a7` -- `AAAA *.joshbairstow.com → 2001:19f0:5801:1313:5400:6ff:fe35:c0a7` +### 1. DNS +Point at the VPS public IP, and **wait for propagation** before step 4: +- `A joshbairstow.com → ` +- `A *.joshbairstow.com → ` (wildcard covers blog/art/code/git + future) +- `AAAA` equivalents if the VPS has IPv6. -> **Set these records to DNS-only (grey cloud), not proxied (orange cloud).** -> The current records point at Cloudflare's proxy. With the proxy ON: Cloudflare -> terminates TLS with its own edge cert (so Caddy's automatic HTTPS is bypassed), -> and it only proxies HTTP(S) ports — **git-over-SSH on 222 and the registry -> would break**. DNS-only makes clients hit the VPS directly, so Caddy issues -> real Let's Encrypt certs via HTTP-01 (no DNS-challenge plugin needed) and all -> ports work. If you later want Cloudflare's proxy/CDN, switch Caddy to the -> DNS-01 challenge with the Cloudflare plugin + an API token — a separate change. +A wildcard record means new subdomains resolve with no DNS change; certs are +still issued per-host via HTTP-01, so no DNS-challenge plugin is required. ### 2. VPS base -Firewall is already handled by the Vultr firewall group (80, 443, 222, 22), so -host `ufw` is optional. Swap already exists. The remaining task is Docker: ```sh -# Install Docker Engine + compose plugin +# Install Docker Engine + compose plugin (official convenience script or distro pkg) curl -fsSL https://get.docker.com | sh -docker --version && docker compose version # confirm -``` -(Re-run `./scripts/check-vps.sh` — `DOCKER` should now report a version.) -### 3. Get the files onto the VPS + bring up the stack -Forgejo doesn't exist yet, so this repo has no server to clone from during -bootstrap. The VPS can't pull from your (NAT'd) desktop either — but your -desktop can push to the VPS, and `docker compose` only needs the files, not -git. So copy them up with rsync (run from your desktop, in `~/git/jb-infra`): -```sh -rsync -av --exclude '.git' --exclude 'runner' --exclude 'scripts' \ - ./ root@45.32.242.27:/opt/jb-infra/ +# Firewall: allow only what we serve. +# 80, 443 -> edge proxy (HTTP/HTTPS, incl. ACME challenges) +# 222 -> Forgejo git-over-SSH +# 22 -> your normal host SSH (keep it!) +ufw allow 80,443/tcp && ufw allow 222/tcp && ufw allow OpenSSH && ufw enable ``` -(`runner/` and `scripts/` are desktop-side tools — they don't belong on the -VPS.) Then on the VPS: +RAM: prefer the **2 GB** Vultr tier. On 1 GB, add swap first: +```sh +fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile +echo '/swapfile none swap sw 0 0' >> /etc/fstab +``` + +### 3. Create the shared network + bring up the stack ```sh -cd /opt/jb-infra docker network create edge # MUST exist before `up` +git clone /opt/jb-infra && cd /opt/jb-infra docker compose up -d docker compose logs -f caddy # watch the first cert issuance ``` -Once Forgejo is live (step 4+), create a `jb-infra` repo inside it and push -this there for its permanent, versioned home; future infra edits then deploy -like any other project. If you don't want git history on the box, a one-shot -`git bundle` is an alternative to rsync (see project notes). ### 4. Configure Forgejo -Browse to `https://git.joshbairstow.com` (cert should be live). The install page -is mostly prepopulated from the env vars in `compose.yml` — confirm these, then -complete it: -- **Run As Username** stays `git` (the container's OS user, *not* your login). -- **Application URL** must be `https://git.joshbairstow.com/` (not - `http://localhost:3000/`) — wrong value breaks `docker login`/push + clone URLs. -- **SSH Server Domain** `git.joshbairstow.com`, **SSH Server Port** `222`, - **Database** `SQLite3`. -- Under server settings: enable **Disable Self-Registration** (personal instance) - and **Hide Email Addresses by Default**; leave "Require Sign-In to View Pages" - off (it would block anonymous clones/pulls). -- Expand **Administrator Account Settings** and create your admin user with a - **lowercase** username (e.g. `josh`) — this becomes your registry path - `git.joshbairstow.com//…`, which must be lowercase. - -Then: +Browse to `https://git.joshbairstow.com` (cert should be live): +- Complete first-run setup; create your admin user with a **lowercase** + username (the registry path is case-sensitive and must be lowercase). - Confirm Actions is enabled (`Site Administration → Actions`). - Create a **Personal Access Token** with **`write:package`** scope (`Settings → Applications`). The automatic Actions token CANNOT push packages. -### 5. Register the desktop runner (always-on) -On the **home desktop** (already has Docker + is in the `docker` group). Put the -binary in place, register in a home-dir working folder (snap Docker needs the -build context under `$HOME`), then install the systemd unit so it survives -reboots and auto-claims jobs: -Run these from the cloned `jb-infra` repo: +### 5. Register the desktop runner +On the **home desktop** (has Docker + ssh): ```sh -# 1. Binary — skip if already present (check: command -v forgejo-runner). -# The unit's ExecStart points at /usr/local/sbin/forgejo-runner. -sudo install -m 0755 forgejo-runner /usr/local/sbin/forgejo-runner - -# 2. Working dir + config (under $HOME so snap Docker can read build contexts). -mkdir -p ~/forgejo-runner && cp runner/config.yml ~/forgejo-runner/ - -# 3. Register IN the working dir so .runner lands there (token: Forgejo → -# Site Admin → Actions → Runners → Create). The subshell keeps cwd in the repo. -( cd ~/forgejo-runner && forgejo-runner register --no-interactive \ - --instance https://git.joshbairstow.com \ - --token \ - --name desktop --labels desktop:host ) # ':host' => run on host, use host Docker - -# 4. Install + enable the always-on service. -sudo cp runner/forgejo-runner.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now forgejo-runner -systemctl status forgejo-runner # expect active (running); runner shows green in Forgejo -``` -Already registered in the wrong directory? Don't re-register — just -`mv .runner ~/forgejo-runner/` (it's location-independent credentials). -The `:host` label is what lets the `docker build` step work (the job runs on the -host using its Docker, not in a socket-less container). See -[`runner/config.yml`](runner/config.yml) and -[`runner/forgejo-runner.service`](runner/forgejo-runner.service). - -The desktop's **Docker daemon must be running and enabled at boot** — the runner -builds against it. `docker login` works without the daemon but `docker build` -won't, so a stopped daemon shows up as *"Cannot connect to the Docker daemon"* -mid-pipeline. Ensure one daemon owns `/var/run/docker.sock`: -```sh -sudo snap stop --disable docker.dockerd # if a crashing snap docker is present -sudo systemctl enable --now docker.service # classic engine, on at boot -docker info | head # verify as your user, no sudo -``` -Fedora gotcha: if `dockerd` fails with -`ZONE_CONFLICT: 'docker0' already bound to 'FedoraWorkstation'`, the cause is a -leftover **NetworkManager** bridge profile for `docker0` (no zone set → lands in -the default zone), which firewalld then re-applies. Delete it, stop NM managing -Docker's bridges (so it can't recur at boot), and start Docker: -```sh -sudo nmcli connection delete docker0 -sudo tee /etc/NetworkManager/conf.d/docker-unmanaged.conf >/dev/null <<'EOF' -[keyfile] -unmanaged-devices=interface-name:docker0;interface-name:br-* -EOF -sudo systemctl reload NetworkManager -sudo ip link delete docker0 2>/dev/null -sudo systemctl reset-failed docker.service && sudo systemctl start docker.service +# Get a registration token: Forgejo → Site Admin → Actions → Runners → Create +forgejo-runner register --no-interactive \ + --instance https://git.joshbairstow.com \ + --token \ + --name desktop --labels desktop:host +forgejo-runner daemon --config /path/to/jb-infra/runner/config.yml ``` +(For always-on, wrap the daemon in a systemd user service.) See +[`runner/config.yml`](runner/config.yml). ### 6. Wire the jb-website repo In the `jb-website` repo settings on Forgejo (`Settings → Actions`): +- **Variables:** `REGISTRY_OWNER` = your lowercase owner. +- **Secrets:** `REGISTRY_USER`, `REGISTRY_TOKEN` (the PAT), `DEPLOY_HOST`, + `DEPLOY_USER`, `DEPLOY_SSH_KEY`. -**Variable:** -- `REGISTRY_OWNER` — your **lowercase** Forgejo username/org; the image namespace - (`git.joshbairstow.com//jb-website`). - -**Secrets:** -- `REGISTRY_USER` — your Forgejo login username (the account that owns the PAT); - the username for `docker login`. Usually equals `REGISTRY_OWNER`. -- `REGISTRY_TOKEN` — the PAT with `write:package` scope; the `docker login` password. -- `DEPLOY_HOST` — `45.32.242.27`. -- `DEPLOY_USER` — the VPS account the runner SSHes in as: `root` to start, or a - dedicated `deploy` user (in the `docker` group) for least privilege. -- `DEPLOY_SSH_KEY` — the **private** key of a *dedicated deploy keypair* (full - file incl. `BEGIN/END` lines; paste multi-line — OpenSSH needs the newlines). - NOT your personal key. Generate + install its public half on the VPS: - ```sh - ssh-keygen -t ed25519 -f ~/.ssh/jb-deploy -C forgejo-ci-deploy -N "" - ssh-copy-id -i ~/.ssh/jb-deploy.pub @45.32.242.27 - cat ~/.ssh/jb-deploy # <- paste this into DEPLOY_SSH_KEY - ``` - -On the **VPS**, put the production compose file in place — rsync it from your -desktop (creates the dir + copies; only the prod `compose.yml`, not the dev one): +On the **VPS**, prepare the deploy target: ```sh -rsync -av --rsync-path="mkdir -p /opt/jb-website && rsync" \ - ~/git/jb-website/compose.yml root@45.32.242.27:/opt/jb-website/ +mkdir -p /opt/jb-website +# copy jb-website/compose.yml here (the production one — pulls, no build) +docker login git.joshbairstow.com # one-time, so `compose pull` can read the package +# (or mark the jb-website package public in Forgejo to skip VPS login) ``` -For `docker compose pull` to read the image, the **package** must be pullable. -Note: **package visibility ≠ repository visibility** — packages are owned by the -user/org, so making the *repo* public does NOT necessarily make the *image* -anonymously pullable, and the package only exists after the first push. After -the first pipeline run, verify from the VPS: -```sh -docker pull git.joshbairstow.com//jb-website:latest -# unauthorized? -> set the PACKAGE public (profile → Packages → jb-website → -# settings), or run `docker login git.joshbairstow.com` once as DEPLOY_USER. -``` -If `DEPLOY_USER` is not root, it must be in the `docker` group; for least -privilege, restrict its `authorized_keys` entry with a forced `command=`. +Create a restricted deploy user whose `authorized_keys` holds the public half +of `DEPLOY_SSH_KEY`. ### 7. First deploy ```sh -# in the jb-website repo (default branch is already `main`). -# Custom SSH port 222 needs the ssh:// URL form (scp-style host:port doesn't work): -git remote add origin ssh://git@git.joshbairstow.com:222//jb-website.git +# in the jb-website repo (rename master→main first; see that repo's README) +git remote add origin git@git.joshbairstow.com:222//jb-website.git git push -u origin main ``` The push triggers `.forgejo/workflows/deploy.yml`: the desktop builds + pushes @@ -276,11 +150,4 @@ No changes to this repo. In the new project: cert and risks Let's Encrypt rate limits. - **Least privilege:** restrict the deploy key in `authorized_keys` with a forced `command=` that only runs the compose pull/up script. -- **Reset the admin password** (no email/old password needed): as an admin via - *Site Administration → User Accounts → (user) → Password*, or from the VPS: - ```sh - docker compose exec -u git forgejo \ - forgejo admin user change-password --username \ - --password 'NEW' --must-change-password=false - ``` ``` diff --git a/runner/forgejo-runner.service b/runner/forgejo-runner.service deleted file mode 100644 index 341ed21..0000000 --- a/runner/forgejo-runner.service +++ /dev/null @@ -1,47 +0,0 @@ -# ---------------------------------------------------------------------------- -# forgejo-runner.service — always-on Forgejo Actions runner on the home desktop. -# -# System service: starts at boot, no login required. Runs as your user (already -# in the `docker` group) so host-execution jobs can run `docker build`. -# -# Install: -# sudo install -m 0755 forgejo-runner /usr/local/sbin/forgejo-runner # the binary -# mkdir -p ~/forgejo-runner && cp config.yml ~/forgejo-runner/ -# cd ~/forgejo-runner -# forgejo-runner register --no-interactive \ -# --instance https://git.joshbairstow.com \ -# --token --name desktop --labels desktop:host -# # ^ writes ./.runner (credentials) into ~/forgejo-runner -# sudo cp forgejo-runner.service /etc/systemd/system/ -# # (edit User=/paths below if your username differs) -# sudo systemctl daemon-reload -# sudo systemctl enable --now forgejo-runner -# systemctl status forgejo-runner # expect: active (running) -# journalctl -u forgejo-runner -f # watch it claim jobs -# -# Why WorkingDirectory is under $HOME: this machine's Docker is snap-based, and -# snap confinement blocks the daemon from reading build contexts outside the -# user's home — so the checkout/build must happen there, not in /opt. -# ---------------------------------------------------------------------------- - -[Unit] -Description=Forgejo Actions runner (jb CI) -Documentation=https://forgejo.org/docs/latest/admin/actions/runner-installation/ -After=network-online.target snap.docker.dockerd.service docker.service -Wants=network-online.target -# Disable the start-rate limiter so it keeps retrying until Docker is ready at boot. -StartLimitIntervalSec=0 - -[Service] -Type=simple -User=joshbairstow -SupplementaryGroups=docker -WorkingDirectory=/home/joshbairstow/forgejo-runner -Environment=HOME=/home/joshbairstow -Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -ExecStart=/usr/local/sbin/forgejo-runner daemon --config /home/joshbairstow/forgejo-runner/config.yml -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target diff --git a/scripts/check-vps.sh b/scripts/check-vps.sh deleted file mode 100755 index f8e6e01..0000000 --- a/scripts/check-vps.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env bash -# check-vps.sh — verify the joshbairstow.com VPS is reachable and correctly -# configured, before and after the jb-infra bootstrap. READ-ONLY: it changes -# nothing on the box or in DNS. -# -# What it checks: -# - ICMP reachability (IPv4, and IPv6 if you pass --ip6) -# - TCP ports 22/80/443/222 (firewall + whether a service is listening yet) -# - SSH login with your key, then reports the box's OS / kernel / RAM / swap / -# Docker / compose / the `edge` network / its own public IPs -# - DNS for the apex, the wildcard, and each subdomain vs the expected IP(s) -# -# Usage: -# ./check-vps.sh -# ./check-vps.sh --ip6 2001:19f0:... --key ~/.ssh/id_ed25519 -# VPS_IP=1.2.3.4 DOMAIN=example.com ./check-vps.sh -# -# Exit status: 0 if the CRITICAL checks pass (SSH login + port 22), else 1. -# Warnings (!) — closed app ports, unset DNS — are EXPECTED before you update -# DNS and bring up the edge/Forgejo stack, so they don't fail the run. - -set -u - -# ---- defaults (override via env or flags) ---------------------------------- -VPS_IP="${VPS_IP:-45.32.242.27}" -VPS_IP6="${VPS_IP6:-}" -DOMAIN="${DOMAIN:-joshbairstow.com}" -SSH_USER="${SSH_USER:-root}" -SSH_PORT="${SSH_PORT:-22}" -SSH_KEY="${SSH_KEY:-}" -SUBDOMAINS=(www blog art code git) -TCP_PORTS=(22 80 443 222) - -while [ $# -gt 0 ]; do - case "$1" in - --ip) VPS_IP="$2"; shift 2;; - --ip6) VPS_IP6="$2"; shift 2;; - --domain) DOMAIN="$2"; shift 2;; - --user) SSH_USER="$2"; shift 2;; - --ssh-port) SSH_PORT="$2"; shift 2;; - --key) SSH_KEY="$2"; shift 2;; - -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0;; - *) echo "unknown arg: $1" >&2; exit 2;; - esac -done - -# ---- output helpers -------------------------------------------------------- -if [ -t 1 ]; then R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; B=$'\e[1;34m'; D=$'\e[2m'; Z=$'\e[0m' -else R=; G=; Y=; B=; D=; Z=; fi -pass=0; warn=0; fail=0 -ok() { printf ' %s✓%s %s\n' "$G" "$Z" "$*"; pass=$((pass+1)); } -nok() { printf ' %s✗%s %s\n' "$R" "$Z" "$*"; fail=$((fail+1)); } -note() { printf ' %s!%s %s\n' "$Y" "$Z" "$*"; warn=$((warn+1)); } -info() { printf ' %s·%s %s\n' "$D" "$Z" "$*"; } -hdr() { printf '\n%s== %s ==%s\n' "$B" "$*" "$Z"; } -have() { command -v "$1" >/dev/null 2>&1; } - -printf '%sVPS pre-flight%s ip=%s ip6=%s domain=%s ssh=%s@%s:%s\n' \ - "$B" "$Z" "$VPS_IP" "${VPS_IP6:-none}" "$DOMAIN" "$SSH_USER" "$VPS_IP" "$SSH_PORT" - -# ---- DNS resolvers (dig → host → getent) ----------------------------------- -resolve_a() { - if have dig; then dig +short A "$1" | grep -E '^[0-9.]+$' - elif have host; then host -t A "$1" 2>/dev/null | sed -n 's/.*has address //p' - else getent ahostsv4 "$1" 2>/dev/null | awk '{print $1}' | sort -u; fi -} -resolve_aaaa() { - if have dig; then dig +short AAAA "$1" | grep -E ':' - elif have host; then host -t AAAA "$1" 2>/dev/null | sed -n 's/.*IPv6 address //p' - else getent ahostsv6 "$1" 2>/dev/null | awk '{print $1}' | sort -u; fi -} - -# ---- TCP port probe (bash /dev/tcp — variant-independent) ------------------- -check_port() { # host port -> "open" | "closed" - if timeout 4 bash -c ">/dev/tcp/$1/$2" 2>/dev/null; then echo open; else echo closed; fi -} - -# ---- 1. ICMP --------------------------------------------------------------- -hdr "ICMP reachability" -if ping -c1 -W3 "$VPS_IP" >/dev/null 2>&1; then - ok "ping $VPS_IP (IPv4)" -else - note "ping $VPS_IP (IPv4) no reply — fine if ICMP isn't allowed in the firewall" -fi -if [ -n "$VPS_IP6" ]; then - if ping -6 -c1 -W3 "$VPS_IP6" >/dev/null 2>&1; then ok "ping $VPS_IP6 (IPv6)" - else note "ping $VPS_IP6 (IPv6) no reply — your local link may lack IPv6"; fi -else - info "no --ip6 given; skipping IPv6 ping (SSH will report the box's v6 address)" -fi - -# ---- 2. TCP ports ---------------------------------------------------------- -hdr "TCP ports (firewall + listeners)" -for p in "${TCP_PORTS[@]}"; do - st=$(check_port "$VPS_IP" "$p") - case "$p:$st" in - 22:open) ok "22 SSH — open" ;; - 22:*) nok "22 SSH — $st (you can't manage the box; check firewall 22 + sshd)" ;; - *:open) ok "$p open (service listening)" ;; - *:*) note "$p $st — expected until the edge/Forgejo stack is up" ;; - esac -done - -# ---- 3. SSH login + remote facts ------------------------------------------- -hdr "SSH login + remote facts" -ssh_opts=(-o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -p "$SSH_PORT") -[ -n "$SSH_KEY" ] && ssh_opts+=(-i "$SSH_KEY") -err=$(mktemp) -remote=$(ssh "${ssh_opts[@]}" "$SSH_USER@$VPS_IP" ' - . /etc/os-release 2>/dev/null - echo "OS=${PRETTY_NAME:-unknown}" - echo "KERNEL=$(uname -r)" - echo "ARCH=$(uname -m)" - mem_kb=$(grep MemTotal /proc/meminfo | tr -dc 0-9); echo "MEM_MB=$((mem_kb/1024))" - swap_kb=$(grep SwapTotal /proc/meminfo | tr -dc 0-9); echo "SWAP_MB=$((swap_kb/1024))" - echo "PUBIP4=$(ip -4 -o addr show scope global 2>/dev/null | sed -n "s@.*inet \([0-9.]*\)/.*@\1@p" | head -1)" - echo "PUBIP6=$(ip -6 -o addr show scope global 2>/dev/null | sed -n "s@.*inet6 \([0-9a-f:]*\)/.*@\1@p" | head -1)" - if command -v docker >/dev/null 2>&1; then echo "DOCKER=$(docker --version 2>/dev/null)"; else echo "DOCKER=none"; fi - echo "COMPOSE=$(docker compose version --short 2>/dev/null || echo none)" - if docker network ls --format "{{.Name}}" 2>/dev/null | grep -qx edge; then echo "EDGE_NET=present"; else echo "EDGE_NET=absent"; fi -' 2>"$err") -rc=$? -if [ $rc -eq 0 ]; then - ok "SSH login as $SSH_USER@$VPS_IP:$SSH_PORT" - # surface every fact, then sanity-check the important ones - while IFS='=' read -r k v; do [ -n "$k" ] && info "$k = $v"; done <<<"$remote" - get() { printf '%s\n' "$remote" | sed -n "s/^$1=//p"; } - case "$(get OS)" in - *[Dd]ebian*13*|*trixie*) ok "OS is Debian 13 (trixie)";; - *[Dd]ebian*) note "Debian, but not 13: $(get OS)";; - *) note "unexpected OS: $(get OS)";; - esac - [ "$(get MEM_MB)" -ge 1900 ] 2>/dev/null && ok "RAM ~2 GB ($(get MEM_MB) MB)" \ - || note "RAM is $(get MEM_MB) MB — expected ~2 GB" - [ "$(get PUBIP4)" = "$VPS_IP" ] && ok "box's public IPv4 matches ($VPS_IP)" \ - || note "box reports IPv4 $(get PUBIP4), expected $VPS_IP" - if [ -n "$(get PUBIP6)" ]; then - if [ -n "$VPS_IP6" ] && [ "$(get PUBIP6)" = "$VPS_IP6" ]; then ok "box's public IPv6 matches" - else info "box's public IPv6 is $(get PUBIP6) — set this as your AAAA records"; fi - fi - [ "$(get DOCKER)" = none ] && note "Docker not installed yet (runbook step 2)" || ok "Docker present: $(get DOCKER)" - [ "$(get EDGE_NET)" = present ] && ok "'edge' network exists" || info "'edge' network not created yet (runbook step 3)" -else - nok "SSH login failed (rc=$rc): $(tr -d '\n' <"$err")" - info "checklist: key uploaded to Vultr? user is '$SSH_USER'? firewall allows 22 from your IP? try --key " -fi -rm -f "$err" - -# ---- 4. DNS ---------------------------------------------------------------- -hdr "DNS records (expecting → $VPS_IP${VPS_IP6:+ / $VPS_IP6})" -check_name() { - local fqdn=$1 a aaaa - a=$(resolve_a "$fqdn" | paste -sd, -) - aaaa=$(resolve_aaaa "$fqdn" | paste -sd, -) - if [ -z "$a$aaaa" ]; then note "$fqdn — no record yet"; return; fi - if printf '%s' "$a" | grep -qw "$VPS_IP"; then ok "$fqdn A → $a" - else note "$fqdn A → ${a:-none} (expected $VPS_IP)"; fi - if [ -n "$VPS_IP6" ]; then - if printf '%s' "$aaaa" | grep -qiw "$VPS_IP6"; then ok "$fqdn AAAA → $aaaa" - else note "$fqdn AAAA → ${aaaa:-none} (expected $VPS_IP6)"; fi - elif [ -n "$aaaa" ]; then info "$fqdn AAAA → $aaaa"; fi -} -check_name "$DOMAIN" -check_name "wildcard-probe.$DOMAIN" # resolves only if *.DOMAIN is set -for s in "${SUBDOMAINS[@]}"; do check_name "$s.$DOMAIN"; done - -# ---- summary --------------------------------------------------------------- -hdr "Summary" -printf ' %s%d passed%s · %s%d warnings%s · %s%d failed%s\n' \ - "$G" "$pass" "$Z" "$Y" "$warn" "$Z" "$R" "$fail" "$Z" -if [ "$fail" -gt 0 ]; then - printf ' %sCritical checks failed%s — see ✗ above.\n' "$R" "$Z"; exit 1 -fi -printf ' No critical failures. Warnings (!) are expected before DNS update / stack bootstrap.\n'