One shared login (htpasswd) for all players, writable. Config is rendered from a template and mounted read-only, so git stays authoritative — the admin console loads but cannot save, by design. Filestash's OIDC/SAML middlewares are enterprise-only, so Keycloak SSO is out; FILES_AUTH keeps htpasswd/passthrough switchable for later. Auth-off (passthrough) is only valid once the host is off the public internet — render-config.sh warns loudly when rendering it. Three traps, all found by testing against the pinned image: - the `local` backend is admin-gated: without LOCAL_BACKEND_SECRET in the connection params every login fails with "backend error - Not Allowed" - the htpasswd plugin only accepts $2a$ bcrypt, so a $2y$ hash from `htpasswd -B` fails every login silently — use `openssl passwd -6`. render-config.sh rejects the wrong format rather than shipping it - APPLICATION_URL/ADMIN_PASSWORD env make filestash rewrite config.json at boot, which fails on the :ro mount — both live in the rendered config Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
4.5 KiB
Bash
Executable File
89 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# render-config.sh — turn *.tmpl files into real configs via envsubst.
|
|
# Substitutes ONLY a small whitelist so literal $-syntax that belongs to the
|
|
# target file (caddy, toml) is left untouched.
|
|
# Halts on any unset var or missing template (cautious: no half-rendered output).
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.." # repo root
|
|
|
|
[ -f .env ] && { set -a; . ./.env; set +a; }
|
|
: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}"
|
|
|
|
# REGISTRATION_MODE (invite|open) -> Drasl RequireInvite boolean. drasl is
|
|
# TOML-only with no env support, so we derive the literal here and substitute it.
|
|
: "${REGISTRATION_MODE:=invite}"
|
|
case "$REGISTRATION_MODE" in
|
|
invite) REGISTRATION_REQUIRE_INVITE=true ;;
|
|
open) REGISTRATION_REQUIRE_INVITE=false ;;
|
|
*) echo "REGISTRATION_MODE must be 'invite' or 'open' (got '$REGISTRATION_MODE')" >&2; exit 1 ;;
|
|
esac
|
|
export REGISTRATION_REQUIRE_INVITE
|
|
|
|
render() { # $1=template $2=output
|
|
[ -f "$1" ] || { echo "missing template: $1" >&2; exit 1; }
|
|
envsubst '${BASE_DOMAIN} ${REGISTRATION_REQUIRE_INVITE}' < "$1" > "$2"
|
|
echo "rendered $2"
|
|
}
|
|
|
|
render drasl/config/config.toml.tmpl drasl/config/config.toml
|
|
render nmsr/config.toml.tmpl nmsr/config.toml
|
|
|
|
# ── filestash (files.${BASE_DOMAIN}) ────────────────────────────────────
|
|
# Filestash has NO env-var config: config.json is the source of truth and the
|
|
# admin console writes it back at runtime. We render it and mount it read-only,
|
|
# so git stays authoritative (the console renders but cannot save — by design).
|
|
#
|
|
# FILES_AUTH picks the identity_provider block. The params fields are JSON
|
|
# *strings* holding escaped JSON (that's Filestash's schema, not a typo) —
|
|
# built here rather than in the template because the shape differs per mode.
|
|
: "${FILES_AUTH:=htpasswd}"
|
|
: "${FILES_SECRET_KEY:?FILES_SECRET_KEY unset (openssl rand -hex 16)}"
|
|
: "${FILES_ADMIN_PASSWORD_HASH:?FILES_ADMIN_PASSWORD_HASH unset (docker run --rm caddy:alpine caddy hash-password --plaintext '<pw>')}"
|
|
: "${FILES_LOCAL_SECRET:?FILES_LOCAL_SECRET unset (openssl rand -hex 24) — unlocks the admin-gated local backend}"
|
|
|
|
case "$FILES_AUTH" in
|
|
htpasswd)
|
|
: "${FILES_USER:?FILES_USER unset (shared login for players)}"
|
|
: "${FILES_PASSWORD_HASH:?FILES_PASSWORD_HASH unset (openssl passwd -6 '<pw>')}"
|
|
# The htpasswd plugin's bcrypt branch only accepts $2a$ — a $2y$ hash (what
|
|
# `htpasswd -B` emits) silently fails every login. $6$ (openssl passwd -6)
|
|
# is the safe, explicitly-supported format. Halt rather than ship a config
|
|
# nobody can log into.
|
|
case "$FILES_PASSWORD_HASH" in
|
|
'$6$'*|'$5$'*|'$2a$'*|'$apr1$'*|'$1$'*) ;;
|
|
'$2y$'*|'$2b$'*)
|
|
echo "FILES_PASSWORD_HASH is bcrypt '${FILES_PASSWORD_HASH:0:4}' — the htpasswd plugin only accepts \$2a\$." >&2
|
|
echo "Use: openssl passwd -6 '<password>'" >&2
|
|
exit 1 ;;
|
|
*) echo "FILES_PASSWORD_HASH is not a recognised hash (want \$6\$… from: openssl passwd -6 '<password>')" >&2; exit 1 ;;
|
|
esac
|
|
FILES_IDP_TYPE="htpasswd"
|
|
FILES_IDP_PARAMS="{\\\"type\\\":\\\"htpasswd\\\",\\\"users\\\":\\\"${FILES_USER}:${FILES_PASSWORD_HASH}\\\"}"
|
|
;;
|
|
passthrough|none)
|
|
# NO LOGIN — anyone who reaches the vhost gets in. Only safe once this host
|
|
# is NOT reachable from the internet (see plan/20-files.md).
|
|
echo "!! FILES_AUTH=$FILES_AUTH — filestash will have NO authentication." >&2
|
|
echo "!! Only valid if files.${BASE_DOMAIN} is unreachable from the internet." >&2
|
|
FILES_IDP_TYPE="passthrough"
|
|
FILES_IDP_PARAMS="{\\\"type\\\":\\\"passthrough\\\",\\\"strategy\\\":\\\"direct\\\"}"
|
|
;;
|
|
*)
|
|
echo "FILES_AUTH must be 'htpasswd', 'passthrough' or 'none' (got '$FILES_AUTH')" >&2; exit 1 ;;
|
|
esac
|
|
export FILES_IDP_TYPE FILES_IDP_PARAMS
|
|
|
|
envsubst '${BASE_DOMAIN} ${FILES_IDP_TYPE} ${FILES_IDP_PARAMS} ${FILES_ADMIN_PASSWORD_HASH} ${FILES_SECRET_KEY} ${FILES_LOCAL_SECRET}' \
|
|
< filestash/config.json.tmpl > filestash/config.json
|
|
|
|
# The hashes contain '$' and the params fields are nested JSON — one bad escape
|
|
# yields a config filestash silently ignores. Fail loudly instead.
|
|
python3 -c 'import json,sys; json.load(open("filestash/config.json"))' \
|
|
|| { echo "filestash/config.json is not valid JSON — refusing to ship it" >&2; exit 1; }
|
|
echo "rendered filestash/config.json (auth=$FILES_AUTH)"
|
|
|
|
# Server mods are no longer rendered here — they come from the distribution repo
|
|
# via tooling/sync-server-mods.sh (run separately in the deploy flow). See
|
|
# plan/04-mods.md.
|