feat: gemeinsames Passwort fuer GUI und Grafana, Dashboard-Embedding
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,22 +60,26 @@ if [ ! -f "$ENV_FILE" ] && [ -f "$LEGACY_ENV" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# On first run, generate all secrets into $ENV_FILE (gitignored) and print
|
# 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
|
# the shared GUI/Grafana login password once. On subsequent runs the
|
||||||
# unchanged, so re-running this script never rotates secrets under existing
|
# existing .env is reused, so re-running this script never rotates secrets
|
||||||
# data.
|
# 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
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
# Compute all values into variables first, then write them SINGLE-QUOTED.
|
# Compute all values into variables first, then write them SINGLE-QUOTED.
|
||||||
# The GUI password hash has the form salt$digest; if written unquoted, the
|
# 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
|
# later `set -a; . "$ENV_FILE"` would shell-expand the "$digest" part and
|
||||||
# corrupt the hash, breaking GUI login. Single quotes make every value a
|
# corrupt the hash, breaking GUI login. Single quotes make every value a
|
||||||
# literal string on sourcing.
|
# literal string on sourcing.
|
||||||
GUI_PASSWORD=$(openssl rand -base64 12)
|
FB_PASSWORD_VAL=$(openssl rand -base64 12)
|
||||||
POSTGRES_PASSWORD_VAL=$(openssl rand -hex 16)
|
POSTGRES_PASSWORD_VAL=$(openssl rand -hex 16)
|
||||||
FINANCE_READ_PASSWORD_VAL=$(openssl rand -hex 16)
|
FINANCE_READ_PASSWORD_VAL=$(openssl rand -hex 16)
|
||||||
FB_API_KEY_VAL=$(openssl rand -hex 32)
|
FB_API_KEY_VAL=$(openssl rand -hex 32)
|
||||||
FB_SESSION_SECRET_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")
|
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")
|
||||||
GRAFANA_ADMIN_PASSWORD_VAL=$(openssl rand -base64 12)
|
|
||||||
cat > "$ENV_FILE" <<EOF
|
cat > "$ENV_FILE" <<EOF
|
||||||
POSTGRES_PASSWORD='$POSTGRES_PASSWORD_VAL'
|
POSTGRES_PASSWORD='$POSTGRES_PASSWORD_VAL'
|
||||||
FINANCE_READ_PASSWORD='$FINANCE_READ_PASSWORD_VAL'
|
FINANCE_READ_PASSWORD='$FINANCE_READ_PASSWORD_VAL'
|
||||||
@@ -83,14 +87,48 @@ FB_API_KEY='$FB_API_KEY_VAL'
|
|||||||
FB_SESSION_SECRET='$FB_SESSION_SECRET_VAL'
|
FB_SESSION_SECRET='$FB_SESSION_SECRET_VAL'
|
||||||
FB_GUI_USER='admin'
|
FB_GUI_USER='admin'
|
||||||
FB_GUI_PASSWORD_HASH='$FB_GUI_PASSWORD_HASH_VAL'
|
FB_GUI_PASSWORD_HASH='$FB_GUI_PASSWORD_HASH_VAL'
|
||||||
GRAFANA_ADMIN_PASSWORD='$GRAFANA_ADMIN_PASSWORD_VAL'
|
FB_PASSWORD='$FB_PASSWORD_VAL'
|
||||||
EOF
|
EOF
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
echo "NEU ERZEUGT: GUI-Login admin / $GUI_PASSWORD (jetzt notieren!)"
|
echo "NEU ERZEUGT: GUI-/Grafana-Login admin / $FB_PASSWORD_VAL (jetzt notieren!)"
|
||||||
echo "Grafana-Login admin / siehe GRAFANA_ADMIN_PASSWORD in $ENV_FILE"
|
elif grep -q '^FB_GUI_PASSWORD_HASH=' "$ENV_FILE" && ! grep -q '^FB_PASSWORD=' "$ENV_FILE"; then
|
||||||
|
# Migrate a pre-existing .env that predates the shared GUI/Grafana
|
||||||
|
# password (independent GUI hash + GRAFANA_ADMIN_PASSWORD) to one shared
|
||||||
|
# FB_PASSWORD. The old GUI password was only ever stored as a salted
|
||||||
|
# hash, so its plaintext cannot be recovered - a new password is
|
||||||
|
# unavoidable here. The stale hash and any GRAFANA_ADMIN_PASSWORD line
|
||||||
|
# (superseded by FB_PASSWORD) are dropped; all other keys (API key,
|
||||||
|
# session secret, DB passwords, ...) are left untouched.
|
||||||
|
FB_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")
|
||||||
|
sed -i "/^FB_GUI_PASSWORD_HASH=/d; /^GRAFANA_ADMIN_PASSWORD=/d" "$ENV_FILE"
|
||||||
|
{
|
||||||
|
echo "FB_GUI_PASSWORD_HASH='$FB_GUI_PASSWORD_HASH_VAL'"
|
||||||
|
echo "FB_PASSWORD='$FB_PASSWORD_VAL'"
|
||||||
|
} >> "$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
|
fi
|
||||||
set -a; . "$ENV_FILE"; set +a
|
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
|
# Sanity check: the GUI password hash must survive shell sourcing intact. An
|
||||||
# unquoted value containing a literal '$' gets mangled by parameter expansion,
|
# unquoted value containing a literal '$' gets mangled by parameter expansion,
|
||||||
# which would silently break GUI login. Require the canonical salt$digest form
|
# 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=$?)"
|
echo "Container '$API_CTR_NAME' started (rc=$?)"
|
||||||
|
|
||||||
# Grafana container (provisioning + dashboards mounted read-only; datasource
|
# 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" \
|
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 \
|
-e FINANCE_READ_PASSWORD \
|
||||||
-v "$GRAFANA_PROVISIONING_DIR:/etc/grafana/provisioning:Z,ro" \
|
-v "$GRAFANA_PROVISIONING_DIR:/etc/grafana/provisioning:Z,ro" \
|
||||||
-v "$GRAFANA_DASHBOARDS_DIR:/var/lib/grafana/dashboards: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"
|
"$GRAFANA_IMAGE"
|
||||||
echo "Container '$GRAFANA_CTR_NAME' started (rc=$?)"
|
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
|
# Generate systemd service files
|
||||||
cd "$USER_SYSTEMD_DIR"
|
cd "$USER_SYSTEMD_DIR"
|
||||||
podman generate systemd --name --new --files "$POD_NAME"
|
podman generate systemd --name --new --files "$POD_NAME"
|
||||||
|
|||||||
@@ -58,4 +58,7 @@
|
|||||||
|
|
||||||
<h2>Grafana-Dashboard</h2>
|
<h2>Grafana-Dashboard</h2>
|
||||||
<iframe class="grafana" src="http://{{ request.url.hostname or '127.0.0.1' }}:8097"></iframe>
|
<iframe class="grafana" src="http://{{ request.url.hostname or '127.0.0.1' }}:8097"></iframe>
|
||||||
|
<p class="hint">Kein Diagramm sichtbar? Einmal in
|
||||||
|
<a href="http://127.0.0.1:8097" target="_blank">Grafana anmelden</a>
|
||||||
|
(gleiches Passwort wie hier).</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user