From b06259521766745d0ccbc0ceb4d5beedc4c826a8 Mon Sep 17 00:00:00 2001 From: Madison Grubb Date: Tue, 9 Jun 2026 09:55:58 -0400 Subject: [PATCH] fix(otlp): resolve auth credentials from environment variables Resolve ${VAR} placeholders in YAML OTLP auth fields at header generation time. HCL configs continue to use env(), which is already evaluated when the config loads. Fix docs that showed unquoted env(OTLP_PASSWORD) syntax. --- .../reference/opentelemetry-otlp-exporter.md | 16 +++- docs/deployment/advanced-scenarios.md | 2 +- mermin/src/otlp/opts.rs | 74 ++++++++++++++++--- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/docs/configuration/reference/opentelemetry-otlp-exporter.md b/docs/configuration/reference/opentelemetry-otlp-exporter.md index d6ca3156..ffc856ac 100644 --- a/docs/configuration/reference/opentelemetry-otlp-exporter.md +++ b/docs/configuration/reference/opentelemetry-otlp-exporter.md @@ -327,7 +327,7 @@ Configure HTTP Basic authentication for the OTLP endpoint. - `pass` attribute - Password for basic authentication. Supports environment variable interpolation via `env(VAR_NAME)`. + Password for basic authentication. Supports environment variable interpolation via `env("VAR_NAME")` in HCL or `${VAR_NAME}` in YAML. **Type:** String @@ -365,13 +365,23 @@ Configure HTTP Basic authentication for the OTLP endpoint. auth = { basic = { user = "mermin" - pass = env(OTLP_PASSWORD) + pass = env("OTLP_PASSWORD") } } } } ``` + ```yaml + export: + traces: + otlp: + auth: + basic: + user: mermin + pass: "${OTLP_PASSWORD}" + ``` + - Using Kubernetes Secrets ```bash @@ -637,7 +647,7 @@ export "traces" { auth = { basic = { user = "mermin" - pass = env(OTLP_PASSWORD) + pass = env("OTLP_PASSWORD") } } diff --git a/docs/deployment/advanced-scenarios.md b/docs/deployment/advanced-scenarios.md index b578570f..2fa0316f 100644 --- a/docs/deployment/advanced-scenarios.md +++ b/docs/deployment/advanced-scenarios.md @@ -624,7 +624,7 @@ export "traces" { auth = { basic = { user = "mermin" - pass = env(OTLP_PASSWORD) # Load from environment + pass = env("OTLP_PASSWORD") # Load from environment } } } diff --git a/mermin/src/otlp/opts.rs b/mermin/src/otlp/opts.rs index 0f1ddce4..f5c150f7 100644 --- a/mermin/src/otlp/opts.rs +++ b/mermin/src/otlp/opts.rs @@ -141,17 +141,20 @@ pub struct AuthOptions { pub bearer: Option, // TODO: Add support for api_key, oauth2, mtls, etc. - ENG-120 } -/// # Example (YAML) -/// ```yaml -/// auth: -/// basic: -/// user: foo -/// pass: env("MY_PASSWORD_ENV_VAR") +/// # Example (HCL) +/// ```hcl +/// auth = { +/// basic = { +/// user = "foo" +/// pass = env("MY_PASSWORD_ENV_VAR") +/// } +/// } /// ``` +/// YAML configs should use `${MY_PASSWORD_ENV_VAR}` instead. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct BasicAuthOptions { pub user: String, - pub pass: String, // TODO: Support environment variable substitution - ENG-120 + pub pass: String, } /// TLS configuration for secure exporter connections. @@ -197,18 +200,30 @@ pub struct TlsOptions { pub client_key: Option, } +/// Resolves `${VAR}` placeholders in YAML config values at auth header generation time. +/// HCL configs use the `env()` function, which is evaluated when the config loads. +fn resolve_env_var(value: &str) -> Result { + if let Some(var_name) = value.strip_prefix("${").and_then(|s| s.strip_suffix('}')) { + std::env::var(var_name).map_err(|_| format!("environment variable '{var_name}' is not set")) + } else { + Ok(value.to_string()) + } +} + impl AuthOptions { pub fn generate_auth_headers(&self) -> Result, String> { let mut headers = HashMap::new(); if let Some(basic) = &self.basic { - let credentials = - general_purpose::STANDARD.encode(format!("{}:{}", basic.user, basic.pass)); + let user = resolve_env_var(&basic.user)?; + let pass = resolve_env_var(&basic.pass)?; + let credentials = general_purpose::STANDARD.encode(format!("{user}:{pass}")); headers.insert("Authorization".to_string(), format!("Basic {credentials}")); } if let Some(bearer) = &self.bearer { - headers.insert("Authorization".to_string(), format!("Bearer {bearer}")); + let token = resolve_env_var(bearer)?; + headers.insert("Authorization".to_string(), format!("Bearer {token}")); } // TODO: Add support for other auth methods like api_key, oauth2, mtls, etc. - ENG-120 @@ -342,4 +357,43 @@ mod tests { assert_eq!(ExporterProtocol::Grpc.to_string(), "grpc"); assert_eq!(ExporterProtocol::HttpBinary.to_string(), "http_binary"); } + + #[test] + fn resolve_env_var_plain_string() { + assert_eq!(resolve_env_var("secret").unwrap(), "secret"); + } + + #[test] + fn resolve_env_var_dollar_form() { + unsafe { std::env::set_var("OTLP_TEST_TOKEN", "bearer_token") }; + assert_eq!( + resolve_env_var("${OTLP_TEST_TOKEN}").unwrap(), + "bearer_token" + ); + unsafe { std::env::remove_var("OTLP_TEST_TOKEN") }; + } + + #[test] + fn resolve_env_var_missing() { + assert!(resolve_env_var("${OTLP_DEFINITELY_MISSING_VAR}").is_err()); + } + + #[test] + fn generate_auth_headers_resolves_pass() { + unsafe { std::env::set_var("OTLP_TEST_PASS", "secret_password") }; + let auth = AuthOptions { + basic: Some(BasicAuthOptions { + user: "mermin".to_string(), + pass: "${OTLP_TEST_PASS}".to_string(), + }), + bearer: None, + }; + let headers = auth.generate_auth_headers().unwrap(); + let expected = general_purpose::STANDARD.encode("mermin:secret_password"); + assert_eq!( + headers.get("Authorization").unwrap(), + &format!("Basic {expected}") + ); + unsafe { std::env::remove_var("OTLP_TEST_PASS") }; + } }