Skip to content

test(dns): cover native DNS activate/deactivate admin authorization; add env secrets threat model - #321

Draft
roodboi wants to merge 2 commits into
nextfrom
wf/e4-dns-auth-tests-threat-b14
Draft

roodboi wants to merge 2 commits into
nextfrom
wf/e4-dns-auth-tests-threat-b14

Conversation

@roodboi

@roodboi roodboi commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

HACK-1127 (unit E4). This PR has two parts.

  1. It closes the test gap that fix(doctor): show the admin prompt reliably during native repairs #320 left on the hack global dns activate and hack global dns deactivate administrator prompt.
  2. It adds the env and secrets threat model.

Final behavior

DNS authorizer seam (src/lib/native-domain-dns-authorization.ts, new)

  • The two authorize closures in handleGlobalDnsActivate and handleGlobalDnsDeactivate are now one function, createNativeDnsAuthorizer({ action, domain, dependencies? }). Its injectable dependencies are isInteractive, confirm and requestAdministrator. The production defaults are the same calls as before: process.stdin.isTTY, confirmSafe with nonInteractive: "decline", and requestAdministratorAuthorization. The confirmation text is the same for both actions.
  • requireNativeAdministratorAuthorization moved out of src/commands/global.ts into the new module, and it now accepts an injected requestAdministrator. When the prompt fails, it throws NativeDnsAdministratorAuthorizationError. The error carries outcome: "authorization-failed" and the closed failure class, never sudo output. The message text is unchanged.
  • src/commands/global.ts loses 48 lines. User-visible behavior is unchanged.

Handler wiring guard (tests/global-dns-authorizer-wiring.test.ts, new)

The handlers need a real macOS host, Homebrew dnsmasq and sudo, so no test runs them. This guard reads the TypeScript syntax tree of src/commands/global.ts and checks each of handleGlobalDnsActivate and handleGlobalDnsDeactivate:

  • It calls createNativeDnsAuthorizer exactly once, with its own action string literal and domain, and stores the result in a const.
  • That binding is the authorize passed to createNativeDnsHostDependencies.
  • Its flow (activateNativeDomainDns or deactivateNativeDomainDns) runs with exactly those dependencies.
  • The only other authorizer allowed is the decline-only async () => false on the deactivate verify-only path.

It is a syntax check of one file, not data flow. No production code changed in this rework.

Threat model (docs/security/env-secrets-threat-model.md, new; linked from docs/env.md)

The document covers:

The doc makes no code change.

Validation (local, macOS, Bun 1.4.2)

  • New tests/native-domain-dns-authorization.test.ts: 8 tests. Each one wires the authorizer into the real activateNativeDomainDns or deactivateNativeDomainDns and into createNativeDnsHostDependencies, with a temp directory, fake privileged and command runners, and one ordered effect log. No real sudo, HOME, keychain, Docker or CLI spawn is used.
    • Activate and deactivate: the log starts with confirm, then a single sudo -v. Every sudo -n step, receipt write and file effect comes after it. The test checks that those effects actually ran, so the assertion is not vacuous. The confirmation texts are asserted exactly.
    • Activate and deactivate, failed prompt (rejected exit 1, timed-out exit 124):
      • The flow fails in phase authorization with NativeDnsAdministratorAuthorizationError, outcome: "authorization-failed". The message never says "denied".
      • The log is exactly confirm, sudo -v.
      • No receipt or dnsmasq file is created. For deactivation, the active receipt and both files are unchanged.
    • Activate and deactivate, decline: the flow fails with "was denied", and the log is exactly confirm. It never reaches sudo.
    • Non-interactive: nothing is prompted and nothing reaches sudo.
    • Every failure class (not-foreground, timed-out, interrupted, rejected) throws. None returns false.
  • The same file has 2 more tests for the production defaults. The administrator prompt stays a fake in both, so no test reaches real sudo.
    • Default interactivity check: with process.stdin.isTTY set to undefined or false, the authorizer returns false and never confirms. With it set to true, it confirms. The original descriptor is restored afterwards.
    • Default confirmation: with HACK_NO_INTERACTIVE=1, the real confirmSafe declines, and the authorizer returns false before sudo. The variable is restored afterwards.
  • New tests/global-dns-authorizer-wiring.test.ts: 2 tests, one per handler.
  • Focused suites, 14 files, 153 pass, 1 skip, 0 fail: global-dns-authorizer-wiring, native-domain-dns-authorization, native-admin-authorization, native-domain-dns-host, native-domain-dns-activate, native-domain-dns-deactivate, native-hack-local-route-cli, doctor-env-key, cli-reference-docs, tests/integration-ownership.test.ts, tests/cli-spawn-env-isolation.test.ts, and the other native-domain-dns-* and native-hack-local-route* suites.
  • bun run --cwd packages/cli typecheck and root bun run typecheck: exit 0.
  • bun run --cwd packages/cli check: 0 errors. There are 34 existing warnings, none in changed files. Biome is clean on the changed TypeScript files.
  • The CLI surface and the rendered guidance are unchanged, so docs:cli-reference, generate:agent-plugins and the fingerprint step were not needed.
  • I checked that every file path cited in the threat model exists on this head.

Negative controls

Each mutation was run against the wiring, authorization, activate, deactivate and admin-authorization suites, then reverted. All 16 are caught.

Mutation Result
global.ts activate: const authorize = async () => true; 1 fail
global.ts deactivate: authorizer built with action: "activate" 1 fail
global.ts activate: host dependencies get authorize: async () => true 1 fail
global.ts deactivate: flow gets { ...deps, authorize: async () => true } 1 fail
global.ts activate: authorizer built with a different domain 1 fail
Default isInteractive: () => true 1 fail
Default isInteractive: () => false 1 fail
Default confirm nonInteractive: "accept" 1 fail
Default confirm initialValue: true, nonInteractive: "accept-default" 1 fail
Default confirm always returns true 1 fail
requireNativeAdministratorAuthorization returns false instead of throwing 3 fail
Remove the administrator call from the authorizer 4 fail
Ignore a decline 2 fail
Skip the interactive-terminal check 1 fail
Run sudo before the confirmation 6 fail
Throw a plain "denied" Error instead of the typed failure 3 fail

Remaining gaps

  • The handlers are guarded by syntax, not run. The wiring guard proves which authorizer each handler installs, but no test executes the handlers themselves. The default requestAdministrator (real sudo -v) is not exercised by any test. nonInteractive: "accept-default" with initialValue: false behaves the same as "decline", so no test can tell them apart.
  • No real macOS run. I did not run hack global dns activate or deactivate on a real host. A real sudo -v prompt, a real dnsmasq restart and a real /etc/resolver write are still unproven for this change.
  • Hosted CI has not been checked on head 62a9d124.
  • The threat model's open gaps are documented but not fixed. Gaps 4 to 6 are concrete code findings that need follow-up changes.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

roodboi and others added 2 commits October 12, 2026 22:27
…mpt and add the env secrets threat model

The `hack global dns activate|deactivate` authorize closures move into
createNativeDnsAuthorizer (src/lib/native-domain-dns-authorization.ts) with
injectable interactivity, confirmation and administrator prompt. A failed
sudo -v now throws NativeDnsAdministratorAuthorizationError (closed
outcome "authorization-failed" and failure class; message unchanged).

Tests wire the authorizer into the real activation/deactivation flows and
host effect boundary and prove: sudo -v runs before every privileged and
file effect, a failed prompt is authorization-failed (not denied) and
changes nothing, and a decline or a non-interactive run never reaches sudo.

Adds docs/security/env-secrets-threat-model.md (HACK-1127), linked from
docs/env.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efaults

tests/global-dns-authorizer-wiring.test.ts checks, on the TypeScript
syntax tree of src/commands/global.ts, that handleGlobalDnsActivate and
handleGlobalDnsDeactivate each build exactly one authorizer with
createNativeDnsAuthorizer({ action: <own action>, domain }), pass it to
createNativeDnsHostDependencies, and run their flow with those
dependencies. The only other authorizer allowed is the decline-only
`async () => false` on the deactivate verify-only path.

tests/native-domain-dns-authorization.test.ts now also covers the
production defaults: the default interactivity check requires a stdin
TTY before confirming, and the default confirmation declines an
explicitly non-interactive run before sudo. The administrator prompt
stays a fake, so no test reaches real sudo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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