docs: record live VPS details + DNS-at-Cloudflare caveat; add pre-flight script
- scripts/check-vps.sh: read-only reachability/ports/SSH-facts/DNS checker - README: real IPv4/IPv6 + Debian 13 + existing swap; firewall already on Vultr - DNS is managed at Cloudflare (NS delegated), not VentraIP — records must be DNS-only (grey cloud) so Caddy issues real certs and port 222 works - fix git remote to ssh:// form for the custom 222 port Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
76df80a26c
commit
75fabc864e
2 changed files with 224 additions and 23 deletions
174
scripts/check-vps.sh
Executable file
174
scripts/check-vps.sh
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#!/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 <path>"
|
||||
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'
|
||||
Loading…
Add table
Add a link
Reference in a new issue