Skip to content

acl-agent: make node_name source (hostname vs kubelet cert CN) configurable - #839

Merged
bfjelds (bfjelds) merged 14 commits into
mainfrom
user/bfjelds/acl-agent-node-name-source
Oct 22, 2026
Merged

bfjelds (bfjelds) merged 14 commits into
mainfrom
user/bfjelds/acl-agent-node-name-source

Conversation

@bfjelds

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

Copy link
Copy Markdown
Member

Why

Allow deployments to derive the Node name from the kubelet client certificate instead of relying on the hostname convention.

Changes

  • Add TRIDENT_ACL_AGENT_KUBERNETES_NODE_NAME_SOURCE: hostname (default) or kubelet-cert. An explicit NODE_NAME override takes precedence.
  • Derive the certificate identity from its system:node:<name> CN.
  • Retry certificate resolution in the orchestrator with cancellation-aware capped exponential backoff. Never fall back to hostname. The one-shot --validate-connection kubernetes diagnostic fails immediately on resolution errors.
  • Add parsing, precedence and retry unit tests, plus Storm checks using phase-specific API servers and exact resolved-name assertions.

Validation at 777fb02e

CI build 1221392 succeeded

@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.

@bfjelds

Copy link
Copy Markdown
Member Author

Added to the scenario: proves actually switches node_name resolution between bfjelds-xubuntu and , via one-shot CLI invocations against a self-signed fake kubelet client cert - no service restart or reboot needed, so it stays fast relative to the rest of the scenario.\n\nCovers: kubelet-cert resolves correctly; hostname/unset resolves to the VMs real hostname and fails against this test ' s single-node fake apiserver; an explicit override still wins regardless of source; a missing/unreadable cert degrades kubelet-cert to the hostname behavior with a logged warning rather than erroring out some other way.\n\nVerified: (unchanged), and clean for .

@bfjelds

Copy link
Copy Markdown
Member Author

Added run-node-name-source to the storm-trident aclagent scenario: proves TRIDENT_ACL_AGENT_KUBERNETES_NODE_NAME_SOURCE actually switches node_name resolution between hostname and kubelet-cert, via one-shot --validate-connection kubernetes CLI invocations against a self-signed fake kubelet client cert - no service restart or reboot needed, so it stays fast relative to the rest of the scenario.

Covers: kubelet-cert resolves correctly; hostname/unset resolves to the VM's real hostname and fails against this test's single-node fake apiserver; an explicit NODE_NAME override still wins regardless of source; a missing/unreadable cert degrades kubelet-cert to the hostname behavior with a logged warning rather than erroring out some other way.

Verified: cargo build/fmt/clippy -p trident-acl-agent (unchanged), go build ./... and gofmt -l clean for tools/.

@bfjelds

Copy link
Copy Markdown
Member Author

Rebuilt binaries/RPMs/VM images and ran the full storm-trident aclagent scenario end-to-end on bfjelds-xubuntu to validate the new test:

=== SUMMARY of storm-trident::scenario::aclagent ===
  deploy-vm...........: PASS
  check-deployment....: PASS
  run-node-resilience.: PASS
  run-node-name-source: PASS
  run-ab-update.......: PASS
  run-rollback........: PASS
  collect-logs........: PASS
  cleanup-vm..........: PASS
=== RESULT ===
OK: passed: 8; total: 8

Along the way, fixed a bug in run-node-name-source's case 4 (missing-cert fallback check): stormssh.SshCommandCombinedOutput discards the captured remote output on a non-zero exit (innerSshCommand returns ("", err)) - the actual stdout/stderr only survives inside the wrapped error's own message. The test now checks err.Error() instead of the (always-empty-on-error) output string. First run caught this for real (failed with an empty "got:" message); fixed and reran clean.

@bfjelds

Copy link
Copy Markdown
Member Author

Addressed review feedback on run-node-name-source (two good catches: the explicit-override check reused the same node name as the cert-derived test, making its success ambiguous in logs; and the hostname checks only proved source-selection via expected failures rather than asserting the actual resolved value).

Redesigned into three sequential phases, each with its own single-node fake apiserver (the fake only ever serves one Node name at a time) seeded with exactly the identity that phase expects:

  • kubelet-cert source -> apiserver seeded with the cert's Subject CN
  • hostname source (explicit + default) -> apiserver seeded with the VM's real hostname, read live via ssh ... hostname rather than assumed equal to --node-name; a missing/unreadable cert's fallback is checked against this same phase, proving it degrades to the real hostname specifically
  • explicit NODE_NAME override -> apiserver seeded with a third, unrelated literal (node-name-source-hostname-override-node)

Every check now asserts the exact Node name --validate-connection's own success output reports fetching (fetched Node "<name>"), via a new expectValidateConnectionOutput helper - not just pass/fail. A phase can only pass by resolving to the value it specifically claims.

Verified end-to-end on bfjelds-xubuntu again: full storm-trident aclagent scenario, 8/8 PASS.

@bfjelds

Copy link
Copy Markdown
Member Author

CI validation: build 1221319 on [GITHUB]-trident-pr-e2e, commit e566aae9 — SUCCEEDED, full storm-trident aclagent scenario 8/8 PASS.

run-node-name-source confirmed doing exactly what's intended, straight from the log:

Phase Env vars Resolved to
kubelet-cert NODE_NAME_SOURCE=kubelet-cert "node-name-source-test-cert-node"
hostname NODE_NAME_SOURCE=hostname "trident-acl-agent-testimg" (VM's real hostname)
missing-cert fallback cert hidden, NODE_NAME_SOURCE=kubelet-cert WARN ... falling back to hostname → "trident-acl-agent-testimg"
explicit override NODE_NAME=node-name-source-hostname-override-node, SOURCE=hostname "node-name-source-hostname-override-node"

All three distinct literals resolved correctly and independently, matching the per-phase apiserver design. No further changes needed.

@bfjelds

Copy link
Copy Markdown
Member Author

Addressed feedback: kubelet-cert source failing to read/parse the cert should not fall back to hostname — it should be treated like a "node not found" condition and fail hard instead.

Changed:

  • default_node_name() is now fallible (Result<String, Error>); AgentConfig::from_vars propagates a kubelet-cert resolution failure as a fatal failed to determine node_name config error — same severity as any other malformed/unsatisfiable TRIDENT_ACL_AGENT_* setting, no silent degrade to a possibly-wrong identity.
  • impl Default for KubernetesConfig now calls hostname_node_name() directly (NodeNameSource::default() is always Hostname, which can't fail), since Default::default() itself must stay infallible.
  • run-node-name-source's missing-cert check now asserts this fatal error (via the error text, since stormssh.SshCommandCombinedOutput discards output on a non-zero exit) instead of a successful hostname fallback.
  • Updated Trident-ACL-Agent.md, the storm README, and TridentAclAgent-Tests.md to describe the fail-hard behavior.

Verified on bfjelds-xubuntu: cargo test/fmt/clippy -p trident-acl-agent clean (16 config tests), full storm-trident aclagent scenario 8/8 PASS with the new behavior exercised end-to-end.

@bfjelds

Copy link
Copy Markdown
Member Author

To directly answer the question: previously, no — exponential backoff retry was NOT happening. A missing/unreadable kubelet-cert cert made AgentConfig::from_vars return Err at config-load time in main.rs (AgentConfig::from_env()?), which exits the process immediately - before the orchestrator, get_node_with_retry, or await_node_recreation (the existing NodeGone retry machinery) ever runs. systemd's Restart=on-failure/fixed RestartSec=5 would then crash-loop at a constant interval, not exponential backoff.

Fixed: an unreadable kubelet-cert at startup is now treated the same way the agent already treats its own Node object not existing yet (NodeGone) - a plausible, expected startup race (kubelet's own TLS bootstrapping not finished yet) to retry and wait out, not a fatal crash.

Changes:

  • KubernetesConfig.node_name → node_name_override: Option<String> (explicit override only). node_name is no longer eagerly resolved during config loading, which is infallible again for this field.
  • New KubernetesConfig::resolve_node_name_once(): a single, fail-fast resolution (override, else node_name_source). Used directly by --validate-connection kubernetes, where fail-fast remains correct for a one-shot diagnostic.
  • Orchestrator gains its own resolved node_name: String field and resolve_node_name_with_retry(), which calls resolve_node_name_once() in a loop using the same NODE_READ_BACKOFF/NODE_READ_BACKOFF_MAX capped-exponential-backoff policy as get_node_with_retry - racing every sleep against shutdown, exactly like the existing NodeGone handling.
  • Orchestrator::from_config now takes &CancellationToken and returns Result<Option<Self>, Error> (None on shutdown during retry), mirroring recover_from_trident_state/await_node_recreation's own early-exit shape.
  • New Rust unit tests prove the retry loop actually retries (Ok(None) on a cancelled shutdown, not an immediate Err) for kubelet-cert, and resolves without any retry for hostname/explicit override.
  • Updated run-node-name-source's missing-cert phase to check --validate-connection's fail-fast error text (failed to resolve node_name) - the long-running retry loop itself is covered by the new Rust unit tests instead of the Go/storm scenario, since proving an indefinite retry needs a controllable cert path (deliberately out of scope here per the earlier discussion) to flip from missing→present without flaking on real timing.
  • Docs updated (Trident-ACL-Agent.md, storm README, TridentAclAgent-Tests.md).

Verified on bfjelds-xubuntu: cargo test -p trident-acl-agent (206 tests, including 3 new retry-behavior tests) + fmt/clippy all clean; full storm-trident aclagent scenario 8/8 PASS.

@bfjelds

Copy link
Copy Markdown
Member Author

CI validation: build 1221392 on [GITHUB]-trident-pr-e2e, commit 777fb02e (the retry-with-backoff change) — SUCCEEDED.

Full storm-trident aclagent scenario: 8/8 PASS.

=== RESULT ===
OK: passed: 8; total: 8

run-node-name-source passed in ~5s, confirming --validate-connection kubernetes's fail-fast behavior is unchanged after moving node_name resolution out of eager config-loading (KubernetesConfig::resolve_node_name_once) — no regression from the refactor. As expected, this VM scenario doesn't exercise the new Orchestrator::resolve_node_name_with_retry backoff loop itself; that's covered by the 3 new Rust unit tests in this same commit (cargo test -p trident-acl-agent), run separately in CI's build/test stage.

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

The description contradicts the final behavior, and several tests depend on the host lacking a readable kubelet certificate.

2 open findings
What changed in this PR

Adds configurable ACL-agent Kubernetes node-name resolution using either the hostname or kubelet certificate CN.

Changes:

  • Adds node-name source configuration and certificate parsing.
  • Adds retry behavior and connection-validation handling.
  • Adds unit, Storm integration, and documentation coverage.
File Description
tools/​storm/​aclagent/​trident.go Registers the new Storm test.
tools/​storm/​aclagent/​tests/​node_name_source.go Tests node-name source behavior end to end.
tools/​storm/​aclagent/​README.md Documents the Storm test.
docs/​Explanation/​Trident-ACL-Agent.md Documents the environment variable.
docs/​Development/​Testing/​TridentAclAgent-Tests.md Updates scenario documentation.
crates/​trident-acl-agent/​src/​main.rs Handles shutdown during resolution.
crates/​trident-acl-agent/​src/​core/​config.rs Implements source configuration and certificate parsing.
crates/​trident-acl-agent/​src/​connection_check.rs Resolves node names for diagnostics.
crates/​trident-acl-agent/​src/​annotations/​orchestrator.rs Adds retrying node-name resolution.
crates/​trident-acl-agent/​Cargo.toml Adds the OpenSSL dependency.
Cargo.lock Records the dependency update.

🧠 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/core/config.rs Outdated
Comment thread crates/trident-acl-agent/src/core/config.rs

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.

Comment thread crates/trident-acl-agent/src/core/config.rs
Comment thread tools/storm/aclagent/tests/node_name_source.go
Comment thread tools/storm/aclagent/tests/node_name_source.go
@bfjelds
bfjelds (bfjelds) marked this pull request as ready for review October 14, 2026 15:14
@bfjelds
bfjelds (bfjelds) requested a review from a team as a code owner October 14, 2026 15:14
Copilot AI balanced review requested due to automatic review settings October 14, 2026 16:19

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

Certificate resolution does not validate the required system:nodes organization, and several comments inaccurately describe runtime behavior.

2 open findings
4 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Enforce system:nodes organization in certificate identity

crates/​trident-acl-agent/​src/​core/​config.rs:569

Issue: Certificate-derived identity does not enforce the required system:nodes organization. Evidence: This code reads only the first Subject CN, so a certificate with CN=system:node:<name> and a missing or different O is accepted even though the PR contract requires both fields. Suggestion: Reject certificates whose Subject has no system:nodes organization and add a negative test for a valid-looking CN with the wrong O.

Low severity Clarify testing guide's shared setup restart

docs/​Development/​Testing/​TridentAclAgent-Tests.md:277

Issue: This testing guide says no service restart is involved, but shared setup performs one unconditional restart before the phases. Evidence: RunNodeNameSource invokes prepareVmForAclAgent, whose command includes systemctl restart trident-acl-agent.service. Suggestion: Clarify that no additional restart or reboot occurs during the assertion phases.

Low severity Correct scenario description of shared service restart

tools/​storm/​aclagent/​README.md:155

Issue: The scenario description incorrectly says the service is never touched or restarted. Evidence: RunNodeNameSource calls prepareVmForAclAgent, which unconditionally runs systemctl restart trident-acl-agent.service; only the individual assertion phases avoid further restarts. Suggestion: Document the one shared setup restart while retaining the no-per-phase-restart distinction.

🧠 Review effort: Balanced

Comment thread crates/trident-acl-agent/src/core/config.rs Outdated
Copilot AI balanced review requested due to automatic review settings October 14, 2026 16:35

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

Certificate resolution omits the required system:nodes organization validation, and test documentation misstates service restart behavior.

0 open findings

4 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Require system:nodes organization in certificate identity validation

crates/​trident-acl-agent/​src/​core/​config.rs:571

Issue: The certificate source does not enforce the required system:nodes Subject organization. Evidence: The PR contract derives identity from both CN=system:node:<name> and O=system:nodes, but this code inspects only the CN, so any certificate with the prefix is accepted even if it is not a kubelet node credential. Suggestion: Require at least one Subject organization exactly equal to system:nodes before returning the CN suffix, and add a rejection test for a matching CN with the wrong/missing organization.

Low severity Correct test description to mention shared service restart

tools/​storm/​aclagent/​README.md:155

Issue: This description still says the test never touches or restarts the service. Evidence: RunNodeNameSource calls prepareVmForAclAgent, which unconditionally runs systemctl restart trident-acl-agent.service (tools/storm/aclagent/tests/update.go:654-657); only the phase assertions themselves are independent one-shot CLI calls. Suggestion: State that shared setup restarts the service once, while no per-phase restart/reboot is needed, and make the same correction in the testing guide.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 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.

🔵 Needs a closer look

Certificate parsing omits the required system:nodes organization check, and test documentation still incorrectly denies the setup restart.

0 open findings

Previously missed (2)

In code that hasn't changed since last review

Medium severity Require system:nodes organization for certificate-derived identity

crates/​trident-acl-agent/​src/​core/​config.rs:575

Issue: Certificate-derived identity ignores the required system:nodes organization. Evidence: This parser inspects only COMMONNAME, so any certificate with a system:node:<name> CN is accepted even when it is not a kubelet node certificate; both the PR contract and linked #839 require the CN and O=system:nodes. Suggestion: Require the subject to contain an exact system:nodes organization before returning the CN suffix, and add regression coverage for missing/wrong organizations.

Low severity Correct documentation about service restart during node name tests

tools/​storm/​aclagent/​README.md:155

Issue: This remaining description still says the test never touches or restarts the service. Evidence: RunNodeNameSource calls prepareVmForAclAgent, which unconditionally runs systemctl restart trident-acl-agent.service (tools/storm/aclagent/tests/update.go:654-657); the new Go comments correctly acknowledge that one shared-setup restart. The same inaccurate claim remains in docs/Development/Testing/TridentAclAgent-Tests.md:275-277. Suggestion: State that setup restarts the service once, while phase assertions are standalone CLI calls requiring no additional restart or reboot.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 14, 2026 18:21

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

No unresolved blocking issues were identified in identity resolution, override precedence, retry handling, or scenario integration.

0 open findings

🧠 Review effort: Balanced

…urable

AKS does not guarantee the lowercased-hostname convention
trident-acl-agent currently assumes for node_name (per the AKS team).
Add TRIDENT_ACL_AGENT_KUBERNETES_NODE_NAME_SOURCE (hostname |
kubelet-cert) to opt into deriving node_name from the Subject CN
(system:node:<name>) of kubelet own client certificate
(/var/lib/kubelet/pki/kubelet-client-current.pem) instead - the
identity kubelet itself authenticates to the API server with, so it is
guaranteed to match the real Node name.

Default stays hostname for backward compatibility. kubelet-cert falls
back to hostname (with a warning) if the cert cannot be read or
parsed, rather than failing startup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Static review identified no unresolved blocking issues; tests were not executed during this review.

0 open findings

🧠 Review effort: Balanced

@bfjelds

Copy link
Copy Markdown
Member Author

Copilot resolve the merge conflicts in this pull request

…t-node-name-source

# Conflicts:
#	crates/trident-acl-agent/src/main.rs

Co-authored-by: bfjelds <9874445+bfjelds@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 19, 2026 18:10

Copilot AI commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Copilot resolve the merge conflicts in this pull request

Merged main and resolved the main.rs conflict, preserving cancellation-aware node-name resolution and upstream shutdown logging. Merge commit: 36cb7d3f.

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

No unresolved blocking issues remain in the configuration, identity resolution, retry handling, or integration changes.

No open issues 🎉

Review effort: Balanced

@bfjelds

Copy link
Copy Markdown
Member Author

Copilot resolve the merge conflicts in this pull request

…t-node-name-source

# Conflicts:
#	crates/trident-acl-agent/src/core/config.rs

Co-authored-by: bfjelds <9874445+bfjelds@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 22:42

Copilot AI commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Copilot resolve the merge conflicts in this pull request

Merged the refreshed main and resolved the config.rs conflict in merge commit 735696a8.

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

No unresolved blocking issues remain in identity resolution, retry and cancellation handling, or test integration.

No open issues 🎉

Review effort: Balanced

@frhuelsz

Copy link
Copy Markdown
Contributor

the effective config log does not include kubernetes.node_name_source or kubernetes.kubelet_cert_path. if resolution fails, we can see that no explicit override was set, but not whether it tried hostname vs certificate or which certificate path it used. please include both fields in redacted_for_logging().

@frhuelsz

Paco Huelsz (frhuelsz) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

retrying is good when the kubelet certificate is temporarily missing during boot. my concern is that a wrong path, corrupt certificate, or invalid CN can never recover, but the agent still retries forever. systemd then reports the service as running even though it never connects to Kubernetes, watches its Node, or processes updates. should permanent resolution failures eventually fail the service or expose a readiness signal?

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0c0c9042-831c-4855-afc4-e6f47d9df584
Copilot AI balanced review requested due to automatic review settings October 22, 2026 16:01
@bfjelds

bfjelds (bfjelds) commented Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

the effective config log does not include kubernetes.node_name_source or kubernetes.kubelet_cert_path. if resolution fails, we can see that no explicit override was set, but not whether it tried hostname vs certificate or which certificate path it used. please include both fields in redacted_for_logging().

Great comment. Added kubernetes.node_name_source and kubernetes.kubelet_cert_path to redacted_for_logging() in b7640d9. The startup log now shows the configured source and certificate path without reading or logging certificate contents. Regression coverage checks both defaults and environment overrides.

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

Static review found no unresolved blocking issues in identity resolution, override precedence, retry cancellation, or Storm integration.

No open issues 🎉

Review effort: Balanced

@bfjelds

Copy link
Copy Markdown
Member Author

retrying is good when the kubelet certificate is temporarily missing during boot. my concern is that a wrong path, corrupt certificate, or invalid CN can never recover, but the agent still retries forever. systemd then reports the service as running even though it never connects to Kubernetes, watches its Node, or processes updates. should permanent resolution failures eventually fail the service or expose a readiness signal?

i'm not super confident on identifying "permanent resolution failures" (versus "reading a partially written file" or something i haven't thought of).

is this something you want to block merging on or something that can be refined later?

@bfjelds
bfjelds (bfjelds) merged commit 06b6dfa into main Oct 22, 2026
31 checks passed
@bfjelds
bfjelds (bfjelds) deleted the user/bfjelds/acl-agent-node-name-source branch October 23, 2026 04:23
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.

4 participants