176 lines
7.5 KiB
Bash
Executable File
176 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# homelab-publish.sh — flip a locally-hosted TrueNAS app to public HTTPS.
|
|
#
|
|
# The "go public" switch from homelab-release-runbook.md §4. Idempotent and
|
|
# additive: it never touches the running app stack, only adds the frp tunnel +
|
|
# IONOS vhost + Let's Encrypt cert. Re-running is safe. To unpublish, remove the
|
|
# frpc proxy block (or run with `--down`) — local-phase hosting keeps working.
|
|
#
|
|
# Usage:
|
|
# ./homelab-publish.sh <project> <frontendHostPort> <frpRemotePort> [subdomain]
|
|
# ./homelab-publish.sh myapp 3001 30011 myapp.plate-software.de
|
|
#
|
|
# --down remove the frpc block + disable the IONOS vhosts (keep the cert)
|
|
# --dry print what would happen without changing anything
|
|
#
|
|
# Prereqs (see runbook §1):
|
|
# - SSH aliases `truenas` and `ionos` configured in ~/.ssh/config
|
|
# - DNS A-record <subdomain> → 82.165.206.45 already propagated
|
|
# - frpc on TrueNAS (systemd) reading /mnt/VM_SSD_Pool/frp/frpc.toml
|
|
# - acme.sh + apache2 on IONOS; /var/www/html webroot for ACME challenge
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ── Fixed infra constants (runbook §1) ──────────────────────────────────────
|
|
FRPC_TOML="/mnt/VM_SSD_Pool/frp/frpc.toml"
|
|
VPS_IP="85.214.154.199" # frps host the IONOS vhost proxies to
|
|
APACHE_SITES="/etc/apache2/sites-available"
|
|
ACME_WEBROOT="/var/www/html"
|
|
SSH_TRUENAS="${SSH_TRUENAS:-truenas}"
|
|
SSH_IONOS="${SSH_IONOS:-ionos}"
|
|
|
|
# ── Args ────────────────────────────────────────────────────────────────────
|
|
DOWN=0; DRY=0; POS=()
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--down) DOWN=1 ;;
|
|
--dry) DRY=1 ;;
|
|
*) POS+=("$a") ;;
|
|
esac
|
|
done
|
|
set -- "${POS[@]:-}"
|
|
|
|
PROJECT="${1:-}"
|
|
FRONTEND_PORT="${2:-}"
|
|
FRP_REMOTE_PORT="${3:-}"
|
|
SUBDOMAIN="${4:-${PROJECT}.plate-software.de}"
|
|
|
|
if [[ -z "$PROJECT" || -z "$FRONTEND_PORT" || -z "$FRP_REMOTE_PORT" ]]; then
|
|
echo "usage: $0 <project> <frontendHostPort> <frpRemotePort> [subdomain] [--down] [--dry]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
VHOST_CONF="${APACHE_SITES}/${SUBDOMAIN}.conf"
|
|
log() { printf '\033[1;34m▶ %s\033[0m\n' "$*"; }
|
|
ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
|
|
warn() { printf '\033[1;33m! %s\033[0m\n' "$*"; }
|
|
run() { if [[ $DRY -eq 1 ]]; then echo " [dry] $*"; else eval "$@"; fi; }
|
|
|
|
# ── Marker for the frpc block so we can find/replace/remove idempotently ─────
|
|
BEGIN="# >>> homelab-publish ${PROJECT} >>>"
|
|
END="# <<< homelab-publish ${PROJECT} <<<"
|
|
|
|
frpc_block() {
|
|
cat <<EOF
|
|
${BEGIN}
|
|
[[proxies]]
|
|
name = "${PROJECT}"
|
|
type = "tcp"
|
|
localIP = "127.0.0.1"
|
|
localPort = ${FRONTEND_PORT}
|
|
remotePort = ${FRP_REMOTE_PORT}
|
|
${END}
|
|
EOF
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
if [[ $DOWN -eq 1 ]]; then
|
|
log "Unpublishing ${SUBDOMAIN} (app keeps running locally)"
|
|
|
|
log "Removing frpc proxy block on TrueNAS"
|
|
run "$SSH_TRUENAS \"sed -i '/${BEGIN}/,/${END}/d' ${FRPC_TOML} && systemctl restart frpc\""
|
|
ok "frpc block removed + frpc reloaded"
|
|
|
|
log "Disabling IONOS vhost"
|
|
run "$SSH_IONOS \"a2dissite ${SUBDOMAIN}.conf 2>/dev/null; apache2ctl configtest && systemctl reload apache2\""
|
|
ok "vhost disabled (cert left in place for quick re-publish)"
|
|
exit 0
|
|
fi
|
|
|
|
# ── Pre-flight: DNS must point at IONOS ──────────────────────────────────────
|
|
log "Pre-flight: checking DNS for ${SUBDOMAIN}"
|
|
RESOLVED="$(dig +short "${SUBDOMAIN}" A | tail -n1 || true)"
|
|
if [[ "$RESOLVED" != "82.165.206.45" ]]; then
|
|
warn "DNS for ${SUBDOMAIN} = '${RESOLVED:-<none>}' (expected 82.165.206.45)."
|
|
warn "Create the A-record in the IONOS panel and let it propagate first."
|
|
[[ $DRY -eq 1 ]] || exit 1
|
|
else
|
|
ok "DNS → 82.165.206.45"
|
|
fi
|
|
|
|
# ── 1. frp tunnel (TrueNAS frpc) ─────────────────────────────────────────────
|
|
log "frp: adding/updating proxy block on TrueNAS (${FRPC_TOML})"
|
|
BLOCK="$(frpc_block)"
|
|
# Remove any existing block for this project, then append the fresh one.
|
|
run "$SSH_TRUENAS \"sed -i '/${BEGIN}/,/${END}/d' ${FRPC_TOML}\""
|
|
if [[ $DRY -eq 1 ]]; then
|
|
echo " [dry] append to ${FRPC_TOML}:"; echo "${BLOCK}" | sed 's/^/ /'
|
|
else
|
|
printf '%s\n' "${BLOCK}" | $SSH_TRUENAS "cat >> ${FRPC_TOML}"
|
|
fi
|
|
run "$SSH_TRUENAS \"systemctl restart frpc\""
|
|
ok "frp tunnel ${FRONTEND_PORT} → frps:${FRP_REMOTE_PORT}"
|
|
|
|
# ── 2. IONOS: HTTP vhost first (for ACME), then cert, then 443 ───────────────
|
|
log "IONOS: writing vhost ${VHOST_CONF}"
|
|
VHOST="$(cat <<EOF
|
|
<VirtualHost *:80>
|
|
ServerName ${SUBDOMAIN}
|
|
Alias /.well-known/acme-challenge/ ${ACME_WEBROOT}/.well-known/acme-challenge/
|
|
ProxyPass /.well-known/acme-challenge/ !
|
|
RewriteEngine On
|
|
RewriteCond %{REQUEST_URI} !^/\\.well-known/acme-challenge/
|
|
RewriteRule ^(.*)\$ https://%{HTTP_HOST}\$1 [R=301,L]
|
|
</VirtualHost>
|
|
<VirtualHost *:443>
|
|
ServerName ${SUBDOMAIN}
|
|
SSLEngine on
|
|
SSLCertificateFile /root/.acme.sh/${SUBDOMAIN}_ecc/${SUBDOMAIN}.cer
|
|
SSLCertificateKeyFile /root/.acme.sh/${SUBDOMAIN}_ecc/${SUBDOMAIN}.key
|
|
SSLCertificateChainFile /root/.acme.sh/${SUBDOMAIN}_ecc/ca.cer
|
|
ProxyPreserveHost On
|
|
ProxyPass / http://${VPS_IP}:${FRP_REMOTE_PORT}/
|
|
ProxyPassReverse / http://${VPS_IP}:${FRP_REMOTE_PORT}/
|
|
RequestHeader set X-Forwarded-Proto https
|
|
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
|
|
</VirtualHost>
|
|
EOF
|
|
)"
|
|
if [[ $DRY -eq 1 ]]; then
|
|
echo " [dry] write ${VHOST_CONF}:"; echo "${VHOST}" | sed 's/^/ /'
|
|
else
|
|
printf '%s\n' "${VHOST}" | $SSH_IONOS "cat > ${VHOST_CONF}"
|
|
fi
|
|
|
|
# Enable HTTP side first so the ACME challenge can be served. If the cert files
|
|
# don't exist yet, the 443 block would fail configtest — so issue the cert, then
|
|
# enable. acme.sh writes the files; we (re)issue idempotently.
|
|
log "IONOS: issuing/renewing Let's Encrypt cert (pinned CA — ZeroSSL stalls)"
|
|
run "$SSH_IONOS \"acme.sh --set-default-ca --server letsencrypt\""
|
|
# Enable the site so the :80 ACME alias is live for webroot validation.
|
|
run "$SSH_IONOS \"a2ensite ${SUBDOMAIN}.conf >/dev/null 2>&1 || true\""
|
|
# Issue only if not already present (idempotent); --force on renew handled by acme cron.
|
|
run "$SSH_IONOS \"test -f /root/.acme.sh/${SUBDOMAIN}_ecc/${SUBDOMAIN}.cer || acme.sh --issue -d ${SUBDOMAIN} -w ${ACME_WEBROOT} --server letsencrypt\""
|
|
ok "cert present for ${SUBDOMAIN}"
|
|
|
|
log "IONOS: enabling vhost + reloading Apache"
|
|
run "$SSH_IONOS \"a2ensite ${SUBDOMAIN}.conf >/dev/null && apache2ctl configtest && systemctl reload apache2\""
|
|
ok "Apache serving https://${SUBDOMAIN}"
|
|
|
|
# ── 3. Verify (force-resolve to dodge stale workstation DNS cache) ───────────
|
|
log "Verifying https://${SUBDOMAIN} (force-resolved to 82.165.206.45)"
|
|
if [[ $DRY -eq 0 ]]; then
|
|
CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 \
|
|
--resolve "${SUBDOMAIN}:443:82.165.206.45" "https://${SUBDOMAIN}/" || echo 000)"
|
|
if [[ "$CODE" =~ ^(200|301|302|307|308)$ ]]; then
|
|
ok "Public HTTPS up — HTTP ${CODE}"
|
|
else
|
|
warn "Got HTTP ${CODE} — check: frpc running? app on :${FRONTEND_PORT}? frps:${FRP_REMOTE_PORT} reachable?"
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
ok "DONE. ${SUBDOMAIN} → frps:${FRP_REMOTE_PORT} → frontend:${FRONTEND_PORT}"
|
|
echo " Remember: record this in homelab-release-runbook.md §2 registry."
|