Skip to content

Commit 31ac6b0

Browse files
authored
Make redaction configurable for APM tracing (#92358)
Closes #92338. When tracing REST requests with APM, we capture HTTP headers as labels on the trace, but redact sensitive values. However, we can't know ahead of time what are all possible sensitive values. Push this redaction into the tracer, and make the redaction terms configurable. Switch the defaults to the APM Java agent's defaults.
1 parent 9d605b5 commit 31ac6b0

File tree

5 files changed

+89
-9
lines changed

5 files changed

+89
-9
lines changed

modules/apm/src/main/java/org/elasticsearch/tracing/apm/APM.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public List<Setting<?>> getSettings() {
101101
APMAgentSettings.APM_ENABLED_SETTING,
102102
APMAgentSettings.APM_TRACING_NAMES_INCLUDE_SETTING,
103103
APMAgentSettings.APM_TRACING_NAMES_EXCLUDE_SETTING,
104+
APMAgentSettings.APM_TRACING_SANITIZE_FIELD_NAMES,
104105
APMAgentSettings.APM_AGENT_SETTINGS,
105106
APMAgentSettings.APM_SECRET_TOKEN_SETTING,
106107
APMAgentSettings.APM_API_KEY_SETTING

modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMAgentSettings.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ void addClusterSettingsListeners(ClusterService clusterService, APMTracer apmTra
5757
});
5858
clusterSettings.addSettingsUpdateConsumer(APM_TRACING_NAMES_INCLUDE_SETTING, apmTracer::setIncludeNames);
5959
clusterSettings.addSettingsUpdateConsumer(APM_TRACING_NAMES_EXCLUDE_SETTING, apmTracer::setExcludeNames);
60+
clusterSettings.addSettingsUpdateConsumer(APM_TRACING_SANITIZE_FIELD_NAMES, apmTracer::setLabelFilters);
6061
clusterSettings.addAffixMapUpdateConsumer(APM_AGENT_SETTINGS, map -> map.forEach(this::setAgentSetting), (x, y) -> {});
6162
}
6263

@@ -143,6 +144,27 @@ void setAgentSetting(String key, String value) {
143144
NodeScope
144145
);
145146

147+
static final Setting<List<String>> APM_TRACING_SANITIZE_FIELD_NAMES = Setting.listSetting(
148+
APM_SETTING_PREFIX + "sanitize_field_names",
149+
List.of(
150+
"password",
151+
"passwd",
152+
"pwd",
153+
"secret",
154+
"*key",
155+
"*token*",
156+
"*session*",
157+
"*credit*",
158+
"*card*",
159+
"*auth*",
160+
"*principal*",
161+
"set-cookie"
162+
),
163+
Function.identity(),
164+
OperatorDynamic,
165+
NodeScope
166+
);
167+
146168
static final Setting<Boolean> APM_ENABLED_SETTING = Setting.boolSetting(
147169
APM_SETTING_PREFIX + "enabled",
148170
false,

modules/apm/src/main/java/org/elasticsearch/tracing/apm/APMTracer.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_ENABLED_SETTING;
4545
import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_NAMES_EXCLUDE_SETTING;
4646
import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_NAMES_INCLUDE_SETTING;
47+
import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_SANITIZE_FIELD_NAMES;
4748

4849
/**
4950
* This is an implementation of the {@link org.elasticsearch.tracing.Tracer} interface, which uses
@@ -65,8 +66,10 @@ public class APMTracer extends AbstractLifecycleComponent implements org.elastic
6566

6667
private List<String> includeNames;
6768
private List<String> excludeNames;
69+
private List<String> labelFilters;
6870
/** Built using {@link #includeNames} and {@link #excludeNames}, and filters out spans based on their name. */
6971
private volatile CharacterRunAutomaton filterAutomaton;
72+
private volatile CharacterRunAutomaton labelFilterAutomaton;
7073
private String clusterName;
7174
private String nodeName;
7275

@@ -86,7 +89,10 @@ record APMServices(Tracer tracer, OpenTelemetry openTelemetry) {}
8689
public APMTracer(Settings settings) {
8790
this.includeNames = APM_TRACING_NAMES_INCLUDE_SETTING.get(settings);
8891
this.excludeNames = APM_TRACING_NAMES_EXCLUDE_SETTING.get(settings);
92+
this.labelFilters = APM_TRACING_SANITIZE_FIELD_NAMES.get(settings);
93+
8994
this.filterAutomaton = buildAutomaton(includeNames, excludeNames);
95+
this.labelFilterAutomaton = buildAutomaton(labelFilters, List.of());
9096
this.enabled = APM_ENABLED_SETTING.get(settings);
9197
}
9298

@@ -109,6 +115,16 @@ void setExcludeNames(List<String> excludeNames) {
109115
this.filterAutomaton = buildAutomaton(includeNames, excludeNames);
110116
}
111117

118+
void setLabelFilters(List<String> labelFilters) {
119+
this.labelFilters = labelFilters;
120+
this.labelFilterAutomaton = buildAutomaton(labelFilters, List.of());
121+
}
122+
123+
// package-private for testing
124+
CharacterRunAutomaton getLabelFilterAutomaton() {
125+
return labelFilterAutomaton;
126+
}
127+
112128
@Override
113129
protected void doStart() {
114130
if (enabled) {
@@ -271,6 +287,12 @@ private void setSpanAttributes(@Nullable Map<String, Object> spanAttributes, Spa
271287
for (Map.Entry<String, Object> entry : spanAttributes.entrySet()) {
272288
final String key = entry.getKey();
273289
final Object value = entry.getValue();
290+
291+
if (this.labelFilterAutomaton.run(key)) {
292+
spanBuilder.setAttribute(key, "[REDACTED]");
293+
continue;
294+
}
295+
274296
if (value instanceof String) {
275297
spanBuilder.setAttribute(key, (String) value);
276298
} else if (value instanceof Long) {
@@ -394,9 +416,9 @@ Map<String, Context> getSpans() {
394416
return spans;
395417
}
396418

397-
private static CharacterRunAutomaton buildAutomaton(List<String> includeNames, List<String> excludeNames) {
398-
Automaton includeAutomaton = patternsToAutomaton(includeNames);
399-
Automaton excludeAutomaton = patternsToAutomaton(excludeNames);
419+
private static CharacterRunAutomaton buildAutomaton(List<String> includePatterns, List<String> excludePatterns) {
420+
Automaton includeAutomaton = patternsToAutomaton(includePatterns);
421+
Automaton excludeAutomaton = patternsToAutomaton(excludePatterns);
400422

401423
if (includeAutomaton == null) {
402424
includeAutomaton = Automata.makeAnyString();

modules/apm/src/test/java/org/elasticsearch/tracing/apm/APMTracerTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88

99
package org.elasticsearch.tracing.apm;
1010

11+
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
1112
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.common.util.concurrent.ThreadContext;
1314
import org.elasticsearch.tasks.Task;
1415
import org.elasticsearch.test.ESTestCase;
1516

1617
import java.util.List;
18+
import java.util.stream.Stream;
1719

1820
import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_ENABLED_SETTING;
1921
import static org.elasticsearch.tracing.apm.APMAgentSettings.APM_TRACING_NAMES_EXCLUDE_SETTING;
@@ -166,6 +168,43 @@ public void test_whenTraceStarted_andSpanNameExcluded_thenSpanIsNotStarted() {
166168
assertThat(apmTracer.getSpans(), hasKey("id3"));
167169
}
168170

171+
/**
172+
* Check that sensitive attributes are not added verbatim to a span, but instead the value is redacted.
173+
*/
174+
public void test_whenAddingAttributes_thenSensitiveValuesAreRedacted() {
175+
Settings settings = Settings.builder().put(APM_ENABLED_SETTING.getKey(), false).build();
176+
APMTracer apmTracer = buildTracer(settings);
177+
CharacterRunAutomaton labelFilterAutomaton = apmTracer.getLabelFilterAutomaton();
178+
179+
Stream.of(
180+
"auth",
181+
"auth-header",
182+
"authValue",
183+
"card",
184+
"card-details",
185+
"card-number",
186+
"credit",
187+
"credit-card",
188+
"key",
189+
"my-credit-number",
190+
"my_session_id",
191+
"passwd",
192+
"password",
193+
"principal",
194+
"principal-value",
195+
"pwd",
196+
"secret",
197+
"secure-key",
198+
"sensitive-token*",
199+
"session",
200+
"session_id",
201+
"set-cookie",
202+
"some-auth",
203+
"some-principal",
204+
"token-for login"
205+
).forEach(key -> assertTrue("Expected label filter automaton to redact [" + key + "]", labelFilterAutomaton.run(key)));
206+
}
207+
169208
private APMTracer buildTracer(Settings settings) {
170209
APMTracer tracer = new APMTracer(settings);
171210
tracer.doStart();

server/src/main/java/org/elasticsearch/rest/RestController.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -461,14 +461,10 @@ private void startTrace(ThreadContext threadContext, RestChannel channel, String
461461
name = restPath;
462462
}
463463

464-
Map<String, Object> attributes = Maps.newMapWithExpectedSize(req.getHeaders().size() + 3);
464+
final Map<String, Object> attributes = Maps.newMapWithExpectedSize(req.getHeaders().size() + 3);
465465
req.getHeaders().forEach((key, values) -> {
466466
final String lowerKey = key.toLowerCase(Locale.ROOT).replace('-', '_');
467-
final String value = switch (lowerKey) {
468-
case "authorization", "cookie", "secret", "session", "set_cookie", "token", "x_elastic_app_auth" -> "[REDACTED]";
469-
default -> String.join("; ", values);
470-
};
471-
attributes.put("http.request.headers." + lowerKey, value);
467+
attributes.put("http.request.headers." + lowerKey, values.size() == 1 ? values.get(0) : String.join("; ", values));
472468
});
473469
attributes.put("http.method", method);
474470
attributes.put("http.url", req.uri());

0 commit comments

Comments
 (0)