FM-600: Proxmox accounts, TLS fingerprint trust, and discovery - #100
Conversation
There was a problem hiding this comment.
7 issues found across 38 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/fleet-api/src/lib.rs">
<violation number="1" location="crates/fleet-api/src/lib.rs:224">
P2: Retries of these newly exposed Proxmox mutations are not idempotent: the create call conflicts, confirmation repeats its write/audit, and deletion returns 404. Thread a caller-scoped `idempotency-key` through the Proxmox mutations or route them through the existing idempotent operation mechanism.</violation>
</file>
<file name="crates/providers/fleet-provider-proxmox/src/lib.rs">
<violation number="1" location="crates/providers/fleet-provider-proxmox/src/lib.rs:361">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
When the handshake fails without a captured certificate, this branch always returns `Connect`, so the documented `NoCertificate` outcome is unreachable and callers cannot distinguish a TLS certificate anomaly from DNS, TCP, or timeout failures. Return `PveTransportError::NoCertificate` for the certificate-less case.</violation>
<violation number="2" location="crates/providers/fleet-provider-proxmox/src/lib.rs:658">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**
When a PVE field exceeds its documented bound, `normalize_resource` silently truncates it, and an out-of-range `vmid` becomes `0`; this contradicts the module’s promise to return a payload error instead of altering discovery data. Validate bounds and return `PveApiError::InvalidPayload` (or an equivalent normalization error) before constructing the resource.</violation>
</file>
<file name="crates/fleet-api/src/proxmox.rs">
<violation number="1" location="crates/fleet-api/src/proxmox.rs:69">
P2: When the PVE token is rejected or revoked, this response tells clients to retry the unchanged request with backoff. Mark authentication failures as non-retryable (`RetryClass::Never`); the operator must replace or repair the stored credential first.</violation>
<violation number="2" location="crates/fleet-api/src/proxmox.rs:79">
P2: When the PVE token lacks a privilege, this catch-all marks the failure retryable with backoff. Add an explicit `Forbidden` source arm with `RetryClass::Never`, while retaining backoff only for transient HTTP or connection failures.</violation>
</file>
<file name="crates/fleet-application/tests/proxmox.rs">
<violation number="1" location="crates/fleet-application/tests/proxmox.rs:300">
P2: This test's name promises the secret is cleared on delete, but it only asserts `list()` is empty. `FakeCredentials.cleared` (line 131) is recorded but never asserted anywhere in the file, and `load` is never checked after delete. Because production `delete` swallows `clear` errors (best-effort in `proxmox.rs`), a regression that stops clearing or leaves the record would still pass this test. Assert that `cleared` contains the account id and/or `load` returns `None` after delete.</violation>
<violation number="2" location="crates/fleet-application/tests/proxmox.rs:428">
P3: The test name says the secret never surfaces in "audit or errors", but the loop only inspects `audit.intents`; no error path is exercised, so an error whose `Debug`/`Display` embeds the secret would pass. Either narrow the name to audit metadata, or force a failure path (e.g. the refusing store in `a_failed_secret_write_removes_the_account`) and assert `format!("{error:?}")` and the rendered intent do not contain the secret.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| let name = entry | ||
| .get("name") | ||
| .and_then(serde_json::Value::as_str) | ||
| .map(|value| value.chars().take(256).collect()); |
There was a problem hiding this comment.
P1: Custom agent: Flag AI Slop and Fabricated Changes
When a PVE field exceeds its documented bound, normalize_resource silently truncates it, and an out-of-range vmid becomes 0; this contradicts the module’s promise to return a payload error instead of altering discovery data. Validate bounds and return PveApiError::InvalidPayload (or an equivalent normalization error) before constructing the resource.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/providers/fleet-provider-proxmox/src/lib.rs, line 658:
<comment>When a PVE field exceeds its documented bound, `normalize_resource` silently truncates it, and an out-of-range `vmid` becomes `0`; this contradicts the module’s promise to return a payload error instead of altering discovery data. Validate bounds and return `PveApiError::InvalidPayload` (or an equivalent normalization error) before constructing the resource.</comment>
<file context>
@@ -1,6 +1,731 @@
+ let name = entry
+ .get("name")
+ .and_then(serde_json::Value::as_str)
+ .map(|value| value.chars().take(256).collect());
+ let status = entry
+ .get("status")
</file context>
|
All 31 findings addressed in 980f2c2: P1s
P2s
P3s
|
There was a problem hiding this comment.
2 issues found across 15 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/fleet-storage-sqlite/migrations/0017_proxmox_accounts.sql">
<violation number="1" location="crates/fleet-storage-sqlite/migrations/0017_proxmox_accounts.sql:14">
P1: Upgrading a database that already applied the previous `0017` now fails at startup because SQLx detects that this migration’s checksum changed. Add a new `0018` migration containing `ALTER TABLE proxmox_accounts ADD COLUMN observed_fingerprint TEXT NOT NULL DEFAULT ''` and leave `0017` unchanged.</violation>
</file>
<file name="crates/fleet-api/src/proxmox.rs">
<violation number="1" location="crates/fleet-api/src/proxmox.rs:272">
P3: `limit=0` means 'default 50' on this endpoint but 'empty page' on `machines list`/`operations list`, which use `params.limit.unwrap_or(DEFAULT_PAGE_LIMIT)` and truncate with `Some(0)`. The same query parameter has different meaning across the published contract. Either drop the `.filter(|limit| *limit > 0)` to match the sibling endpoints, or update the sibling handlers and the shared doc so `0` means default everywhere.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| port INTEGER NOT NULL, | ||
| token_id TEXT NOT NULL, | ||
| fingerprint TEXT NOT NULL DEFAULT '', | ||
| observed_fingerprint TEXT NOT NULL DEFAULT '', |
There was a problem hiding this comment.
P1: Upgrading a database that already applied the previous 0017 now fails at startup because SQLx detects that this migration’s checksum changed. Add a new 0018 migration containing ALTER TABLE proxmox_accounts ADD COLUMN observed_fingerprint TEXT NOT NULL DEFAULT '' and leave 0017 unchanged.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fleet-storage-sqlite/migrations/0017_proxmox_accounts.sql, line 14:
<comment>Upgrading a database that already applied the previous `0017` now fails at startup because SQLx detects that this migration’s checksum changed. Add a new `0018` migration containing `ALTER TABLE proxmox_accounts ADD COLUMN observed_fingerprint TEXT NOT NULL DEFAULT ''` and leave `0017` unchanged.</comment>
<file context>
@@ -2,13 +2,16 @@
port INTEGER NOT NULL,
token_id TEXT NOT NULL,
fingerprint TEXT NOT NULL DEFAULT '',
+ observed_fingerprint TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL
) STRICT;
</file context>
| // more than it returns. | ||
| let limit = params | ||
| .limit | ||
| .filter(|limit| *limit > 0) |
There was a problem hiding this comment.
P3: limit=0 means 'default 50' on this endpoint but 'empty page' on machines list/operations list, which use params.limit.unwrap_or(DEFAULT_PAGE_LIMIT) and truncate with Some(0). The same query parameter has different meaning across the published contract. Either drop the .filter(|limit| *limit > 0) to match the sibling endpoints, or update the sibling handlers and the shared doc so 0 means default everywhere.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/fleet-api/src/proxmox.rs, line 272:
<comment>`limit=0` means 'default 50' on this endpoint but 'empty page' on `machines list`/`operations list`, which use `params.limit.unwrap_or(DEFAULT_PAGE_LIMIT)` and truncate with `Some(0)`. The same query parameter has different meaning across the published contract. Either drop the `.filter(|limit| *limit > 0)` to match the sibling endpoints, or update the sibling handlers and the shared doc so `0` means default everywhere.</comment>
<file context>
@@ -229,25 +243,50 @@ pub struct ConfirmProxmoxFingerprintRequest {
+ // more than it returns.
+ let limit = params
+ .limit
+ .filter(|limit| *limit > 0)
+ .unwrap_or(DEFAULT_PAGE_LIMIT)
+ .min(MAX_PAGE_LIMIT);
</file context>
…unds, migration split, regression tests
|
Second round addressed in 2575b87:
|
Closes #99 (FM-600).
Implementation approach (from the issue)
FM-S08 chose the fallback: a small
reqwesttransport with a custom rustls verifier that pins the host certificate's SHA-256 fingerprint, typed provider DTOs at the boundary, and Fleet-owned UPID handling. This PR delivers the accounts/trust/discovery slice.What landed
fleet-provider-proxmox):PveTransportport +ReqwestPveTransporton reqwest 0.12 + rustlsring— the TLS stack the tailscale provider already uses, no new dependency class, no duplicated reqwest major, no aws-lc-rs. ThePinningVerifierSHA-256-pins the leaf certificate and refuses everything else at the handshake; verification is never disabled. An unpinned host is probed with an observe-only policy that captures the leaf fingerprint and refuses before any credential is sent — the FM-201 SSH pin/decide/confirm flow over TLS. Decoding is tolerant of PVE's loose/null shapes (all-Option,data: null= empty, per-resource isolation into warnings).fleet-application/src/proxmox.rs): multi-accountProxmoxAccountrecords (name, host, port, token id, fingerprint state) behindproxmox.read/proxmox.config(catalog 36 → 38); the token secret lives in the encrypted secret store asproxmox/<account-id>and is resolved just in time; a failed secret write removes the half-made account. The explicit-trust gate: discovery is locked until the fingerprint is confirmed (proxmox_unconfirmed), and a pinned host whose certificate changes reports the mismatch with both fingerprints as evidence (proxmox_fingerprint_mismatch) — never an empty list.0017_proxmox_accounts(unique names, fingerprint empty until confirmed);ProxmoxAccountRepositoryover the application port./api/v1/proxmox/accounts{,/{id}{,/observe,/confirm,/discovery}}in the OpenAPI doc (client regenerated),fleetctl proxmox accounts|create|delete|observe|confirm|discover(secret read from stdin, never argv), all from the same use cases.token_idkey (contains "token") — provenance is the account name, credential material stays out entirely.Tests
FLEET_PVE_LIVE=1: pinned connect, wrong-fingerprint refusal at the handshake, observe-only refusal capturing the fingerprint.cargo xtask verifypasses; PVE 8.x evidence remains the recorded FM-S08 deviation on the real-cluster suite.Non-goals (respected)
Guest lifecycle (epic #11), associations/guest-agent data (epic #10), template/clone/snapshot (epic #12), Lab leases.
Summary by cubic
Closes FM-600. Fleet previously had no Proxmox integration; this PR adds multi-account Proxmox management with TLS certificate fingerprint pinning and cluster discovery.
What changed
observecaptures a fingerprint without sending credentials;confirmpins that fingerprint, and discovery stays locked until confirmed./api/v1/proxmox/accountsendpoints andfleetctl proxmoxcommands for listing, creating, observing, confirming, discovering, and deleting accounts.proxmox.configpermission; reads requireproxmox.read; account create, confirm, and delete emit audit events.0017_proxmox_accountsand0018_proxmox_observed_fingerprintcreate the schema automatically on startup.Written for commit 2575b87. Summary will update on new commits.