Files

91 lines
3.8 KiB
Bash
Executable File

#!/bin/sh
# Kestrove self-host setup: asks for your domain and email, writes .env, checks DNS and starts Kestrove.
# Safe to run again: it keeps an existing .env unless you choose to replace it.
set -eu
cd "$(dirname "$0")"
say() { printf '%s\n' "$*"; }
ask() { # prompt default -> REPLY
if [ -n "$2" ]; then printf '%s [%s]: ' "$1" "$2"; else printf '%s: ' "$1"; fi
read -r REPLY || REPLY=""
[ -n "$REPLY" ] || REPLY=$2
}
die() { say "setup: $*" >&2; exit 1; }
command -v docker >/dev/null 2>&1 || die "Docker is not installed. See https://docs.docker.com/engine/install/"
docker compose version >/dev/null 2>&1 || die "Docker Compose v2 is missing (the 'docker compose' command)."
docker info >/dev/null 2>&1 || die "Docker is not running, or this user can't use it (try sudo)."
if [ -f .env ]; then
ask "A .env already exists. Keep it? (Y/n)" "Y"
case "$REPLY" in [nN]*) ;; *) KEEP=1 ;; esac
fi
if [ -z "${KEEP:-}" ]; then
say ""
say "Kestrove needs a domain name, e.g. vault.example.com, with a DNS record pointing at this server."
ask "Domain" ""
DOMAIN=$(printf '%s' "$REPLY" | tr 'A-Z' 'a-z' | sed -e 's#^https\{0,1\}://##' -e 's#/.*$##')
printf '%s' "$DOMAIN" | grep -Eq '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$' || die "'$DOMAIN' is not a domain name"
ask "Email for Let's Encrypt certificate notices" ""
ACME_EMAIL=$REPLY
printf '%s' "$ACME_EMAIL" | grep -Eq '^[^@ ]+@[^@ ]+\.[^@ ]+$' || die "'$ACME_EMAIL' is not an email address"
# 24 characters from an unambiguous alphabet (no 0/O, 1/I/L), in groups of four.
CODE=$(LC_ALL=C tr -dc 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' </dev/urandom | head -c 24 | sed 's/..../&-/g; s/-$//')
umask 077
sed -e "s|^DOMAIN=.*|DOMAIN=$DOMAIN|" -e "s|^ACME_EMAIL=.*|ACME_EMAIL=$ACME_EMAIL|" -e "s|^KV_SETUP_CODE=.*|KV_SETUP_CODE=$CODE|" \
.env.example > .env
say "Wrote .env (readable only by you)."
fi
# shellcheck disable=SC1091
. ./.env
[ -n "${DOMAIN:-}" ] || die ".env has no DOMAIN"
# DNS check: the domain should resolve to one of this machine's public addresses (warn only; NAT and
# proxies make this check imperfect).
PUBLIC_IP=$(curl -fsS -4 --max-time 5 https://api.ipify.org 2>/dev/null || true)
RESOLVED=$( (getent ahostsv4 "$DOMAIN" 2>/dev/null || true) | awk '{print $1; exit}')
[ -n "$RESOLVED" ] || RESOLVED=$( (nslookup "$DOMAIN" 2>/dev/null || true) | awk '/^Address: /{print $2}' | grep -v ':' | tail -1)
if [ -z "$RESOLVED" ]; then
say "Warning: $DOMAIN does not resolve yet. Create its DNS A record, wait a minute, then run this again."
ask "Start anyway? (y/N)" "N"
case "$REPLY" in [yY]*) ;; *) exit 1 ;; esac
elif [ -n "$PUBLIC_IP" ] && [ "$RESOLVED" != "$PUBLIC_IP" ]; then
say "Warning: $DOMAIN points to $RESOLVED, but this server's public address is $PUBLIC_IP."
say " If you use Cloudflare, set the record to \"DNS only\" (grey cloud). HTTPS setup fails otherwise."
ask "Start anyway? (y/N)" "N"
case "$REPLY" in [yY]*) ;; *) exit 1 ;; esac
fi
say "Starting Kestrove…"
docker compose pull -q
docker compose up -d
say ""
say "Waiting for the HTTPS certificate (up to a minute)…"
i=0
until curl -fsS -o /dev/null --max-time 5 "https://$DOMAIN/" 2>/dev/null; do
i=$((i + 1))
if [ "$i" -ge 24 ]; then
say "Not reachable over HTTPS yet. Check: docker compose logs web"
say "Ports 80 and 443 must be open to the internet for Let's Encrypt."
break
fi
sleep 5
done
say ""
say "Kestrove is running at https://$DOMAIN"
say ""
say "Next: open https://$DOMAIN, choose Create account, and enter this setup code:"
say ""
if [ -n "${KV_SETUP_CODE:-}" ]; then
say " $KV_SETUP_CODE"
else
say " (printed in: docker compose logs server | grep SETUP_TOKEN)"
fi
say ""
say "Save your Emergency Kit when the web vault offers it. Then add your phone and computers from the"
say "web vault (Devices > Add a device) by scanning the QR code in the Kestrove apps."