From 2b9ea4cfcea14f5c33fea055e8daa6383794ebd1 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Mon, 5 Jan 2026 13:01:05 -0500 Subject: [PATCH 1/9] chore: update jwks crate --- Cargo.lock | 162 +++++++++++++++++++++++++++- crates/apollo-mcp-server/Cargo.toml | 2 +- 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index becba98ba..60d43527c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,6 +641,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -1385,9 +1391,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1664,6 +1672,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -2069,9 +2094,8 @@ dependencies = [ [[package]] name = "jwks" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c940b91cfb3b56645bab00ff7a7919c5decf1bc5c8f9327b6dcff34e2d95c9" +version = "0.5.1" +source = "git+https://github.com/chenhunghan/jwks?tag=v0.5.1#1d006620588c018cb2ab0e227392bc8f0fa28d1f" dependencies = [ "base64", "jsonwebtoken", @@ -2180,6 +2204,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lz-str" version = "0.2.1" @@ -2950,6 +2980,61 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.41" @@ -3148,6 +3233,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", @@ -3155,6 +3241,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -3162,6 +3250,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3170,6 +3259,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -3369,15 +3459,41 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3755,6 +3871,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -4081,6 +4203,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -4122,6 +4259,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -4717,6 +4864,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/apollo-mcp-server/Cargo.toml b/crates/apollo-mcp-server/Cargo.toml index 7ab31fda5..389638392 100644 --- a/crates/apollo-mcp-server/Cargo.toml +++ b/crates/apollo-mcp-server/Cargo.toml @@ -29,7 +29,7 @@ http = "1.3.1" humantime-serde = "1.1.1" jsonschema = "0.33.0" jsonwebtoken = "9" -jwks = "0.4.0" +jwks = { git = "https://github.com/chenhunghan/jwks", tag = "v0.5.1" } lz-str = "0.2.1" opentelemetry = "0.30.0" opentelemetry-otlp = { version = "0.30.0", features = [ From 7c8785fdc7fc47093060216fbc3e263bca9efdec Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Mon, 5 Jan 2026 13:01:35 -0500 Subject: [PATCH 2/9] feat: add TLS configuration --- crates/apollo-mcp-server/src/auth.rs | 70 +++++++++++++++++++ .../src/auth/networked_token_validator.rs | 11 ++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/crates/apollo-mcp-server/src/auth.rs b/crates/apollo-mcp-server/src/auth.rs index ee7622c97..135aa0a14 100644 --- a/crates/apollo-mcp-server/src/auth.rs +++ b/crates/apollo-mcp-server/src/auth.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use axum::{ Json, Router, extract::{Request, State}, @@ -28,6 +30,43 @@ pub(crate) use valid_token::ValidToken; use valid_token::ValidateToken; use www_authenticate::WwwAuthenticate; +impl TlsConfig { + /// Build a reqwest client configured with the TLS settings + pub fn build_client(&self) -> Result { + let mut builder = reqwest::Client::builder(); + + // Add custom CA certificate if provided + if let Some(ca_cert_path) = &self.ca_cert { + if let Ok(cert_bytes) = std::fs::read(ca_cert_path) { + if let Ok(cert) = reqwest::Certificate::from_pem(&cert_bytes) { + builder = builder.add_root_certificate(cert); + tracing::debug!("Added custom CA certificate from {:?}", ca_cert_path); + } else { + tracing::warn!( + "Failed to parse CA certificate from {:?}, continuing without it", + ca_cert_path + ); + } + } else { + tracing::warn!( + "Failed to read CA certificate from {:?}, continuing without it", + ca_cert_path + ); + } + } + + // Accept invalid certs if configured (development only) + if self.danger_accept_invalid_certs { + tracing::warn!( + "TLS certificate validation is disabled. This is insecure and should only be used for development." + ); + builder = builder.danger_accept_invalid_certs(true); + } + + builder.build() + } +} + /// Auth configuration options #[derive(Debug, Clone, Deserialize, JsonSchema)] pub struct Config { @@ -56,6 +95,27 @@ pub struct Config { /// Whether to disable the auth token passthrough to upstream API #[serde(default)] pub disable_auth_token_passthrough: bool, + + /// TLS configuration for connecting to OAuth servers + #[serde(default)] + pub tls: TlsConfig, +} + +/// TLS configuration for OAuth server connections +#[derive(Debug, Clone, Default, Deserialize, JsonSchema)] +pub struct TlsConfig { + /// Path to additional CA certificates to trust (PEM format). + /// Use this when your OAuth server uses a self-signed certificate + /// or a certificate signed by a private CA. + pub ca_cert: Option, + + /// Whether to accept invalid TLS certificates. + /// + /// **WARNING**: This is insecure and should only be used for development/testing. + /// When enabled, the server will accept any certificate, including self-signed + /// and expired certificates, without validation. + #[serde(default)] + pub danger_accept_invalid_certs: bool, } impl Config { @@ -123,10 +183,19 @@ async fn oauth_validate( ) }; + // Build HTTP client with TLS configuration + let client = auth_config.tls.build_client().map_err(|e| { + tracing::error!("Failed to build HTTP client for OAuth validation: {e}"); + tracing::Span::current().record("reason", "client_build_error"); + tracing::Span::current().record("status_code", StatusCode::INTERNAL_SERVER_ERROR.as_u16()); + unauthorized_error() + })?; + let validator = NetworkedTokenValidator::new( &auth_config.audiences, auth_config.allow_any_audience, &auth_config.servers, + &client, ); let token = token.ok_or_else(|| { tracing::Span::current().record("reason", "missing_token"); @@ -172,6 +241,7 @@ mod tests { resource_documentation: None, scopes: vec!["read".to_string()], disable_auth_token_passthrough: false, + tls: TlsConfig::default(), } } diff --git a/crates/apollo-mcp-server/src/auth/networked_token_validator.rs b/crates/apollo-mcp-server/src/auth/networked_token_validator.rs index bd8f21a0b..ec6257608 100644 --- a/crates/apollo-mcp-server/src/auth/networked_token_validator.rs +++ b/crates/apollo-mcp-server/src/auth/networked_token_validator.rs @@ -10,14 +10,21 @@ pub(super) struct NetworkedTokenValidator<'a> { audiences: &'a [String], allow_any_audience: bool, upstreams: &'a Vec, + client: &'a reqwest::Client, } impl<'a> NetworkedTokenValidator<'a> { - pub fn new(audiences: &'a [String], allow_any_audience: bool, upstreams: &'a Vec) -> Self { + pub fn new( + audiences: &'a [String], + allow_any_audience: bool, + upstreams: &'a Vec, + client: &'a reqwest::Client, + ) -> Self { Self { audiences, allow_any_audience, upstreams, + client, } } } @@ -49,7 +56,7 @@ impl ValidateToken for NetworkedTokenValidator<'_> { async fn get_key(&self, server: &Url, key_id: &str) -> Option { let oidc_url = build_oidc_url(server); - let jwks = Jwks::from_oidc_url(oidc_url) + let jwks = Jwks::from_oidc_url_with_client(self.client, oidc_url.as_str()) .await .inspect_err(|e| { warn!("could not fetch OIDC information from {server}: {e}"); From d1f4a931d3889ab39cd1e85fd5a5829eb374dd9a Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Mon, 5 Jan 2026 13:04:14 -0500 Subject: [PATCH 3/9] docs: update config reference --- docs/source/config-file.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/config-file.mdx b/docs/source/config-file.mdx index 1bb8fe010..9aa6da4fb 100644 --- a/docs/source/config-file.mdx +++ b/docs/source/config-file.mdx @@ -251,6 +251,8 @@ These fields are under the top-level `transport` key, nested under the `auth` ke | `resource_documentation` | `string` | | Optional link to more documentation relating to this MCP server | | `scopes` | `List` | | List of queryable OAuth scopes from the upstream OAuth servers | | `disable_auth_token_passthrough` | `bool` | `false` | Optional flag to disable passing validated Authorization header to downstream API | +| `tls.ca_cert` | `string` | | Path to additional CA certificates to trust (PEM format). | +| `tls.danger_accept_invalid_certs`| `bool` | `false` | Accept invalid TLS certificates. **Warning**: Insecure—use only for development/testing. | Below is an example configuration using `StreamableHTTP` transport with authentication: @@ -286,6 +288,14 @@ transport: - read - mcp - profile + + # Optional TLS configuration for connecting to OAuth servers + tls: + # Path to CA certificate for private CA or self-signed OAuth server certs + # Use when your OAuth server uses a self-signed certificate or a certificate signed by a private CA + ca_cert: /path/to/ca-certificate.pem + # Only set to true for development/testing (insecure!) + # danger_accept_invalid_certs: false ``` ### Telemetry From 14f466f101b935c78555474056ca46c9b10dc636 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Mon, 5 Jan 2026 13:42:00 -0500 Subject: [PATCH 4/9] test: add tests for TLS configuration --- Cargo.lock | 1 + crates/apollo-mcp-server/Cargo.toml | 1 + crates/apollo-mcp-server/src/auth.rs | 145 +++++++++++++++++++++++---- docs/source/config-file.mdx | 4 +- 4 files changed, 132 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60d43527c..138069c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,7 @@ dependencies = [ "serde_json", "serde_yaml", "syn 2.0.106", + "tempfile", "thiserror 2.0.17", "tokio", "tokio-util", diff --git a/crates/apollo-mcp-server/Cargo.toml b/crates/apollo-mcp-server/Cargo.toml index 389638392..a04efb5e7 100644 --- a/crates/apollo-mcp-server/Cargo.toml +++ b/crates/apollo-mcp-server/Cargo.toml @@ -69,6 +69,7 @@ url.workspace = true [dev-dependencies] assert_fs = "1" +tempfile = "3" chrono = { version = "0.4.41", default-features = false, features = ["now"] } figment = { version = "0.10.19", features = ["test"] } insta.workspace = true diff --git a/crates/apollo-mcp-server/src/auth.rs b/crates/apollo-mcp-server/src/auth.rs index 135aa0a14..31890c374 100644 --- a/crates/apollo-mcp-server/src/auth.rs +++ b/crates/apollo-mcp-server/src/auth.rs @@ -30,29 +30,39 @@ pub(crate) use valid_token::ValidToken; use valid_token::ValidateToken; use www_authenticate::WwwAuthenticate; +/// Errors that can occur when building a TLS-configured HTTP client +#[derive(Debug, thiserror::Error)] +pub enum TlsConfigError { + #[error("Failed to read CA certificate from {path}: {source}")] + CertificateRead { + path: PathBuf, + source: std::io::Error, + }, + #[error("Failed to parse CA certificate from {path}: invalid PEM format")] + CertificateParse { path: PathBuf }, + #[error("Failed to build HTTP client: {0}")] + ClientBuild(#[from] reqwest::Error), +} + impl TlsConfig { /// Build a reqwest client configured with the TLS settings - pub fn build_client(&self) -> Result { + pub fn build_client(&self) -> Result { let mut builder = reqwest::Client::builder(); // Add custom CA certificate if provided if let Some(ca_cert_path) = &self.ca_cert { - if let Ok(cert_bytes) = std::fs::read(ca_cert_path) { - if let Ok(cert) = reqwest::Certificate::from_pem(&cert_bytes) { - builder = builder.add_root_certificate(cert); - tracing::debug!("Added custom CA certificate from {:?}", ca_cert_path); - } else { - tracing::warn!( - "Failed to parse CA certificate from {:?}, continuing without it", - ca_cert_path - ); + let cert_bytes = + std::fs::read(ca_cert_path).map_err(|e| TlsConfigError::CertificateRead { + path: ca_cert_path.clone(), + source: e, + })?; + let cert = reqwest::Certificate::from_pem(&cert_bytes).map_err(|_| { + TlsConfigError::CertificateParse { + path: ca_cert_path.clone(), } - } else { - tracing::warn!( - "Failed to read CA certificate from {:?}, continuing without it", - ca_cert_path - ); - } + })?; + builder = builder.add_root_certificate(cert); + tracing::debug!("Added custom CA certificate from {:?}", ca_cert_path); } // Accept invalid certs if configured (development only) @@ -63,7 +73,7 @@ impl TlsConfig { builder = builder.danger_accept_invalid_certs(true); } - builder.build() + Ok(builder.build()?) } } @@ -308,4 +318,105 @@ mod tests { assert!(www_auth.contains("resource_metadata")); assert!(!www_auth.contains("scope=")); } + + mod tls_config { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn default_config_builds_client() { + let config = TlsConfig::default(); + let client = config.build_client(); + assert!(client.is_ok()); + } + + #[test] + fn danger_accept_invalid_certs_builds_client() { + let config = TlsConfig { + ca_cert: None, + danger_accept_invalid_certs: true, + }; + let client = config.build_client(); + assert!(client.is_ok()); + } + + #[test] + fn valid_ca_cert_is_loaded() { + // Create a temporary file with a valid PEM certificate + // This is the ISRG Root X1 certificate (Let's Encrypt root CA) + let mut temp_file = NamedTempFile::new().unwrap(); + let test_cert = r#"-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE-----"#; + temp_file.write_all(test_cert.as_bytes()).unwrap(); + + let config = TlsConfig { + ca_cert: Some(temp_file.path().to_path_buf()), + danger_accept_invalid_certs: false, + }; + let client = config.build_client(); + assert!(client.is_ok()); + } + + #[test] + fn missing_ca_cert_file_returns_error() { + let config = TlsConfig { + ca_cert: Some("/nonexistent/path/to/cert.pem".into()), + danger_accept_invalid_certs: false, + }; + let result = config.build_client(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + TlsConfigError::CertificateRead { .. } + )); + } + + #[test] + fn invalid_pem_returns_error() { + // Create a temporary file with invalid PEM content + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(b"not a valid certificate").unwrap(); + + let config = TlsConfig { + ca_cert: Some(temp_file.path().to_path_buf()), + danger_accept_invalid_certs: false, + }; + let result = config.build_client(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + TlsConfigError::CertificateParse { .. } + )); + } + } } diff --git a/docs/source/config-file.mdx b/docs/source/config-file.mdx index 9aa6da4fb..939dfa907 100644 --- a/docs/source/config-file.mdx +++ b/docs/source/config-file.mdx @@ -251,8 +251,8 @@ These fields are under the top-level `transport` key, nested under the `auth` ke | `resource_documentation` | `string` | | Optional link to more documentation relating to this MCP server | | `scopes` | `List` | | List of queryable OAuth scopes from the upstream OAuth servers | | `disable_auth_token_passthrough` | `bool` | `false` | Optional flag to disable passing validated Authorization header to downstream API | -| `tls.ca_cert` | `string` | | Path to additional CA certificates to trust (PEM format). | -| `tls.danger_accept_invalid_certs`| `bool` | `false` | Accept invalid TLS certificates. **Warning**: Insecure—use only for development/testing. | +| `tls.ca_cert` | `string` | | Path to a CA certificate to trust (PEM format). | +| `tls.danger_accept_invalid_certs`| `bool` | `false` | Accept invalid TLS certificates. Warning: Insecure. Use only for development or testing. | Below is an example configuration using `StreamableHTTP` transport with authentication: From 0023a08f686cd9b49308ef34b7b13246f2255970 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Mon, 5 Jan 2026 13:47:33 -0500 Subject: [PATCH 5/9] chore: changeset --- .changesets/feat_tls_config.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changesets/feat_tls_config.md diff --git a/.changesets/feat_tls_config.md b/.changesets/feat_tls_config.md new file mode 100644 index 000000000..9bb6eea9b --- /dev/null +++ b/.changesets/feat_tls_config.md @@ -0,0 +1,19 @@ +### Add TLS configuration options for auth - @DaleSeo PR #536 + +Adds TLS configuration options for connecting to OAuth servers during token validation. + +When the MCP server validates OAuth tokens, it connects to upstream OAuth servers to fetch JWKS keys. Previously, this required those servers to have certificates trusted by the system's default CA bundle. This change allows users to trust custom CA certificates or disable validation for development environments. + +```yaml +transport: + streamable_http: + auth: + servers: + - https://auth.example.com + audiences: + - my-audience + resource: https://mcp.example.com/mcp + tls: + ca_cert: /path/to/ca-certificate.pem + danger_accept_invalid_certs: false # dev/testing only +``` \ No newline at end of file From 4631d8584c6779f2c4eb58dc8eeaa7cffafd8da0 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Tue, 6 Jan 2026 10:30:58 -0500 Subject: [PATCH 6/9] refactor: build HTTP client at startup instead of per-request --- crates/apollo-mcp-server/src/auth.rs | 59 ++++++++++++------- crates/apollo-mcp-server/src/errors.rs | 3 + .../src/server/states/starting.rs | 8 ++- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/crates/apollo-mcp-server/src/auth.rs b/crates/apollo-mcp-server/src/auth.rs index 31890c374..291690691 100644 --- a/crates/apollo-mcp-server/src/auth.rs +++ b/crates/apollo-mcp-server/src/auth.rs @@ -128,8 +128,18 @@ pub struct TlsConfig { pub danger_accept_invalid_certs: bool, } +/// Internal state for the auth middleware, containing both config and pre-built HTTP client +#[derive(Clone)] +struct AuthState { + config: Config, + client: reqwest::Client, +} + impl Config { - pub fn enable_middleware(&self, router: Router) -> Router { + /// Enable auth middleware on the router. + /// + /// Builds the HTTP client at startup to validate TLS configuration eagerly. + pub fn enable_middleware(&self, router: Router) -> Result { if self.allow_any_audience { warn!( "allow_any_audience is enabled - audience validation is disabled. This reduces security." @@ -138,10 +148,19 @@ impl Config { /// Simple handler to encode our config into the desired OAuth 2.1 protected /// resource format - async fn protected_resource(State(auth_config): State) -> Json { - Json(auth_config.into()) + async fn protected_resource( + State(auth_state): State, + ) -> Json { + Json(auth_state.config.into()) } + // Build HTTP client with TLS configuration + let client = self.tls.build_client()?; + let auth_state = AuthState { + config: self.clone(), + client, + }; + // Set up auth routes. NOTE: CORs needs to allow for get requests to the // metadata information paths. let cors = CorsLayer::new() @@ -152,27 +171,26 @@ impl Config { "/.well-known/oauth-protected-resource", get(protected_resource), ) - .with_state(self.clone()) + .with_state(auth_state.clone()) .layer(cors); // Merge with MCP server routes - Router::new() - .merge(auth_router) - .merge(router.layer(axum::middleware::from_fn_with_state( - self.clone(), - oauth_validate, - ))) + Ok(Router::new().merge(auth_router).merge(router.layer( + axum::middleware::from_fn_with_state(auth_state, oauth_validate), + ))) } } /// Validate that requests made have a corresponding bearer JWT token #[tracing::instrument(skip_all, fields(status_code, reason))] async fn oauth_validate( - State(auth_config): State, + State(auth_state): State, token: Option>>, mut request: Request, next: Next, ) -> Result)> { + let auth_config = &auth_state.config; + // Consolidated unauthorized error for use with any fallible step in this process let unauthorized_error = || { let mut resource = auth_config.resource.clone(); @@ -193,19 +211,11 @@ async fn oauth_validate( ) }; - // Build HTTP client with TLS configuration - let client = auth_config.tls.build_client().map_err(|e| { - tracing::error!("Failed to build HTTP client for OAuth validation: {e}"); - tracing::Span::current().record("reason", "client_build_error"); - tracing::Span::current().record("status_code", StatusCode::INTERNAL_SERVER_ERROR.as_u16()); - unauthorized_error() - })?; - let validator = NetworkedTokenValidator::new( &auth_config.audiences, auth_config.allow_any_audience, &auth_config.servers, - &client, + &auth_state.client, ); let token = token.ok_or_else(|| { tracing::Span::current().record("reason", "missing_token"); @@ -255,10 +265,17 @@ mod tests { } } + fn test_auth_state(config: Config) -> AuthState { + AuthState { + config, + client: reqwest::Client::new(), + } + } + fn test_router(config: Config) -> Router { Router::new() .route("/test", get(|| async { "ok" })) - .layer(from_fn_with_state(config, oauth_validate)) + .layer(from_fn_with_state(test_auth_state(config), oauth_validate)) } #[tokio::test] diff --git a/crates/apollo-mcp-server/src/errors.rs b/crates/apollo-mcp-server/src/errors.rs index 1e86faf76..1f7ad2743 100644 --- a/crates/apollo-mcp-server/src/errors.rs +++ b/crates/apollo-mcp-server/src/errors.rs @@ -106,6 +106,9 @@ pub enum ServerError { #[error("Failed to load apps: {0}")] Apps(String), + + #[error("TLS configuration error: {0}")] + Tls(#[from] crate::auth::TlsConfigError), } /// An MCP tool error diff --git a/crates/apollo-mcp-server/src/server/states/starting.rs b/crates/apollo-mcp-server/src/server/states/starting.rs index 0f4907604..973cbc575 100644 --- a/crates/apollo-mcp-server/src/server/states/starting.rs +++ b/crates/apollo-mcp-server/src/server/states/starting.rs @@ -180,7 +180,13 @@ impl Starting { ($router:expr, $auth:ident) => {{ let mut router = $router; if let Some(auth) = $auth { - router = auth.enable_middleware(router); + match auth.enable_middleware(router) { + Ok(r) => router = r, + Err(e) => { + error!("Failed to enable auth middleware: {}", e); + return Err(e.into()); + } + } } router From e3a8fdb8ba5012d9aab075d2f83f229d98f638cf Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 7 Jan 2026 08:50:49 -0500 Subject: [PATCH 7/9] Update .changesets/feat_tls_config.md Co-authored-by: Michelle Mabuyo --- .changesets/feat_tls_config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changesets/feat_tls_config.md b/.changesets/feat_tls_config.md index 9bb6eea9b..eec0d7514 100644 --- a/.changesets/feat_tls_config.md +++ b/.changesets/feat_tls_config.md @@ -15,5 +15,5 @@ transport: resource: https://mcp.example.com/mcp tls: ca_cert: /path/to/ca-certificate.pem - danger_accept_invalid_certs: false # dev/testing only + danger_accept_invalid_certs: false # Set this to true for development or testing purposes only ``` \ No newline at end of file From 84c3ba609dafc46170bc11fcfda5c2d1c3be2698 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Wed, 7 Jan 2026 08:51:11 -0500 Subject: [PATCH 8/9] Update docs/source/config-file.mdx Co-authored-by: Michelle Mabuyo --- docs/source/config-file.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/config-file.mdx b/docs/source/config-file.mdx index 939dfa907..d5b0ee69e 100644 --- a/docs/source/config-file.mdx +++ b/docs/source/config-file.mdx @@ -294,7 +294,7 @@ transport: # Path to CA certificate for private CA or self-signed OAuth server certs # Use when your OAuth server uses a self-signed certificate or a certificate signed by a private CA ca_cert: /path/to/ca-certificate.pem - # Only set to true for development/testing (insecure!) + # Set this to true for development or testing purposes only # danger_accept_invalid_certs: false ``` From b4d6e5015b4c454f9ca900742b4634a915e8352a Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Wed, 7 Jan 2026 08:56:28 -0500 Subject: [PATCH 9/9] docs: format config-file.mdx --- docs/source/config-file.mdx | 167 ++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 84 deletions(-) diff --git a/docs/source/config-file.mdx b/docs/source/config-file.mdx index d5b0ee69e..0c051a175 100644 --- a/docs/source/config-file.mdx +++ b/docs/source/config-file.mdx @@ -15,23 +15,22 @@ All fields are optional. ### Top-level options -| Option | Type | Default | Description | -| :--------------- | :-------------------- | :----------------------- | :--------------------------------------------------------------- | -| `cors` | `Cors` | | CORS configuration | -| `custom_scalars` | `FilePath` | | Path to a [custom scalar map](/apollo-mcp-server/custom-scalars) | -| `endpoint` | `URL` | `http://localhost:4000/` | The target GraphQL endpoint | -| `forward_headers`| `List` | `[]` | Headers to forward from MCP clients to GraphQL API | -| `graphos` | `GraphOS` | | Apollo-specific credential overrides | -| `headers` | `Map` | `{}` | List of hard-coded headers to include in all GraphQL requests | -| `health_check` | `HealthCheck` | | Health check configuration | -| `introspection` | `Introspection` | | Introspection configuration | -| `logging` | `Logging` | | Logging configuration | -| `operations` | `OperationSource` | | Operations configuration | -| `overrides` | `Overrides` | | Overrides for server behavior | -| `schema` | `SchemaSource` | | Schema configuration | -| `transport` | `Transport` | | The type of server transport to use | -| `telemetry` | `Telemetry` | | Configuration to export metrics and traces via OTLP | - +| Option | Type | Default | Description | +| :---------------- | :-------------------- | :----------------------- | :--------------------------------------------------------------- | +| `cors` | `Cors` | | CORS configuration | +| `custom_scalars` | `FilePath` | | Path to a [custom scalar map](/apollo-mcp-server/custom-scalars) | +| `endpoint` | `URL` | `http://localhost:4000/` | The target GraphQL endpoint | +| `forward_headers` | `List` | `[]` | Headers to forward from MCP clients to GraphQL API | +| `graphos` | `GraphOS` | | Apollo-specific credential overrides | +| `headers` | `Map` | `{}` | List of hard-coded headers to include in all GraphQL requests | +| `health_check` | `HealthCheck` | | Health check configuration | +| `introspection` | `Introspection` | | Introspection configuration | +| `logging` | `Logging` | | Logging configuration | +| `operations` | `OperationSource` | | Operations configuration | +| `overrides` | `Overrides` | | Overrides for server behavior | +| `schema` | `SchemaSource` | | Schema configuration | +| `transport` | `Transport` | | The type of server transport to use | +| `telemetry` | `Telemetry` | | Configuration to export metrics and traces via OTLP | ### GraphOS @@ -59,6 +58,7 @@ To forward dynamic header values from the client, use the [`forward_headers` opt The `forward_headers` option allows you to forward specific headers from incoming MCP client requests to your GraphQL API. This is useful for: + - Multi-tenant applications (forwarding tenant IDs) - A/B testing (forwarding experiment IDs) - Geo information (forwarding country codes) @@ -82,14 +82,15 @@ forward_headers: Don't use header forwarding to pass through sensitive credentials such as API keys or access tokens. -

+
+
According to [MCP security best practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#token-passthrough) and the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#access-token-privilege-restriction), token passthrough introduces serious security risks: - **Audience confusion**: If the MCP Server accepts tokens not intended for it, it can violate OAuth's trust boundaries. - **Confused deputy problem**: If an unvalidated token is passed downstream, a downstream API may incorrectly trust it as though it were validated by the MCP Server. -
+
Apollo MCP Server supports OAuth 2.1 authentication that follows best practices and aligns with the MCP authorization model. See our [authorization guide](/apollo-mcp-server/auth) for implementation details and how to use the [auth configuration](#auth). @@ -216,25 +217,25 @@ transport: The available fields depend on the value of the nested `type` key. The default type is `stdio`: -| Option | Description | -| :-------------------- | :--------------------------------------------------------------------------------------------------------------- | -| `"stdio"` | Use standard IO for communication between the server and client | -| `"streamable_http"` | Host the MCP server on the configuration, using streamable HTTP messages | -| `"sse"` | Host the MCP server on the supplied config, using SSE for communication. Deprecated in favor of `StreamableHTTP` | +| Option | Description | +| :------------------ | :--------------------------------------------------------------------------------------------------------------- | +| `"stdio"` | Use standard IO for communication between the server and client | +| `"streamable_http"` | Host the MCP server on the configuration, using streamable HTTP messages | +| `"sse"` | Host the MCP server on the supplied config, using SSE for communication. Deprecated in favor of `StreamableHTTP` | ##### Transport Type Specific options Some transport types support further configuration. For both `streamable_http` and `sse`, you can set the `address` and `port`. For `streamable_http`, you can also set `stateful_mode`. -| Option | Type | Default | Description | -| :-------------- | :--------- | :---------- | :----------------------------------------------------------------------------------- | -| `address` | `IpAddr` | `127.0.0.1` | The IP address to bind to | -| `port` | `u16` | `8000` | The port to bind to | -| `stateful_mode` | `bool` | `true` | Flag to enable or disable stateful mode and session management. Not supported by SSE | +| Option | Type | Default | Description | +| :-------------- | :------- | :---------- | :----------------------------------------------------------------------------------- | +| `address` | `IpAddr` | `127.0.0.1` | The IP address to bind to | +| `port` | `u16` | `8000` | The port to bind to | +| `stateful_mode` | `bool` | `true` | Flag to enable or disable stateful mode and session management. Not supported by SSE | -For Apollo MCP Server `≤v1.0.0`, the default `port` value is `5000`. In `v1.1.0`, the default `port` option was changed to `8000` to avoid conflicts with common development tools and services that typically use port 5000 (such as macOS AirPlay, Flask development servers, and other local services). +For Apollo MCP Server `≤v1.0.0`, the default `port` value is `5000`. In `v1.1.0`, the default `port` option was changed to `8000` to avoid conflicts with common development tools and services that typically use port 5000 (such as macOS AirPlay, Flask development servers, and other local services). @@ -242,17 +243,17 @@ For Apollo MCP Server `≤v1.0.0`, the default `port` value is `5000`. In `v1.1. These fields are under the top-level `transport` key, nested under the `auth` key. Learn more about [authorization and authentication](/apollo-mcp-server/auth). -| Option | Type | Default | Description | -| :------------------------------- | :------------- | :------ | :------------------------------------------------------------------------------------------------- | -| `servers` | `List` | | List of upstream delegated OAuth servers (must support OIDC metadata discovery endpoint) | -| `audiences` | `List` | `[]` | List of accepted audiences from upstream signed JWTs (ignored if `allow_any_audience` is `true`) | -| `allow_any_audience` | `bool` | `false` | Set to `true` to skip audience validation entirely (use with caution) | -| `resource` | `string` | | The externally available URL pointing to this MCP server. Can be `localhost` when testing locally. | -| `resource_documentation` | `string` | | Optional link to more documentation relating to this MCP server | -| `scopes` | `List` | | List of queryable OAuth scopes from the upstream OAuth servers | -| `disable_auth_token_passthrough` | `bool` | `false` | Optional flag to disable passing validated Authorization header to downstream API | -| `tls.ca_cert` | `string` | | Path to a CA certificate to trust (PEM format). | -| `tls.danger_accept_invalid_certs`| `bool` | `false` | Accept invalid TLS certificates. Warning: Insecure. Use only for development or testing. | +| Option | Type | Default | Description | +| :-------------------------------- | :------------- | :------ | :------------------------------------------------------------------------------------------------- | +| `servers` | `List` | | List of upstream delegated OAuth servers (must support OIDC metadata discovery endpoint) | +| `audiences` | `List` | `[]` | List of accepted audiences from upstream signed JWTs (ignored if `allow_any_audience` is `true`) | +| `allow_any_audience` | `bool` | `false` | Set to `true` to skip audience validation entirely (use with caution) | +| `resource` | `string` | | The externally available URL pointing to this MCP server. Can be `localhost` when testing locally. | +| `resource_documentation` | `string` | | Optional link to more documentation relating to this MCP server | +| `scopes` | `List` | | List of queryable OAuth scopes from the upstream OAuth servers | +| `disable_auth_token_passthrough` | `bool` | `false` | Optional flag to disable passing validated Authorization header to downstream API | +| `tls.ca_cert` | `string` | | Path to a CA certificate to trust (PEM format). | +| `tls.danger_accept_invalid_certs` | `bool` | `false` | Accepts invalid TLS certificates. Set this to `true` for development or testing purposes only. | Below is an example configuration using `StreamableHTTP` transport with authentication: @@ -300,37 +301,35 @@ transport: ### Telemetry -| Option | Type | Default | Description | -| :-------------- | :---------- | :-------------------------- | :--------------------------------------- | -| `service_name` | `string` | "apollo-mcp-server" | The service name in telemetry data. | -| `version` | `string` | Current crate version | The service version in telemetry data. | -| `exporters` | `Exporters` | `null` (Telemetry disabled) | Configuration for telemetry exporters. | +| Option | Type | Default | Description | +| :------------- | :---------- | :-------------------------- | :------------------------------------- | +| `service_name` | `string` | "apollo-mcp-server" | The service name in telemetry data. | +| `version` | `string` | Current crate version | The service version in telemetry data. | +| `exporters` | `Exporters` | `null` (Telemetry disabled) | Configuration for telemetry exporters. | #### Exporters -| Option | Type | Default | Description | -| :--------- | :---------- | :-------------------------- | :--------------------------------------- | -| `metrics` | `Metrics` | `null` (Metrics disabled) | Configuration for exporting metrics. | -| `tracing` | `Tracing` | `null` (Tracing disabled) | Configuration for exporting traces. | - +| Option | Type | Default | Description | +| :-------- | :-------- | :------------------------ | :----------------------------------- | +| `metrics` | `Metrics` | `null` (Metrics disabled) | Configuration for exporting metrics. | +| `tracing` | `Tracing` | `null` (Tracing disabled) | Configuration for exporting traces. | #### Metrics -| Option | Type | Default | Description | -| :-------------------- | :---------------------- | :-------------------------- | :--------------------------------------------- | -| `otlp` | `OTLP Metric Exporter` | `null` (Exporting disabled) | Configuration for exporting metrics via OTLP. | -| `omitted_attributes` | `List` | | List of attributes to be omitted from metrics. | - +| Option | Type | Default | Description | +| :------------------- | :--------------------- | :-------------------------- | :--------------------------------------------- | +| `otlp` | `OTLP Metric Exporter` | `null` (Exporting disabled) | Configuration for exporting metrics via OTLP. | +| `omitted_attributes` | `List` | | List of attributes to be omitted from metrics. | #### OTLP Metrics Exporter -| Option | Type | Default | Description | -|:--------------|:-----------------------------------------------|:------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `endpoint` | `URL` | `http://localhost:4317` | URL to export data to. Requires full path. | -| `protocol` | `string` | `grpc` | Specifies the export protocol. Supported values are `grpc` and `http/protobuf`. | -| `temporality` | `MetricTemporality` | `Cumulative` | Specifies how additive quantities are expressed over time. `Cumulative` means reported values include previous measurements, and `Delta` means they don't. | -| `metadata` | Key-value pairs | | Key-value pairs for metadata. This field applies only when `protocol` is `grpc`. | -| `headers` | Key-value pairs | | Key-value pairs for headers. This field applies only when `protocol` is `http/protobuf`. | +| Option | Type | Default | Description | +| :------------ | :------------------ | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `endpoint` | `URL` | `http://localhost:4317` | URL to export data to. Requires full path. | +| `protocol` | `string` | `grpc` | Specifies the export protocol. Supported values are `grpc` and `http/protobuf`. | +| `temporality` | `MetricTemporality` | `Cumulative` | Specifies how additive quantities are expressed over time. `Cumulative` means reported values include previous measurements, and `Delta` means they don't. | +| `metadata` | Key-value pairs | | Key-value pairs for metadata. This field applies only when `protocol` is `grpc`. | +| `headers` | Key-value pairs | | Key-value pairs for headers. This field applies only when `protocol` is `http/protobuf`. | @@ -373,20 +372,20 @@ export APOLLO_MCP_TELEMETRY__EXPORTERS__METRICS__OTLP__METADATA__API_KEY="${YOUR #### Traces -| Option | Type | Default | Description | -| :-------------------- | :--------------------- | :-------------------------- | :--------------------------------------------- | -| `otlp` | `OTLP Trace Exporter` | `null` (Exporting disabled) | Configuration for exporting traces via OTLP. | -| `sampler` | `SamplerOption` | `ALWAYS_ON` | Configuration to control sampling of traces. | -| `omitted_attributes` | `List` | | List of attributes to be omitted from traces. | +| Option | Type | Default | Description | +| :------------------- | :-------------------- | :-------------------------- | :-------------------------------------------- | +| `otlp` | `OTLP Trace Exporter` | `null` (Exporting disabled) | Configuration for exporting traces via OTLP. | +| `sampler` | `SamplerOption` | `ALWAYS_ON` | Configuration to control sampling of traces. | +| `omitted_attributes` | `List` | | List of attributes to be omitted from traces. | #### OTLP Trace Exporter -| Option | Type | Default | Description | -|:-------------|:---------------------------------------------|:------------------------|:-----------------------------------------------------------------------------------------| -| `endpoint` | `URL` | `http://localhost:4317` | URL to export data to. Requires full path. | -| `protocol` | `string` | `grpc` | Specifies the export protocol. Supported values are `grpc` and `http/protobuf`. | -| `metadata` | Key-value pairs | | Key-value pairs for metadata. This field applies only when `protocol` is `grpc`. | -| `headers` | Key-value pairs | | Key-value pairs for headers. This field applies only when `protocol` is `http/protobuf`. | +| Option | Type | Default | Description | +| :--------- | :-------------- | :---------------------- | :--------------------------------------------------------------------------------------- | +| `endpoint` | `URL` | `http://localhost:4317` | URL to export data to. Requires full path. | +| `protocol` | `string` | `grpc` | Specifies the export protocol. Supported values are `grpc` and `http/protobuf`. | +| `metadata` | Key-value pairs | | Key-value pairs for metadata. This field applies only when `protocol` is `grpc`. | +| `headers` | Key-value pairs | | Key-value pairs for headers. This field applies only when `protocol` is `http/protobuf`. | @@ -408,19 +407,19 @@ telemetry: #### MetricTemporality -| Option | Type | Description | -|:-------------|:------------|:-------------------------------| -| `cumulative` | `string` | Cumulative temporality option. | -| `delta` | `string` | Delta temporality option. | -| `lowmemory` | `string` | Low memory temporality option. | +| Option | Type | Description | +| :----------- | :------- | :----------------------------- | +| `cumulative` | `string` | Cumulative temporality option. | +| `delta` | `string` | Delta temporality option. | +| `lowmemory` | `string` | Low memory temporality option. | #### SamplerOption -| Option | Type | Description | -| :----------- | :-------- | :------------------------------------------------------- | -| `always_on` | `string` | All traces will be exported. | -| `always_off` | `string` | Sampling is turned off, no traces will be exported. | -| `0.0-1.0` | `f64` | Percentage of traces to export. | +| Option | Type | Description | +| :----------- | :------- | :-------------------------------------------------- | +| `always_on` | `string` | All traces will be exported. | +| `always_off` | `string` | Sampling is turned off, no traces will be exported. | +| `0.0-1.0` | `f64` | Percentage of traces to export. | ## Example config files