Skip to content

Repository files, collaboration, releases and project activity.

ARCHFORGE HISTORY

Blame · scripts/configure-domain.sh

Branch main · the Commit author is not necessarily a verified ArchForge account.

b8619f148earchadmin2026-10-111 #!/usr/bin/env bash
b8619f148earchadmin2026-10-112 # Configure existing independent status server domain and Caddy managed ACME TLS.
2109dd33b8archadmin2026-10-123 # Does not touch ArchForge checkout, SQLite, admin account or main ArchForge website.
b8619f148earchadmin2026-10-114 set -Eeuo pipefail
b8619f148earchadmin2026-10-115 umask 077
b8619f148earchadmin2026-10-116 fail(){ printf '[ERROR] %s\n' "$*" >&2; exit 1; }
b8619f148earchadmin2026-10-117 info(){ printf '[*] %s\n' "$*"; }
b8619f148earchadmin2026-10-118 ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
b8619f148earchadmin2026-10-119 # shellcheck source=domain-lib.sh
b8619f148earchadmin2026-10-1110 source "$ROOT_DIR/scripts/domain-lib.sh"
b8619f148earchadmin2026-10-1111 [[ $EUID -eq 0 ]] || fail 'Run: sudo bash ./configure-domain.sh (or bash as root)'
b8619f148earchadmin2026-10-1112 [[ -t 0 ]] || fail 'An interactive SSH terminal is required'
b8619f148earchadmin2026-10-1113 [[ -f /etc/os-release ]] || fail 'Cannot check operating system'
b8619f148earchadmin2026-10-1114 # No arbitrary environment path overrides for privileged install operations.
b8619f148earchadmin2026-10-1115 source /etc/os-release
b8619f148earchadmin2026-10-1116 [[ ${ID:-} == debian && ${VERSION_ID:-} == 13 ]] || fail 'This setup is for Debian 13 only'
b8619f148earchadmin2026-10-1117 
b8619f148earchadmin2026-10-1118 CFG=/etc/archforge-status/status.env
b8619f148earchadmin2026-10-1119 CADDYFILE=/etc/caddy/Caddyfile
b8619f148earchadmin2026-10-1120 SITEFILE=/etc/caddy/conf.d/archforge-status.caddy
b8619f148earchadmin2026-10-1121 [[ -f "$CFG" && ! -L "$CFG" ]] || fail 'Status configuration missing. Run install.sh before domain setup.'
b8619f148earchadmin2026-10-1122 [[ -f "$CADDYFILE" && ! -L "$CADDYFILE" ]] || fail 'Caddyfile missing. Install Caddy first.'
b8619f148earchadmin2026-10-1123 for prog in caddy systemctl curl dig python3; do
b8619f148earchadmin2026-10-1124  command -v "$prog" >/dev/null || fail "$prog is missing. Install with: apt-get install -y caddy curl dnsutils python3"
b8619f148earchadmin2026-10-1125 done
b8619f148earchadmin2026-10-1126 mkdir -p /etc/caddy/conf.d
b8619f148earchadmin2026-10-1127 [[ ! -L "$SITEFILE" ]] || fail 'Refusing to change a symlinked Caddy site configuration'
b8619f148earchadmin2026-10-1128 OLD_DOMAIN=$(af_read_public_domain "$CFG") || fail 'STATUS_PUBLIC_URL is invalid in status.env'
b8619f148earchadmin2026-10-1129 
b8619f148earchadmin2026-10-1130 printf '\n==============================================\n'
b8619f148earchadmin2026-10-1131 printf ' ArchForge Status — Domein & SSL Wizard\n Debian 13 • Caddy (automatic HTTPS)\n==============================================\n\n'
b8619f148earchadmin2026-10-1132 printf 'Current domain: %s\n' "$OLD_DOMAIN"
b8619f148earchadmin2026-10-1133 read -rp "New/confirmed status domain [${OLD_DOMAIN}]: " DOMAIN
b8619f148earchadmin2026-10-1134 DOMAIN=${DOMAIN:-$OLD_DOMAIN}
b8619f148earchadmin2026-10-1135 DOMAIN=${DOMAIN,,}
b8619f148earchadmin2026-10-1136 af_valid_domain "$DOMAIN" || fail 'Invalid domain. Use a complete host such as status.archforge.nl.'
b8619f148earchadmin2026-10-1137 read -rp 'Public IPv4 of THIS Debian VPS (VPS 2): ' IPV4
b8619f148earchadmin2026-10-1138 af_valid_ipv4 "$IPV4" || fail 'Please enter a valid IPv4 address of VPS 2.'
b8619f148earchadmin2026-10-1139 python3 - "$IPV4" <<'PYIP' || fail 'VPS IP must be a public, globally routable IPv4 address.'
b8619f148earchadmin2026-10-1140 import ipaddress,sys
b8619f148earchadmin2026-10-1141 assert ipaddress.IPv4Address(sys.argv[1]).is_global
b8619f148earchadmin2026-10-1142 PYIP
b8619f148earchadmin2026-10-1143 read -rp 'Public IPv6 of VPS 2 (Enter if no working IPv6): ' IPV6
b8619f148earchadmin2026-10-1144 [[ -z "$IPV6" ]] || af_valid_ipv6 "$IPV6" || fail 'Invalid IPv6 address'
b8619f148earchadmin2026-10-1145 read -rp 'ACME contact email (recommended, Enter to skip): ' EMAIL
b8619f148earchadmin2026-10-1146 af_valid_email "$EMAIL" || fail 'Invalid email'
b8619f148earchadmin2026-10-1147 
b8619f148earchadmin2026-10-1148 printf '\nChecking public DNS for %s ...\n' "$DOMAIN"
b8619f148earchadmin2026-10-1149 A_RECORDS=$(af_dns_records A "$DOMAIN")
b8619f148earchadmin2026-10-1150 AAAA_RECORDS=$(af_dns_records AAAA "$DOMAIN")
b8619f148earchadmin2026-10-1151 printf 'DNS A: %s\n' "${A_RECORDS:-[not found]}"
b8619f148earchadmin2026-10-1152 printf 'DNS AAAA: %s\n' "${AAAA_RECORDS:-[not found]}"
b8619f148earchadmin2026-10-1153 if [[ -z "$A_RECORDS" ]] || [[ "$(printf '%s\n' "$A_RECORDS" | sort -u)" != "$IPV4" ]]; then
b8619f148earchadmin2026-10-1154  printf '\n[DNS NOT READY] Add/change this record at your DNS provider:\n'
b8619f148earchadmin2026-10-1155  printf 'Type A | Host %s | Value %s\n' "$DOMAIN" "$IPV4"
b8619f148earchadmin2026-10-1156  printf 'Remove any other A records for this subdomain pointing elsewhere.\n'
b8619f148earchadmin2026-10-1157  printf 'Wait for DNS propagation, then rerun this script. No configuration changed.\n'
b8619f148earchadmin2026-10-1158  exit 2
b8619f148earchadmin2026-10-1159 fi
b8619f148earchadmin2026-10-1160 if [[ -n "$AAAA_RECORDS" ]]; then
b8619f148earchadmin2026-10-1161  [[ -n "$IPV6" ]] || fail 'DNS AAAA exists but no VPS 2 IPv6 was supplied. Remove incorrect AAAA or rerun with your real IPv6. No changes made.'
b8619f148earchadmin2026-10-1162  python3 - "$IPV6" "$AAAA_RECORDS" <<'PY' || fail 'DNS AAAA does not match the VPS 2 IPv6 address. No changes made.'
b8619f148earchadmin2026-10-1163 import ipaddress, sys
b8619f148earchadmin2026-10-1164 expected = ipaddress.IPv6Address(sys.argv[1])
b8619f148earchadmin2026-10-1165 found = [ipaddress.IPv6Address(line) for line in sys.argv[2].splitlines() if line.strip()]
b8619f148earchadmin2026-10-1166 assert found and all(value == expected for value in found)
b8619f148earchadmin2026-10-1167 PY
b8619f148earchadmin2026-10-1168 fi
b8619f148earchadmin2026-10-1169 if [[ -n "$IPV6" && -z "$AAAA_RECORDS" ]]; then
b8619f148earchadmin2026-10-1170  info 'No AAAA record found. IPv6 is optional; IPv4 HTTPS can work without it.'
b8619f148earchadmin2026-10-1171 fi
b8619f148earchadmin2026-10-1172 
b8619f148earchadmin2026-10-1173 printf '\nCaddy needs TCP 80/443 reachable from the internet to issue a certificate.\n'
b8619f148earchadmin2026-10-1174 printf 'Set DNS directly to VPS 2 (not through VPS 1). Check the VPS provider firewall.\n'
b8619f148earchadmin2026-10-1175 if command -v ss >/dev/null; then
b8619f148earchadmin2026-10-1176  printf '\nCurrent port listeners (check for nginx/apache conflicts):\n'
b8619f148earchadmin2026-10-1177  ss -ltnp | grep -E '(^State|:80[[:space:]]|:443[[:space:]])' || true
b8619f148earchadmin2026-10-1178 fi
b8619f148earchadmin2026-10-1179 if command -v ufw >/dev/null && ufw status | grep -q '^Status: active'; then
b8619f148earchadmin2026-10-1180  read -rp 'UFW is active. Allow TCP 80 and 443? Type ALLOW: ' UFW_ANSWER
b8619f148earchadmin2026-10-1181  if [[ "$UFW_ANSWER" == ALLOW ]]; then
b8619f148earchadmin2026-10-1182  ufw allow 80/tcp
b8619f148earchadmin2026-10-1183  ufw allow 443/tcp
b8619f148earchadmin2026-10-1184  else
b8619f148earchadmin2026-10-1185  info 'UFW rules untouched; make sure both ports are reachable.'
b8619f148earchadmin2026-10-1186  fi
b8619f148earchadmin2026-10-1187 fi
b8619f148earchadmin2026-10-1188 printf '\nWill configure HTTPS for https://%s and update STATUS_PUBLIC_URL.\n' "$DOMAIN"
b8619f148earchadmin2026-10-1189 printf 'The status database, administrator and other secrets will be preserved.\n'
b8619f148earchadmin2026-10-1190 read -rp 'Type ENABLE to apply and start Caddy: ' CONFIRM
b8619f148earchadmin2026-10-1191 [[ "$CONFIRM" == ENABLE ]] || fail 'Cancelled without changing application or Caddy settings.'
b8619f148earchadmin2026-10-1192 
b8619f148earchadmin2026-10-1193 # Backup all impacted files before changing them. Restore on validation/service failure.
b8619f148earchadmin2026-10-1194 TEMP=$(mktemp -d /root/.archforge-tls-setup.XXXXXXXX)
b8619f148earchadmin2026-10-1195 chmod 700 "$TEMP"
b8619f148earchadmin2026-10-1196 cleanup(){ rm -rf -- "$TEMP"; }
b8619f148earchadmin2026-10-1197 trap cleanup EXIT
b8619f148earchadmin2026-10-1198 cp -a -- "$CFG" "$TEMP/status.env"
b8619f148earchadmin2026-10-1199 cp -a -- "$CADDYFILE" "$TEMP/Caddyfile"
b8619f148earchadmin2026-10-11100 if [[ -f "$SITEFILE" ]]; then cp -a -- "$SITEFILE" "$TEMP/site.caddy"; fi
b8619f148earchadmin2026-10-11101 restore_previous() {
b8619f148earchadmin2026-10-11102  printf '[!] Domain setup failed; restoring previous local configuration.\n' >&2
b8619f148earchadmin2026-10-11103  cp -a -- "$TEMP/status.env" "$CFG"
b8619f148earchadmin2026-10-11104  cp -a -- "$TEMP/Caddyfile" "$CADDYFILE"
b8619f148earchadmin2026-10-11105  if [[ -f "$TEMP/site.caddy" ]]; then cp -a -- "$TEMP/site.caddy" "$SITEFILE"; else rm -f -- "$SITEFILE"; fi
b8619f148earchadmin2026-10-11106  systemctl reload caddy >/dev/null 2>&1 || true
b8619f148earchadmin2026-10-11107 }
b8619f148earchadmin2026-10-11108 if ! af_render_caddy "$DOMAIN" "$EMAIL" > "$SITEFILE"; then
b8619f148earchadmin2026-10-11109  restore_previous
b8619f148earchadmin2026-10-11110  fail 'Unable to generate safe Caddy configuration'
b8619f148earchadmin2026-10-11111 fi
b8619f148earchadmin2026-10-11112 chmod 644 "$SITEFILE"
b8619f148earchadmin2026-10-11113 if ! grep -Eq '^[[:space:]]*import[[:space:]]+/etc/caddy/conf\.d/\*\.caddy[[:space:]]*$' "$CADDYFILE"; then
b8619f148earchadmin2026-10-11114  printf '\nimport /etc/caddy/conf.d/*.caddy\n' >> "$CADDYFILE"
b8619f148earchadmin2026-10-11115 fi
b8619f148earchadmin2026-10-11116 if ! caddy validate --config "$CADDYFILE"; then
b8619f148earchadmin2026-10-11117  restore_previous
b8619f148earchadmin2026-10-11118  fail 'Caddy validation failed. Inspect other domain bindings and Caddyfile imports.'
b8619f148earchadmin2026-10-11119 fi
b8619f148earchadmin2026-10-11120 if ! systemctl enable --now caddy || ! systemctl reload caddy; then
b8619f148earchadmin2026-10-11121  restore_previous
b8619f148earchadmin2026-10-11122  fail 'Caddy could not start/reload. Check port 80/443 conflicts and journalctl -u caddy.'
b8619f148earchadmin2026-10-11123 fi
b8619f148earchadmin2026-10-11124 if ! af_set_public_domain "$CFG" "$DOMAIN"; then
b8619f148earchadmin2026-10-11125  restore_previous
b8619f148earchadmin2026-10-11126  fail 'Could not update STATUS_PUBLIC_URL. Application secrets were preserved.'
b8619f148earchadmin2026-10-11127 fi
b8619f148earchadmin2026-10-11128 if systemctl is-active --quiet archforge-status; then
b8619f148earchadmin2026-10-11129  if ! systemctl restart archforge-status; then
b8619f148earchadmin2026-10-11130  restore_previous
b8619f148earchadmin2026-10-11131  systemctl restart archforge-status || true
b8619f148earchadmin2026-10-11132  fail 'Status service restart failed. Old domain settings restored.'
b8619f148earchadmin2026-10-11133  fi
b8619f148earchadmin2026-10-11134 else
b8619f148earchadmin2026-10-11135  info 'archforge-status is not active. Start it separately once admin setup is complete.'
b8619f148earchadmin2026-10-11136 fi
b8619f148earchadmin2026-10-11137 
b8619f148earchadmin2026-10-11138 info 'Caddy accepts the domain configuration and has started certificate management.'
b8619f148earchadmin2026-10-11139 info 'Verifying a trusted HTTPS certificate from the local Caddy instance...'
b8619f148earchadmin2026-10-11140 TLS_OK=false
b8619f148earchadmin2026-10-11141 for attempt in 1 2 3 4 5; do
b8619f148earchadmin2026-10-11142  if curl --silent --show-error --fail --max-time 8 --resolve "${DOMAIN}:443:127.0.0.1" "https://${DOMAIN}/healthz" -o /dev/null 2>"$TEMP/curl.err"; then
b8619f148earchadmin2026-10-11143  TLS_OK=true
b8619f148earchadmin2026-10-11144  break
b8619f148earchadmin2026-10-11145  fi
b8619f148earchadmin2026-10-11146  sleep 3
b8619f148earchadmin2026-10-11147 done
b8619f148earchadmin2026-10-11148 printf '\n==============================================\n'
b8619f148earchadmin2026-10-11149 printf ' Domain configured: https://%s\n' "$DOMAIN"
b8619f148earchadmin2026-10-11150 printf ' Admin panel: https://%s/admin/login\n' "$DOMAIN"
b8619f148earchadmin2026-10-11151 if [[ "$TLS_OK" == true ]]; then
b8619f148earchadmin2026-10-11152  printf ' TLS: locally verified (trusted certificate, live /healthz)\n'
b8619f148earchadmin2026-10-11153 else
b8619f148earchadmin2026-10-11154  printf ' TLS: NOT YET VERIFIED — issuance may still be pending!\n'
b8619f148earchadmin2026-10-11155  printf ' Check DNS, public TCP 80/443, provider firewall and Caddy logs.\n'
b8619f148earchadmin2026-10-11156  printf ' Logs: journalctl -u caddy -n 80 --no-pager\n'
b8619f148earchadmin2026-10-11157  printf ' Direct check: curl -Iv https://%s\n' "$DOMAIN"
b8619f148earchadmin2026-10-11158 fi
b8619f148earchadmin2026-10-11159 printf '==============================================\n'
b8619f148earchadmin2026-10-11160 # DNS correctness and validation succeeded, but TLS handshake may need time/ports.
b8619f148earchadmin2026-10-11161 # Do not claim the public site is already reachable in that case.