Files
prose-contracts/scripts/capture-dsh-token.sh
T
abiba-bot 266fa1f835 fix(dsh-web-auth): Authentik-gated :80 login + non-disruptive token capture
Correct the dsh-web authentication fix after parent correction:

- Remove the unauthenticated :8081 endpoint (0.0.0.0 bind with no
  auth_request = full Authentik bypass for the LAN). The script now removes
  /etc/nginx/sites-enabled/dsh.token automatically if it reappears.
- Put the login path inside the Authentik-gated :80 server block as
  location = /dsh-web-login; proxy to dsh-web with Host =
  tankodhs.sysloggh.net so the 30-day cookie is bound to the public
  authority, never to 127.0.0.1:3080.
- Isolate the rotating token in a generated include
  /etc/dsh-web/nginx-login.conf; reload nginx only when it changes.
- Replace the disruptive capture (systemctl stop/start dsh-web) with a
  non-disruptive read of the running service's journal, scoped to the
  current systemd invocation so a restarted process's stale token is never
  reused while the new banner is still pending.
- Keep x-dsh-task-board-proxy-token and Host $ak_origin_host intact in '/'.
- Document the corrected design (B4) in zulip-health.prose.md, v3.2.0.

Live-verified 2026-09-11: no auth bypass (302), :8081 refused (000), a
cookie minted before two dsh-web restarts still returns 200, the refreshed
token mints a fresh cookie, and the systemd ExecStartPost/timer refreshes
the token automatically without touching dsh-web.
2026-09-11 16:52:45 +00:00

142 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# capture-dsh-token.sh — refresh the dsh-web login token WITHOUT restarting dsh-web.
#
# Context (CT 112 / tankodhs.sysloggh.net)
# ----------------------------------------
# The dsh-web UI (systemd unit `dsh-web.service`, 127.0.0.1:3080) prints a random
# launch token to the journal on every start:
#
# dsh web: http://127.0.0.1:3080/?token=<TOKEN>
#
# That token is the only way to bootstrap the authority-bound 30-day browser
# cookie. It rotates on every dsh-web start, so the Authentik-gated
# `location = /dsh-web-login` in /etc/nginx/sites-available/dsh must always
# reference the token of the RUNNING process.
#
# This script:
# 1. reads the LATEST launch token from the running service's journal — it
# NEVER stops or starts dsh-web,
# 2. records it in /etc/dsh-web/launch-token,
# 3. regenerates the nginx include /etc/dsh-web/nginx-login.conf (the
# `proxy_pass ...?token=` line consumed by /dsh-web-login),
# 4. validates with `nginx -t` and reloads ONLY when the token changed,
# rolling the include back if validation fails,
# 5. removes the legacy unauthenticated :8081 endpoint if it ever reappears.
#
# Idempotent and safe to run at any time (systemd ExecStartPost or timer).
set -euo pipefail
umask 077
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
JOURNAL_UNIT="dsh-web.service"
TOKEN_FILE="/etc/dsh-web/launch-token"
INCLUDE_FILE="/etc/dsh-web/nginx-login.conf"
SITE_ENABLED="/etc/nginx/sites-enabled/dsh"
LEGACY_8081="/etc/nginx/sites-enabled/dsh.token"
STASH_DIR="/etc/nginx/sites-available"
log() { printf 'capture-dsh-token: %s\n' "$*" >&2; }
die() { printf 'capture-dsh-token: ERROR: %s\n' "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "must run as root"
# ── 0. Remove the legacy unauthenticated :8081 endpoint, if present ─────────
# It bypassed Authentik entirely (listened on 0.0.0.0:8081 with no auth_request)
# and must never come back. Stash it rather than delete so it is auditable.
if [ -e "$LEGACY_8081" ] || [ -L "$LEGACY_8081" ]; then
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
STASHED="$STASH_DIR/dsh.token.disabled-$STAMP"
mv "$LEGACY_8081" "$STASHED"
if nginx -t >/dev/null 2>&1; then
nginx -s reload
log "removed legacy :8081 endpoint -> $STASHED"
else
mv "$STASHED" "$LEGACY_8081"
die "nginx config test failed after removing $LEGACY_8081; restored it"
fi
fi
# ── 1. Read the latest launch token from the RUNNING service ────────────────
# Scope the journal to the service's CURRENT invocation. While a restarted
# process is still booting (~25s before it prints the banner), the newest token
# in the journal still belongs to the PREVIOUS process; without this filter an
# ExecStartPost run would silently keep the stale token. Never stop/start dsh-web.
INVOCATION="$(systemctl show -p InvocationID --value "$JOURNAL_UNIT" 2>/dev/null || true)"
JOURNAL_ARGS=(-u "$JOURNAL_UNIT")
if [ -n "$INVOCATION" ] && [ "$INVOCATION" != "n/a" ]; then
JOURNAL_ARGS+=("_SYSTEMD_INVOCATION_ID=$INVOCATION")
else
log "WARNING: no invocation id for $JOURNAL_UNIT; using latest journal token"
fi
extract_token() {
grep -oE 'dsh web: https?://[^[:space:]]+[?&]token=[^[:space:]]+' \
| tail -n1 | sed -E 's/.*[?&]token=//' || true
}
TOKEN=""
for _ in $(seq 1 60); do
TOKEN="$(journalctl "${JOURNAL_ARGS[@]}" --no-pager -o cat 2>/dev/null | extract_token)"
[ -n "$TOKEN" ] && break
sleep 1
done
# Fallback: the current invocation's start banner may have been rotated out of
# the journal; the newest matching line overall is then the best available.
if [ -z "$TOKEN" ]; then
log "WARNING: no token for the current invocation; falling back to newest journal token"
TOKEN="$(journalctl -u "$JOURNAL_UNIT" --no-pager -o cat 2>/dev/null | extract_token)"
fi
[ -n "$TOKEN" ] || die "no launch token found in the $JOURNAL_UNIT journal"
# The token must be safe to embed in a URI and in the nginx config.
printf '%s' "$TOKEN" | grep -qE '^[A-Za-z0-9._~+/=:@-]+$' \
|| die "captured token contains unsupported characters"
# ── 2. Record the token (atomic, private) ──────────────────────────────────
mkdir -p "$(dirname "$TOKEN_FILE")"
if ! printf '%s\n' "$TOKEN" | cmp -s - "$TOKEN_FILE" 2>/dev/null; then
printf '%s\n' "$TOKEN" > "$TOKEN_FILE.tmp"
chmod 600 "$TOKEN_FILE.tmp"
mv "$TOKEN_FILE.tmp" "$TOKEN_FILE"
log "recorded new launch token in $TOKEN_FILE"
fi
# ── 3. Regenerate the nginx login include (reload only when it changes) ────
NEW_INCLUDE="$(mktemp "$INCLUDE_FILE.XXXXXX")"
printf 'proxy_pass http://127.0.0.1:3080/?token=%s;\n' "$TOKEN" > "$NEW_INCLUDE"
chmod 600 "$NEW_INCLUDE"
if [ -f "$INCLUDE_FILE" ] && cmp -s "$NEW_INCLUDE" "$INCLUDE_FILE"; then
rm -f "$NEW_INCLUDE"
log "token unchanged; nginx not reloaded"
exit 0
fi
[ -e "$SITE_ENABLED" ] || { rm -f "$NEW_INCLUDE"; die "$SITE_ENABLED missing; refusing to reload"; }
RESTORE=""
if [ -f "$INCLUDE_FILE" ]; then
RESTORE="$(mktemp "$INCLUDE_FILE.bak.XXXXXX")"
cp -p "$INCLUDE_FILE" "$RESTORE"
fi
mv "$NEW_INCLUDE" "$INCLUDE_FILE"
chmod 600 "$INCLUDE_FILE"
if ! nginx -t >/dev/null 2>&1; then
if [ -n "$RESTORE" ]; then
mv "$RESTORE" "$INCLUDE_FILE"
else
rm -f "$INCLUDE_FILE"
fi
die "nginx config test failed; previous include restored"
fi
if [ -n "$RESTORE" ]; then
rm -f "$RESTORE"
fi
nginx -s reload
log "token changed; nginx reloaded"
log "login endpoint: https://tankodhs.sysloggh.net/dsh-web-login (Authentik-gated)"