diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 993efb4e05f..97ae6827011 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -11,6 +11,10 @@ Notes](../../RELEASENOTES.md). cause the measurement to be recorded incorrectly. ([#7951](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7951)) +* Fixed self-diagnostics preamble redaction of quoted or encoded PEM material + and prefixed JWTs. + ([#7955](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7955)) + ## 1.20.0 Released 2026-Oct-15 diff --git a/src/OpenTelemetry/Internal/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicy.cs b/src/OpenTelemetry/Internal/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicy.cs index c77ffa5d61c..dbecc591dfb 100644 --- a/src/OpenTelemetry/Internal/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicy.cs +++ b/src/OpenTelemetry/Internal/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicy.cs @@ -33,6 +33,7 @@ internal static class SelfDiagnosticsEnvironmentVariablePolicy private const string ResourceAttributesVarName = "OTEL_RESOURCE_ATTRIBUTES"; private const string PemArmourPrefix = "-----BEGIN"; + private const string Base64PemArmourPrefix = "LS0tLS1CRUdJT"; private const string JwtPrefix = "eyJ"; private static readonly char[] NewLineChars = ['\r', '\n']; @@ -215,7 +216,7 @@ internal static bool ShouldDisplayValue(string name) internal static string GetDisplayValue(string name, string value) { // Applied ahead of everything else, including the safe list: no path, endpoint, or scalar - // setting legitimately spans lines or opens with PEM armour, so content that does is key + // setting legitimately spans lines or contains PEM armour, so content that does is key // material pasted where a reference was expected. if (ContainsInlineSecretMaterial(value)) { @@ -263,8 +264,32 @@ private static string GetUriAuthority(string value) private static bool ContainsInlineSecretMaterial(string value) => value.IndexOfAny(NewLineChars) >= 0 - || value.TrimStart().StartsWith(PemArmourPrefix, StringComparison.Ordinal) - || value.TrimStart().StartsWith(JwtPrefix, StringComparison.Ordinal); + || Contains(value, PemArmourPrefix) + || Contains(value, Base64PemArmourPrefix) + || ContainsJwt(value); + + private static bool Contains(string value, string marker) +#if NET || NETSTANDARD2_1_OR_GREATER + => value.Contains(marker, StringComparison.Ordinal); +#else + => value.IndexOf(marker, StringComparison.Ordinal) >= 0; +#endif + + private static bool ContainsJwt(string value) + { + for (var index = value.IndexOf(JwtPrefix, StringComparison.Ordinal); + index >= 0; + index = value.IndexOf(JwtPrefix, index + JwtPrefix.Length, StringComparison.Ordinal)) + { + // Match token boundaries without treating a path such as /opt/keyJar/client.key as a JWT. + if (index == 0 || !char.IsLetterOrDigit(value[index - 1])) + { + return true; + } + } + + return false; + } /// /// Redacts OTEL_RESOURCE_ATTRIBUTES per key=value pair rather than wholesale. diff --git a/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicyTests.cs b/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicyTests.cs index 1893472f17a..825f74ebc79 100644 --- a/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicyTests.cs +++ b/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsEnvironmentVariablePolicyTests.cs @@ -46,6 +46,43 @@ public void InlineJwtMaterial_IsRedacted(string name) Assert.Equal(Redacted, SelfDiagnosticsEnvironmentVariablePolicy.GetDisplayValue(name, Jwt)); } + [Theory] + [InlineData("OTEL_EXPORTER_OTLP_CERTIFICATE", "\"")] + [InlineData("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "\"")] + [InlineData("OTEL_EXPORTER_OTLP_CLIENT_KEY", "\"")] + [InlineData("OTEL_EXPORTER_OTLP_CERTIFICATE", "'")] + [InlineData("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "'")] + [InlineData("OTEL_EXPORTER_OTLP_CLIENT_KEY", "'")] + public void QuotedInlineKeyMaterial_IsRedacted(string name, string quote) + { + const string Pem = "-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADAN\\n-----END PRIVATE KEY-----"; + + Assert.Equal(Redacted, SelfDiagnosticsEnvironmentVariablePolicy.GetDisplayValue(name, $"{quote}{Pem}{quote}")); + } + + [Theory] + [InlineData("prefix:-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADAN\\n-----END PRIVATE KEY-----")] + [InlineData("LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTgo=")] + [InlineData("\"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature\"")] + [InlineData("Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature")] + [InlineData("keyJar Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature")] + public void InlineSecretMaterial_IsRedactedWithPrefixesOrEncoding(string value) => + Assert.Equal(Redacted, SelfDiagnosticsEnvironmentVariablePolicy.GetDisplayValue("OTEL_EXPORTER_OTLP_CLIENT_KEY", value)); + + [Fact] + public void InlineKeyMaterial_InSafeResourceAttribute_IsRedacted() => + Assert.Equal( + Redacted, + SelfDiagnosticsEnvironmentVariablePolicy.GetDisplayValue( + "OTEL_RESOURCE_ATTRIBUTES", + "service.name=-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADAN\\n-----END PRIVATE KEY-----")); + + [Theory] + [InlineData("/opt/keyJar/client.key")] + [InlineData("/opt/keyJar/keyJar/client.key")] + public void PathsContainingJwtPrefix_KeepTheirValue(string path) => + Assert.Equal(path, SelfDiagnosticsEnvironmentVariablePolicy.GetDisplayValue("OTEL_EXPORTER_OTLP_CLIENT_KEY", path)); + [Fact] public void MultiLineValue_IsRedacted() => Assert.Equal(Redacted, SelfDiagnosticsEnvironmentVariablePolicy.GetDisplayValue("OTEL_SERVICE_NAME", "first\r\nsecond")); diff --git a/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsPreambleTests.cs b/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsPreambleTests.cs index 9e2f5a74b25..488b99d316b 100644 --- a/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsPreambleTests.cs +++ b/test/OpenTelemetry.Tests/Diagnostics/SelfDiagnosticsPreambleTests.cs @@ -86,6 +86,25 @@ public void KnownSafeValuesMode_RedactsCredentialCarryingVariables() Assert.DoesNotContain("super-secret-token", preamble, StringComparison.Ordinal); } + [Theory] + [InlineData(EnvironmentVariableLogMode.KnownSafeValues)] + [InlineData(EnvironmentVariableLogMode.Names)] + [InlineData(EnvironmentVariableLogMode.None)] + public void RestrictiveModes_DoNotDiscloseQuotedInlineKeyMaterial(EnvironmentVariableLogMode mode) + { + using var scope = EnvironmentVariableScope.Create( + "OTEL_EXPORTER_OTLP_CLIENT_KEY", + "\"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADAN\\n-----END PRIVATE KEY-----\""); + + var preamble = Build(mode); + + Assert.DoesNotContain("MIIEvQIBADAN", preamble, StringComparison.Ordinal); + if (mode == EnvironmentVariableLogMode.KnownSafeValues) + { + Assert.Contains("OTEL_EXPORTER_OTLP_CLIENT_KEY = [REDACTED]", preamble, StringComparison.Ordinal); + } + } + [Fact] public void KnownSafeValuesMode_ReducesEndpointsToTheirAuthority() { @@ -103,14 +122,17 @@ public void KnownSafeValuesMode_ReducesEndpointsToTheirAuthority() [Fact] public void AllValuesMode_DisclosesEverythingVerbatim() { + const string Pem = "\"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADAN\\n-----END PRIVATE KEY-----\""; using var scope = EnvironmentVariableScope.Create( (UnclassifiedVar, UnclassifiedValue), - ("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer opt-in-token")); + ("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer opt-in-token"), + ("OTEL_EXPORTER_OTLP_CLIENT_KEY", Pem)); var preamble = Build(EnvironmentVariableLogMode.AllValues); Assert.Contains($"{UnclassifiedVar} = {UnclassifiedValue}", preamble, StringComparison.Ordinal); Assert.Contains("opt-in-token", preamble, StringComparison.Ordinal); + Assert.Contains($"OTEL_EXPORTER_OTLP_CLIENT_KEY = {Pem}", preamble, StringComparison.Ordinal); } [Fact]