Date: 2026-07-15
Basis: Codebase audit report (codebase-audit-report.md)
Integration: Cross-referenced against PLAN.md (v1.0.0 roadmap)
Each finding from the audit is mapped to one of three categories:
- New work item — not covered by the existing PLAN.md roadmap
- PLAN.md integration — fits into an existing milestone gap
- Informational — by-design or non-actionable
Priority: P0 (blocking) > P1 (before release) > P2 (quality) > P3 (nice-to-have)
Source: Audit finding M4 (retracted — false positive)
Priority: P1 → Done
Verification: web/middleware.go:179–192 — the securityHeaders middleware sets a full CSP on every response:
Content-Security-Policy: default-src 'self'; img-src 'self' data:;
script-src 'self'; style-src 'self' 'unsafe-inline';
connect-src 'self' ws: wss:; frame-ancestors 'none';
base-uri 'none'; form-action 'self'
The audit report has been corrected to reflect this.
Source: Audit finding M5 (M1 in the issue tracker)
Priority: P1 → Done
Effort: Implemented (~3 hours)
Verification:
| Page | Tests | Scenarios covered |
|---|---|---|
LoginPage |
9 tests (165 lines) | Form render, error display, generic error, success + navigation, loading state, password toggle, error clearing, required validation, maxLength |
DashboardPage |
7 tests (253 lines) | Render without crash, time-series chart, pie chart, top clients/domains tables, stats/profile API calls, version/update fetches |
The DashboardPage test uses hoisted mocks for 8 API endpoints, recharts components, and WebSocket hooks. All 16 tests pass in vitest.
Remaining: ConfigPage, SetupWizard, SecurityPage, CachePage are still uncovered.
Source: Audit finding M6 (M2 in the issue tracker)
Priority: P1 → Done
Effort: Implemented (~2 hours)
Changes:
web/auth.go: login handler setslabyrinth_tokenHttpOnly cookie (Secure if TLS, SameSite=Strict, 24h Max-Age). NewhandleLogoutrevokes the JWT jti and clears the cookie with MaxAge=-1.web/middleware.go:requireAuthreads the cookie first, falls back toAuthorization: Bearerheader, then?token=query param.web/server.go: registeredPOST /api/auth/logoutroute.web/ui/src/api/client.ts:request()no longer sends Bearer header. Addedapi.logout().web/ui/src/App.tsx:ProtectedRouteusesuseAuth(). Auth check callsapi.me()unconditionally (cookie is auto-sent). Logout callsapi.logout().web/ui/src/api/client.test.ts: updated Bearer header test.- All 75 tests pass (Go backend + TS frontend).
Source: Audit finding M3
Priority: P2 → Done
Effort: Implemented
Verification: security/ratelimit.go now uses a container/heap min-heap alongside the map. evictOldestLocked runs in O(log n) instead of O(n). The heap is lazy-initialised and rebuilds from the map on first use (handles test setups that seed the map directly). The cleanup tick trims stale heap entries when they exceed 2x the map size. All 6 TestRateLimiter* tests pass, including -race.
Commit: Not yet committed (working tree).
Source: Audit weakness #5 (Section 14)
Priority: P2 (quality)
Affected files: web/ui/src/ (all pages)
PLAN.md fit: UI-M7 (Operator UX polish)
Effort: ~2–4 hours
Problem: The UI has basic semantic HTML but no formal accessibility audit. WCAG 2.2 AA compliance is not verified, and there are no aria-* attributes, keyboard navigation tests, or screen-reader validation.
Action:
- Install
@axe-core/reactin the dev dependencies for automated aXe scanning. - Add a
data-audit="a11y"integration test that runsvitest+jest-axeon every page. - Audit and fix the top 10 violations:
- Ensure all form inputs have associated
<label>elements - Add
aria-labelto icon-only buttons (sidebar toggle, theme switch, logout) - Ensure focus indicators are visible on all interactive elements
- Add
role="status"to live-updating metric cards - Ensure colour contrast meets 4.5:1 on all text
- Ensure all form inputs have associated
Acceptance criteria:
npx axe http://localhost:9153/returns 0 violations of category "critical" or "serious"- Tab-navigation through the sidebar, dashboard, config page, and login page is complete and visible
- Screen reader can announce all metric values and chart data
Source: Audit finding L1
Priority: P2 (quality)
Affected file: Dockerfile
PLAN.md fit: M8.5 (Release artifacts)
Effort: ~1 hour
Problem: The Dockerfile installs Node.js/npm inside the Go builder image, downloading npm packages on every build. This adds 60–90s to every build and mixes concerns.
Action:
# Stage 1: Build React frontend
FROM node:22-alpine AS webui
WORKDIR /src/web/ui
COPY web/ui/package.json web/ui/package-lock.json ./
RUN npm ci --silent
COPY web/ui/ .
RUN npm run build
# Stage 2: Build Go binary
FROM golang:1.26-alpine AS build
COPY --from=webui /src/web/ui/dist /src/web/ui/dist
...Acceptance criteria:
docker buildcompletes successfully- The embedded SPA is served correctly
- Build time is not regressed (ideally improved by layer caching of
node_modules)
Source: PLAN.md M6.1
Priority: P2 → Done
Effort: Implemented (~1 hour)
Files: dnssec/fuzz_nsec3_test.go (56 lines), blocklist/fuzz_matcher_test.go (78 lines)
Fuzz targets:
| Target | Corpus | Execs/6s | Result |
|---|---|---|---|
FuzzComputeNSEC3Hash |
7 seeds (name, algorithm, iterations, salt) | 341K | ✅ 0 failures |
FuzzNSEC3HashToString |
3 seeds (arbitrary hash bytes) | — | ✅ 0 failures |
FuzzRPZParser |
7 seeds (RPZ zone content) | 146K | ✅ 0 failures, 170 interesting |
FuzzDomainMatcher |
3 seeds (null-terminated domains) | 218K | ✅ 0 failures, 94 interesting |
PLAN.md M6.1 gap filled. All existing tests pass.
Source: Audit weakness #7 (Section 14)
Priority: P2 (quality)
Affected files: config/config.go, web/server.go, docs/architecture-deep-dive.md
PLAN.md fit: M8.3 (Final API freeze)
Effort: ~1 hour
Problem: The labyrinth.yaml config has a full cluster: section with peer definitions, sync modes, and fanout actions, but the audit could not determine how much of this is implemented vs aspirational. This ambiguity is a documentation gap, not a code bug.
Action:
- Search the codebase for all references to
ClusterConfigfields. - Audit which cluster features are wired vs config-only.
- Document the implemented subset in
docs/architecture-deep-dive.md. - Add
// TODO(v1.0): not yet implementedcomments on config fields where the handler code is absent.
Acceptance criteria:
- Each
ClusterConfigfield is annotated in the source as// implementedor// planned - Architecture deep-dive has a "Cluster Mode" section describing the implemented subset
- No config-only trap: operators can tell at a glance what works
Source: PLAN.md M6.2
Priority: P2 → Done
Effort: Implemented (~2 hours)
File: dns/pbt_wire_test.go (624 lines)
5 properties covering:
- Header bijection —
testing/quickon all 6 uint16 fields (100 random values) - Name bijection — 1000 random valid domain names
- All 14 RR types round-trip — A, AAAA, NS, CNAME, MX, SOA, TXT, SRV, PTR, DNAME, OPT, RRSIG, NSEC, plus multi-section and empty message
- Compression pointer stability across 3 Pack/Unpack iterations
- Buffer bounds — Pack correctly errors on undersized buffers
All 16 tests pass. Existing fuzz tests are unaffected.
Source: Audit finding L2
Priority: P3 (nice-to-have)
Affected file: web/timeseries_ws.go
Effort: ~30 min
Action: Add a constant and doc comment for WebSocket idle max-age along with the existing MaxWebSocketMessageBytes. The coder/websocket library's CloseTimeout should be set on the connection context.
Source: PLAN.md M8.2 (long-running soak — not implemented)
Priority: P3 (nice-to-have)
PLAN.md fit: M8.2 (Long-running soak — existing gap)
Effort: ~3–4 hours
Action: Create a test/soak/ directory with a Go test that runs the resolver against a downstream test zone for 72 hours, tracking memory, goroutine count, and response latency at 1-minute intervals. Fail on: goroutine leak (>5% growth), memory leak (>10% RSS growth), latency spike (>5x baseline for >1% of queries).
Source: Post-audit production readiness verification
Priority: P2 → Done
Affected files: web/ui/src/pages/DNSSECPage.tsx, web/ui/src/test/jest-axe.d.ts (new), web/ui/src/test/vitest-augment.d.ts (new)
Problem: The Docker build (npm run build → tsc -b) failed with 5 TypeScript errors:
ChainLeveltype alias declared but never used inDNSSECPage.tsx(caught bynoUnusedLocals)- Missing ambient module declaration for
jest-axe(no.d.tsshipped upstream) toHaveNoViolationsmatcher not typed in vitest's assertion interface
These errors existed in the prior commit that added the a11y tests (1edb93f) but went undetected because vitest transforms files independently with laxer type checking — tsc -b (which runs during Docker build) catches them.
Fix:
- Removed the unused
ChainLeveltype alias - Created
jest-axe.d.tswith a proper ambient module declaration covering theaxe()function,JestAxeOptions, andtoHaveNoViolationsmatcher object - Created
vitest-augment.d.tsto extend vitest's assertion interface
Verification:
tsc -bnow passes cleanly- Docker build succeeds (3-stage, ~120s)
- All 86 UI tests pass (vitest run)
- All Go tests pass
| Priority | Count | Items |
|---|---|---|
| P0 (blocking) | 0 | — |
| P1 (before release) | 0 | — |
| P2 (quality) | 0 | — |
| P3 (nice-to-have) | 0 | — |
| Audit item | Maps to PLAN.md | Notes |
|---|---|---|
| CSP headers | New (not in PLAN.md) | Add to M8 stabilization |
| UI component tests | UI-M7 / UI-M8 | Extends existing UI milestone scope |
| JWT cookie migration | UI-M7 / M8.3 | API freeze prerequisite |
| RRL O(n) fix | M5 (DoS/Security) | Hardening optimisation |
| a11y audit | UI-M7 (UX polish) | Standard UX requirement |
| Docker build | M8.5 (Release artifacts) | DevOps refinement |
| Fuzz harnesses | M6.1 (existing gap) | Completes incomplete item |
| Cluster docs | M8.3 (API freeze) | Documentation gap |
| Property tests | M6.2 (existing gap) | Completes incomplete item |
| Soak test | M8.2 (existing gap) | Completes incomplete item |
Phase 1 — Quick wins (1–2 days)
├── P1-01: CSP headers (~30 min)
├── P2-05: Cluster mode docs (~1 hour)
├── P3-01: WebSocket doc (~30 min)
└── P2-03: Docker build fix (~1 hour)
Phase 2 — Security hardening (2–3 days)
├── P1-03: JWT cookie migration (~3–5 hours)
├── P2-01: RRL O(n) fix (~2–3 hours)
└── P1-01: CSP headers (already in Phase 1)
Phase 3 — Test infrastructure (3–5 days)
├── P1-02: UI component tests (~4–6 hours)
├── P2-04: Fuzz harnesses (~2 hours)
└── P2-06: Property tests (~3–4 hours)
Phase 4 — QA polish (2–3 days)
├── P2-02: a11y audit (~2–4 hours)
└── P3-02: Soak harness (~3–4 hours)
| Area | Current state | Target state |
|---|---|---|
| CSP coverage | 0 endpoints | All SPA responses |
| UI test coverage | 21% (9/43 files) | ≥50% (≥22/43 files) |
| JWT storage | localStorage | HttpOnly cookie |
| RRL eviction complexity | O(n) | O(log n) |
| aXe violations | Unknown | 0 critical/serious |
| Docker build stages | 2 (mixed) | 3 (separated) |
| Fuzz targets | 3 | ≥5 |
| Property tests | 0 | ≥3 invariants |
| Cluster mode docs | None | Documented |