4.2 KiB
Task 4 — Unix-socket DWH verification report
Scope
Implemented the standalone Linux verifier at tools/dwh-auth/internal/service and wired the exact command:
dwh-auth serve --registry-root ABSOLUTE_CANONICAL --socket ABSOLUTE_CANONICAL
The service accepts only GET /verify. It returns empty 204 responses with X-DWH-Key-ID for verified v1 or reserved legacy credentials; credential failures are generic empty 401 responses, and registry/integrity faults are empty 503 responses. Other paths/methods return empty 404/405.
Security decisions
- Exactly one
X-API-Keyheader, maximum 128 bytes. - Strict
thtdwh_v1.parsing precedes legacy lookup; non-v1 values alone may use the reserved legacy record. - Registry integrity is checked before every verification request, so unrelated malformed/unsafe records fail closed with
503. - Logs emit only timestamp, decision code, and (when safely parsed or verified) public key ID; test sentinels prove no key, digest, description, or query value is emitted.
servevalidates canonical absolute paths, performs startupStore.Check, and reports service startup errors as non-secretintegrity failure.- Socket collisions that are regular files, directories, symlinks, live sockets, or foreign-owned stale sockets are refused. Only an owned stale Unix socket after
ECONNREFUSEDcan be reclaimed. - Published sockets are mode
0660; cancellation calls graceful shutdown and removes only a revalidated same-device/same-inode owned socket. A test seam proves a changed path is retained rather than unlinked.
Required supporting security fix
Commit 943f809 (fix: reject duplicate legacy DWH records) tightens the Task 2 registry contract: a synthetically valid active plus revoked legacy pair is now an integrity failure. It is intentionally separate from the Task 4 commit.
TDD evidence
RED was observed for the missing handler, listener/configuration API, CLI wiring, unrelated-registry corruption, active+revoked legacy state, and cleanup replacement race. Each increment was then implemented minimally and rerun GREEN.
Verification
All commands were executed in official golang:1.26.5, with only this worktree mounted:
gofmt -w cmd internal/command internal/service
go test ./internal/service ./internal/command -count=1
go test ./... -count=1
go test -race ./... -count=1
go vet ./...
git diff --check
All passed. A dependency scan also found no third-party Go dependencies.
Scope boundary
No Nginx, systemd, real Unix socket, real registry, credential, legacy stack, or external service was changed. All test data was synthetic and temporary.
Follow-up hardening: runtime read-only registry and socket parent
The Task 5 storage contract uses root:dwh-auth SGID directories (2750) and a service account with read-only group access. The original registry reader path was incompatible because shared locks were opened O_RDWR and lazily created as 0600; secure-directory validation also rejected SGID.
The runtime path now uses registry.OpenReadOnly: it opens only preprovisioned root, active, revoked, and .writer.lock paths, and rejects Add/Revoke. The administrative Open path bootstraps the lock through the exclusive writer path. Shared lock acquisition opens the existing root:dwh-auth 0640 lock O_RDONLY with LOCK_SH; writer acquisition remains O_RDWR with LOCK_EX, preserving cross-process snapshot exclusion. Secure directories allow SGID but still reject setuid, sticky, group-write, and world-write bits.
Task 5 must create .writer.lock as 0640 root:dwh-auth alongside the 2750 root:dwh-auth registry directories before the service starts.
The socket parent must be a canonical non-symlink directory owned by the service EUID and not group/world writable. This removes the bind-to-chmod and path-replacement exposure from other principals. The remaining POSIX path race is bounded to trusted processes sharing the service EUID inside that non-contendible parent.
Additional verification (official golang:1.26.5, worktree only): focused securefile/registry/service/command tests, full tests, full race tests, vet, plus ten race repetitions each for cross-store snapshot readers, OpenReadOnly, and listener tests: all PASS.