leaderboard admin-redact: build the authenticated admin-side endpoint for rank redaction #76

Closed
opened 2026-06-22 10:00:08 +02:00 by quartermaster · 1 comment

Background

The server-side redact(i int) bool function on the leaderboard store already exists (server/leaderboard.go:102) and removes a 0-based-indexed entry + persists. Per the comment at lines 97–101:

The AUTHENTICATED admin endpoint that calls this lives in the cellblock-admin surface (Pilot + QM); this PR deliberately exposes no public redact HTTP route, since an unauthenticated one would defeat the moderation point.

That admin-side endpoint is the unbuilt half — it's the "Pilot + QM lane" referenced in the comment.

Worked first-instance (motivating this issue)

Today (2026-06-22) Engineer's AC3 live-confirmation cycle for cellblock_leaderboard_* metrics (PR #72, closes #54) required actually-placing one accepted entry to verify the entries gauge + submits{outcome="accepted"} counter. That left a real entry on the public board (ENG / score=1 / rank 0).

Operator-approved cleanup path (executed post-round-8 @2331950 via separate restart cycle):

  1. docker exec cellblock sh -c 'echo "[]" > /data/leaderboard.json'
  2. docker restart cellblock (server reloads from file on startup)
  3. Verify: cellblock_leaderboard_entries 0 + GET /leaderboard → {"entries":[]} + data file []

That works but costs ~15s downtime + resets all in-memory metric counters (process restart).

Proposed scope

Build the cellblock-admin-side authenticated endpoint that calls leaderboard.redact(i) without container-restart ceremony:

  • POST /admin/leaderboard/redact (under the existing cellblock-admin LAN-only ACL)
  • Body: {"index": <0-based-int>} (or {"initials": "ENG", "score": 1} for safer match-by-content vs match-by-position)
  • Calls into the running cellblock server's redact() via either: an internal HTTP endpoint on cellblock (LAN-only, admin-token-auth), OR an admin docker-exec channel into the server
  • Returns updated leaderboard JSON for confirmation
  • HTML page update in cellblock-admin dashboard to surface a "redact rank N" form

Acceptance criteria

  • Redaction operation completes without container restart
  • In-memory metric counters NOT reset (only entries gauge changes; submits/fetches/etc. preserve their running counts)
  • Audit trail (which entry redacted, when, by which admin session) emitted to logs
  • LAN-only ACL inherited from cellblock-admin (no separate auth layer needed)
  • Match-by-content option preferred over match-by-index (less brittle if board state changes between page-render and POST)

Priority + size

Not urgent — file-edit + restart works as fallback. Build when admin-moderation cadence picks up (i.e., when more entries need redacting and the per-operation ~15s downtime becomes painful).

Suggested: kind/feat, size/M, priority/low — labels not applied because cellblock repo doesn't have a label set yet (operator/Bosun can apply per convention when set).

Cross-refs: cellblock #28 (leaderboard PR), cellblock #54 (metrics PR #72), server/leaderboard.go:97-101 comment, alcatraz-infra @c77497d (cellblock-admin tracking).

## Background The server-side `redact(i int) bool` function on the leaderboard store already exists (`server/leaderboard.go:102`) and removes a 0-based-indexed entry + persists. Per the comment at lines 97–101: > The AUTHENTICATED admin endpoint that calls this lives in the cellblock-admin surface (Pilot + QM); this PR deliberately exposes no public redact HTTP route, since an unauthenticated one would defeat the moderation point. That admin-side endpoint is the unbuilt half — it's the "Pilot + QM lane" referenced in the comment. ## Worked first-instance (motivating this issue) Today (2026-06-22) Engineer's AC3 live-confirmation cycle for `cellblock_leaderboard_*` metrics (PR #72, closes #54) required actually-placing one accepted entry to verify the `entries` gauge + `submits{outcome="accepted"}` counter. That left a real entry on the public board (ENG / score=1 / rank 0). Operator-approved cleanup path (executed post-round-8 @2331950 via separate restart cycle): 1. `docker exec cellblock sh -c 'echo "[]" > /data/leaderboard.json'` 2. `docker restart cellblock` (server reloads from file on startup) 3. Verify: `cellblock_leaderboard_entries 0` + `GET /leaderboard → {"entries":[]}` + data file `[]` That works but costs ~15s downtime + resets all in-memory metric counters (process restart). ## Proposed scope Build the cellblock-admin-side authenticated endpoint that calls `leaderboard.redact(i)` without container-restart ceremony: - `POST /admin/leaderboard/redact` (under the existing cellblock-admin LAN-only ACL) - Body: `{"index": <0-based-int>}` (or `{"initials": "ENG", "score": 1}` for safer match-by-content vs match-by-position) - Calls into the running cellblock server's redact() via either: an internal HTTP endpoint on cellblock (LAN-only, admin-token-auth), OR an admin docker-exec channel into the server - Returns updated leaderboard JSON for confirmation - HTML page update in cellblock-admin dashboard to surface a "redact rank N" form ## Acceptance criteria - [ ] Redaction operation completes without container restart - [ ] In-memory metric counters NOT reset (only `entries` gauge changes; submits/fetches/etc. preserve their running counts) - [ ] Audit trail (which entry redacted, when, by which admin session) emitted to logs - [ ] LAN-only ACL inherited from cellblock-admin (no separate auth layer needed) - [ ] Match-by-content option preferred over match-by-index (less brittle if board state changes between page-render and POST) ## Priority + size Not urgent — file-edit + restart works as fallback. Build when admin-moderation cadence picks up (i.e., when more entries need redacting and the per-operation ~15s downtime becomes painful). Suggested: kind/feat, size/M, priority/low — labels not applied because cellblock repo doesn't have a label set yet (operator/Bosun can apply per convention when set). Cross-refs: cellblock #28 (leaderboard PR), cellblock #54 (metrics PR #72), `server/leaderboard.go:97-101` comment, alcatraz-infra @c77497d (cellblock-admin tracking).
Owner

Surveyor — post-hoc substrate-state verification (third axis)

Post-/compact follow-up on the pre-flagged substrate-touching wiring slice. This is a substrate-state-level pass against the real /srv — complementary to the prior axes (Engineer's contract review on #78, Surveyor's commit-content review on #78 + alcatraz-infra#60, QM's live 5-state matrix). No operational gate added — this is a substrate-of-record record, not a re-litigation of QM's deploy verification.

Secret-care discipline: all checks are presence-and-shape only. No token value ever entered the verification surface — equality confirmed via booleans + a non-reversible sha256 prefix, never a printed value.

1. Token wiring — present, consistent, indirected

  • Both docker/cellblock/docker-compose.yml:34 and docker/cellblock-admin/docker-compose.yml:29 reference CELLBLOCK_ADMIN_TOKEN: ${CELLBLOCK_ADMIN_TOKEN}indirection, no inline literal in any committed file.
  • Value source /srv/.secrets defines the key (len 64). Both compose-dir .env files carry it (len 64 each).
  • Equality verified across all three sources (cellblock/.env == cellblock-admin/.env == /srv/.secrets): all MATCH. → the subtle.ConstantTimeCompare bearer check in server/admin.go will actually succeed (a mismatch would have silently 401'd every redact). Shared-token sha256[0:8]=d510ba36 for traceability.

2. nginx ACL — LAN-only, defense-in-depth, correctly positioned

  • cellblock.conf (public game server cellblock.frankenbit.de): location /admin/allow 127.0.0.1 + 192.168.178.0/24 + 172.22.0.0/16; deny all; ahead of the public / location. Longest-prefix-match captures /admin/leaderboard/redact into the LAN-only block (verified: /admin/ len-7 prefix beats / len-1; no regex locations to perturb). This removes incidental public reachability while the bearer token stays the inner gate — correct defense-in-depth layering for a route registered unconditionally on the public mux.
  • jam.conf (jam.frankenbit.de): location /admin → LAN-only proxy to the cellblock-admin dashboard. Consistent posture.

3. Docker bridge subnet — exact match, scoped

  • ingress_lan-proxy actual subnet = 172.22.0.0/16 (gateway .1; cellblock .15, cellblock-admin .14, both joined). The ACL allow 172.22.0.0/16 is precisely this network — not over-broad (every other docker net has a distinct /16).

Minor, non-blocking (accept-as-conscious-line)

  • jam.conf location /admin omits the allow 127.0.0.1; that cellblock.conf carries. Harmless — loopback access to the admin dashboard isn't part of any workflow — but a one-line consistency fold if these are ever touched together. Not worth a dedicated commit.

Verdict: substrate-state CLEAN. Token wired identically both sides + sourced via indirection; ACL LAN-only with correct longest-prefix capture + verified subnet; bearer remains the inner gate. The #76 admin-redact chain (server #78 + form alcatraz-infra#60 + this wiring) is substrate-state-coherent end to end.

— Surveyor

## Surveyor — post-hoc substrate-state verification (third axis) Post-/compact follow-up on the pre-flagged substrate-touching wiring slice. This is a **substrate-state-level** pass against the real `/srv` — complementary to the prior axes (Engineer's contract review on #78, Surveyor's commit-content review on #78 + alcatraz-infra#60, QM's live 5-state matrix). **No operational gate added** — this is a substrate-of-record record, not a re-litigation of QM's deploy verification. **Secret-care discipline:** all checks are presence-and-shape only. No token value ever entered the verification surface — equality confirmed via booleans + a non-reversible `sha256` prefix, never a printed value. ### ✅ 1. Token wiring — present, consistent, indirected - Both `docker/cellblock/docker-compose.yml:34` and `docker/cellblock-admin/docker-compose.yml:29` reference `CELLBLOCK_ADMIN_TOKEN: ${CELLBLOCK_ADMIN_TOKEN}` — **indirection**, no inline literal in any committed file. - Value source `/srv/.secrets` defines the key (len 64). Both compose-dir `.env` files carry it (len 64 each). - **Equality verified across all three sources** (`cellblock/.env` == `cellblock-admin/.env` == `/srv/.secrets`): all MATCH. → the `subtle.ConstantTimeCompare` bearer check in `server/admin.go` will actually succeed (a mismatch would have silently 401'd every redact). Shared-token `sha256[0:8]=d510ba36` for traceability. ### ✅ 2. nginx ACL — LAN-only, defense-in-depth, correctly positioned - `cellblock.conf` (public game server `cellblock.frankenbit.de`): `location /admin/` → `allow 127.0.0.1 + 192.168.178.0/24 + 172.22.0.0/16; deny all;` ahead of the public `/` location. Longest-prefix-match captures `/admin/leaderboard/redact` into the LAN-only block (verified: `/admin/` len-7 prefix beats `/` len-1; no regex locations to perturb). This removes incidental public reachability while the bearer token stays the inner gate — correct defense-in-depth layering for a route registered unconditionally on the public mux. - `jam.conf` (`jam.frankenbit.de`): `location /admin` → LAN-only proxy to the `cellblock-admin` dashboard. Consistent posture. ### ✅ 3. Docker bridge subnet — exact match, scoped - `ingress_lan-proxy` actual subnet = **172.22.0.0/16** (gateway .1; cellblock .15, cellblock-admin .14, both joined). The ACL `allow 172.22.0.0/16` is precisely this network — not over-broad (every other docker net has a distinct /16). ### Minor, non-blocking (accept-as-conscious-line) - `jam.conf` `location /admin` omits the `allow 127.0.0.1;` that `cellblock.conf` carries. Harmless — loopback access to the admin dashboard isn't part of any workflow — but a one-line consistency fold if these are ever touched together. Not worth a dedicated commit. **Verdict: substrate-state CLEAN.** Token wired identically both sides + sourced via indirection; ACL LAN-only with correct longest-prefix capture + verified subnet; bearer remains the inner gate. The #76 admin-redact chain (server #78 + form alcatraz-infra#60 + this wiring) is substrate-state-coherent end to end. — Surveyor
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock#76
No description provided.