security(discovery): enforce Zone isolation and retire unsafe legacy writers
Bind Agent observations, review, enrichment and telemetry to current Zone
and inventory authority. Preserve immutable MAC ownership and historical
snapshots with stale-result rejection and transactional security audits.
Retire unbound direct/range/scheduled discovery and one/bulk legacy summary
writes in both modes. Require reconciled exact inventory identity or
explicit Zone-scoped IP selection for CSV metadata imports.
Independent Blocker #1 application-security re-audit: PASS on tree
cf863aadc9. Evidence: 141 focused PostgreSQL/HTTP
tests passed; Chromium and PowerShell checks passed. Two documented baseline
source assertions remain unchanged. Production deployment is not approved.
This commit is contained in:
1 parent
e898f283ab
commit
8b0d32cd6b
26 files changed
+3798
-4092
No files matched your search
@@ -0,0 +1,219 @@
|
||||
Blocker #1 discovery isolation: candidate review and compatibility contract
|
||||
=======================================================================
|
||||
Review candidate only. Blocker #1 is NOT declared PASS. No production cutover is
|
||||
authorized. Base: e898f283ab0fc21bac77b63db1c4a9ad35e21562 (Stage A / draft PR #6).
|
||||
No schema migration, new constraints, backfill, or runtime DDL is introduced.
|
||||
|
||||
Authority and observations
|
||||
-------------------------
|
||||
All machine discovery/telemetry routes retain per-Agent HTTP authentication.
|
||||
Authority comes from the persisted Agent -> Site/Zone/network assignment, never
|
||||
client Site, room, Zone, target-Agent names, or a body-supplied Agent identity.
|
||||
Conflicting body Agent IDs are rejected. Disabled/unassigned/revoked routing fails
|
||||
closed. Check-in cannot establish or change an Agent's initial assignment: use
|
||||
existing authenticated enrollment or the separately reviewed legacy transition.
|
||||
Unassigned inventory is not implicitly claimed by check-in or snapshots.
|
||||
|
||||
Discovery reports are bounded (128 items, 1 MiB body; 1,024 observations/minute per
|
||||
Agent). IP literals must be in the current Zone networks. MACs must be canonical
|
||||
ASCII unicast addresses. Explicit IDs, IP aliases, MAC aliases and supplied serial
|
||||
must agree with current inventory and immutable historical MAC ownership. Global
|
||||
historical MAC/serial collision checks reveal no foreign inventory IDs. Duplicate
|
||||
current IPs within a Zone are conflicts; distinct Zones may use the same IP.
|
||||
|
||||
Each observation has a stable server ID and version. Raw reports and original
|
||||
provenance are retained, with an atomic discovery.observed audit recording the
|
||||
current authority, raw hash, inventory fingerprint and observation version. Exact
|
||||
retries deduplicate; discovery clients must send discovered_at for a fresh scan
|
||||
(the supplied Windows scanner does). Approval requires a current, proven,
|
||||
non-superseded observation less than 15 minutes old. Legacy/null/mislinked rows
|
||||
without this evidence remain visible but cannot authorize approval or enrichment.
|
||||
There is no automatic evidence backfill, deletion, identity merge or ownership
|
||||
transfer. An authorized operator must investigate conflicts and obtain a fresh
|
||||
observation. Discovery does not implement an ownership-transfer workflow.
|
||||
|
||||
Human policy remains global administration: agents.view for discovery display;
|
||||
agents.manage for stable-ID review/enrichment; miners.control for scan/poll
|
||||
requests. The UI displays authoritative Agent/Zone and escapes all untrusted text.
|
||||
IP-only approval is unsupported. Submit discovery_id, version and one of
|
||||
add_to_inventory / ignore / non_miner. All three revalidate current authority and
|
||||
identity under locks; only that observation changes. Adding new inventory also
|
||||
requires a valid MAC. Repeat the same accepted decision/version to receive its
|
||||
recorded response without another audit or insertion. Stale/different decisions
|
||||
conflict; refresh before retrying. Audit actor is the authenticated human.
|
||||
|
||||
Read commands and enrichment
|
||||
----------------------------
|
||||
Enrichment is selected by discovery_id/version, routed to its actual observing
|
||||
Agent, and bound to the current authority and IP. No arbitrary-IP or hardcoded
|
||||
Plexus fallback exists. Scan/poll requests require an explicit registered Agent ID;
|
||||
optional UUID request_id provides explicit retry idempotency. New requests without
|
||||
request_id receive distinct IDs. Queue claim rejects legacy/unscoped/stale reads
|
||||
with atomic cancellation/audit and preserves unknown delivery outcome for any
|
||||
previous claim evidence. Existing Stage A physical rejection remains in force.
|
||||
|
||||
Before enrichment miner I/O, the upgraded Windows child submits command_id,
|
||||
discovery_id and version to authenticated POST /api/agent/discovery-target. Only
|
||||
a current claimed enrichment command returns a canonical IP. Enrichment submission
|
||||
revalidates the same binding and appends a NEW child observation for human review;
|
||||
it never resets the parent's decision or reparents history. V1/V2/V3 share the
|
||||
same boundary. Replaying old V2 identity merge inputs cannot merge inventory.
|
||||
|
||||
Known-miners returns a short-lived signed miner_version bound to Agent authority
|
||||
and the current miner ID/IP/MAC/updated_at fingerprint. Every miner/pool/API3
|
||||
snapshot or known-miner enrichment must carry this token and matching identifiers.
|
||||
All Windows polling clients propagate it and batch at 128. A concurrent update,
|
||||
revocation or reassignment may invalidate in-flight telemetry: discard the stale
|
||||
response, refetch known-miners and poll again. Do not fall back to IP matching.
|
||||
Descriptive enrichment does not change MAC/IP/serial/ownership. Raw diagnostics
|
||||
are retained in append-only audits, not empty status rows that hide measurements.
|
||||
API3 retains raw snapshots, safe descriptive metadata, actual summary/pool/board
|
||||
measurements and compatibility counters. Failed/metadata-only API3 responses do
|
||||
not replace the most recent measurements. Nested identity contradictions fail.
|
||||
|
||||
Transactions and database roles
|
||||
------------------------------
|
||||
All discovery writers/read-command claims take bounded locks in a shared order:
|
||||
advisory transaction lock 743190031, SHARE on Agent/Zone/network tables, SHARE ROW
|
||||
EXCLUSIVE on miners, then row locks. These table locks prevent phantom inserts
|
||||
from legacy writers which do not use the advisory lock. Agent routing/check-in
|
||||
uses guarded updates. No miner network I/O occurs while these transactions run.
|
||||
Audit insertion, inventory mutation and the observation decision commit together;
|
||||
any error rolls back all changes. Audit-denial and real UPDATE-trigger failure,
|
||||
concurrent approvals, cross-Zone MAC races, Zone disable and claim-vs-review are
|
||||
covered with real PostgreSQL connections and registered HTTP authorization routes.
|
||||
Lock timeout is 5 seconds; statement timeout 15 seconds. Controlled contention
|
||||
returns conflict/retry. Security favors correctness over fleet throughput here.
|
||||
|
||||
Required release review: validate privileges/lock behavior for the actual deployed
|
||||
DB role in a disposable replica and measure expected fleet load. Tests use an
|
||||
isolated database owner plus a limited role with audit INSERT denied. They do not
|
||||
certify the production role. No new uniqueness constraint is proposed, so do not
|
||||
remove the transaction/table locks without a separately reviewed replacement.
|
||||
|
||||
Reproduction
|
||||
------------
|
||||
Use a disposable checkout of this candidate with an already available backend
|
||||
image containing backend/requirements.txt. Never mount production credentials or
|
||||
.env files. From the candidate root:
|
||||
|
||||
MINEROPS_REVIEW_BACKEND_IMAGE=<existing-image> scripts/test-discovery-security.sh
|
||||
|
||||
The runner creates a unique internal Docker network, tmpfs PostgreSQL 16, no
|
||||
published ports, applies the unchanged existing Alembic chain and runs the two
|
||||
PostgreSQL discovery suites. A trap destroys only its own resources. Tests use
|
||||
simulated Agents and FastAPI's registered routes, without app startup/schedulers.
|
||||
|
||||
PowerShell (isolated PowerShell 7 host/container with simulated/loopback I/O):
|
||||
pwsh -NoProfile -File agents/windows/tests/Test-MinerOpsDiscovery.ps1
|
||||
pwsh -NoProfile -File agents/windows/tests/Test-MinerOpsControl.ps1
|
||||
pwsh -NoProfile -File agents/windows/tests/Test-MinerOpsDiagnosticTransport.ps1
|
||||
|
||||
Browser (Python 3, Node, Playwright and its Chromium/runtime installed externally):
|
||||
python3 backend/tests/browser/export_discovery_page.py . /tmp/discovery-page.html
|
||||
node backend/tests/browser/discovery-security.cjs /tmp/discovery-page.html
|
||||
|
||||
The browser test intercepts all requests, supplies harmless event-marker input,
|
||||
and verifies inert rendering and exact same-IP discovery_id/version selection.
|
||||
Its trailing baseline argument demonstrates the baseline stored-markup defect.
|
||||
|
||||
Other regression suites (explicit disposable DB guards apply):
|
||||
tests.test_stage_a_containment_postgres
|
||||
tests.test_stage_a_delivery_evidence_postgres
|
||||
tests.test_stage_a_schedule_cancellation_postgres
|
||||
tests.test_agent_zones_postgres
|
||||
tests.test_agent_credential_transition_postgres
|
||||
tests.test_agent_enrollment_postgres
|
||||
|
||||
Baseline source-test exceptions retained unchanged
|
||||
-------------------------------------------------
|
||||
backend/tests/test_agent_commands.py:
|
||||
AgentEnrichmentIsolationSourceTests.test_legacy_config_remains_explicitly_legacy_only
|
||||
requires the old minerops_require_agent_secret source string / fixed Plexus config.
|
||||
The authenticated config route already changed before this candidate. Replace the
|
||||
obsolete source assertion with real legacy/authenticated route tests in separate
|
||||
work; do not weaken credential authentication to satisfy the string assertion.
|
||||
|
||||
AgentSourceContractTests.test_deployment_preserves_local_state_and_registers_established_tasks
|
||||
blanket-bans the quoted agent-config.json filename in installer source, although
|
||||
the existing credential-recovery installer legitimately preserves/recovers local
|
||||
configuration. Follow up with behavioral installation/state-preservation tests on
|
||||
Windows. Do not remove the recovery path to satisfy the blanket source assertion.
|
||||
|
||||
Both failures reproduce on unchanged 2d89e80 and unchanged e898f28; both method
|
||||
bodies remain identical. They are not silent skips or weakened assertions here.
|
||||
The external-fixture legacy-retirement PostgreSQL test class also requires its
|
||||
own migration-case setup and is not certified by the discovery test runner.
|
||||
|
||||
Outstanding gates
|
||||
-----------------
|
||||
Independent security review of this exact isolated candidate is required before
|
||||
any Blocker #1 PASS or commit approval. Native Windows PowerShell 5.1/SYSTEM and
|
||||
fleet-scale/production-role validation remain outstanding. Unsupported older
|
||||
Agent payloads fail closed and require coordinated upgrade; this is an intentional
|
||||
protocol compatibility change, not transparent mixed-version support.
|
||||
|
||||
Stage A application containment remains a separate audited boundary. Physical
|
||||
execution is disabled; device identity binding and Blocker #3 Stages B-E remain
|
||||
outstanding. Preserve STAGE_A_OLD_AGENT_CUTOVER.txt: verified network fencing must
|
||||
precede stopping/draining/upgrading old Agents. Cached commands cannot be revoked
|
||||
by server cancellation alone. Offline/unreachable Agents stay blocked; restart,
|
||||
reconnection and rollback must not restore an unsafe executor. This candidate
|
||||
neither performs nor authorizes that cutover or any production deployment.
|
||||
|
||||
Independent-audit remediation addendum (October 16, 2026)
|
||||
-------------------------------------------------------
|
||||
The original candidate (99f515924dfc85fe1e1e98361688f1060329c090) failed independent
|
||||
review for F1-F3 in retained legacy callers. This corrected candidate requires a
|
||||
new independent audit. It does not claim Blocker #1 PASS.
|
||||
|
||||
Retired in Hub AND non-Hub modes, with HTTP 410 after existing authentication and
|
||||
permission checks:
|
||||
POST /api/miners/discover-one
|
||||
POST /api/scan-ranges/{scan_range_id}/discover-now
|
||||
POST /api/scanner/run-known-now
|
||||
POST /api/tools/legacy-summary-refresh-one
|
||||
POST /api/tools/legacy-summary-refresh-missing
|
||||
All definitions/aliases of their identity matcher, discovery writer, scheduler
|
||||
adapters, summary writer and global-IP helper reject before transport/database
|
||||
access. Startup creates no local scanner thread; status reports retired/disabled.
|
||||
Ordinary miners.update therefore cannot use discovery to replace a control board
|
||||
or clear a MAC. The dedicated board workflow and immutable MAC ownership remain.
|
||||
|
||||
Functional implication: legacy local discovery, automatic local refresh, and
|
||||
legacy one/bulk summary backfill are unavailable even outside Hub mode. Operators
|
||||
must use authenticated Agent observation/review and inventory-bound Agent polling.
|
||||
Legacy summary replies lack trustworthy identity; there is no retained unversioned
|
||||
write or in-flight read to revalidate. No denied request mutates inventory, audit,
|
||||
or telemetry. The new shared discovery architecture is preserved.
|
||||
|
||||
CSV remains an explicit global administrative workflow protected by miners.update.
|
||||
Each row requires Miner ID, current MAC, serial, or IP plus Agent Zone ID. Zone may
|
||||
come from the query/UI input or a row column; conflicting selections are rejected.
|
||||
All supplied ID/MAC/serial/IP/Site/Zone evidence must agree with current inventory.
|
||||
A failed MAC/serial/ID never falls back to global IP. Missing inventory is reported
|
||||
unmatched; contradictory or ambiguous selection returns row-specific 409, malformed
|
||||
or insufficient identification 422. A failure rolls back the entire import.
|
||||
IP-only rows need a Zone even when only one global match currently exists. Exact
|
||||
MAC/ID/serial selection remains usable by global administrators across Sites.
|
||||
CSV updates only model/type/firmware, never ownership, IP, MAC or telemetry.
|
||||
Duplicate columns are rejected rather than silently using the last value.
|
||||
|
||||
CSV acquires the existing discovery lock order, including the miners table lock,
|
||||
before resolving any row. It holds the locks through metadata changes and one
|
||||
atomic audit containing every changed inventory ID/namespace and old/new values.
|
||||
Lock contention is bounded by existing discovery timeouts; large fleet throughput
|
||||
and actual deployed-role privileges still require independent release validation.
|
||||
Preview uses identical identification checks and rolls back. Repeated imports do
|
||||
not create inventory or telemetry; each applied request retains its import audit.
|
||||
No migration or containment/control policy changes are part of this remediation.
|
||||
|
||||
Regression entrypoint: scripts/test-discovery-security.sh now includes
|
||||
tests.test_legacy_inventory_security_postgres
|
||||
with registered HTTP requests, actual PostgreSQL locks/concurrent ownership moves,
|
||||
actual audit INSERT denial rollback, retired helper/alias coverage, both modes,
|
||||
permission boundaries, ambiguous/conflicting CSV selection, legitimate imports,
|
||||
preview/replay, and retained historical snapshots. All devices are simulated or
|
||||
transport-patched; the runner uses an internal network and disposable database.
|
||||
Full execution evidence and integrity manifest are retained outside the candidate
|
||||
in the remediation evidence directory referenced by the final report.
|
||||
Reference in new issue
Block a user