Files
ThothII/docs/superpowers/plans/2026-08-20-dwh-rest-per-installation-auth.md
T

27 KiB
Raw Blame History

DWH REST Per-Installation Authentication Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build, document, and safely activate a reusable server-side dwh-auth service that gives every remote ThothII installation an independently revocable /dwh/ API key without changing portable ThothII connector behavior.

Architecture: A standalone Linux Go binary uses only the standard library, stores versioned credential digests in protected atomic files, and serves one fail-closed verification endpoint over a Unix socket. Host Nginx uses that endpoint through auth_request; PSD runs the binary as an independent hardened systemd service, while the local ThothII server continues to use postgres_direct.

Tech Stack: Go 1.26.0/toolchain 1.26.5, standard library only, Linux systemd, Nginx auth_request, Bash verification scripts, Markdown documentation, Git.

Global Constraints

  • Preserve the portable REST contract: successful clients continue to send exactly one X-API-Key header.
  • Do not modify postgres_direct or ssh_tunnel, and do not add DWH commands to portable tht.
  • Do not add dwh-auth to ThothII Compose or tht start/stop.
  • Build Linux amd64/arm64 only; Mac and Windows do not need this binary.
  • Use only the Go standard library; do not add SQLite, CGO, or runtime packages.
  • New keys use thtdwh_v1.<16-character-base64url-key-id>.<43-character-base64url-secret> with 256 random secret bits.
  • Permit one temporary unversioned legacy_raw record with public ID legacy-shared; revoke it after Mac migration.
  • Store SHA-256 digests and non-secret metadata only; never emit digests through CLI or HTTP.
  • Default to no expiry; optional expiry is RFC3339 UTC and fail-closed.
  • Read/write real keys only through absolute protected files. Never put keys in argv, env, Git, logs, JSON output, or ordinary config.
  • Credential failures are generic 401; service/registry/integrity failures become 503.
  • PSD uses independent systemd plus a Unix socket. Stopping ThothII must not stop DWH auth.
  • Do not preserve/migrate legacy test sessions, Qdrant indexes, or Ollama caches.
  • Do not stop or modify the legacy ThothII stack in this plan.
  • Do not mutate PSD Nginx, systemd, registry paths, or real keys before Task 9 authorization.
  • When delegated, use Luna for deterministic execution and Terra for review. The primary executor owns secret-bearing production steps and reports sanitized results.
  • Keep unrelated brain/ changes and other dirty content out of every commit.

Task 1: Freeze credential and record contracts

Files:

  • Create: tools/dwh-auth/go.mod
  • Create: tools/dwh-auth/internal/credential/credential.go
  • Create: tools/dwh-auth/internal/credential/credential_test.go
  • Create: tools/dwh-auth/internal/record/record.go
  • Create: tools/dwh-auth/internal/record/record_test.go

Interfaces:

  • Consumes: Go standard-library randomness, hashing, constant-time comparison, base64url, JSON, and time.

  • Produces: credential.Generate(io.Reader) (Material, error), credential.VerifyV1([]byte, record.Digest) (string, bool), credential.VerifyLegacy([]byte, record.Digest) bool, record.Validate(Record) error, and the record JSON schema.

  • Step 1: Add the isolated module

module github.com/aritmolab/thothii/tools/dwh-auth

go 1.26.0

toolchain go1.26.5
  • Step 2: Write failing format and validation tests

Freeze:

const Prefix = "thtdwh_v1"
const KeyIDEncodedLength = 16
const SecretEncodedLength = 43
const MaxHeaderBytes = 128

type Material struct {
    Value  []byte
    KeyID  string
    Digest record.Digest
}

Tests use a deterministic 44-byte reader and assert exact segments/lengths, 32 decoded secret bytes, valid digest, changed-byte rejection, comma/duplicate rejection, padding/whitespace rejection, and 129-byte rejection. Legacy tests hash opaque raw values without v1 parsing. Record tests cover every invalid enum, identifier, timestamp, digest, description, expiry, and revocation combination.

  • Step 3: Prove RED
cd tools/dwh-auth
go test ./internal/credential ./internal/record -count=1

Expected: non-zero because implementations are absent.

  • Step 4: Implement the exact record schema
type Kind string

const (
    SchemaVersion = 1
    KindV1        Kind = "per_installation_v1"
    KindLegacyRaw Kind = "legacy_raw"
    LegacyKeyID        = "legacy-shared"
)

type Digest [32]byte

type Record struct {
    SchemaVersion    int        `json:"schema_version"`
    Kind             Kind       `json:"credential_kind"`
    KeyID            string     `json:"key_id"`
    InstallationID   string     `json:"installation_id"`
    Description      string     `json:"description,omitempty"`
    SecretSHA256     string     `json:"secret_sha256"`
    CreatedAt        time.Time  `json:"created_at"`
    ExpiresAt        *time.Time `json:"expires_at,omitempty"`
    RevokedAt        *time.Time `json:"revoked_at,omitempty"`
    RevocationReason string     `json:"revocation_reason,omitempty"`
}

Require schema 1; exact kind/key relationship; installation ID [a-z0-9][a-z0-9-]{0,62}; metadata at most 160 UTF-8 characters without controls; canonical 32-byte base64url digest; UTC timestamps; expiry after creation; paired revocation fields.

  • Step 5: Implement generation and verification

Read 12 random key-ID bytes and 32 secret bytes, use base64.RawURLEncoding, emit the 70-byte canonical value, hash the complete value, and compare via subtle.ConstantTimeCompare. Legacy accepts 1–128 opaque bytes without ASCII controls and hashes the entire raw value.

  • Step 6: Prove GREEN and standard-library-only
cd tools/dwh-auth
gofmt -w internal/credential internal/record
go test ./internal/credential ./internal/record -count=1
go list -deps ./... | grep -v '^github.com/aritmolab/thothii/tools/dwh-auth' | grep '\.'

Expected: tests pass; dependency scan exits 1 with no third-party path.

  • Step 7: Commit
git add tools/dwh-auth/go.mod tools/dwh-auth/internal/credential tools/dwh-auth/internal/record
git commit -m "feat: define DWH installation credentials"

Task 2: Implement the protected atomic registry

Files:

  • Create: tools/dwh-auth/internal/securefile/securefile_linux.go
  • Create: tools/dwh-auth/internal/securefile/securefile_linux_test.go
  • Create: tools/dwh-auth/internal/registry/store.go
  • Create: tools/dwh-auth/internal/registry/store_test.go

Interfaces:

  • Consumes: Task 1 records and key IDs.

  • Produces: registry.Open, Store.Add, Store.Find, Store.FindLegacy, Store.List, Store.Revoke, and Store.Check.

  • Step 1: Write failing security tests

Cover regular protected files; symlinked roots/directories/records/secret files; unsafe modes; unknown/duplicate/trailing JSON; filename mismatch; partial files; concurrent reads; and revoked state winning when both records exist. Use t.TempDir and Linux build tags.

  • Step 2: Prove RED
cd tools/dwh-auth
go test ./internal/securefile ./internal/registry -count=1
  • Step 3: Implement no-follow bounded reads/writes

Use syscall.Open with O_NOFOLLOW|O_CLOEXEC, compare Fstat/Lstat, require regular files, reject group/world write, cap record reads at 4096 bytes, and detect size changes. Secret input is 0600; generated output uses O_CREAT|O_EXCL and 0600.

  • Step 4: Implement state contracts
type State string

const (
    StateActive  State = "active"
    StateRevoked State = "revoked"
)

type PublicRecord struct {
    Kind             record.Kind `json:"credential_kind"`
    KeyID            string      `json:"key_id"`
    InstallationID   string      `json:"installation_id"`
    Description      string      `json:"description,omitempty"`
    CreatedAt        time.Time   `json:"created_at"`
    ExpiresAt        *time.Time  `json:"expires_at,omitempty"`
    State            State       `json:"state"`
    RevokedAt        *time.Time  `json:"revoked_at,omitempty"`
    RevocationReason string      `json:"revocation_reason,omitempty"`
}

Add uses exclusive temp, canonical JSON plus newline, record mode 0640, sync, rename, and directory sync. Revoke publishes revoked state before removing active state. Find checks revoked first. FindLegacy allows at most one legacy_raw; multiple records are an integrity fault.

  • Step 5: Prove GREEN
cd tools/dwh-auth
gofmt -w internal/securefile internal/registry
go test -race ./internal/securefile ./internal/registry -count=1
  • Step 6: Commit
git add tools/dwh-auth/internal/securefile tools/dwh-auth/internal/registry
git commit -m "feat: add protected DWH credential registry"

Task 3: Add the secret-safe administrative CLI

Files:

  • Create: tools/dwh-auth/internal/command/command.go
  • Create: tools/dwh-auth/internal/command/command_test.go
  • Create: tools/dwh-auth/cmd/dwh-auth/main.go

Interfaces:

  • Consumes: Tasks 1–2.

  • Produces: command.Run(context.Context, []string, io.Reader, io.Writer, io.Writer) int.

  • Step 1: Write failing CLI tests

Freeze exits 0 success, 2 unsafe invocation, 3 not found, 4 integrity/filesystem. Prove create writes once to new 0600; import reads 0600; no overwrite; list/status omit digest; revoke requires reason; relative/secret-valued flags fail; sentinel secret never enters output.

  • Step 2: Prove RED
cd tools/dwh-auth
go test ./internal/command -count=1
  • Step 3: Implement only this grammar
dwh-auth --registry-root ABS key create --installation-id ID [--description TEXT] [--expires-at RFC3339] --output ABS
dwh-auth --registry-root ABS key import --legacy-raw --installation-id legacy-shared --from-file ABS
dwh-auth --registry-root ABS key list [--json]
dwh-auth --registry-root ABS key status --key-id ID [--json]
dwh-auth --registry-root ABS key revoke --key-id ID --reason TEXT
dwh-auth --registry-root ABS check [--json]
dwh-auth serve --registry-root ABS --socket ABS

No aliases, env fallback, interactive secret, or plaintext output. Create prints only created key_id=<id> installation_id=<id> output=<path>.

  • Step 4: Implement failure rollback

Create writes/syncs protected output before record publication and removes output if publication fails. Import never modifies its source. Cleanup uncertainty returns exit 4 and path only.

  • Step 5: Prove GREEN
cd tools/dwh-auth
gofmt -w cmd internal/command
go test -race ./internal/command -count=1
go test ./... -count=1
  • Step 6: Commit
git add tools/dwh-auth/cmd tools/dwh-auth/internal/command
git commit -m "feat: add DWH credential administration CLI"

Task 4: Implement Unix-socket verification

Files:

  • Create: tools/dwh-auth/internal/service/service.go
  • Create: tools/dwh-auth/internal/service/service_test.go
  • Modify: tools/dwh-auth/internal/command/command.go
  • Modify: tools/dwh-auth/internal/command/command_test.go

Interfaces:

  • Consumes: Tasks 1–3.

  • Produces: service.New(*registry.Store, *log.Logger, func() time.Time) http.Handler, service.ListenAndServe(context.Context, Config) error, and GET /verify.

  • Step 1: Write failing decision tests

Valid v1=204/key ID; legacy=204/legacy ID; absent/duplicate/malformed/unknown/changed/expired/ revoked=401; corrupt/unsafe/multiple-legacy=503; other path=404; other method=405. Bodies empty. Logs contain timestamp, decision code, parsed public ID only—never secret/digest/description/query.

  • Step 2: Prove RED
cd tools/dwh-auth
go test ./internal/service -count=1
  • Step 3: Implement handler/listener

Require exactly one header value and 128-byte bound. Try strict v1 then reserved legacy for non-v1. Require canonical absolute socket/registry paths, refuse non-socket collision, remove only a proven owned stale socket, chmod 0660, and stop on context cancellation.

  • Step 4: Wire serve fail-closed

Dispatch serve before administrative write access. Open registry read-only, run Check, and refuse startup on malformed or unsafe state. A well-formed expired record remains valid registry data but always authenticates as denied.

  • Step 5: Verify
cd tools/dwh-auth
gofmt -w cmd internal/command internal/service
go test -race ./... -count=1
go vet ./...
  • Step 6: Commit
git add tools/dwh-auth/cmd tools/dwh-auth/internal/command tools/dwh-auth/internal/service
git commit -m "feat: serve DWH authentication over Unix socket"

Task 5: Add reproducible packaging and hardened templates

Files:

  • Create: docker/dwh-auth.Dockerfile
  • Create: scripts/build-dwh-auth.sh
  • Create: scripts/test-dwh-auth-build-contract.sh
  • Create: deploy/dwh-auth/dwh-auth.service
  • Create: deploy/dwh-auth/dwh-auth.tmpfiles.conf
  • Create: deploy/dwh-auth/nginx-http.conf.example
  • Create: deploy/dwh-auth/nginx-dwh-location.conf.example

Interfaces:

  • Consumes: Task 4 binary.

  • Produces: static Linux amd64/arm64 outputs and generic deployment templates.

  • Step 1: Write failing contract

Require the exact pinned builder digest from docker/tht.Dockerfile, no require in go.mod, non-empty ELF outputs, and service directives User=dwh-auth, Group=www-data, SupplementaryGroups=dwh-auth, NoNewPrivileges=true, ProtectSystem=strict, ProtectHome=true, RestrictAddressFamilies=AF_UNIX, empty CapabilityBoundingSet, UMask=0007. Tmpfiles creates root/active/revoked as root:dwh-auth mode 2750.

  • Step 2: Prove RED
bash scripts/test-dwh-auth-build-contract.sh
  • Step 3: Add static builds

Docker runs tests, then builds only linux/amd64 and linux/arm64 with CGO_ENABLED=0, -trimpath, -ldflags='-s -w'. Build script accepts --output ABSOLUTE_DIRECTORY, rejects unsafe paths, and defaults to dist/dwh-auth.

  • Step 4: Add systemd/storage templates
ExecStart=/usr/local/sbin/dwh-auth serve --registry-root /var/lib/dwh-auth --socket /run/dwh-auth/verify.sock

Service writes only RuntimeDirectory=dwh-auth, reads registry, and has no secret environment.

  • Step 5: Add Nginx templates

HTTP map/zone rate key contains only remote address plus parsed public ID, never secret; rate is 20/s. Location uses Unix auth, GET /verify, no body, uses a trailing-slash upstream so public /dwh/rpc/ping?x reaches PostgREST as /rpc/ping?x, clears X-API-Key before PostgREST, burst 100, keeps the public /dwh/ authorization boundary while stripping that prefix upstream, maps auth infrastructure failure to 503.

  • Step 6: Prove GREEN
bash -n scripts/build-dwh-auth.sh
bash -n scripts/test-dwh-auth-build-contract.sh
bash scripts/test-dwh-auth-build-contract.sh

Expected final output: dwh-auth build and deployment contract passed.

  • Step 7: Commit
git add docker/dwh-auth.Dockerfile scripts/build-dwh-auth.sh scripts/test-dwh-auth-build-contract.sh deploy/dwh-auth
git commit -m "build: package standalone DWH authentication service"

Task 6: Gate Nginx and CI behavior

Files:

  • Create: scripts/test-dwh-auth-nginx-integration.sh
  • Create: scripts/test-dwh-auth-nginx-contract.sh
  • Modify: .github/workflows/deployment.yml

Interfaces:

  • Consumes: Tasks 1–5.

  • Produces: structural/runtime Nginx gates and dwh-auth-linux CI.

  • Step 1: Write failing structural checks

Require effective (non-comment) directives:

auth_request /_check_dwh_key;
proxy_method GET;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-API-Key $http_x_api_key;
proxy_set_header X-API-Key "";
error_page 500 =503 @dwh_auth_unavailable;

Negative fixtures remove each and test public verifier, TCP authenticator, PostgREST bypass, full secret in rate key, and failure mapped to success.

  • Step 2: Prove RED
bash scripts/test-dwh-auth-nginx-contract.sh
  • Step 3: Implement runtime smoke

Use one temp root, synthetic v1/legacy keys, temp auth/Nginx Unix sockets, and a static marker behind auth. Assert v1=200/body unchanged, legacy=200, invalid=401, revoked=401, expired=401, stopped authenticator=503. Trap only recorded PIDs and exact temp root.

  • Step 4: Add CI job

Checkout without credentials, Go 1.26.5 keyed by tools/dwh-auth/go.mod, install Ubuntu nginx-light, then:

(cd tools/dwh-auth && go test -race ./... -count=1 && go vet ./...)
bash scripts/test-dwh-auth-build-contract.sh
bash scripts/test-dwh-auth-nginx-contract.sh
bash scripts/test-dwh-auth-nginx-integration.sh
  • Step 5: Run locally to GREEN

Expected: all exit 0; output includes case/status only.

  • Step 6: Commit
git add scripts/test-dwh-auth-nginx-integration.sh scripts/test-dwh-auth-nginx-contract.sh .github/workflows/deployment.yml
git commit -m "test: gate DWH authentication integration"

Task 7: Write and verify operator documentation

Files:

  • Create: docs/install/dwh-auth-server.md
  • Create: docs/install/dwh-auth-client-enrollment.md
  • Create: docs/install/dwh-auth-tls.md
  • Create: docs/operations/psd-dwh-auth-rollout.md
  • Create: docs/testing/dwh-auth-manual-acceptance.md
  • Create: docs/testing/evidence/psd-dwh-auth-rollout-report-template.md
  • Create: scripts/verify-dwh-auth-docs.sh
  • Create: scripts/test-verify-dwh-auth-docs.sh
  • Modify: docs/install/local-workspace-registry.md
  • Modify: docs/install/server-workspace-registry.md
  • Modify: docs/install/psd-workspace-setup.md
  • Modify: docs/guida-utente.md
  • Modify: docs/index.md
  • Modify: mkdocs.yml

Interfaces:

  • Consumes: Tasks 1–6.

  • Produces: generic server/client/TLS manuals, PSD runbook, manual acceptance, sanitized evidence, navigation, docs gates.

  • Step 1: Write failing docs fixtures

Require prerequisites/install/issue/deliver/GUI/headless/rotate/revoke/TLS/fingerprint/renewal/ rollback/401/503/evidence/uninstall. Reject key/digest literals, curl -k, disabled TLS, secrets in env/argv, world-readable files, raw Nginx capture, and Compose coupling.

  • Step 2: Prove RED
bash scripts/test-verify-dwh-auth-docs.sh
  • Step 3: Write generic manuals

Document exact paths, identities/modes, safe commands, one key per installation, rotation generations, optional expiry, protected delivery, GUI vault, headless API_KEY_FILE, /rpc/ping, positive/negative checks, registry backup, revocation, troubleshooting.

  • Step 4: Write TLS/PSD/evidence documents

Document current self-issued .it certificate and missing .com coverage; TLS_CA_FILE; openssl x509 -noout -fingerprint -sha256; out-of-band confirmation; renewal order; never bypass verification. PSD runbook mirrors Tasks 9–10 and excludes legacy test sessions/indexes/caches. Evidence contains public IDs, paths, modes, timestamps, checksums, statuses, owners, approvals only.

  • Step 5: Link existing manuals

Link local/server workspace docs, PSD setup, user guide, docs index, MkDocs. Explicitly state postgres_direct and ssh_tunnel do not use these keys.

  • Step 6: Prove GREEN
bash scripts/test-verify-dwh-auth-docs.sh
bash scripts/verify-dwh-auth-docs.sh
bash scripts/test-verify-workspace-install-docs.sh
bash scripts/auth-docs-smoke.sh
  • Step 7: Commit
git add docs/install/dwh-auth-server.md docs/install/dwh-auth-client-enrollment.md docs/install/dwh-auth-tls.md docs/operations/psd-dwh-auth-rollout.md docs/testing/dwh-auth-manual-acceptance.md docs/testing/evidence/psd-dwh-auth-rollout-report-template.md scripts/verify-dwh-auth-docs.sh scripts/test-verify-dwh-auth-docs.sh docs/install/local-workspace-registry.md docs/install/server-workspace-registry.md docs/install/psd-workspace-setup.md docs/guida-utente.md docs/index.md mkdocs.yml
git commit -m "docs: explain per-installation DWH access"

Task 8: Run source and portability checkpoint

Files:

  • Modify only after a focused test fails: files from Tasks 1–7.
  • Record locally: .artifacts/dwh-auth/source-verification.md.

Interfaces:

  • Consumes: complete candidate.

  • Produces: reviewed clean SHA eligible for PSD mutation gate.

  • Step 1: Run focused gates

(cd tools/dwh-auth && go test -race ./... -count=1 && go vet ./...)
bash scripts/test-dwh-auth-build-contract.sh
bash scripts/test-dwh-auth-nginx-contract.sh
bash scripts/test-dwh-auth-nginx-integration.sh
bash scripts/test-verify-dwh-auth-docs.sh
  • Step 2: Run portability regressions
bash scripts/test-default-compose.sh
bash scripts/test-unified-compose.sh
bash scripts/test-no-deployment-coupling-scope.sh
bash scripts/test-no-deployment-coupling.sh
bash scripts/test-compose-secret-policy.sh
bash scripts/test-verify-workspace-install-docs.sh
(cd tools/tht && go test ./... -count=1)

Expected: all pass; Compose unchanged; tht has no DWH-key command; Mac/Windows need no new binary.

  • Step 3: Check scope and leaks
git diff --check
git status --short
git log --oneline --decorate -8
rg -n --hidden --glob '!.git/**' --glob '!docs/superpowers/**' 'legacy-shared\.[A-Za-z0-9_-]|thtdwh_v1\.[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{43}' .

Expected: whitespace clean; only intended evidence untracked; secret scan exits 1; reviewed commits.

  • Step 4: Terra review

Give Terra design, plan, diff, tests. Require requirement matrix and severity-ranked findings. Resolve Important/Critical with focused tests and commits; record Minor.

  • Step 5: Freeze
git rev-parse HEAD
git status --porcelain=v1

Expected: full SHA recorded; execution worktree clean. Do not push/merge/install here.


Task 9: Install on PSD without changing public Nginx

Files/objects:

  • Install: /usr/local/sbin/dwh-auth
  • Install: /etc/systemd/system/dwh-auth.service
  • Install: /usr/lib/tmpfiles.d/dwh-auth.conf
  • Create: /var/lib/dwh-auth/active/, /var/lib/dwh-auth/revoked/, /run/dwh-auth/verify.sock
  • Protect: /root/dwh-auth-provision/
  • Evidence: owner-approved protected rollout directory

Interfaces:

  • Consumes: frozen Task 8 SHA/binary.

  • Produces: healthy local authenticator with legacy and Mac identities, disconnected from Nginx.

  • Step 1: Stop for explicit mutation authorization

Present SHA, results, exact targets, rollback, legacy-stack non-impact. Without explicit approval, stop with Activity 1 IN_DISCUSSION.

  • Step 2: Verify preconditions read-only

Require x86_64, Nginx group www-data, free target names, systemd metadata, current nginx -t, current /dwh/ diagnostic, and regular root-owned 0600 /root/dwh-auth-provision/legacy-shared.key. Never print/hash that key.

  • Step 3: Build/fingerprint frozen binary
bash scripts/build-dwh-auth.sh --output /tmp/dwh-auth-release
sha256sum /tmp/dwh-auth-release/dwh-auth-linux-amd64

Record binary/source SHA only.

  • Step 4: Install service

Create no-login group/user; install binary 0755; install unit/tmpfiles 0644; run systemd-tmpfiles --create dwh-auth.conf; verify owners/modes. Do not start until check passes.

  • Step 5: Import legacy and issue Mac key

Import fixed protected legacy file with --legacy-raw --installation-id legacy-shared. Create non-expiring psd-mac-primary in root-owned 0600 /root/dwh-auth-provision/psd-mac-primary.key. Capture public metadata only.

  • Step 6: Start/test disconnected service

Run check, systemd-analyze verify, daemon-reload, enable/start, verify socket. Via protected curl configs test new=204, legacy=204, random=401, absent=401 on Unix socket. Check bounded journal for absence of both keys.

  • Step 7: Record checkpoint

Retain checksums, public IDs/modes, hardening/socket metadata, statuses, rollback. Do not touch Nginx.


Task 10: Roll out dual-key Nginx and close Activity 1

Files/objects:

  • Create: /etc/nginx/conf.d/dwh-auth-rate-limit.conf
  • Modify: /etc/nginx/sites-available/policlinicosandonato
  • Preserve: protected pre-change copies
  • Update: Mac vault or protected API_KEY_FILE
  • Update: docs/operations/psd-server-survey-remediation-checklist.md
  • Complete: protected rollout evidence template

Interfaces:

  • Consumes: Task 9 checkpoint, both protected keys, CA/fingerprint, explicit Nginx approval.

  • Produces: per-installation /dwh/, revoked legacy, verified Mac, Activity 1 PASS.

  • Step 1: Stop for separate Nginx/client gate

Present files, backups, reload/rollback, delivery channel, observation duration, expected 204/401/503. Obtain explicit approval distinct from Task 9.

  • Step 2: Prepare candidate

Create timestamped root-owned 0600 backups. Change only DWH auth plus rate map/zone. Preserve upstream http://127.0.0.1:3001/; keep vector locations byte-identical; clear API key before PostgREST; include no literal key.

  • Step 3: Validate before reload

Run the structural checker and a secret scan that emits only PASS/FAIL metadata, install candidates, and run sudo nginx -t. Never run or retain a raw diff, nginx -T, or configuration dump: a legacy Nginx file can contain the exposed key. On failure restore backups before reload and record sanitized FAIL.

  • Step 4: Reload/prove dual-key

Through real .it HTTPS and approved CA test /dwh/rpc/ping via protected curl configs: legacy success, new success, random 401, absent 401. Roll back on response/TLS/unrelated health change.

  • Step 5: Deliver/configure Mac

Use approved protected channel. Verify CA fingerprint, configure vault or headless API_KEY_FILE, run Validate workspace source, Test workspace connections, /rpc/ping. Record public IDs, fingerprint confirmation, timestamp, result only.

  • Step 6: Observe/revoke legacy

After approved window, revoke legacy-shared with reason shared-credential-rotation. New Mac still succeeds; legacy returns 401. Bounded logs contain no key/digest.

  • Step 7: Verify rollback scope

Prove Nginx backups/commands readable. Do not preserve/migrate/restore/stop legacy ThothII data or stack.

  • Step 8: Close only Activity 1

Complete sanitized evidence. Set PASS only with new success, legacy 401, service/Nginx PASS, clean logs, rollback, owner acceptance. Advance Current activity to 2; overall remains SURVEY_NO_GO.

  • Step 9: Commit non-secret status
git add docs/operations/psd-server-survey-remediation-checklist.md PROJECT_STATE.md
git commit -m "docs: record PSD DWH credential rotation"

Expected: public IDs/dates/evidence references/statuses only; no keys, digests, certificate body, connection string, raw log, or protected evidence.


Final execution boundary

Task 10 closes only Activity 1. It does not authorize Project A, stop legacy ThothII, or install the new application. Resume Activity 2 and remaining blockers before a fresh SURVEY_GO.