From c81ee950ba6909c4faf8f0a80aa23c5a64496dcd Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 3 Jun 2026 14:42:52 -0700 Subject: [PATCH] backend tls: support config dump Signed-off-by: John Howard --- crates/agentgateway/src/control/mod.rs | 9 ++- crates/agentgateway/src/http/backendtls.rs | 62 +++++++++++++++++-- .../src/types/local_tests/llm_normalized.snap | 3 +- .../local_tests/llm_simple_normalized.snap | 3 +- .../src/types/local_tests/mcp_normalized.snap | 4 +- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/crates/agentgateway/src/control/mod.rs b/crates/agentgateway/src/control/mod.rs index 98913ad7b2..3c014c7ab1 100644 --- a/crates/agentgateway/src/control/mod.rs +++ b/crates/agentgateway/src/control/mod.rs @@ -17,7 +17,7 @@ use tower::Service; use crate::client::{ApplicationTransport, Transport}; use crate::http::HeaderValue; -use crate::http::backendtls::{BackendTLS, PerAlpnConfig, SYSTEM_TRUST}; +use crate::http::backendtls::{BackendTLS, BackendTLSInfo, PerAlpnConfig, SYSTEM_TRUST}; use crate::types::agent::Target; use crate::*; @@ -32,15 +32,21 @@ pub enum RootCert { impl RootCert { pub async fn to_client_config(&self) -> anyhow::Result { + let mut metadata = BackendTLSInfo { + alpn: Some(vec!["h2".to_string()]), + ..Default::default() + }; let roots = match self { RootCert::File(f) => { let certfile = tokio::fs::read(f).await?; + metadata.root = Some(String::from_utf8_lossy(&certfile).into()); let certs = CertificateDer::pem_slice_iter(&certfile).collect::, _>>()?; let mut roots = rustls::RootCertStore::empty(); roots.add_parsable_certificates(certs); roots }, RootCert::Static(b) => { + metadata.root = Some(String::from_utf8_lossy(b).into()); let certs = CertificateDer::pem_slice_iter(b).collect::, _>>()?; let mut roots = rustls::RootCertStore::empty(); roots.add_parsable_certificates(certs); @@ -57,6 +63,7 @@ impl RootCert { Ok(BackendTLS { hostname_override: None, config: PerAlpnConfig::new(Arc::new(ccb), false), + metadata, }) } } diff --git a/crates/agentgateway/src/http/backendtls.rs b/crates/agentgateway/src/http/backendtls.rs index 193d98c0e0..e250b81863 100644 --- a/crates/agentgateway/src/http/backendtls.rs +++ b/crates/agentgateway/src/http/backendtls.rs @@ -1,15 +1,19 @@ use std::path::PathBuf; use std::sync::Arc; +use agent_core::strng; +use agent_core::strng::Strng; use once_cell::sync::Lazy; use rustls::ClientConfig; use rustls_pki_types::pem::PemObject; use rustls_pki_types::{CertificateDer, ServerName}; use serde::Serializer; +use tracing::trace; -use crate::transport; +use crate::serdes::schema_ser; use crate::transport::tls; use crate::types::agent::{parse_cert, parse_key}; +use crate::{apply, transport}; pub static SYSTEM_TRUST: Lazy = Lazy::new(|| LocalBackendTLS::default().try_into().unwrap()); @@ -75,6 +79,7 @@ impl PerAlpnConfig { pub struct BackendTLS { pub hostname_override: Option>, pub config: PerAlpnConfig, + pub metadata: BackendTLSInfo, } impl BackendTLS { @@ -117,10 +122,56 @@ impl serde::Serialize for BackendTLS { where S: Serializer, { - // TODO: store raw pem so we can send it back - serializer.serialize_none() + serde::Serialize::serialize(&self.metadata, serializer) } } + +#[apply(schema_ser!)] +#[derive(Default)] +pub struct BackendTLSInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub cert: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hostname: Option, + #[serde(default, skip_serializing_if = "is_false")] + pub insecure: bool, + #[serde(default, skip_serializing_if = "is_false")] + pub insecure_host: bool, + #[serde(default, skip_serializing_if = "is_false")] + pub system_roots: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub alpn: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub subject_alt_names: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub key_exchange_groups: Option>, +} + +impl BackendTLSInfo { + pub fn from_resolved(tls: &ResolvedBackendTLS) -> Self { + Self { + cert: tls.cert.as_ref().map(pem_to_string), + root: tls.root.as_ref().map(pem_to_string), + hostname: tls.hostname.clone(), + insecure: tls.insecure, + insecure_host: tls.insecure_host, + system_roots: tls.root.is_none(), + alpn: tls.alpn.clone(), + subject_alt_names: tls.subject_alt_names.clone(), + key_exchange_groups: tls.key_exchange_groups.clone(), + } + } +} + +fn pem_to_string(pem: impl AsRef<[u8]>) -> Strng { + strng::new(String::from_utf8_lossy(pem.as_ref())) +} + +fn is_false(value: &bool) -> bool { + !*value +} static SYSTEM_ROOT: Lazy = Lazy::new(rustls_native_certs::load_native_certs); @@ -162,10 +213,12 @@ pub struct ResolvedBackendTLS { impl ResolvedBackendTLS { pub fn try_into(self) -> anyhow::Result { + let metadata = BackendTLSInfo::from_resolved(&self); let mut roots = rustls::RootCertStore::empty(); if let Some(root) = self.root { let certs = CertificateDer::pem_slice_iter(&root).collect::, _>>()?; - roots.add_parsable_certificates(certs); + let (valid, invalid) = roots.add_parsable_certificates(certs); + trace!(valid, invalid, "added root certificates") } else { // TODO: we probably should do this once globally! for cert in &crate::http::backendtls::SYSTEM_ROOT.certs { @@ -219,6 +272,7 @@ impl ResolvedBackendTLS { Ok(BackendTLS { hostname_override: self.hostname.map(|s| s.try_into()).transpose()?, config: PerAlpnConfig::new(Arc::new(cc), allow_custom_alpn), + metadata, }) } } diff --git a/crates/agentgateway/src/types/local_tests/llm_normalized.snap b/crates/agentgateway/src/types/local_tests/llm_normalized.snap index 97ef1e5c5b..9b2a501a04 100644 --- a/crates/agentgateway/src/types/local_tests/llm_normalized.snap +++ b/crates/agentgateway/src/types/local_tests/llm_normalized.snap @@ -87,7 +87,8 @@ backends: pathPrefix: ~ tokenize: false inlinePolicies: - - backendTLS: ~ + - backendTLS: + systemRoots: true - backendAuth: key: value: "" diff --git a/crates/agentgateway/src/types/local_tests/llm_simple_normalized.snap b/crates/agentgateway/src/types/local_tests/llm_simple_normalized.snap index c6575f1d41..1eede718fe 100644 --- a/crates/agentgateway/src/types/local_tests/llm_simple_normalized.snap +++ b/crates/agentgateway/src/types/local_tests/llm_simple_normalized.snap @@ -259,7 +259,8 @@ backends: capacity: 1 rejected: {} inlinePolicies: - - backendTLS: ~ + - backendTLS: + systemRoots: true - requestHeaderModifier: remove: - x-gateway-model-name diff --git a/crates/agentgateway/src/types/local_tests/mcp_normalized.snap b/crates/agentgateway/src/types/local_tests/mcp_normalized.snap index 514f8ab1e6..7ed96a091f 100644 --- a/crates/agentgateway/src/types/local_tests/mcp_normalized.snap +++ b/crates/agentgateway/src/types/local_tests/mcp_normalized.snap @@ -1,6 +1,5 @@ --- source: crates/agentgateway/src/types/local_tests.rs -assertion_line: 137 description: "Config normalization test for mcp: YAML -> LocalConfig -> NormalizedLocalConfig -> YAML" --- binds: @@ -53,7 +52,8 @@ backends: namespace: "" target: "example.com:443" inlinePolicies: - - backendTLS: ~ + - backendTLS: + systemRoots: true - backend: mcp: name: ns/name/bind/3000/listener0/default/route0/backend0