Builds deploy/.env from .env.example, generating SECRET_KEY and both database passwords with openssl (base64/hex only, so nothing needs quoting in the env file or the compose parser). Derives TS_HOSTNAME and ALLOWED_HOSTS from the tailnet hostname, optionally sets CORS_ALLOWED_ORIGINS/CSRF_TRUSTED_ORIGINS for split deployments, forces DEBUG=False and writes the file with mode 600. Modes: --no-prompt (defaults only), --update (refresh hostnames/auth key while keeping the existing secrets, reading the stored FQDN from ALLOWED_HOSTS), --force (rotate everything, with the SECRET_KEY warning in the docs). Refuses to overwrite an existing file otherwise. Verified: all modes, updated FQDN preservation, mode 600, and docker compose config accepting the generated file.
145 lines
4.8 KiB
Bash
Executable File
145 lines
4.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# Generate deploy/.env from .env.example with fresh secrets.
|
|
#
|
|
# ./gen_env.sh # interactive: asks for the auth key/hostnames
|
|
# ./gen_env.sh --no-prompt # secrets + defaults only
|
|
# ./gen_env.sh --update # refresh hostnames/authkey, KEEP existing secrets
|
|
# ./gen_env.sh --force # regenerate everything (new SECRET_KEY!)
|
|
#
|
|
# SECRET_KEY and the database passwords come from openssl. Rotating
|
|
# SECRET_KEY invalidates signed media URLs and stored e621 API keys, which is
|
|
# why --update keeps it.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
FORCE=0
|
|
UPDATE=0
|
|
NO_PROMPT=0
|
|
TS_AUTHKEY=""
|
|
FQDN=""
|
|
FRONTEND=""
|
|
DEFAULT_FQDN="j621.rainbow-herring.ts.net"
|
|
|
|
usage() {
|
|
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--force) FORCE=1 ;;
|
|
--update) UPDATE=1 ;;
|
|
--no-prompt) NO_PROMPT=1 ;;
|
|
--ts-authkey) TS_AUTHKEY="${2:?missing value}"; shift ;;
|
|
--hostname) FQDN="${2:?missing value}"; shift ;;
|
|
--frontend) FRONTEND="${2:?missing value}"; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
command -v openssl >/dev/null || { echo "openssl is required." >&2; exit 1; }
|
|
command -v python3 >/dev/null || { echo "python3 is required." >&2; exit 1; }
|
|
[ -f .env.example ] || { echo "Run me from the deploy/ directory." >&2; exit 1; }
|
|
|
|
if [ -f .env ] && [ "$FORCE" -eq 0 ] && [ "$UPDATE" -eq 0 ]; then
|
|
echo "deploy/.env already exists."
|
|
echo " --update keeps the existing secrets and just refreshes the rest"
|
|
echo " --force regenerates everything, including SECRET_KEY and DB passwords"
|
|
exit 1
|
|
fi
|
|
|
|
existing() { grep -E "^$1=" .env 2>/dev/null | head -1 | cut -d= -f2- || true; }
|
|
|
|
gen_key() { openssl rand -base64 48 | tr -d '\n'; }
|
|
gen_password() { openssl rand -hex 24; }
|
|
|
|
if [ "$UPDATE" -eq 1 ] && [ -f .env ]; then
|
|
SECRET_KEY="$(existing SECRET_KEY)"
|
|
DB_PASSWORD="$(existing DB_PASSWORD)"
|
|
DB_ROOT_PASSWORD="$(existing DB_ROOT_PASSWORD)"
|
|
TS_AUTHKEY="${TS_AUTHKEY:-$(existing TS_AUTHKEY)}"
|
|
# TS_HOSTNAME is the short label; the full FQDN lives in ALLOWED_HOSTS.
|
|
EXISTING_HOSTS="$(existing ALLOWED_HOSTS)"
|
|
FQDN="${FQDN:-${EXISTING_HOSTS%%,*}}"
|
|
fi
|
|
|
|
# Fill whatever is still missing.
|
|
SECRET_KEY="${SECRET_KEY:-$(gen_key)}"
|
|
DB_PASSWORD="${DB_PASSWORD:-$(gen_password)}"
|
|
DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-$(gen_password)}"
|
|
|
|
if [ "$NO_PROMPT" -eq 0 ]; then
|
|
if [ -z "$TS_AUTHKEY" ]; then
|
|
read -rp "Tailscale auth key (tskey-..., Enter to fill in later): " TS_AUTHKEY
|
|
fi
|
|
if [ -z "$FQDN" ]; then
|
|
read -rp "Tailnet hostname of this deployment [$DEFAULT_FQDN]: " FQDN
|
|
fi
|
|
FQDN="${FQDN:-$DEFAULT_FQDN}"
|
|
if [ -z "$FRONTEND" ]; then
|
|
read -rp "Frontend hostname for the split deploys (optional, Enter to skip): " FRONTEND
|
|
fi
|
|
fi
|
|
FQDN="${FQDN:-$DEFAULT_FQDN}"
|
|
|
|
# Derive the rest from the tailnet hostname.
|
|
TS_HOSTNAME="${FQDN%%.*}"
|
|
ALLOWED_HOSTS="$FQDN,localhost,127.0.0.1"
|
|
CORS_ALLOWED_ORIGINS="${FRONTEND:+https://$FRONTEND}"
|
|
CSRF_TRUSTED_ORIGINS="${FRONTEND:+https://$FRONTEND}"
|
|
|
|
J621_SECRET_KEY="$SECRET_KEY" \
|
|
J621_DB_PASSWORD="$DB_PASSWORD" \
|
|
J621_DB_ROOT_PASSWORD="$DB_ROOT_PASSWORD" \
|
|
J621_TS_AUTHKEY="$TS_AUTHKEY" \
|
|
J621_TS_HOSTNAME="$TS_HOSTNAME" \
|
|
J621_ALLOWED_HOSTS="$ALLOWED_HOSTS" \
|
|
J621_CORS_ALLOWED_ORIGINS="$CORS_ALLOWED_ORIGINS" \
|
|
J621_CSRF_TRUSTED_ORIGINS="$CSRF_TRUSTED_ORIGINS" \
|
|
python3 - <<'PY'
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
text = Path(".env.example").read_text()
|
|
|
|
def apply(name):
|
|
value = os.environ.get(f"J621_{name}")
|
|
if not value:
|
|
return text
|
|
line = f"{name}={value}"
|
|
pattern = re.compile(rf"^(?:# )?{re.escape(name)}=.*$", re.M)
|
|
if pattern.search(text):
|
|
return pattern.sub(line, text, count=1)
|
|
return text + f"\n{line}\n"
|
|
|
|
for name in (
|
|
"SECRET_KEY",
|
|
"TS_AUTHKEY",
|
|
"TS_HOSTNAME",
|
|
"ALLOWED_HOSTS",
|
|
"CORS_ALLOWED_ORIGINS",
|
|
"CSRF_TRUSTED_ORIGINS",
|
|
"DB_PASSWORD",
|
|
"DB_ROOT_PASSWORD",
|
|
):
|
|
text = apply(name)
|
|
|
|
text = re.sub(r"^DEBUG=.*$", "DEBUG=False", text, count=1, flags=re.M)
|
|
Path(".env").write_text(text)
|
|
PY
|
|
|
|
chmod 600 .env
|
|
|
|
echo
|
|
echo "Wrote deploy/.env (mode 600):"
|
|
echo " SECRET_KEY $([ "$UPDATE" -eq 1 ] && echo 'kept from the existing file' || echo 'generated with openssl')"
|
|
echo " DB passwords $([ "$UPDATE" -eq 1 ] && echo 'kept from the existing file' || echo 'generated with openssl')"
|
|
echo " TS_HOSTNAME $TS_HOSTNAME"
|
|
echo " ALLOWED_HOSTS $ALLOWED_HOSTS"
|
|
[ -n "$CORS_ALLOWED_ORIGINS" ] && echo " cross-origin $CORS_ALLOWED_ORIGINS"
|
|
[ -z "$TS_AUTHKEY" ] && echo " TS_AUTHKEY still empty - paste your Tailscale auth key before starting"
|
|
echo
|
|
echo "Next: docker compose -f compose.yml up -d (or a compose.tailnet*.yml variant)"
|