# Follow-Ups: windev Redeploy, LDAP Test Fixtures, Runner Hygiene — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development > (opus implementers; controller verifies ops evidence; final review pass). **Goal:** Close the five items surfaced by the 2026-08-07 live-actions cycle: repair windev's crash-looping gateway and finish the deferred SEC-36 dashboard verification (NEXT-07), fix the DashboardLdapLiveTests fixture drift (NEXT-06), resolve the unexpected macOS instance runner, harden runner-1's plaintext registration token, and verify the cargo Bearer fix. **Architecture:** Three independent live streams (windev serial: 1→2→3; repo test fix: 4; Gitea/runner hygiene: 5, 6) run concurrently; task 7 closes out docs/trackers. No contract, gateway-logic, or client changes — one test-file edit (Task 4) plus live ops plus docs. **Tech Stack:** SSH + PowerShell `-EncodedCommand` (windev 10.100.0.48), SSH + docker compose (10.100.0.35), Gitea admin API (`gitea.dohertylan.com`, token via `~/.zshenv` `GITEA_TOKEN`), NSSM, xUnit live-LDAP suite, GLAuth at `10.100.0.35:3893`. --- ## Preflight facts (verified before planning) - Unpushed local mxaccessgw commits: `0566716`, `9760497`, `5b153da`, `41e8648` (all docs-only). **`origin/main` = `a346d51`** — contains all current code, so windev can build from `origin/main` without any push. - windev (`10.100.0.48`): NSSM service `MxAccessGw`; deployed Server build of 2026-06-25 (Auth 0.1.2.0, supports auth-DB schema 2) crash-loops on `C:\ProgramData\MxGateway\gateway-auth.db` migrated to schema 3 on 2026-07-15 (`AuthStoreMigrationException`, ~3.8k–10k Hosting-failed events/day). The NEW LDAP secret is already staged as the 10th `AppEnvironmentExtra` entry (SEC-36 Task 3) — preserve it. - `DashboardLdapLiveTests.cs` (`src/ZB.MOM.WW.MxGateway.IntegrationTests/`): uses `admin`/`admin123` (3 tests) and `readonly`/`readonly123`. Directory reality (`scadaproj/infra/glauth/config.toml`): `admin` exists, password is the standard dev test password (`password`, hash `5e884898…42d8` — same as `multi-role`), and IS in GwAdmin (othergroups `[5610, 5701]`); `readonly` does not exist; `gw-viewer` (primarygroup 5611 = GwReader, NOT GwAdmin) is the natural not-an-admin fixture. Test binds `MxGateway:Ldap` from `appsettings.json` (**`Server: localhost`**) + env overrides — so the live run needs `MxGateway__Ldap__Server=10.100.0.35` as well as `MxGateway__Ldap__ServiceAccountPassword` (from Mac user-secrets, never printed). - Gitea instance runners (`GET /api/v1/admin/actions/runners`): id 1 `gitea-runner` (cap 4), id 4 `macos-local-Josephs-MacBook-Pro` (**unexpected, online, labels overlap ubuntu-latest**), id 5 `gitea-runner-2` (cap 2). - `10.100.0.35:/opt/gitea/docker-compose.yml` (+ `docker-compose.yml.bak-tst30`): runner-1's registration token inline in plaintext env, file world-readable. runner-2 uses `GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /run/secrets/runner_token` ← 0600 `/opt/gitea/runner_token`. runner-1 data volume `/opt/gitea/runner:/data` (its `.runner` credential persists — the registration env is only needed for first registration). - Cargo Bearer fix already applied (`~/.zshenv`, backup `~/.zshenv.bak-cli39`) and documented (`docs/ClientPackaging.md`, commit `5b153da`). Task 7 verifies; no further action expected. ## Secret hygiene (binding, all tasks) - Never print the LDAP service-account password, `GITEA_TOKEN`, cargo token, runner registration tokens, or API keys — not in commands, logs, commits, or reports. Read the LDAP password from `dotnet user-secrets list` into an env var without echoing (e.g. `export MxGateway__Ldap__ServiceAccountPassword="$(dotnet user-secrets list --project src/ZB.MOM.WW.MxGateway.Server | awk -F' = ' '/ServiceAccountPassword/ {print $2}')"`). - Documented dev **test users** (`multi-role`/`password`, `admin`/`password`, `gw-viewer`/`password`) are NOT secrets — glauth.md publishes them; fine in code/commits. - SSH→windev PowerShell: always `powershell -NoProfile -EncodedCommand `; never put secrets inside EncodedCommand blobs or argv. --- ### Task 1: NEXT-07 — Recon windev deployment layout + schema support **Classification:** standard — read-only recon, but its output gates a service redeploy **Estimated implement time:** ~5 min **Parallelizable with:** Task 4, Task 5, Task 6 **Files:** none edited. SSH recon on `10.100.0.48` + repo/scadaproj reads on the Mac. Determine everything Task 2 needs, and confirm the fresh-deploy path is safe: 1. `nssm get MxAccessGw Application`, `AppDirectory`, `AppParameters`, `AppEnvironmentExtra` (count entries; do NOT print values of secret-bearing entries — names only). 2. Inventory the deployed dir: path, `ZB.MOM.WW.MxGateway.Server.exe` timestamp, whether `appsettings.json`/`appsettings.Production.json` in the deploy dir differ from repo `origin/main` (diff; windev-specific config must survive the redeploy). 3. Confirm build feasibility on windev: `dotnet --list-sdks` (need 10.x), locate an existing mxaccessgw checkout/worktree (CI uses `scripts/ci/windev-worker-ci.ps1` — find its worktree path) or pick a fresh clone location. Confirm `git fetch` reaches `origin/main` = `a346d514dd24e775640e5667aa7cd8e561fec68a`. 4. Confirm current code supports auth-DB schema 3: find the auth-store supported-schema constant (ZB.MOM.WW.Auth packages — check the package version the Server at `origin/main` references, and/or the migration code in the shared scadaproj libs) and state the evidence. **If current code does NOT support schema 3, STOP — report, do not deploy.** 5. Gateway endpoints for verification: bound URLs/ports (from deployed config/env), dashboard scheme (http vs https → cookie will be `MxGatewayDashboard` vs `__Host-…`). 6. Check what migrated the DB to schema 3 on 2026-07-15 (event log / file timestamps) — only to confirm schema 3 is the shared-lib current version, not an anomaly. **Step: report** all findings as structured text (no secrets); no changes, no commits. ### Task 2: NEXT-07 — Build current Server on windev and redeploy the service **Classification:** high-risk — replaces a running (crash-looping) service's binaries **Estimated implement time:** ~10 min **Parallelizable with:** none (needs Task 1) **Files:** none in repo. windev filesystem + NSSM only. Using Task 1's facts: 1. On windev, fetch/checkout `origin/main` (`a346d51…`) in the build worktree/clone. 2. `dotnet publish src/ZB.MOM.WW.MxGateway.Server -c Release` (match deployed layout/RID from Task 1; framework-dependent vs self-contained must match what NSSM `Application` points at). 3. Stop the service (`nssm stop MxAccessGw`), confirm process exited. 4. Backup: deployed dir → sibling `*.bak-next07` copy; copy `C:\ProgramData\MxGateway\gateway-auth.db` (+ `-wal`/`-shm` if present) to `gateway-auth.db.bak-next07`. **Never delete the live DB.** 5. Deploy publish output over the deploy dir, then restore any windev-specific config files identified in Task 1 (do not clobber live overrides; NSSM env entries are untouched by file copies but verify count unchanged after start). 6. `nssm start MxAccessGw`; verify: service state RUNNING and stable ≥60 s (no restart cycle), Application event log shows clean host start and **zero new `AuthStoreMigrationException` / `Hosting failed to start`** after the start timestamp, bound port answers (e.g. dashboard root or health endpoint returns HTTP). 7. Rollback if unhealthy: stop, restore `*.bak-next07` dir, start, report. **Step: report** deployed SHA, verification evidence, backup paths. No repo commits. ### Task 3: SEC-36 deferred verification + NEXT-07/runbook closeout **Classification:** standard **Estimated implement time:** ~6 min **Parallelizable with:** none (needs Task 2) **Files:** - Modify: `docs/runbooks/SEC-36-ldap-credential-rotation.md` (Correction 3 — mark the deferred dashboard check done, dated) - Modify: `archreview/2026-07-12/remediation/90-candidate-findings-next-cycle.md` (NEXT-07 → resolved 2026-08-07, evidence one-liner) 1. Complete SEC-36 step 4 against the repaired windev gateway: log in to the dashboard as `multi-role`/`password` end-to-end. Preferred: `curl` flow — GET `/login` (capture antiforgery token + cookie), POST credentials, expect success redirect + auth cookie (name per Task 1 scheme). If the login page resists scripting (Blazor circuit), report exactly why and fall back to asserting a fresh `DashboardLdapLiveTests` green run (Task 4) plus windev log evidence of successful LDAP bind on a manual attempt. 2. Update the two docs; commit locally (`docs(sec-36,next-07): …`), do NOT push. ### Task 4: NEXT-06 — Fix DashboardLdapLiveTests fixtures to match the shared directory **Classification:** small — one test file, but must go green against live GLAuth **Estimated implement time:** ~6 min **Parallelizable with:** Task 1, Task 5, Task 6 **Files:** - Modify: `src/ZB.MOM.WW.MxGateway.IntegrationTests/DashboardLdapLiveTests.cs` - Modify: `archreview/2026-07-12/remediation/90-candidate-findings-next-cycle.md` (NEXT-06 → resolved) - Possibly modify: `docs/GatewayTesting.md` (live-LDAP opt-in row: document the `MxGateway__Ldap__Server` override needed when GLAuth is not localhost) 1. Read `DashboardAuthenticator` first: confirm a user who binds successfully but maps to no role yields `Succeeded == false` (drives the gw-viewer fixture). 2. Fix fixtures: `admin`/`admin123` → `admin`/`password` (positive + wrong-password + unreachable tests); `readonly`/`readonly123` → `gw-viewer`/`password` (exercises user-binds-but-lacks-GwAdmin; keep the no-password-leak assertion, updating the asserted literal). Update XML doc comments to match. Keep MXAccess-repo style rules (TreatWarningsAsErrors). 3. Build: `dotnet build src/ZB.MOM.WW.MxGateway.IntegrationTests` (macOS OK — net10.0). 4. Live run (env only, never echo the password): `MXGATEWAY_RUN_LIVE_LDAP_TESTS=1 MxGateway__Ldap__Server=10.100.0.35 MxGateway__Ldap__ServiceAccountPassword= dotnet test … --filter FullyQualifiedName~DashboardLdapLiveTests` → expect **5/5 passed** (this is also positive live proof of the SEC-36 service-account bind). 5. Update tracker row (+ GatewayTesting.md if the Server-override note is missing); commit locally (`test(ldap): …`), do NOT push. ### Task 5: Resolve the unexpected macOS instance runner (id 4) **Classification:** standard — evidence-gated removal of a live runner registration **Estimated implement time:** ~6 min **Parallelizable with:** Task 1, Task 4, Task 6 **Files:** - Modify: `docs/runbooks/TST-30-second-ci-runner.md` (the Correction paragraph mentions "id 4 — an unrelated local macOS runner" — update to final state) 1. Evidence, local: `pgrep -fl act_runner`, `launchctl list | grep -i act`, `brew services list | grep -i act`, look for `~/.runner`/act_runner config dirs. Evidence, Gitea (token from `~/.zshenv`, never printed): runner detail for id 4 (labels, last online), and whether any recent runs' jobs report `runner_id == 4` (`GET /repos/{owner}/{repo}/actions/runs?…` → `…/runs/{id}/jobs` for both `mxaccessgw` and `lmxopcua` recent runs). 2. Decision rule: the runner advertises ubuntu labels from a macOS host, so it can steal Linux container jobs → **remove it** unless evidence shows it deliberately serves jobs the docker runners cannot (none expected). Removal = stop the local act_runner process AND disable its autostart (launchd/brew), then `DELETE /api/v1/admin/actions/runners/4`. Keep the local config file (renamed `*.disabled-2026-08-07`) so re-registering with mac-specific labels stays easy; note the re-registration recipe in the runbook edit. 3. Verify: admin runner list shows only ids 1 and 5, both online; no act_runner process locally; a `pgrep` after 60 s still empty (nothing respawned). 4. Update the TST-30 runbook correction paragraph; commit locally, do NOT push. ### Task 6: Harden runner-1's registration token on 10.100.0.35 **Classification:** standard — touches the live CI stack's compose file **Estimated implement time:** ~7 min **Parallelizable with:** Task 1, Task 4, Task 5 **Files:** none in repo (host `/opt/gitea/` only; runbook note lands in Task 7 if needed). 1. Preconditions on the host: confirm runner-1's `/data/.runner` exists in its volume (registration credential persists → the registration env var is no longer needed); confirm both runners idle (no `act_runner`-spawned job containers, no in-progress runs via API) before recreating. 2. Edit `/opt/gitea/docker-compose.yml` (backup first → `docker-compose.yml.bak-tst30b`): replace runner-1's inline `GITEA_RUNNER_REGISTRATION_TOKEN: ` with the same `_FILE`/secrets pattern runner-2 uses (`/opt/gitea/runner_token`, 0600). Do NOT touch the `gitea` service definition. 3. `docker compose up -d --no-deps` the runner-1 service only; verify it comes back online in the admin runner list and its `.runner` identity is unchanged (still id 1). 4. Tighten perms: `chmod 600 /opt/gitea/docker-compose.yml docker-compose.yml.bak-tst30 docker-compose.yml.bak-tst30b` (verify compose stack still operable by the deploy user). 5. Rotate the leaked registration token if the deployment allows: `docker exec … gitea actions generate-runner-token` (or admin API) — if Gitea offers no invalidation of the old value, say so explicitly in the report (residual risk: LAN actor could register a rogue runner until rotation) rather than claiming it rotated. 6. Verify CI still works: trigger nothing; just confirm both runners online and the token file perms; a real push lands naturally later. Report evidence. ### Task 7: Closeout — cargo Bearer verification, docs/tracker sync, commits **Classification:** small **Estimated implement time:** ~5 min **Parallelizable with:** none (needs Tasks 3, 4, 5, 6) **Files:** - Modify: `archreview/2026-07-12/remediation/90-candidate-findings-next-cycle.md` (final state of NEXT-06/NEXT-07 rows if Tasks 3/4 left anything) - Possibly modify: `docs/GatewayTesting.md` / TST-30 runbook (runner topology now ids 1+5 only; token-hardening note) 1. Cargo Bearer verification (no printing): assert `~/.zshenv` line matches `CARGO_REGISTRIES_DOHERTJ2_GITEA_TOKEN="Bearer …"` via `grep -c`, confirm `docs/ClientPackaging.md` note present (commit `5b153da`); check no other credential location (CI secrets, windev profiles) publishes to cargo — expected none. 2. Sweep: every doc touched this cycle consistent (runbooks, trackers, GatewayTesting.md); `git grep` for stale phrases ("crash-loop… pending", "id 4", "admin123") and fix. 3. Commit remaining doc changes locally; do NOT push. List the full unpushed stack in the report. --- ## Out of scope - Pushing any mxaccessgw commits (user decides; stack listed at closeout). - The five next-cycle candidate findings other than NEXT-06/NEXT-07. - Auth-DB restore path for windev (fresh deploy chosen — preserves schema-3 data). - `ci.yml` changes (labels, concurrency groups). ## Dependency graph ``` {1} → 2 → 3 ┐ {4} ├→ 7 {5} │ {6} ─────────┘ ```