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.
This commit is contained in:
+118
-89
@@ -1,112 +1,141 @@
|
||||
#!/bin/bash
|
||||
# capture-dsh-token.sh — start dsh-web, capture its token, update nginx
|
||||
# Run on CT112 (tankodhs.sysloggh.net)
|
||||
#!/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. Restarts the dsh-web service
|
||||
# 2. Captures the token URL from the journal
|
||||
# 3. Extracts the token value
|
||||
# 4. Writes the token to /etc/dsh-web/launch-token
|
||||
# 5. Creates an nginx config that exposes a /dsh-web-login endpoint
|
||||
# 6. Reloads nginx
|
||||
|
||||
# 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"
|
||||
|
||||
# Kill any existing dsh-web instance first
|
||||
systemctl stop dsh-web 2>/dev/null || true
|
||||
sleep 2
|
||||
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"
|
||||
|
||||
# Record the time we started the service (for --since filter)
|
||||
START_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
log() { printf 'capture-dsh-token: %s\n' "$*" >&2; }
|
||||
die() { printf 'capture-dsh-token: ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# Start dsh-web
|
||||
systemctl start dsh-web
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
|
||||
# Wait for the token to appear in the journal (up to 30 seconds)
|
||||
TOKEN=""
|
||||
for i in {1..30}; do
|
||||
TOKEN=$(journalctl -u dsh-web.service --since "$START_TIME" --output=cat 2>/dev/null | grep -m1 "dsh web: http://" | grep -oP "(?<=dsh web: )(https?://[^ ]+)" | head -1 || true)
|
||||
if [ -n "$TOKEN" ]; then
|
||||
break
|
||||
# ── 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
|
||||
echo "ERROR: token not captured within 30s" >&2
|
||||
exit 1
|
||||
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
|
||||
|
||||
# Extract the token value (everything after "?token=")
|
||||
TOKEN_VALUE=$(echo "$TOKEN" | grep -oP "(?<=token=)[^ ]+")
|
||||
# ── 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"
|
||||
|
||||
# Reject tokens that could break nginx config or the request URI
|
||||
if ! printf '%s' "$TOKEN_VALUE" | grep -qE '^[A-Za-z0-9._~+/=%:@-]+$'; then
|
||||
echo "ERROR: token contains unsupported characters" >&2
|
||||
exit 1
|
||||
if [ -f "$INCLUDE_FILE" ] && cmp -s "$NEW_INCLUDE" "$INCLUDE_FILE"; then
|
||||
rm -f "$NEW_INCLUDE"
|
||||
log "token unchanged; nginx not reloaded"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Write the token to a restricted file
|
||||
mkdir -p /etc/dsh-web
|
||||
printf '%s\n' "$TOKEN_VALUE" > /etc/dsh-web/launch-token
|
||||
chmod 600 /etc/dsh-web/launch-token
|
||||
echo "Captured dsh-web launch token"
|
||||
[ -e "$SITE_ENABLED" ] || { rm -f "$NEW_INCLUDE"; die "$SITE_ENABLED missing; refusing to reload"; }
|
||||
|
||||
# Create the nginx config with the token (using printf to control expansion)
|
||||
NGINX_ENABLED="/etc/nginx/sites-enabled/dsh.token"
|
||||
NGINX_STAGE_DIR="/etc/nginx/sites-available"
|
||||
mkdir -p "$NGINX_STAGE_DIR"
|
||||
|
||||
TMP_CONFIG="$(mktemp "$NGINX_STAGE_DIR/dsh.token.XXXXXX")"
|
||||
BACKUP=""
|
||||
if [ -f "$NGINX_ENABLED" ]; then
|
||||
BACKUP="$(mktemp "$NGINX_STAGE_DIR/dsh.token.bak.XXXXXX")"
|
||||
cp -p "$NGINX_ENABLED" "$BACKUP"
|
||||
RESTORE=""
|
||||
if [ -f "$INCLUDE_FILE" ]; then
|
||||
RESTORE="$(mktemp "$INCLUDE_FILE.bak.XXXXXX")"
|
||||
cp -p "$INCLUDE_FILE" "$RESTORE"
|
||||
fi
|
||||
|
||||
{
|
||||
printf "server {\n"
|
||||
printf " listen 127.0.0.1:8081;\n"
|
||||
printf " server_name _;\n"
|
||||
printf " \n"
|
||||
printf " location = /dsh-web-login {\n"
|
||||
printf " proxy_pass http://127.0.0.1:3080/?token=%s;\n" "$TOKEN_VALUE"
|
||||
printf " proxy_http_version 1.1;\n"
|
||||
printf " proxy_set_header Host 127.0.0.1:3080;\n"
|
||||
printf " proxy_set_header X-Real-IP \$remote_addr;\n"
|
||||
printf " proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n"
|
||||
printf " }\n"
|
||||
printf " \n"
|
||||
printf " location / {\n"
|
||||
printf " proxy_pass http://127.0.0.1:3080;\n"
|
||||
printf " proxy_http_version 1.1;\n"
|
||||
printf " proxy_set_header Host 127.0.0.1:3080;\n"
|
||||
printf " proxy_set_header X-Real-IP \$remote_addr;\n"
|
||||
printf " proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n"
|
||||
printf " }\n"
|
||||
printf "}\n"
|
||||
} > "$TMP_CONFIG"
|
||||
chmod 600 "$TMP_CONFIG"
|
||||
mv "$NEW_INCLUDE" "$INCLUDE_FILE"
|
||||
chmod 600 "$INCLUDE_FILE"
|
||||
|
||||
mv "$TMP_CONFIG" "$NGINX_ENABLED"
|
||||
CONFIG_APPLIED=1
|
||||
restore_on_exit() {
|
||||
if [ "$CONFIG_APPLIED" -eq 1 ]; then
|
||||
if [ -n "$BACKUP" ]; then
|
||||
if cp -p "$BACKUP" "$NGINX_ENABLED" 2>/dev/null; then rm -f "$BACKUP"; fi
|
||||
else
|
||||
rm -f "$NGINX_ENABLED"
|
||||
fi
|
||||
if ! nginx -t >/dev/null 2>&1; then
|
||||
if [ -n "$RESTORE" ]; then
|
||||
mv "$RESTORE" "$INCLUDE_FILE"
|
||||
else
|
||||
rm -f "$INCLUDE_FILE"
|
||||
fi
|
||||
}
|
||||
trap restore_on_exit EXIT
|
||||
|
||||
if nginx -t; then
|
||||
/usr/sbin/nginx -s reload
|
||||
CONFIG_APPLIED=0
|
||||
if [ -n "$BACKUP" ]; then rm -f "$BACKUP"; fi
|
||||
echo "Token captured and nginx reloaded"
|
||||
else
|
||||
echo "ERROR: nginx config test failed; rolling back" >&2
|
||||
exit 1
|
||||
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)"
|
||||
|
||||
+109
-38
@@ -3,7 +3,7 @@ kind: responsibility
|
||||
name: zulip-health
|
||||
description: Multi-platform health monitor for the Zulip messaging mesh spanning Platform A (pi/Abiba Zulip bridge), Platform B (Tanko on DSH), and Platform C (Agent Zero Docker). Verifies bot registration, DM delivery, and cross-platform connectivity. Mumuni is no longer monitored from this host — she runs on her own container (kagentz CT 105 on minipve, .14) and is monitored on her side.
|
||||
title: Zulip Mesh Health Monitor — Multi-Platform
|
||||
version: 3.1.0
|
||||
version: 3.2.0
|
||||
runtime_contract: 2
|
||||
agent: abiba
|
||||
report_only_agents:
|
||||
@@ -262,45 +262,116 @@ logged/reported as a warning — reported, never healed on.
|
||||
| HTTP status outside the expected set | Log/report as a warning — reported, never healed on |
|
||||
**B4: dsh-web Authentication (Tanko — restart-persistent login)**
|
||||
|
||||
The dsh-web UI is token-gated. Each dsh-web process generates a unique
|
||||
launch token printed to the journal at startup. The token is used to mint
|
||||
a 30-day authentication cookie. After the first authenticated login,
|
||||
subsequent requests use the cookie — no token required.
|
||||
The dsh-web UI is token-gated. On every start the process prints a random
|
||||
launch token to the journal:
|
||||
|
||||
**Login endpoint**: `http://127.0.0.1:8081/dsh-web-login` (inside CT 112)
|
||||
|
||||
**Token capture script**: `/opt/deepseek-harness/capture-dsh-token.sh` (CT 112)
|
||||
|
||||
The script:
|
||||
1. Restarts the dsh-web service
|
||||
2. Captures the token URL from the journal
|
||||
3. Extracts the token value
|
||||
4. Writes the token to `/etc/dsh-web/launch-token`
|
||||
5. Creates an nginx config that exposes the `/dsh-web-login` endpoint on port 8081
|
||||
6. Reloads nginx
|
||||
|
||||
**Authentication flow**:
|
||||
1. Access `http://127.0.0.1:8081/dsh-web-login` → 303 redirect
|
||||
2. The redirect includes a `Set-Cookie` header with the `dsh-auth-*` cookie
|
||||
3. The cookie has a 30-day expiry and is authority-bound to `127.0.0.1:3080`
|
||||
4. Subsequent requests to `http://127.0.0.1:3080/` use the cookie for authentication
|
||||
5. After 30 days, the cookie expires and a new token exchange is required
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
# Check if the login endpoint is working:
|
||||
ssh root@192.168.68.15 "pct exec 112 -- curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/dsh-web-login"
|
||||
# Expected: 303
|
||||
|
||||
# Mint the 30-day cookie from the login endpoint:
|
||||
ssh root@192.168.68.15 "pct exec 112 -- rm -f /tmp/dsh.jar"
|
||||
ssh root@192.168.68.15 "pct exec 112 -- curl -s -c /tmp/dsh.jar -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/dsh-web-login"
|
||||
# Expected: 303
|
||||
|
||||
# Check if the stored cookie authenticates against dsh-web:
|
||||
ssh root@192.168.68.15 "pct exec 112 -- curl -s -b /tmp/dsh.jar -o /dev/null -w '%{http_code}' http://127.0.0.1:3080/"
|
||||
# Expected: 200
|
||||
```
|
||||
dsh web: http://127.0.0.1:3080/?token=<TOKEN>
|
||||
```
|
||||
|
||||
The token only bootstraps an authority-bound, HMAC-signed browser cookie with a
|
||||
30-day lifetime. The signing secret is durable in
|
||||
`/root/.dsh/.credentials.yaml` (key `client-connection/browser-session`), so a
|
||||
cookie minted once keeps working across `dsh-web` restarts; the launch token
|
||||
itself rotates on every restart.
|
||||
|
||||
**Login endpoint (public, Authentik-gated):**
|
||||
`https://tankodhs.sysloggh.net/dsh-web-login`
|
||||
|
||||
It lives inside the Authentik-gated `:80` server block
|
||||
(`/etc/nginx/sites-available/dsh`, symlinked from
|
||||
`/etc/nginx/sites-enabled/dsh`) as `location = /dsh-web-login`, guarded by
|
||||
`auth_request /outpost.goauthentik.io/auth/nginx`. It proxies to dsh-web with
|
||||
`Host: tankodhs.sysloggh.net`, so the minted cookie is bound to the public
|
||||
authority — never to `127.0.0.1:3080`. The token-dependent line is isolated in
|
||||
the generated include `/etc/dsh-web/nginx-login.conf`:
|
||||
|
||||
```
|
||||
proxy_pass http://127.0.0.1:3080/?token=<TOKEN>;
|
||||
```
|
||||
|
||||
**Token refresh (non-disruptive):**
|
||||
`/opt/deepseek-harness/capture-dsh-token.sh` (source:
|
||||
`scripts/capture-dsh-token.sh`) reads the latest launch token from the journal
|
||||
**of the service's current invocation**, writes `/etc/dsh-web/launch-token` and
|
||||
regenerates `/etc/dsh-web/nginx-login.conf`, reloading nginx only when the token
|
||||
changed (`nginx -t` guards the reload, with rollback). It **never stops or
|
||||
starts `dsh-web`**. It is triggered by the `dsh-web.service` drop-in
|
||||
`/etc/systemd/system/dsh-web.service.d/20-token-refresh.conf`
|
||||
(`ExecStartPost=/bin/systemctl --no-block start dsh-web-token.service`) and by
|
||||
`dsh-web-token.timer` every 2 minutes for reconciliation.
|
||||
|
||||
<details><summary>Installed systemd wiring (CT 112)</summary>
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/dsh-web-token.service
|
||||
[Unit]
|
||||
Description=Refresh the dsh-web launch token for the nginx login endpoint
|
||||
After=dsh-web.service
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/deepseek-harness/capture-dsh-token.sh
|
||||
|
||||
# /etc/systemd/system/dsh-web-token.timer
|
||||
[Unit]
|
||||
Description=Periodically refresh the dsh-web login token
|
||||
[Timer]
|
||||
OnBootSec=90s
|
||||
OnUnitActiveSec=120s
|
||||
AccuracySec=10s
|
||||
Persistent=true
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
# /etc/systemd/system/dsh-web.service.d/20-token-refresh.conf
|
||||
[Service]
|
||||
ExecStartPost=/bin/systemctl --no-block start dsh-web-token.service
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> **Do NOT reintroduce the `:8081` endpoint.** It listened on `0.0.0.0:8081`
|
||||
> with no `auth_request` and was a full Authentik bypass for anyone on the LAN.
|
||||
> The script now removes `/etc/nginx/sites-enabled/dsh.token` automatically if
|
||||
> it ever reappears.
|
||||
|
||||
**Authentication flow:**
|
||||
1. `GET https://tankodhs.sysloggh.net/dsh-web-login`
|
||||
2. Unauthenticated → Authentik sign-in; once authenticated the request reaches
|
||||
dsh-web with `Host: tankodhs.sysloggh.net`.
|
||||
3. dsh-web accepts the launch token on `GET /`, writes the
|
||||
`dsh-auth-<authority-hash>` cookie (30 days, `HttpOnly`, `SameSite=Strict`)
|
||||
and returns `303` to `/`.
|
||||
4. Every later request through `/` presents that cookie; the token is not needed
|
||||
again until the cookie expires or a new browser is used.
|
||||
|
||||
**Verification** (amdpve vantage):
|
||||
```bash
|
||||
# 1. Login endpoint is Authentik-gated: unauthenticated -> 302 (not 200/303).
|
||||
ssh root@192.168.68.15 "pct exec 112 -- curl -s -o /dev/null -w '%{http_code}\n' \
|
||||
-H 'Host: tankodhs.sysloggh.net' http://127.0.0.1/dsh-web-login"
|
||||
# Expected: 302
|
||||
|
||||
# 2. Legacy :8081 endpoint is gone (connection refused -> 000).
|
||||
ssh root@192.168.68.15 "pct exec 112 -- curl -s --max-time 3 -o /dev/null \
|
||||
-w '%{http_code}\n' http://192.168.68.122:8081/"
|
||||
# Expected: 000
|
||||
|
||||
# 3. Backend cookie mint + reuse (exactly what /dsh-web-login proxies to).
|
||||
TOKEN=$(ssh root@192.168.68.15 "pct exec 112 -- cat /etc/dsh-web/launch-token")
|
||||
ssh root@192.168.68.15 "pct exec 112 -- curl -s -D - -o /dev/null \
|
||||
-H 'Host: tankodhs.sysloggh.net' 'http://127.0.0.1:3080/?token=$TOKEN'"
|
||||
# Expected: HTTP/1.1 303 + set-cookie: dsh-auth-... (authority tankodhs.sysloggh.net)
|
||||
|
||||
# 4. Token refresh is non-disruptive and idempotent.
|
||||
ssh root@192.168.68.15 "pct exec 112 -- /opt/deepseek-harness/capture-dsh-token.sh"
|
||||
# Expected: "token unchanged; nginx not reloaded" when nothing changed
|
||||
```
|
||||
|
||||
**Restart durability (acceptance):** after `systemctl restart dsh-web`, (a) a
|
||||
cookie minted before the restart still returns `200` on `/`, and (b) the
|
||||
refreshed `/etc/dsh-web/nginx-login.conf` carries the new token and mints a
|
||||
fresh cookie. Both verified live 2026-09-11.
|
||||
|
||||
|
||||
### Step 4: Platform C — Agent Zero (kagentz, CT 105 via Docker host .14)
|
||||
|
||||
Reference in New Issue
Block a user