Skip to content

feat(health): add switch mTLS support#3297

Merged
jayzhudev merged 2 commits into
NVIDIA:mainfrom
jayzhudev:health/switch-mtls-support
Jul 10, 2026
Merged

feat(health): add switch mTLS support#3297
jayzhudev merged 2 commits into
NVIDIA:mainfrom
jayzhudev:health/switch-mtls-support

Conversation

@jayzhudev

@jayzhudev jayzhudev commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Add switch-specific mTLS support for switch collectors.

This introduces a new [tls.switch] config profile for NVUE REST, NVUE gNMI, NMX-T, and NMX-C switch-host connections. It includes switch CA bundle, client certificate, client key, and optional TLS server name for DNS-SAN verification while connecting to discovered switch IPs.

Added switch TLS material loading, validation, and HTTP/gRPC TLS construction. Cert material is validated at startup and when the HTTP client provider or gRPC channels are built, with clear errors for missing, unreadable, malformed, mismatched, not-yet-valid, or expired material.

Cert rotation behavior:

  • NVUE REST and NMX-T share one cached HTTP client provider for the [tls.switch] profile.
  • NVUE REST and NMX-T reload cert files on the shared HTTP reload cadence, based on the shortest enabled HTTP switch collector poll interval.
  • Refreshed cert files are adopted after the reload window expires and a new shared HTTP client is built.
  • NVUE gNMI and NMX-C reload cert files when long-lived streams reconnect.
  • Existing in-flight HTTP requests and gRPC streams are not proactively closed on file changes.

Note:

For periodic HTTP collectors, tls_server_name is used for SNI and certificate verification, not DNS resolution. Request URLs and HTTP Host headers stay on the discovered switch IP. This lets all switch targets share one HTTP client for the single [tls.switch] material set while still connecting to each discovered switch IP.

Tradeoff:

This PR first considered rebuilding mTLS HTTP clients per switch target per poll iteration because it gives very direct reload semantics: read certs, validate certs, build client, scrape target. That is simple, but it is expensive at scale. At 20k switch targets (imaginary) and 30s poll intervals, that shape can cause about 20k cert read/parse/validate cycles and 20k HTTP client constructions per interval.

The final implementation uses hyper + hyper-rustls for the mTLS HTTP path instead of reqwest. reqwest::Client is expensive to construct and is immutable after construction, and its resolver override is client-wide for a host name, which does not fit this routing model: all switches may share one TLS server name, while TCP must connect to many different switch IPs.

The Hyper path keeps URL host / HTTP Host as the switch IP, applies tls_server_name only to SNI and certificate verification, and shares one cached HTTP client provider for the [tls.switch] profile. Cert reload is attempted on the first HTTP collector use after the reload window expires, with the window based on the shortest enabled HTTP switch collector poll interval. This avoids per-target TLS/client construction in the steady state without tying client reuse to switch inventory changes.

Related issues

Closes #3122

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

Manual testing was performed on a primary switch for both mTLS and non-mTLS modes.

@jayzhudev jayzhudev self-assigned this Jul 9, 2026
@jayzhudev jayzhudev requested a review from a team as a code owner July 9, 2026 05:52
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8be4b40f-0f53-4ac3-b11f-4772facee52b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Adds switch mTLS support for the health service. It introduces TLS config and validation, validates cert material at startup, threads TLS state through discovery, and updates NVUE, NMX-T, and NMX-C collectors to use switch-specific HTTPS/mTLS connection paths.

Changes

mTLS support for switch collectors

Layer / File(s) Summary
TLS config schema and validation
crates/health/src/config.rs, crates/health/Cargo.toml, crates/health/example/config.example.toml
Adds TlsConfig/MtlsProfileConfig, validates paths and DNS names, rejects dangerous TLS-skip flags when tls.switch is set, and updates dependencies and example docs.
TLS material validation and client builders
crates/health/src/tls.rs
Adds TLS material validation, HTTP/gRPC client builders, reloadable HTTP client caching, and test coverage for valid and invalid material.
Service startup and discovery TLS wiring
crates/health/src/lib.rs
Adds TLS error conversion, startup preflight, and passes resolved TLS config into discovery initialization.
Discovery context and endpoint host helper
crates/health/src/discovery/context.rs, crates/health/src/discovery/spawn.rs, crates/health/src/endpoint/model.rs
Stores TLS state in discovery context, derives URI-safe switch hosts, and forwards TLS config into collector startup.
NVUE gNMI client TLS support
crates/health/src/collectors/mod.rs, crates/health/src/collectors/nvue/gnmi/client.rs, crates/health/src/collectors/nvue/gnmi/subscriber.rs
Updates gNMI client construction and collector wiring to accept optional mTLS config and use switch connect hosts.
NVUE REST client TLS support
crates/health/src/collectors/nvue/rest/client.rs, crates/health/src/collectors/nvue/rest/collector.rs
Refactors REST polling to use cached legacy or TLS clients, connect-IP-based URLs, and per-iteration client refresh.
NMX-T collector TLS support
crates/health/src/collectors/nmxt.rs
Adds HTTPS-aware URL construction, TLS client selection, and updated scrape flow and tests.
NMX-C collector TLS support
crates/health/src/collectors/nmxc.rs
Adds optional gRPC mTLS config, switch-host URI construction, and tonic TLS channel setup.

Estimated code review effort: 4 (Complex) | ~75 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding switch mTLS support.
Description check ✅ Passed The description is clearly related to the switch mTLS collector changes and matches the implemented scope.
Linked Issues check ✅ Passed The changes implement switch mTLS for NVUE, NMX-T, and NMX-C with validation, errors, and reload behavior as requested.
Out of Scope Changes check ✅ Passed The diff stays focused on switch mTLS wiring, config, client construction, and docs without obvious unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

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

🧹 Nitpick comments (4)
crates/health/src/collectors/nmxt.rs (1)

727-753: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer value_scenarios! for this total-operation table.

nmxt_endpoint_url is a total operation, and the companion nmxc.rs test in this same PR already expresses the identical scheme-switching table via value_scenarios!. Adopting the shared carbide-test-support helper here removes the hand-rolled TestCase struct and keeps the two collectors consistent.

♻️ Suggested table-driven rewrite
#[test]
fn nmxt_endpoint_url_switches_scheme_when_tls_enabled() {
    use carbide_test_support::value_scenarios;

    value_scenarios!(
        run = |tls_enabled| nmxt_endpoint_url("10.0.0.9", tls_enabled);

        "endpoint URL scheme" {
            false => "http://10.0.0.9:9352/xcset/nvlink_domain_telemetry".to_string(),
            true => "https://10.0.0.9:9352/xcset/nvlink_domain_telemetry".to_string(),
        }
    );
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/collectors/nmxt.rs` around lines 727 - 753, The
`test_nmxt_endpoint_url_switches_scheme_when_tls_enabled` case should use the
shared `carbide_test_support::value_scenarios!` helper instead of a custom
`TestCase` table. Rewrite the test as a compact table-driven scenario around
`nmxt_endpoint_url`, matching the existing `nmxc.rs` style in this PR, and
remove the hand-rolled loop/struct so both collectors stay consistent.

Source: Coding guidelines

crates/health/src/tls.rs (1)

391-397: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer .ok() over let _ = ... to discard the install result.

Discarding the Result from install_default() via let _ = ... matches the exact anti-pattern flagged by the style guide.

♻️ Proposed fix
-    INSTALL_PROVIDER.call_once(|| {
-        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
-    });
+    INSTALL_PROVIDER.call_once(|| {
+        rustls::crypto::aws_lc_rs::default_provider().install_default().ok();
+    });

As per coding guidelines, "Avoid using let _unused = foo(); to discard errors. Prefer .ok() to convert errors into Options, which are safely discardable."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/tls.rs` around lines 391 - 397, The ensure_rustls_provider
function discards the result of
rustls::crypto::aws_lc_rs::default_provider().install_default() using let _,
which violates the style guideline. Update the INSTALL_PROVIDER.call_once
closure in ensure_rustls_provider to call .ok() on install_default() instead, so
the result is intentionally and idiomatically ignored without the anti-pattern.

Source: Coding guidelines

crates/health/src/discovery/context.rs (1)

246-258: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

new_with_tls_config's explicit parameter looks redundant for its only production caller.

lib.rs::run_service computes let tls_config = config.tls.switch.clone(); and passes it explicitly into new_with_tls_config, but that value is always identical to what the .or_else(|| config.tls.switch.clone()) fallback (line 297) would already produce from the same Arc<Config>. The extra parameter and pub(crate) constructor add a second API surface that isn't exercised by any test (the added test only calls the plain new()). Consider dropping new_with_tls_config and having run_service call DiscoveryLoopContext::new(limiter, metrics_manager, config.clone()) directly, unless a future caller genuinely needs to inject a different tls_config than config.tls.switch.

As per path instructions, "Abstractions should justify their existence: Do not add abstractions 'just in case'. Wait until there is a real requirement for them."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/discovery/context.rs` around lines 246 - 258, The extra
tls_config parameter and pub(crate) constructor on
DiscoveryLoopContext::new_with_tls_config appear redundant because run_service
can already derive the same value from the shared Config. Remove
new_with_tls_config if there is no real alternate caller, and update run_service
to construct DiscoveryLoopContext via DiscoveryLoopContext::new(limiter,
metrics_manager, config.clone()) so the fallback logic stays in one place and
the API surface is smaller.

Source: Path instructions

crates/health/src/endpoint/model.rs (1)

76-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a plain String return instead of Cow<'_, str> since both branches always allocate.

Both match arms return Cow::Owned(...); there is no borrowed path, so the Cow indirection doesn't buy anything here — a String return would be equally cheap and simpler for callers.

Separately, the same "bracket IPv6, pass through IPv4" formatting logic is now duplicated in at least three places: BmcAddr::to_url() (this file), this new switch_connect_host_for_uri(), and RestClient::new()'s host derivation (crates/health/src/collectors/nvue/rest/client.rs). Extracting one shared helper (e.g. fn uri_host(ip: IpAddr) -> String) would remove the risk of the three copies drifting apart.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/health/src/endpoint/model.rs` around lines 76 - 81, The
switch_connect_host_for_uri method always returns an owned value in both
IpAddr::V4 and IpAddr::V6 branches, so replace the Cow<'_, str> return type with
String and keep the same IPv4 pass-through / IPv6 bracket formatting. Also
factor the duplicated URI host formatting logic shared with BmcAddr::to_url()
and RestClient::new() into one helper (for example, a uri_host-style function)
and update all callers to use it so the behavior stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/health/src/collectors/nmxt.rs`:
- Around line 727-753: The
`test_nmxt_endpoint_url_switches_scheme_when_tls_enabled` case should use the
shared `carbide_test_support::value_scenarios!` helper instead of a custom
`TestCase` table. Rewrite the test as a compact table-driven scenario around
`nmxt_endpoint_url`, matching the existing `nmxc.rs` style in this PR, and
remove the hand-rolled loop/struct so both collectors stay consistent.

In `@crates/health/src/discovery/context.rs`:
- Around line 246-258: The extra tls_config parameter and pub(crate) constructor
on DiscoveryLoopContext::new_with_tls_config appear redundant because
run_service can already derive the same value from the shared Config. Remove
new_with_tls_config if there is no real alternate caller, and update run_service
to construct DiscoveryLoopContext via DiscoveryLoopContext::new(limiter,
metrics_manager, config.clone()) so the fallback logic stays in one place and
the API surface is smaller.

In `@crates/health/src/endpoint/model.rs`:
- Around line 76-81: The switch_connect_host_for_uri method always returns an
owned value in both IpAddr::V4 and IpAddr::V6 branches, so replace the Cow<'_,
str> return type with String and keep the same IPv4 pass-through / IPv6 bracket
formatting. Also factor the duplicated URI host formatting logic shared with
BmcAddr::to_url() and RestClient::new() into one helper (for example, a
uri_host-style function) and update all callers to use it so the behavior stays
consistent.

In `@crates/health/src/tls.rs`:
- Around line 391-397: The ensure_rustls_provider function discards the result
of rustls::crypto::aws_lc_rs::default_provider().install_default() using let _,
which violates the style guideline. Update the INSTALL_PROVIDER.call_once
closure in ensure_rustls_provider to call .ok() on install_default() instead, so
the result is intentionally and idiomatically ignored without the anti-pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 74882ac4-4302-4a10-90e4-61acbe72eb2d

📥 Commits

Reviewing files that changed from the base of the PR and between aaac45b and 3a575e6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • crates/health/Cargo.toml
  • crates/health/example/config.example.toml
  • crates/health/src/collectors/mod.rs
  • crates/health/src/collectors/nmxc.rs
  • crates/health/src/collectors/nmxt.rs
  • crates/health/src/collectors/nvue/gnmi/client.rs
  • crates/health/src/collectors/nvue/gnmi/subscriber.rs
  • crates/health/src/collectors/nvue/rest/client.rs
  • crates/health/src/collectors/nvue/rest/collector.rs
  • crates/health/src/config.rs
  • crates/health/src/discovery/context.rs
  • crates/health/src/discovery/spawn.rs
  • crates/health/src/endpoint/model.rs
  • crates/health/src/lib.rs
  • crates/health/src/tls.rs

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🔍 Container Scan Summary

Service Total Critical High Medium Low Other
boot-artifacts-aarch64 3 0 0 3 0 0
boot-artifacts-x86_64 3 0 0 3 0 0
forge-admin-cli-x86_64 271 13 34 91 7 126
machine-validation-runner 800 37 234 291 43 195
machine_validation 800 37 234 291 43 195
machine_validation-aarch64 800 37 234 291 43 195
nvmetal-carbide 800 37 234 291 43 195
TOTAL 3477 161 970 1261 179 906

Per-CVE detail lives in the per-service grype-* artifacts (JSON + SARIF). Severity counts only — no CVE IDs published here.

@jayzhudev jayzhudev force-pushed the health/switch-mtls-support branch from 0a35cb3 to 2acc945 Compare July 9, 2026 07:37
jayzhudev added 2 commits July 9, 2026 17:11
Signed-off-by: Jay Zhu <jayzhu@nvidia.com>
Reuse one mTLS HTTP client for the shared [tls.switch] profile
instead of rebuilding reqwest clients per switch target. This keeps
TLS reload bounded to the configured reload window and avoids
per-target cert parsing/client construction at scale.

Signed-off-by: Jay Zhu <jayzhu@nvidia.com>
@jayzhudev jayzhudev force-pushed the health/switch-mtls-support branch from 2acc945 to 746c2ac Compare July 9, 2026 23:11
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

@yoks

yoks commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Do we expect single cert/key for all switches health connects to?

@jayzhudev

Copy link
Copy Markdown
Contributor Author

Do we expect single cert/key for all switches health connects to?

Yes, as I understand that's the expectation. Support of "cert domains" for different groups of switches is intended, but today one site-wide cert group is used (and supported by RMS). When switches use different server cert groups, the client cert bundle can stay the same.

If we later need different switch server CAs or TLS server names per switch group, we could add multiple TLS profiles and a mapping from switch groups to profiles. We don't have this as an immediate requirement yet, but will address as use cases evolve.

@jayzhudev jayzhudev merged commit 1a1b1e2 into NVIDIA:main Jul 10, 2026
61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Verify

Development

Successfully merging this pull request may close these issues.

feat: Support mTLS for NVUE, NMX-T, NMX-C collectors in hw-health

2 participants