Skip to content

trident-acl-agent: verify Node systemUUID matches local hardware - #837

Open
bfjelds (bfjelds) wants to merge 13 commits into
mainfrom
user/bfjelds/acl-agent-node-uuid-verify
Open

bfjelds (bfjelds) wants to merge 13 commits into
mainfrom
user/bfjelds/acl-agent-node-uuid-verify

Conversation

@bfjelds

@bfjelds bfjelds (bfjelds) commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Fixes trident-acl-agent so a stale/recycled Node name can't be mistaken for this machine's own Node object. Opt-in, disabled by default.

Change

  • NodeClient::get_node and NodeClient::watch_node can verify the fetched Node's status.nodeInfo.systemUUID against this host's own /sys/class/dmi/id/product_uuid (the same source kubelet uses to populate systemUUID). On mismatch, logs a warning with both UUIDs and returns K8sClientError::NodeGone, so every caller - connection check, startup recovery, reconcile loop, and the long-lived watch stream - treats a misidentified Node exactly like a missing one.
  • Verification is skipped (logged at warn), not treated as a mismatch, when the Node's systemUUID is empty or the local product_uuid can't be read - either case only means the UUID is undetermined, not that the Node describes a different machine.
  • Gated behind TRIDENT_ACL_AGENT_KUBERNETES_VALIDATE_NODE_UUID, a real boolean config field (loaded via envy, like every other trident-acl-agent setting) that must be set to the literal string true to enable verification. Disabled (false) by default: the assumption that kubelet's systemUUID always traces back to this machine's product_uuid hasn't been confirmed across every environment this agent runs in, so it's opt-in per-deployment rather than unconditional.
  • Extracted the DMI product_uuid path/read into a shared osutils::dmi helper, now reused by trident-acl-agent and trident's tracestream metadata instead of each defining its own copy of the path and read-with-fallback logic.
  • Documented the config field and behavior (including watch-stream coverage) in docs/Explanation/Trident-ACL-Agent.md.

Testing

NodeClient::get_node now compares the fetched Node's
status.nodeInfo.systemUUID against this host's own hardware
identity (/sys/class/dmi/id/product_uuid, the same source kubelet
uses to populate systemUUID). On mismatch, logs a warning with both
UUIDs and returns K8sClientError::NodeGone, so the agent treats a
misidentified Node the same as a missing one (retry/await recreation)
instead of acting on a Node object that does not describe this
machine.

If the local product UUID cannot be read, verification is skipped
(logged at warn) rather than failing closed.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 16:54
@bfjelds
bfjelds (bfjelds) requested a review from a team as a code owner October 8, 2026 16:54
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Watched Node objects bypass identity verification, allowing reconciliation against recycled Nodes.

2 open findings
What changed in this PR

Adds hardware UUID validation to prevent the ACL agent from accepting a stale Kubernetes Node object.

Changes:

  • Compares Node and local DMI system UUIDs.
  • Treats mismatches as NodeGone.
  • Adds focused identity-verification tests.
File Description
crates/​trident-acl-agent/​src/​annotations/​k8s.rs Adds Node identity verification and tests.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/trident-acl-agent/src/annotations/k8s.rs Outdated
Comment thread crates/trident-acl-agent/src/annotations/k8s.rs Outdated
NodeClient's new systemUUID verification (previous commit) treats
any Node whose status.nodeInfo.systemUUID doesn't match the local
/sys/class/dmi/id/product_uuid as not found. The storm E2E harness's
fake apiserver never set systemUUID on its seeded Node, so every
get_node call against it would now return NodeGone and run-ab-update,
run-rollback, and run-node-resilience would all fail immediately.

Fix: NewSeedNode takes the real systemUUID to seed onto the fake
Node, and each of the three call sites (RunABUpdate, RunRollback,
RunNodeResilience) reads it from the VM over SSH
(/sys/class/dmi/id/product_uuid) before constructing the node store.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 17:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Watch events bypass identity verification and can still trigger servicing for a same-name replacement Node.

2 open findings

🧠 Review effort: Balanced

Adds phase 3 to run-node-resilience: sets the fake Node's
status.nodeInfo.systemUUID to a value that does not match the VM's
real /sys/class/dmi/id/product_uuid, restarts trident-acl-agent, and
confirms the startup Node read (a) logs the mismatch distinctly from
a plain 404 and (b) still funnels into the same NodeGone /
await_node_recreation path as phases 1-2 (no crash, same PID,
resumes once the systemUUID matches again).

NodeStore gains SetSystemUUID for this, mirroring SetReadyCondition.
New fast unit tests in apiserver_test.go cover the fake apiserver
seeding/serving systemUUID correctly, independent of the full VM
harness.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 18:04
@bfjelds

Copy link
Copy Markdown
Member Author

Added run-node-resilience phase 3 (storm E2E) + fast Go unit tests covering the systemUUID-mismatch case end-to-end: sets a mismatched systemUUID on the fake Node, restarts trident-acl-agent, and confirms it (a) logs the mismatch distinctly from a plain 404 and (b) treats it the same as NodeGone — no crash, same PID, resumes once the systemUUID matches again. See tools/storm/aclagent/tests/node_resilience.go and proxies/apiserver_test.go.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Long-lived watch events bypass identity verification and can still trigger operations for mismatched hardware.

2 open findings

🧠 Review effort: Balanced

cadvisor (which kubelet uses to populate status.nodeInfo.systemUUID)
reads /sys/class/dmi/id/product_uuid directly, same as our local check.
But if cadvisor fails to read that file on the kubelet side, it logs an
error and still lets node registration proceed with an empty
systemUUID rather than failing - an empty value is evidence kubelet
could not determine the UUID, not evidence the Node describes a
different machine. Treat it the same as an unreadable local file: skip
verification instead of treating it as a mismatch, to avoid a false
positive that would otherwise lock us out of a healthy node forever.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 18:27
@bfjelds

Copy link
Copy Markdown
Member Author

Pushed a follow-up fix after investigating whether AKS nodes guarantee status.nodeInfo.systemUUID matches /sys/class/dmi/id/product_uuid:

Confirmed via cadvisor source that kubelet populates systemUUID by reading the exact same file we check against. However, if cadvisor fails to read that file on the kubelet side, it logs an error but still lets node registration proceed with an empty systemUUID rather than failing. An empty value only means kubelet could not determine the UUID - it is not evidence the Node describes a different machine.

Updated verify_node_identity to skip verification (warn + Ok) when the Node reports an empty systemUUID, same as when our local file is unreadable, instead of treating it as a mismatch. Without this, a rare kubelet-side DMI read failure could have falsely locked us out of an otherwise-healthy node forever.

Updated/added unit tests accordingly (missing_node_system_info_skips_verification, empty_node_system_uuid_skips_verification).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Watch-delivered Nodes still bypass identity verification and can trigger reconciliation for mismatched hardware.

3 open findings

🧠 Review effort: Balanced

Comment thread tools/storm/aclagent/proxies/apiserver.go Outdated
/sys/class/dmi/id/product_uuid is root-only (0400). readVmProductUUID
ran a plain cat over SSH as the non-root test user, which failed with
Permission denied and aborted run-node-resilience before it could even
start (build 1220454: \cat: /sys/class/dmi/id/product_uuid: Permission
denied\). Every other privileged command in this harness is already
prefixed with sudo; this one was missed.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 18:40
@bfjelds

Copy link
Copy Markdown
Member Author

Fixed the storm run-node-resilience failure from build 1220454: readVmProductUUID ran a plain cat /sys/class/dmi/id/product_uuid over SSH as the non-root test user, which hit Permission denied (file is 0400 root-only) and aborted the whole aclagent scenario before any assertions ran. Every other privileged command in this harness already uses sudo; added it here too.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Watch-delivered Nodes remain unverified and can still trigger servicing against recycled hardware.

3 open findings

🧠 Review effort: Balanced

cargo fmt --check failed in CI (build 1220474, Check amd64 job) on the
single-line warn! added in the previous commit - rustfmt collapses it
onto one line rather than wrapping it.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 18:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Watch-delivered Nodes still bypass identity validation and can trigger reconciliation for recycled hardware.

3 open findings

🧠 Review effort: Balanced

Make the systemUUID-vs-local-product_uuid check opt-in via
TRIDENT_ACL_AGENT_VALIDATE_NODE_UUID (presence-only, same convention as
osutils::container::DOCKER_ENVIRONMENT). Disabled by default: the
assumption that kubelet systemUUID always traces back to this machines
own product_uuid is well-supported in general but has not been
confirmed across every environment this agent runs in, so it should be
validated and opted into per-deployment rather than enabled
unconditionally.

Enabled it for the storm aclagent E2E scenario via the
trident-acl-agent.service override.conf drop-in baked into both the
base and update VM images, so run-node-resilience Phase 3 (and the
general NodeGone handling) continues to exercise this path.

Documented the new env var and behavior in
docs/Explanation/Trident-ACL-Agent.md.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The opt-in validation is consistently applied, documented, and covered by unit and VM-level tests.

1 open finding
5 resolved since last review

🧠 Review effort: Balanced

Addresses the remaining open item on PR #837: RunNodeResilience now has
a Phase 4 that triggers a systemUUID mismatch via NodeStore.SetSystemUUID
with the agent already running and watching - no restart - proving
NodeClient::watch_node catches it via verify_node_identity exactly like
phase 3 proves for the explicit-GET path, and that the same
NodeGone/recovery handling applies with a provably stable MainPID
throughout (no restart happened).

Validated end-to-end against a live VM: rebuilt trident-acl RPM from
this branch (v9882e27c), rebuilt trident-vm-acl-agent-testimage.qcow2
via Image Customizer (the prior artifact predated
TRIDENT_ACL_AGENT_VALIDATE_NODE_UUID being baked into the image config
and could not have exercised phase 3 or 4 at all), and ran the full
aclagent storm suite:

  deploy-vm..........: PASS
  check-deployment...: PASS
  run-node-resilience: PASS   (all 4 phases, including the new one)
  run-ab-update......: PASS
  run-rollback.......: PASS
  collect-logs.......: PASS
  cleanup-vm.........: PASS

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 21:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Both presence-only environment checks incorrectly disable validation when the variable contains non-UTF-8 bytes.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Use Unicode-safe environment presence check in get_node

crates/​trident-acl-agent/​src/​annotations/​k8s.rs:97

Issue: This presence-only gate still depends on the value being valid Unicode. Evidence: std::env::var returns Err(VarError::NotUnicode) for a present non-UTF-8 value, so get_node silently disables validation despite the documented “value is ignored” contract. Suggestion: use var_os(...).is_some() to test presence without decoding the value.

This issue also appears on line 170 of the same file.

🧠 Review effort: Balanced

Addresses review feedback on PR #837:

TRIDENT_ACL_AGENT_VALIDATE_NODE_UUID was implemented as a raw
std::env::var(...).is_ok() presence check in k8s.rs, bypassing the
envy-based AgentConfig/KubernetesConfig system every other
trident-acl-agent setting goes through. Two consequences:

- Non-Unicode safety: std::env::var() returns Err for a present value
  that isn't valid UTF-8, silently disabling verification despite the
  documented "presence-only, value ignored" contract.
- The var's name didn't follow the established
  TRIDENT_ACL_AGENT_<SECTION>_<FIELD> convention (no section segment),
  because there was no existing boolean-flag precedent in this config
  system to follow in the first place.

Renamed to TRIDENT_ACL_AGENT_KUBERNETES_VALIDATE_NODE_UUID and added a
real `validate_node_uuid: bool` field to KubernetesConfig/
RawKubernetesConfig, loaded through the same envy::prefixed(...)
mechanism as every other Kubernetes setting (explicit "true"/"false",
default false, empty value falls back to the default like every other
field here). NodeClient now stores this as a plain bool at
construction time instead of re-reading the environment on every
get_node/watch_node call.

Updated the baked-in systemd override.conf, docs, and storm e2e
harness comments to the new name. Validated end-to-end against a live
VM: rebuilt the trident-acl RPM and trident-vm-acl-agent-testimage.qcow2
with this change, and re-ran the full aclagent storm suite - all 7
test cases pass, including run-node-resilience's systemUUID mismatch
phases (3 and 4), now driven by the new config field instead of the
old env var.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 23:04
@bfjelds

Copy link
Copy Markdown
Member Author

Addressed in 7dc0304 — and more thoroughly than just the var_os swap originally suggested. Migrated TRIDENT_ACL_AGENT_VALIDATE_NODE_UUID onto this crate's established envy-based config system: renamed to TRIDENT_ACL_AGENT_KUBERNETES_VALIDATE_NODE_UUID, added a real validate_node_uuid: bool field to KubernetesConfig, loaded through envy::prefixed(...) like every other Kubernetes setting. NodeClient now stores this as a plain bool at construction instead of re-reading the environment on every get_node/watch_node call, which also fixes the Unicode-safety gap for free (envy's std::env::vars()-based collection skips non-UTF8 pairs rather than erroring, consistent with "malformed/absent" handling everywhere else in this config system).

Validated end-to-end against a live VM: rebuilt the trident-acl RPM and trident-vm-acl-agent-testimage.qcow2 with this change, and re-ran the full storm-trident run aclagent suite - all 7 test cases pass, including run-node-resilience's systemUUID mismatch phases (3 and 4), now driven by the new config field.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Kubernetes writes remain vulnerable to a Node-recycling race despite the new read validation.

1 open finding
Previously missed (1)

In code that hasn't changed since last review

Medium severity Validate node identity before writes and enforce atomic preconditions

crates/​trident-acl-agent/​src/​annotations/​k8s.rs:92

Issue: The identity check protects GET/watch inputs but not writes, so a Node name recycled during an in-flight operation can still be mutated by this host. Evidence: servicing pauses watch consumption while reconcile_node runs, and run_with_status_heartbeat calls publish_status repeatedly; patch_node_metadata patches by name without verifying identity or using a precondition. A replacement can therefore receive this machine's status annotations before its queued watch event is checked. Suggestion: when validation is enabled, base each PATCH on a verified read and add an atomic server-side precondition (for example, the verified object's resourceVersion), mapping mismatch/conflict to NodeGone.

🧠 Review effort: Balanced

Comment thread docs/Explanation/Trident-ACL-Agent.md
Comment thread crates/osutils/src/dmi.rs Outdated
Comment thread crates/osutils/src/dmi.rs Outdated
Comment thread crates/osutils/src/dmi.rs Outdated
Comment thread crates/trident-acl-agent/src/annotations/k8s.rs Outdated
Comment thread crates/trident-acl-agent/src/annotations/k8s.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation and coverage are coherent; only a non-blocking inaccurate test comment remains.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread tools/storm/aclagent/tests/node_resilience.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An empty readable local UUID incorrectly causes an indefinite NodeGone recovery loop.

2 open findings

🧠 Review effort: Balanced

Comment thread crates/trident-acl-agent/src/annotations/k8s.rs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d61c42e7-3c43-4fe5-8c89-89d38ff9528b
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pending-commit startup recovery can still perform side effects and publish to a mismatched Node before UUID validation occurs.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

Comment on lines +90 to +92
let node = self.api.get(name).await.map_err(map_kube_error)?;
if self.validate_identity {
verify_node_identity(&node, name, dmi::read_product_uuid())?;
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d61c42e7-3c43-4fe5-8c89-89d38ff9528b
Copilot AI balanced review requested due to automatic review settings October 10, 2026 18:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The shared reader changes the established telemetry asset ID representation for valid uppercase UUIDs.

1 open finding
Previously missed (1)

In code that hasn't changed since last review

Medium severity UUID normalization changes telemetry asset_id representation

crates/​trident/​src/​logging/​tracestream.rs:355

Issue: This changes the existing telemetry asset_id representation instead of only sharing its reader. Evidence: dmi::read_product_uuid parses the raw text into OsUuid, whose Display canonicalizes valid UUIDs to lowercase (crates/sysdefs/src/osuuid.rs:36-40,57-62); the removed reader returned the trimmed sysfs text verbatim. An uppercase product UUID therefore acquires a different asset ID after upgrade, and PLATFORM_INFO is reused in telemetry and diagnostics. Suggestion: preserve the trimmed raw value for the telemetry path and parse/normalize only for the Kubernetes identity comparison, or expose separate raw and typed DMI readers.

🧠 Review effort: Balanced

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.

3 participants