Skip to content

Exact-target control gate (DNS TXT / HTTPS file / recorded authority review) - #12

Merged
CompleteDotTech merged 8 commits into
mainfrom
feat/target-control
Oct 23, 2026
Merged

CompleteDotTech merged 8 commits into
mainfrom
feat/target-control

Conversation

@CompleteDotTech

@CompleteDotTech CompleteDotTech commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Refs CompleteTech-LLC/complete.tech#17 (gap 1, exact-target control). Targets main (#10 and #11 merged; main merged in). Scanning stays disabled by default. When enabled, the gate is on by default: every terms version is strict under config terms_policy (required, explicit per served version), and only an r2-or-earlier version listed as legacy is exempt.

What changes

Mailbox confirmation shows control of an email address, not of the website, and a referral code shows neither. For every job whose terms version is strict under terms_policy (the default for any version; frozen into the job at creation as control_required; any profile broader than r2 always requires it), confirmation no longer queues it: it waits as awaiting_control and cannot be claimed, started, admitted or given a detailed report until control of the exact hostname is shown:

  • DNS TXT at _ct-scan-challenge.<host> = ct-scan-verify=<value>, or HTTPS file https://<host>/.well-known/ct-scan-challenge.txt containing the value. The customer asks for a check on a private page (GET only displays; nonce-protected POST acts). At most 3 checks, >= 1 minute apart; each is one TXT lookup or one HTTPS request, fixed path, no redirects, 4 KiB, 10 s; no probes. The worker takes a scheduled check (control/claim), is admitted exactly once per lease through the same guard as scans (admit kind control: cancellation, expiry, lease, budget, pacing, attempt consumed in the admission compare-and-set; the admission is recorded on the lease (control_admitted), so a second admit under the same lease is refused 409 already_admitted for the whole two-minute lease, and the worker ends the check without retrying), runs it in the unprivileged child, and relays what it saw (control/result, length-bounded, untrusted); WordPress compares with a stored hash. A result counts only if an admission happened under that lease. Three failures (or all checks spent) => blocked (target_control_failed): never scanned, no detailed report, no fallback.
  • The published challenge and the page capability are two independent secrets (found in review: a single value let anyone who read the DNS record burn the customer's attempts).
  • Recorded manual authority review (admin): evidence (20-1000 chars), reviewer account, target, time and expiry (the authorization expiry) are stored; an expired review ends the authorization. Refused while a check is in flight (control_state = checking).
  • The HTTPS file match ignores surrounding whitespace and a leading UTF-8 BOM. The DNS check does not follow a CNAME at the challenge name, and a check made before a new record propagates can see a cached negative answer and still uses an attempt (documented and stated on the customer page).
  • Jobs created before the schema 5 upgrade have control_required = 0; a row with a missing value is treated as requiring control.
  • Verification traffic is serialized with scans worker-wide (enforced in WordPress).
  • Lead intake stays distinct from signature: nothing here creates a job without a signed, read-back acceptance, and a caller/referrer cannot supply or satisfy a challenge.

Fail-closed behavior

Any anomaly (unknown method/profile, unconfirmed recipient, lapsed lease, no admission, wrong lease/scope/method, oversized or malformed result, DB fault, lost compare-and-set, cancelled/expired/revoked) refuses and changes nothing; a result is never accepted without an admission; a job that required control (or whose control_required value is missing) and has no verified control is cancelled by every liveness check; a lease is admitted at most once; control state is cleared by Stop, revoke and expiry.

Tests (local only; no third-party host or resolver)

  • control.php (WordPress/MariaDB, re-runs itself with the strict terms_policy): confirmation behavior, limits (3 attempts, 1/min, pacing, budget, one admission per lease even after the interval passes), authority review refused mid-check, BOM handling, missing control_required fails closed, forged/malformed/near-miss values, spent tasks, lapsed leases, Stop/expiry mid-check, DB faults, serialization, blocked => no scan/report, manual review (authorization, evidence, expiry), and cases that defend each central invariant directly. Mutation-checked.
  • test_control.py (34): DNS wire parsing (compression loops, truncation, id/question/source spoofing, caps), a loopback UDP DNS server, a local HTTP server (redirects not followed, 4096-byte edge, non-UTF8, slow server, refused admission reaches nothing), the real scanner entry point, a real child process under the supervisor (kind enforcement, output validation), and the service loop.
  • deployment/test-http-control.py: the customer pages and the signed worker API over real HTTP (the CI step takes ~61 s for the one-minute interval).
  • CI on the previous head: green (ubuntu/windows, py3.10/3.14, WordPress/Docker, root-only isolation).
  • Independent adversarial review found 1 high, 3 medium and several low/nit items; all addressed in the last commit (see its message).

Reviewer: please attack

Any path from a control_required job to scan traffic without verified control; the challenge/capability separation; the requested -> checking -> pending/blocked state machine and attempt accounting; match logic; the DNS client; admin authority review.

Not done / needs the owner

  • Product/legal: whether any r2-or-earlier version should stay legacy (exempt) in terms_policy; every other version requires control; whether a customer may get a fresh challenge after blocked; per-contact/domain/IP quotas for fresh challenges (spec section 4.5) are not implemented because there is no reissue.
  • The customer pages' wording is functional only (TODO(owner)); form/CTA/email/caller-script copy is out of scope here.
  • DNS answers come from the container's first configured resolver; confirm the deployment's resolver and egress policy.

Fail-closed review round 2 (coordinator findings)

Fixed in this stack, each with tests that fail without the fix (mutation-checked):

  • Per-request admission guard for the scan worker #10: activation requires egress_enforced: true (loader refuses otherwise); docs/README/example config state it; unverified nftables egress sketch added (not a claim of enforcement). In-flight bound is exactly 10 s from the grant (no grace). Authorization read from the DB, not the object cache. A completed result needs >= 1 admitted request. Failed schema upgrade: one error_log, 5-minute back-off, plugin 0.2.0. Special-purpose IP ranges listed explicitly (CGNAT, documentation, benchmarking, 3fff::/20, 6to4, NAT64, ...), tested independent of the interpreter's ipaddress.
  • Verify bound acceptance evidence; accepted copy for the verified customer #11: bound evidence is required for every version (terms_policy, strict by default; legacy only for r2-or-earlier); the loader refuses to enable without an explicit entry for the served version. ct_scan_target_summary() is the one canonical summary and now includes limits and period.
  • Exact-target control gate (DNS TXT / HTTPS file / recorded authority review) #12: control is required by the same policy (replaces the opt-in list).
  • Retention mechanics: closure, per-version deadlines, holds, deletion tombstones #13: non-legacy retention floored at 6 years (words/null/0/1 mean 6); tombstone scope is a keyed MAC; deletion needs closed_at >= created_at and a sane clock; retention runs outside the queue lock (own lock, throttled, one short queue lock per deletion) so Stop never queues behind a batch.

CompleteDotTech and others added 3 commits October 22, 2026 07:40
… authority review

Mailbox confirmation no longer queues a job whose terms version requires proof of
control of the exact hostname (config target_control_versions, frozen per job).
The customer publishes a one-time challenge (DNS TXT at _ct-scan-challenge.<host>
or /.well-known/ct-scan-challenge.txt) and asks for a check: at most three, a
minute apart, each one TXT lookup or one HTTPS request (fixed path, no redirect,
4 KiB, 10 s). The worker takes a scheduled check, is admitted once through the
admission guard (kind control, attempt consumed in the admission's
compare-and-set), runs it in the unprivileged child and relays what it saw;
WordPress compares it with the stored token hash. Three failures block the job;
a blocked job is never scanned and gets no detailed report. A recorded manual
authority review (reviewer, evidence, target, time, expiry) can stand in.

Schema 5 adds the control_* columns. Fixtures: a loopback DNS server, a local
HTTP server, a real child process, WordPress/MariaDB, and an HTTP round trip.

Refs CompleteTech-LLC/complete.tech#17 (gap 1).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…lity; close review gaps

Independent review found that the published challenge doubled as the control
page's bearer token, so anyone able to read the DNS record or file could spend the
customer's three checks and block the job. The page now has its own capability
(never published); the challenge shown on it is derived from it with a keyed hash.

Also: a job whose every check is spent is blocked even if the last result was lost;
scans and control checks cannot be claimed at the same time; profiles broader than
r2 always require control; the DNS wait has one overall deadline; the control gate
gets the scan deadline; blocked jobs are included in the retention scrub; the
schema readback names every column; authority-review evidence is shown in the
admin queue; and tests now defend each central invariant directly (forced
unverified queued rows, a fourth admission, unconfirmed rows, revoked pages,
unknown methods, serialization in both directions).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Merge of the evidence and admission fixes. control_required is now frozen from the terms
policy: every version is strict (requires control of the exact target) unless an
r2-or-earlier version is explicitly grandfathered as legacy. Fixtures and the HTTP test run
the strict policy; the legacy flow is covered by the default-policy suites.

Co-Authored-By: Claude Code <noreply@anthropic.com>
… control_required

- WordPress records the admission on the lease (control_admitted, set in the
  admission compare-and-set, reset at claim). A second admit kind=control under
  the same lease is refused 409 already_admitted, even after the one-minute
  interval has passed within the two-minute lease; the result route also
  requires the flag. The worker treats already_admitted as a final refusal.
- ct_scan_control_required(): a missing control_required value counts as
  required (liveness, confirmation and detailed-report gates).
- Authority review is refused while a check is in flight (control_state
  checking), and its compare-and-set includes the state it read.
- HTTPS file match strips a leading UTF-8 BOM.
- Docs and customer page: CNAME at the challenge name is not followed, DNS
  negative caching can consume a check made before propagation, and jobs
  created before schema 5 have control_required = 0.
- Tests: same-lease re-admission after the interval (attempts unchanged),
  review refused mid-check, BOM accepted/near-misses refused, missing
  control_required, worker gate does not retry already_admitted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@CompleteDotTech
CompleteDotTech changed the base branch from feat/bound-acceptance-evidence to main October 23, 2026 19:40
CompleteDotTech and others added 3 commits October 23, 2026 15:43
Brings in the #10 and #11 review fixes (probe busy/inconclusive handling,
bound-evidence pair comparisons, duplicate evidence refusal, accepted-copy
live check).

Resolutions:
- admission.py probe(): keep main's bounded queue_busy retry and use this
  branch's per-kind route ("probe" for http, "control_probe" for control).
  Test added for the control route.
- ct_scan_send_accepted_copy(): main's live-job check allowed only
  queued/leased/running, but confirmation now leaves control-required jobs
  in awaiting_control and sends the copy then, so the default path would
  never send it. awaiting_control is accepted as live; every other check
  (authorization live, recipient, terms hash, compare-and-set) is kept.
  evidence-strict.php covers confirmation into awaiting_control; docs updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s_policy activation

- ct_scan_admin_send_report() compared the raw `_ct_terms_sha256` meta with
  the job's lowercased terms_hash, so an upper-case recorded hash refused a
  reviewed report. Normalise it as the accepted-copy path does; test added
  in integration.php.
- docs/WORDPRESS.md deployment step 5 and the README activation note now say
  an explicit `terms_policy` entry for the served terms version is required
  to enable the integration, with an upgrade note: without it the worker API
  answers 503, new acceptances are not queued and in-flight jobs stall.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The awaiting-control confirmation test checked accepted_copy_status for
'sent' (the function's return value); the stored state is 'accepted'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@CompleteDotTech
CompleteDotTech marked this pull request as ready for review October 23, 2026 19:55
@CompleteDotTech
CompleteDotTech merged commit 6627140 into main Oct 23, 2026
10 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T19:56:14.421253Z 9e69c38 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-23T19:57:08.274848Z 9e69c38 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

CompleteDotTech added a commit that referenced this pull request Oct 23, 2026
Brings in the merged #10/#11/#12 review fixes (probe busy-retry, list-of-pairs
evidence comparison, control_admitted and 409 already_admitted, fail-closed
ct_scan_control_required(), terms_policy docs) under the retention branch.

Conflict: the schema's required jobs columns now list both control_admitted
and the retention/hold/identity columns. The schema version moves to 7 so an
install that recorded 6 before control_admitted existed re-runs the upgrade.

Retention coherence with the control gate: awaiting_control (pending or
checking) is not terminal and is never closed; a restored row recognised by a
tombstone is no longer deleted while it holds a lease (leased/running scan or a
control check in progress), so the one-at-a-time gate is never freed while its
traffic may be in flight. Tests cover both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant