diff --git a/create_pod_finance.sh b/create_pod_finance.sh index a617cae..55bd35c 100755 --- a/create_pod_finance.sh +++ b/create_pod_finance.sh @@ -60,22 +60,26 @@ if [ ! -f "$ENV_FILE" ] && [ -f "$LEGACY_ENV" ]; then fi # On first run, generate all secrets into $ENV_FILE (gitignored) and print -# the GUI login password once. On subsequent runs the existing .env is reused -# unchanged, so re-running this script never rotates secrets under existing -# data. +# the shared GUI/Grafana login password once. On subsequent runs the +# existing .env is reused, so re-running this script never rotates secrets +# under existing data - except FB_GUI_PASSWORD_HASH, which is kept in sync +# with FB_PASSWORD on every run further below. That is what makes the +# password-change procedure a one-liner: edit FB_PASSWORD in $ENV_FILE, run +# this script again - the GUI hash (below) and Grafana's admin password +# (Grafana-sync retry loop, further down near the Grafana container) both +# follow automatically, with no separate hash-regeneration step. if [ ! -f "$ENV_FILE" ]; then # Compute all values into variables first, then write them SINGLE-QUOTED. # The GUI password hash has the form salt$digest; if written unquoted, the # later `set -a; . "$ENV_FILE"` would shell-expand the "$digest" part and # corrupt the hash, breaking GUI login. Single quotes make every value a # literal string on sourcing. - GUI_PASSWORD=$(openssl rand -base64 12) + FB_PASSWORD_VAL=$(openssl rand -base64 12) POSTGRES_PASSWORD_VAL=$(openssl rand -hex 16) FINANCE_READ_PASSWORD_VAL=$(openssl rand -hex 16) FB_API_KEY_VAL=$(openssl rand -hex 32) FB_SESSION_SECRET_VAL=$(openssl rand -hex 32) - FB_GUI_PASSWORD_HASH_VAL=$(python3 -c "import hashlib,secrets,sys;s=secrets.token_hex(16);pw=sys.argv[1];print(s+'\$'+hashlib.pbkdf2_hmac('sha256',pw.encode(),s.encode(),200000).hex())" "$GUI_PASSWORD") - GRAFANA_ADMIN_PASSWORD_VAL=$(openssl rand -base64 12) + FB_GUI_PASSWORD_HASH_VAL=$(python3 -c "import hashlib,secrets,sys;s=secrets.token_hex(16);pw=sys.argv[1];print(s+'\$'+hashlib.pbkdf2_hmac('sha256',pw.encode(),s.encode(),200000).hex())" "$FB_PASSWORD_VAL") cat > "$ENV_FILE" <> "$ENV_FILE" + chmod 600 "$ENV_FILE" + echo "MIGRATION: gemeinsames Passwort fuer GUI+Grafana eingefuehrt (altes GUI-Passwort war nur als Hash gespeichert und nicht rekonstruierbar)." + echo "MIGRATION: GUI-/Grafana-Login admin / $FB_PASSWORD_VAL (jetzt notieren!)" fi set -a; . "$ENV_FILE"; set +a +# Keep FB_GUI_PASSWORD_HASH in sync with FB_PASSWORD on every run (creation +# and migration above already produce a matching pair, so this is a no-op +# for them; it only rewrites $ENV_FILE when a user has edited FB_PASSWORD by +# hand since the last run - see the password-change procedure comment +# above). +if ! python3 -c " +import hashlib, sys +pw, stored = sys.argv[1], sys.argv[2] +salt, digest = stored.split('\$', 1) +sys.exit(0 if hashlib.pbkdf2_hmac('sha256', pw.encode(), salt.encode(), 200000).hex() == digest else 1) +" "$FB_PASSWORD" "$FB_GUI_PASSWORD_HASH" 2>/dev/null; then + FB_GUI_PASSWORD_HASH=$(python3 -c "import hashlib,secrets,sys;s=secrets.token_hex(16);pw=sys.argv[1];print(s+'\$'+hashlib.pbkdf2_hmac('sha256',pw.encode(),s.encode(),200000).hex())" "$FB_PASSWORD") + sed -i "s|^FB_GUI_PASSWORD_HASH=.*|FB_GUI_PASSWORD_HASH='$FB_GUI_PASSWORD_HASH'|" "$ENV_FILE" + chmod 600 "$ENV_FILE" + echo "FB_PASSWORD-Aenderung erkannt: FB_GUI_PASSWORD_HASH in $ENV_FILE neu abgeleitet." +fi + # Sanity check: the GUI password hash must survive shell sourcing intact. An # unquoted value containing a literal '$' gets mangled by parameter expansion, # which would silently break GUI login. Require the canonical salt$digest form @@ -186,9 +224,13 @@ podman run -d --name "$API_CTR_NAME" --pod "$POD_NAME" \ echo "Container '$API_CTR_NAME' started (rc=$?)" # Grafana container (provisioning + dashboards mounted read-only; datasource -# yaml interpolates FINANCE_READ_PASSWORD) +# yaml interpolates FINANCE_READ_PASSWORD). GF_SECURITY_ALLOW_EMBEDDING +# permits the GUI's iframe to embed dashboards; anonymous access stays OFF +# (no GF_AUTH_ANONYMOUS_* vars are set) - only the iframe X-Frame-Options +# restriction is relaxed, authentication is unaffected. podman run -d --name "$GRAFANA_CTR_NAME" --pod "$POD_NAME" \ - -e GF_SECURITY_ADMIN_PASSWORD="$GRAFANA_ADMIN_PASSWORD" \ + -e GF_SECURITY_ADMIN_PASSWORD="$FB_PASSWORD" \ + -e GF_SECURITY_ALLOW_EMBEDDING=true \ -e FINANCE_READ_PASSWORD \ -v "$GRAFANA_PROVISIONING_DIR:/etc/grafana/provisioning:Z,ro" \ -v "$GRAFANA_DASHBOARDS_DIR:/var/lib/grafana/dashboards:Z,ro" \ @@ -196,6 +238,39 @@ podman run -d --name "$GRAFANA_CTR_NAME" --pod "$POD_NAME" \ "$GRAFANA_IMAGE" echo "Container '$GRAFANA_CTR_NAME' started (rc=$?)" +# GF_SECURITY_ADMIN_PASSWORD above only takes effect on a FRESH Grafana data +# dir (initial admin bootstrap); an existing install keeps its previously +# set password. So sync Grafana's admin password to FB_PASSWORD explicitly +# on every script run via the CLI (idempotent: resetting to the same +# password is harmless). grafana-oss:12.1.0 ships the unified `grafana` +# binary, which exposes this as `grafana cli admin reset-admin-password` +# (verified in-image: `grafana cli admin reset-admin-password --help` +# succeeds); the legacy standalone `grafana-cli` binary is kept as a +# fallback in case of a differently-built image. Retry like the pg_isready +# wait above, since Grafana needs a moment after container start to +# initialize its SQLite db before the CLI can operate on it. This runs +# against the just-started container (before the stop/recreate-via-systemd +# further below); the result persists because GRAFANA_DATA_DIR is a +# bind-mounted directory shared with the systemd-managed restart. +echo "Syncing Grafana admin password with FB_PASSWORD..." +for attempt in $(seq 1 30); do + if podman exec "$GRAFANA_CTR_NAME" grafana cli admin reset-admin-password "$FB_PASSWORD" \ + >/dev/null 2>&1; then + echo "Grafana admin password synced (grafana cli)." + break + fi + if podman exec "$GRAFANA_CTR_NAME" grafana-cli admin reset-admin-password "$FB_PASSWORD" \ + >/dev/null 2>&1; then + echo "Grafana admin password synced (grafana-cli fallback)." + break + fi + sleep 2 + if [ "$attempt" -eq 30 ]; then + echo "ERROR: could not sync Grafana admin password in time." >&2 + exit 1 + fi +done + # Generate systemd service files cd "$USER_SYSTEMD_DIR" podman generate systemd --name --new --files "$POD_NAME" diff --git a/finance/app/templates/index.html b/finance/app/templates/index.html index 38a3515..a541711 100644 --- a/finance/app/templates/index.html +++ b/finance/app/templates/index.html @@ -58,4 +58,7 @@

Grafana-Dashboard

+

Kein Diagramm sichtbar? Einmal in + Grafana anmelden + (gleiches Passwort wie hier).

{% endblock %}