Skip to main content
RunBook Academy

← All break/fix scenarios in Observability

advancedobservability-security~45 min

Break/Fix: Telemetry Contains Sensitive Customer Data

Reported symptoms

  • ●The nightly secret scan flags about 1,900 log lines in Loki carrying an Authorization bearer value and a card number in the message body, all from the checkout service, first appearing 11 days ago
  • ●The same scan across Tempo is clean: spans from the same requests show Bearer [REDACTED] where the token would be, so the redaction is provably working
  • ●A Grafana panel broke 11 days ago and was filed as an unrelated dashboard bug - it selects a checkout stream that now returns nothing, while a differently labelled checkout stream appeared the same day
  • ●Loki ingestion for checkout rose about 30 percent on that same day and was written off at the time as a marketing campaign
  • ●The data protection officer reports, from a separate thread, that a customer erasure request turned up that customer email in the observability platform, which is not on the inventory of systems holding personal data
  • ●The platform team can demonstrate that redaction is configured and healthy: the processors are in the collector configuration, the collector is running, and the CI redaction test passes
  • ●Grafana Alloy is still running on every host with its Luhn and replace stages intact, and nobody has changed its configuration in five months

Evidence

  • · A LogQL query over the checkout stream returns lines whose body contains a bearer token and a 16-digit number; the oldest is timestamped at 14:07 on the day of the deploy 11 days ago
  • · The same trace ID appears on a redacted span in Tempo and on an unredacted log line in Loki, which proves both records came from the same request and took different paths
  • · The old stream selector job="checkout" has no lines after that timestamp; a new stream service_name="checkout" starts at the same timestamp
  • · The collector configuration lists the redaction processors under service.pipelines.traces.processors and does not list them under service.pipelines.logs.processors
  • · On the checkout hosts, the newest file under the application log directory is 11 days old - the service no longer writes the log file that Alloy tails
  • · The checkout deploy record for that day reads: switch log export from file to OTLP, no functional change. Nothing in the diff touches logging content
  • · The CI redaction test constructs a synthetic span, sends it through the collector configuration, and asserts the token is replaced. It contains no log record
  • · The audit dashboard reports zero tier-0 findings for the last 30 days, because its detector queries run against the trace store
Diagnosis and resolutionclick to reveal

Root cause

Nothing in the redaction configuration was wrong, and nothing about it changed. The checkout service stopped writing its log file and started exporting log records over OTLP, and that one transport change moved every log record from one agent pipeline to a different one. The two agent pipelines are independent and always were: a log line tailed from a file was only ever seen by Alloy, whose loki.process stages carried the Luhn masking that caught card numbers and the replace stage that caught bearer tokens, while a record shipped over OTLP is only ever seen by the collector. The collector has redaction, but it is listed under the traces pipeline and not under the logs pipeline. That gap had existed since the configuration was written and had never mattered, because until the deploy no log record had ever entered the collector. A change elsewhere made a dormant gap load-bearing. Every control that should have caught it was scoped the same way as the gap: the CI test feeds a span, the audit detector queries the trace store, and the team validated redaction by looking at Tempo. All three answered honestly about traces, and all three were silent about logs. The broken dashboard and the 30 percent ingestion rise were the same event seen from two other angles - Loki labels an OTLP record from its resource attributes, so the stream name changed, and an OTLP log record carries more structure than the line it replaced.

Remediation

Resist the urge to fix the configuration first. It is one line, you can see it, and it will feel like progress - but the tokens already in Loki are live credentials for real users, and closing the pipeline does nothing about them. Work the leak-incident order instead: contain, notify, rotate, redact, document. Contain by restricting read access to the affected tenant and invalidating existing Grafana sessions, since a role change does not evict a session that is already open, and by snapshotting the chunk store so the evidence survives the deletion you are about to perform. Notify legal inside the four-hour window and tell them plainly that the exposure began 11 days ago, because the disclosure clock is driven by the window, not by when you noticed. Rotate every credential that appeared in those 11 days; for a checkout service that is every authenticated session in the window, which means a user-visible mass logout that somebody has to own. Only then redact: add the redaction processors to the logs pipeline, and submit a Loki delete request for the affected stream and window, which needs a TSDB index, retention enabled on the compactor and deletion mode set to filter-and-delete, and which applies only after the cancellation period. Two traps deserve naming. A transform processor with a bad OTTL statement is a configuration error that stops the collector starting, and the rollback that follows restores a configuration with no log redaction at all while reporting itself a success - so roll the change to one collector, confirm it started and that records are still arriving, and only then continue. And the tempting containment of switching checkout log ingestion off entirely leaves a payment service unobservable during a live security incident; if you choose it, say so out loud, name an owner and set an end time, because that is a hold, not a fix.

Verification

Prove the log path, not the configuration. Send a synthetic offender - a marker value that is not a real credential and that you can search for unambiguously - through the same OTLP log path checkout uses, and require it to arrive in Loki masked. Before trusting that, run the same synthetic through the previous configuration and require it to arrive unmasked, so you know the test can fail; a redaction test that has never failed has never been tested. Confirm the collector is actually running after the change and that log records are still being accepted, because a redaction fix that stops the pipeline is an outage on top of a leak. Extend the CI test to construct a log record as well as a span, and watch it fail against the old configuration before it passes against the new one. Re-run the store scan across both signals and without binding the query to a stream label, since a label rename is exactly what a selector-scoped scan cannot see. Verify the rotation by confirming the old tokens are refused, not by confirming new ones were issued. And verify the deletion after the cancellation period has elapsed by re-querying the window and requiring no matches, remembering that a screenshot taken before the delete landed is already outside your control.

Prevention

Treat a change of transport as a change of control surface. Moving a service from file tailing to OTLP export does not move its redaction rules with it, and the deploy that does it will be described as having no functional change, because from the application side it does not. Make the reviewable unit of a collector configuration the pipelines block rather than the processors block: a processor that exists is not a processor that runs, and the only place that distinction is visible is in the per-signal processor list. Test each control against the signal it protects - a redaction test built from spans certifies traces and says nothing about logs or metrics, and the same is true of the audit detectors, so scope them to the store and to content rather than to a stream selector. Alert when a telemetry source goes quiet: Alloy stopped shipping checkout logs at 14:07 and nothing anywhere said so, and a no-lines-for-N-minutes alert on a known stream would have named the day within the hour. Keep the classification tiers attached to the fields rather than to the pipeline, hold any unclassified field at the strictest tier, and put the observability platform on the inventory of systems that hold personal data, because a data protection officer who has to discover that during an erasure request has already found the gap the hard way.

Reported symptoms

The nightly secret scan is a boring job that produces nothing. This morning it produces 1,900 findings, all in Loki, all from the checkout service, all carrying an Authorization bearer value and a sixteen-digit number in the message body. The oldest is 11 days old.

The platform team’s first response is that the scanner must be wrong, and they have good grounds for saying so:

  • Redaction is configured. The collector configuration contains an attributes/redact processor and a transform/redact processor, both reviewed, both committed.
  • Redaction is running. The collector is healthy and has not restarted.
  • Redaction is tested. CI runs a synthetic offender through the configuration on every change and asserts the token is replaced. It passed this morning.
  • Redaction is demonstrably effective. Run the same scan across Tempo: clean. Spans from the checkout service show Bearer [REDACTED] exactly where the token should have been masked.
  • Alloy has not changed. Its stage.luhn and stage.replace blocks are still there, still running, untouched for five months.
  • The audit dashboard agrees. Zero tier-0 findings for 30 days.

Three other things happened 11 days ago and were filed separately by three different people. A Grafana panel stopped returning data and was raised as a dashboard bug. Loki ingestion for checkout rose about 30 percent and was attributed to a marketing campaign. And in an unrelated thread, the data protection officer reports that a customer erasure request has turned up that customer’s email address in the observability platform, which is not on the inventory of systems that hold personal data.

Nobody has connected any of these. There is no incident yet, and there are 1,900 live bearer tokens in a log store.

Evidence provided

Read-only / Safethe first offending line, and the exact minute it starts
$ logcli query --limit 3 --from 2026-08-02T00:00:00Z --forward '{service_name="checkout"} |= "Bearer"'
2026-08-02T14:07:11Z {service_name="checkout"} POST /api/v1/charge authorization="Bearer eyJhbGciOi..." pan="4111 1111 1111 1111" amount=49.99 trace_id=8f7c3a1b2e9d4c50
2026-08-02T14:07:12Z {service_name="checkout"} POST /api/v1/charge authorization="Bearer eyJhbGciOi..." pan="4111 1111 1111 1111" amount=12.00 trace_id=1d4e77a90b3c1f22
2026-08-02T14:07:12Z {service_name="checkout"} POST /api/v1/charge authorization="Bearer eyJhbGciOi..." pan="4111 1111 1111 1111" amount=8.75  trace_id=c02b9f6e13a7d481

Illustrative output

The same request, in the other store:

Read-only / Safeone trace ID, two records, two outcomes
$ curl -s -H 'Accept: application/json' 'http://tempo.obs.example.com:3200/api/traces/8f7c3a1b2e9d4c50' | jq -r '.. | .attributes? // empty | .[] | select(.key | test("authorization|pan")) | [.key, .value.stringValue] | @tsv'
http.request.header.authorization   Bearer [REDACTED]

Illustrative output

The stream itself changed shape at the same minute:

# Two selectors, one service. Compare the line counts either side of 14:07.
for SEL in '{job="checkout"}' '{service_name="checkout"}'; do
  printf '%s ' "$SEL"
  logcli query --quiet --limit 0 --from 2026-08-02T14:00:00Z \
    --to 2026-08-02T15:00:00Z --output raw "$SEL" | wc -l
done

The old selector goes to zero at 14:07. The new one starts at 14:07.

Read-only / Safethe processors exist; the question is which pipelines list them
$ yq '.service.pipelines | to_entries | map({signal: .key, processors: .value.processors}) ' /etc/otelcol/config.yaml
- signal: traces
processors: [memory_limiter, attributes/redact, transform/redact, batch]
- signal: logs
processors: [memory_limiter, batch]
- signal: metrics
processors: [memory_limiter, batch]

Illustrative output

On a checkout host, the file Alloy tails:

Read-only / Safenothing has been written to it for 11 days
$ ssh checkout-3.app.example.com 'ls -lt /var/log/checkout/ | head -3'
total 1841204
-rw-r--r-- 1 checkout checkout 214748364 Aug  2 14:07 app.log
-rw-r--r-- 1 checkout checkout 214748364 Aug  2 09:31 app.log.1

Illustrative output

And the change that everybody has already dismissed:

Read-only / Safeone commit in twelve days, and it does not touch logging content
$ git -C /srv/checkout log --oneline --since=12.days -- deploy/ internal/telemetry/
a91f4c2 checkout: switch log export from file to OTLP (no functional change)

Illustrative output

Work the evidence before reading on

Three controls say the redaction works. One store says it does not. Both are telling the truth.

  1. The same trace ID appears on a masked span and an unmasked log line. Two records, one request, two outcomes. What is different about the two records, and where does that difference first appear?
  2. Alloy has not changed and is still running. What is it processing?
  3. The CI test passes and the audit detector is clean. Write down, in one sentence each, exactly what each of them proves - and then write down what each of them does not cover.
  4. Three tickets were filed on 2 August by three people: a broken dashboard, a 30 percent ingestion rise, and nothing at all about the third. What single event produces all three?

Before continuing: you have found the configuration line that explains everything and you can fix it in thirty seconds. What are you leaking while you do that, who owns the decision, and what does fixing the configuration first cost you?

Root cause

1. Two agent pipelines, and a record can only be in one

A log line tailed from a file is seen by Alloy and never by the collector. A record shipped over OTLP is seen by the collector and never by Alloy. This is not a subtlety of the deployment; it is the shape of the pipeline, and it means redaction rules do not follow a record - the record chooses which rules apply by choosing which agent carries it.

The checkout service used to write app.log. Alloy tailed it and ran stage.luhn, which masks numbers that pass the Luhn checksum, and stage.replace, which rewrote the bearer token. Card numbers and tokens were being caught, correctly, on every line, by a component nobody had touched in five months.

At 14:07 on 2 August the service stopped writing that file.

2. The gap was already there and had never mattered

The collector configuration lists the redaction processors under service.pipelines.traces.processors. It does not list them under service.pipelines.logs.processors. That has been true since the configuration was written.

It had no consequence, because no log record had ever entered the collector. The logs pipeline existed, was syntactically valid, was reviewed, and carried nothing. A dormant gap is invisible in review precisely because it is not wrong yet: a reviewer looking at that file sees redaction configured and a logs pipeline that is not in use.

The deploy did not create the defect. It made the defect load-bearing, from a change whose description - and whose diff - contains nothing about redaction, logging content, or security.

3. Every control was scoped like the gap

This is the part worth sitting with, because the leak was survivable and the eleven days were not.

  • The CI test constructs a span, runs it through the collector configuration, and asserts the token is replaced. It is a correct test of the traces pipeline. It has never contained a log record.
  • The audit detector queries the trace store. It reported zero findings because there were zero findings in Tempo.
  • The team’s own spot check - run the scan against Tempo, see it clean - was the same test a third time.

Three independent controls, all honest, all answering a question about traces. The signal that was leaking was the one none of them looked at, and the reason is the same in all three cases: each was built at the moment the traces pipeline was the thing being secured, and each inherited that scope without ever stating it.

4. The other two tickets are the same event

Loki derives stream labels for an OTLP record from its resource attributes, so the checkout stream stopped being selected by the old label and started being selected by one derived from service.name. That is the broken dashboard panel. An OTLP log record also carries structure the plain file line did not, which is most of the 30 percent ingestion rise.

Three tickets, three teams, three plausible independent explanations, one deploy. The estate had all the evidence it needed on day one and no mechanism for putting three tickets on the same table.

Resolution

  1. Declare an incident and put the three tickets on the same table. The dashboard bug, the ingestion rise and the erasure request are the same event; until somebody says so, three teams keep working three problems.
  2. Contain. Restrict read access to the affected Loki tenant, and invalidate existing Grafana sessions - a role change does not evict a session that is already open, and the audit log will show reads after the restriction if you skip this.
  3. Preserve the evidence before you remove anything. Snapshot the chunk store to a preservation bucket. You are about to submit a delete request, and the post-mortem and any regulatory response both need the record you are deleting.
  4. Notify legal inside the four-hour window, and tell them the exposure window is eleven days rather than one morning. The disclosure clock is driven by the window and by the moment of confirmation, not by how long you have known.
  5. Rotate every credential that appeared in the window. On a checkout service that is every authenticated session over eleven days, which is a user-visible mass logout. Name the owner of that decision; it is not the platform team.
  6. Now fix the pipeline. Add attributes/redact and transform/redact to service.pipelines.logs.processors. Roll it to one collector first, confirm the process started, and confirm log records are still being accepted before touching the rest of the fleet.
  7. Submit the Loki delete request for the affected stream and window. Check the preconditions first - TSDB index, retention_enabled on the compactor, deletion_mode: filter-and-delete - and use the cancellation period deliberately: spend it confirming the selector matches only what you intend, rather than treating it as an unwanted delay.
  8. Decide explicitly what to do about checkout log ingestion in the meantime. Switching it off removes the leak and leaves a payment service unobservable during a live security incident. If you choose it, call it a hold, give it an owner and an end time, and say what would change the decision.
  9. Close the loop on the deploy record rather than only on the collector configuration. The defect is that a transport migration was reviewed as though it had no security surface, and the next service to migrate will be reviewed by whoever reads that record.

Verification

  1. Test the path, not the file. Send a synthetic offender - a distinctive marker value that is not a real credential - through the same OTLP log path checkout uses, and require it to arrive in Loki masked.
  2. Prove the test can fail. Run the same synthetic against the previous configuration and require it to arrive unmasked. A redaction check that has never produced a failure has never been tested, and it is the single cheapest thing to get wrong here.
  3. Confirm the collector is running and accepting log records after the change. A redaction fix that stops the pipeline is an outage stacked on a leak, and it will be reported as a successful deploy.
  4. Extend the CI test to construct a log record as well as a span, and watch the new case fail against the old configuration before it passes against the new one. A test added green proves nothing.
  5. Re-run the store scan across both signals, without binding the query to a stream selector. The scan that missed eleven days was scoped by a label convention, and a label rename is exactly what such a scan cannot see.
  6. Verify the rotation by attempting authentication with an old token and requiring it to be refused. A new credential existing is not the same as the old one being dead.
  7. Verify the deletion after the cancellation period has elapsed, by re-querying the exact window and requiring no matches. Before that point the request is accepted and the data is still readable.
  8. Confirm the observability platform is now on the inventory of systems holding personal data, and that the next erasure request will reach it without a data protection officer having to discover it.

Prevention

  • Treat a change of transport as a change of control surface. Moving a service from file tailing to OTLP export moves every record from one agent to another, and the redaction rules do not travel with it. The deploy will honestly be described as having no functional change, because from the application’s side there is none.
  • Make the reviewable unit of a collector configuration the service.pipelines block, not the processors block. A processor that exists is not a processor that runs, and the per-signal processor list is the only place that distinction is visible.
  • Test every control against the signal it protects. A redaction test built from spans certifies traces and is silent about logs and metrics. Write down the scope of each control next to the control, because scope is inherited silently from the day it was written.
  • Scope audit detectors to the store and to content rather than to a stream selector. A detector bound to a label convention stops seeing a stream the moment the convention changes, and reports zero findings while doing so.
  • Alert when a telemetry source goes quiet. Alloy stopped shipping checkout logs at 14:07 and nothing said so. A no-lines-for-N-minutes alert on a known stream would have named the date and the minute within the hour, and would have turned an eleven-day exposure into a same-day one.
  • Keep classification tiers attached to fields rather than to pipelines, and hold an unclassified field at the strictest tier until security and legal classify it. The tier travels with the field through whichever agent carries it.