#!/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= # # 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. reloads nginx ONLY when the token differs from the token nginx actually # loaded (tracked in an applied-state stamp written only after a successful # reload), rolling the include back on failure so the next run retries, # 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" STAMP_FILE="/etc/dsh-web/nginx-login.conf.applied" 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 TS="$(date -u +%Y%m%dT%H%M%SZ)" STASHED="$STASH_DIR/dsh.token.disabled-$TS" mv "$LEGACY_8081" "$STASHED" if ! nginx -t >/dev/null 2>&1; then die "nginx config test failed after disabling $LEGACY_8081 (kept disabled at $STASHED). Fix the nginx config and rerun; the legacy :8081 endpoint will NOT be restored." fi if ! nginx -s reload; then die "nginx reload failed after disabling $LEGACY_8081 (kept disabled at $STASHED). Fix nginx and rerun; the legacy :8081 endpoint will NOT be restored." fi log "removed legacy :8081 endpoint -> $STASHED" 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" # The stamp records the token nginx actually loaded. Comparing against it (not # the on-disk include) means a failed or interrupted reload is retried on the # next run instead of being mistaken for success. APPLIED="" [ -f "$STAMP_FILE" ] && APPLIED="$(cat "$STAMP_FILE" 2>/dev/null || true)" if [ "$APPLIED" = "$TOKEN" ]; then if [ -f "$INCLUDE_FILE" ] && cmp -s "$NEW_INCLUDE" "$INCLUDE_FILE"; then rm -f "$NEW_INCLUDE" log "token unchanged; nginx not reloaded" exit 0 fi mv "$NEW_INCLUDE" "$INCLUDE_FILE" chmod 600 "$INCLUDE_FILE" log "include file repaired to match the token nginx already serves" 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 ! nginx -s reload; then if [ -n "$RESTORE" ]; then mv "$RESTORE" "$INCLUDE_FILE" else rm -f "$INCLUDE_FILE" fi die "nginx reload failed; previous include restored; will retry next run" fi if [ -n "$RESTORE" ]; then rm -f "$RESTORE" fi printf '%s\n' "$TOKEN" > "$STAMP_FILE.tmp" chmod 600 "$STAMP_FILE.tmp" mv "$STAMP_FILE.tmp" "$STAMP_FILE" log "token changed; nginx reloaded" log "login endpoint: https://tankodhs.sysloggh.net/dsh-web-login (Authentik-gated)"