Skip to content

feat(metrics): add opt-in TLS and mTLS support - #3877

Open
gmenher wants to merge 22 commits into
NVIDIA:mainfrom
gmenher:codex/secure-metrics-runtime
Open

gmenher wants to merge 22 commits into
NVIDIA:mainfrom
gmenher:codex/secure-metrics-runtime

Conversation

@gmenher

@gmenher gmenher commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add opt-in TLS and mutual TLS support for OpenShell's dedicated metrics listener while preserving plaintext metrics as the default for compatibility.

This is the first implementation PR for #3664. It intentionally focuses on the runtime listener, operator-provided Secrets, and the metrics trust boundary; chart-managed metrics PKI, monitoring resources, and metrics ingress policy configuration remain follow-up work.

Related Issue

Fixes #3664

Changes

  • Add dedicated metrics TLS configuration through gateway TOML, CLI flags, and environment variables.
  • Serve /metrics over HTTPS when metrics TLS is configured, with optional client-certificate authentication.
  • Keep the metrics client CA independent from the gateway client CA, and reject Helm configurations that reuse the gateway client CA Secret or the sandbox client TLS Secret (server.tls.clientTlsSecretName, which certgen seeds with the gateway CA); the comparison is by Secret name only.
  • Mount operator-provided server TLS and metrics client CA Secrets read-only through the Helm chart.
  • Reuse the existing TLS reload machinery while identifying metrics-specific reload logs and OCSF events.
  • When metrics TLS material is missing or malformed at startup, emit a metrics-specific lifecycle error, disable only the metrics listener, and never fall back to plaintext. Gateway and health listeners remain available.
  • Rate-limit rejected TLS-handshake warnings: retain per-connection diagnostics at DEBUG, warn immediately for the first rejection, then report suppressed rejections at most once per minute. Rejections and timeouts use separate limiters so each periodic warning stays attributable to one event type.
  • Document the runtime configuration, Helm values, trust boundary, secure scraper requirements, and startup/reload behavior, including the difference between a missing Secret (pod startup blocks with FailedMount) and malformed contents in an existing Secret (only the metrics listener is disabled).
  • Add runtime, configuration, Helm rendering, trust-boundary, lifecycle-log, and rate-limiter coverage.

Validation

  • cargo fmt --check
  • cargo test -p openshell-core metrics_tls --lib
  • cargo test -p openshell-server metrics_tls --lib
  • cargo test -p openshell-server --features test-support (full suite)
  • cargo test -p openshell-server tls_metrics_listener_serves_https_and_rejects_plaintext --lib
  • cargo test -p openshell-server metrics_mtls_trusts_only_the_metrics_client_ca --lib
  • cargo test -p openshell-server test_reload_invalid_preserves_old --lib
  • mise run helm:test (415 tests)
  • mise run helm:lint
  • mise run helm:docs:check
  • mise run docs
  • go build ./... and go test ./... in sdk/go
  • Kubernetes E2E was run with a fresh ephemeral k3d cluster on the branch before the rebase; the runner cleaned up the cluster after completion. A re-run on the rebased branch is currently blocked by a local toolchain incompatibility unrelated to this PR: on macOS 27 with Rust < 1.99, cargo's default stripping of release build-dependencies produces proc-macro dylibs that dyld rejects (mis-aligned LINKEDIT string pool, fixed in Rust 1.99). It runs locally with CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_STRIP=none.

mise run ci is not marked successful: its concurrently launched Go tasks use incompatible local compiler-cache artifacts (go1.26.7 versus go1.26.2). The same Go build and test commands pass when run directly after clearing the local Go cache; this branch does not modify Go code.

The required Strix scan could not run in this environment because Docker could not pull usestrix/strix:latest (registry access denied).

Checklist

  • Follows Conventional Commits.
  • Commits are signed off (DCO).
  • Architecture and deployment documentation updated.

@copy-pr-bot

copy-pr-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@gmenher
gmenher force-pushed the codex/secure-metrics-runtime branch 3 times, most recently from b4b7be9 to f6b2b19 Compare October 6, 2026 12:14

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

cc-review summary

Reviewed at f6b2b199e with a 6-dimension agent pass plus CodeRabbit. The feature is well-scoped and security-careful: client-cert auth is enforced with no fail-open, an incomplete TLS config fails closed with no plaintext fallback, the reload machinery is reused cleanly and tolerates bad reloads, and plaintext stays the default. The standout is the trust-boundary design: keeping the metrics client CA independent from the gateway client CA, and rejecting Helm configs that reuse it, correctly avoids widening the gateway client CA's authority to metrics scraping. All eight goals are delivered. No correctness or security defect.

Two non-blocking Production recommendations below, the first worth addressing before merge. Test-depth, Helm-maintainability, and naming/doc findings are omitted here to keep this focused; happy to share them.

Source: cc-review (6 agents + CodeRabbit)

Comment thread crates/openshell-server/src/lib.rs Resolved
Comment thread crates/openshell-server/src/lib.rs Resolved
@gmenher
gmenher force-pushed the codex/secure-metrics-runtime branch from f6b2b19 to 68274d2 Compare October 22, 2026 12:01
@gmenher

gmenher commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and addressed the two production-readiness follow-ups:

  • invalid or unavailable metrics TLS material now disables only the metrics listener, without plaintext fallback or gateway/health disruption;
  • rejected metrics TLS handshakes are rate-limited at WARN, with per-connection detail retained at DEBUG.

Focused Rust, Helm, documentation, and Kubernetes validation are recorded in the PR description. @rhuss, when you have a moment, could uou take another look? Thanks!!

@gmenher
gmenher force-pushed the codex/secure-metrics-runtime branch from 68274d2 to d42b91d Compare October 23, 2026 14:31

@krishicks krishicks left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Written by Claude Code, reviewed by @krishicks

The overall shape looks good: a separate config type, partial input fails instead of falling back, and the listener never drops to plaintext. Inline comments below; the two Helm items are the most important.

Comment thread deploy/helm/openshell/README.md.gotmpl Resolved
Comment thread deploy/man/openshell-gateway.8.md Resolved
Comment thread crates/openshell-server/src/lib.rs Resolved
@gmenher
gmenher force-pushed the codex/secure-metrics-runtime branch from d42b91d to 75d54e1 Compare October 27, 2026 22:54
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The chart rejected only a metrics.tls.clientCaSecretName that matched the
gateway client CA Secret by name. In the default pkiInitJob mode, certgen
also writes the gateway CA as ca.crt into the sandbox client TLS Secret
(server.tls.clientTlsSecretName), which sandbox workloads mount for
control-plane mTLS. Pointing the metrics client CA at that Secret passed
the existing check and would have trusted every sandbox client
certificate on the metrics listener.

Reject server.tls.clientTlsSecretName as well, document in values.yaml,
the chart README, the gateway configuration docs, and the debug skill
that both checks compare Secret names only and cannot detect a different
Secret holding the same CA, and cover the default and custom Secret
names with rendering tests.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The secure metrics scraping docs said a missing or malformed metrics TLS
Secret leaves the metrics listener disabled while the gateway keeps
serving. On Helm that is only true for a Secret that exists with
malformed contents. The metrics TLS volumes are non-optional Secret
mounts, so a Secret that is missing entirely, or a client CA Secret
without a ca.crt key, fails the mount and the gateway pod never starts.

State both outcomes in the chart README, the gateway configuration
docs, and the cluster debugging skill. A loud FailedMount is preferable
to a quietly missing scrape target, so the mounts stay non-optional.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The man page synopsis implied --metrics-require-client-auth accepts a
space-separated BOOL, but the flag is declared with require_equals, so
only the bare flag and the =VALUE forms parse. Document the synopsis as
[--metrics-require-client-auth[=BOOL]], spell out the accepted forms in
the description, and pin them with a CLI parsing test so the synopsis
and the parser cannot drift apart.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Handshake rejections and timeouts shared a single rate limiter, so the
periodic WARN reported combined suppressed/total counts under the
message of whichever event happened to trigger it. During a burst of
untrusted client certificates, an operator could see the timeout
message with a count made up mostly of certificate rejections.

Give each event type its own limiter so warnings and counts stay
attributable, and pin the behavior with a test where a timeout during a
rejection burst still warns immediately.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The listener identification change retagged the gateway reload OCSF
message from 'TLS certificate config reloaded successfully' to
'gateway TLS certificate config reloaded successfully', but the
pre-existing ocsf_log test that pins the JSONL message was not
updated, so the openshell-server suite failed deterministically. The
test's intent is exactly that a gateway reload reaches JSONL, so pin
the listener-tagged message.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The chart README stated the sandbox client TLS Secret is seeded with
the gateway CA as a general fact. certgen only writes that CA when the
chart manages PKI; with cert-manager the Secret is operator-provided.
The reuse rejection applies in every mode, so state the seeding claim
only for chart-managed PKI.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The tests module already imports command, so the super:: prefix
tripped the unused-qualifications lint.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
The metrics TLS flag resolution rejected key-without-cert,
cert-without-key, and a lone client CA with actionable errors, but no
test pinned those boundaries. Cover all three so a future refactor of
the partial-input validation cannot silently drop them.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Preparing a server configuration with metrics TLS material but no
enabled metrics listener errors out, but no test pinned that boundary.
Cover it through prepare_server_config_with_drivers so the validation
cannot be silently dropped.

Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
@gmenher
gmenher force-pushed the codex/secure-metrics-runtime branch from 75d54e1 to 5e17b77 Compare October 27, 2026 23:08
@gmenher
gmenher requested a review from krishicks October 27, 2026 23:23
@rhuss

rhuss commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@krishicks could you please re-assess the latest updates (and maybe add an topic:observability label ?). I believe we are close :)

@krishicks krishicks added the topic:observability Logging, metrics, and observability work label Oct 29, 2026
Comment thread .opencode-memory.md Resolved
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>
Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com>

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

topic:observability Logging, metrics, and observability work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(metrics): opt-in secure gateway metrics endpoint

3 participants