← All runbooks in Observability
Runbook: Respond to a Telemetry Data Leak
1 · Prerequisites
Confirm every item is in place before any state change.
- Sensitive Data in Telemetry
- Redaction Strategies
- Redaction Tools
- Loki and PII
- Auditing Telemetry for Leaks
- Leak Incident Response
- The data classification tier list, and the name of the person who owns it - the tier decides the clock, and the tier is not the platform team's call
- Contact details for the security on-call and the legal owner, reachable now rather than during business hours
- Knowledge of whether the Loki compactor in this estate is actually configured to accept delete requests, established BEFORE the incident
- Write access to the source repository that emits the field, or the name of the team that has it
- A preservation target for evidence that is not the store you are about to modify
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Establish what leaked, in what form, and where it landed. A credential in a log line, a card number in a span attribute and an email address in a Loki label are three different incidents: the first is rotatable, the second is unremovable, and the third is also a cardinality problem. Name the store, the tenant, the stream or service, and the field.
- · Classify the value against the tier list before deciding anything. Tier 0 (credential) and tier 1 (PAN or government ID) start a security and legal clock immediately; tier 2 (direct PII) and tier 3 (quasi-identifier) are tickets with deadlines. If the tier is unclear, hold it at the strictest tier until the owner says otherwise.
- · Bound the exposure window: when the field first appeared, and who could have read it since. The two ends of that interval are the number the incident is measured by, and the second end does not close until the credential is revoked - not when the redaction rule merges.
- · Do not spread the value while investigating it. A LogQL line filter containing the literal secret writes it into Grafana's per-user query history, into shell history, and into any proxy access log in front of Loki. Search for the field NAME or a pattern, not the value.
- · Confirm who can currently read the affected store. Grafana data source permissions and Loki's per-tenant
X-Scope-OrgIDseparation bound the audience; neither removes anything. Knowing the current audience is what makes the containment decision real. - · Check whether the store even supports removal. Loki can apply delete requests only with a TSDB index,
retention_enabledon the compactor, a configureddelete_request_store, anddeletion_modeset tofilter-onlyorfilter-and-delete. Tempo has no equivalent at all. Establish this now; discovering it at the redaction phase costs a day. - · Identify every consumer of a leaked credential before rotating it. Rotation without an inventory converts a security incident into an outage, and the consumer you forget is the one that pages at 04:00.
- · Decide the evidence position with the security owner. If the incident may be investigated or is subject to a hold, a snapshot of the affected chunks must exist before any deletion or retention change is applied.
- · Confirm that the alert that fired is a real finding and not a detector false positive - a test fixture, a documentation example, or a redacted placeholder that matches the pattern. Confirm by field name and location, still without printing the value.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Open the incident and start the record. One channel, timestamped, with the runbook link pinned. Every later phase is reconstructed from this record, and the post-incident timeline is built from it rather than from memory.
- 2Preserve the evidence before you change anything. Snapshot the affected chunks or blocks to a preservation location outside the store. Every remediation from here on is destructive to evidence, and the delete request you are about to file is the one action that cannot be un-filed once its cancellation window closes.
- 3Contain the audience. Restrict read access to the affected tenant or data source to the incident team, and invalidate existing sessions - a permission change does not evict a session that is already open. This bounds who can see the value; it removes nothing.
- 4ROTATE, if the value is a credential. Issue the replacement at the source system, distribute it to every consumer from the inventory, confirm each one works, and only then revoke the old one. Revocation is the step that ends the exposure. Nothing you do to the telemetry store ends it.
- 5Prove the old credential is dead by deliberately attempting to use it and requiring the attempt to fail. "Superseded" is not "revoked"; a token that has been replaced but not invalidated is still a live credential sitting in a log store.
- 6Notify. Hand security and legal the classification, the exposure window, the containment evidence and the store inventory. The disclosure decision is theirs and it has a statutory clock; the engineering job is to give them what they need to make it, not to make it for them.
- 7Stop the emission at the source. Remove the field from the application call site and add it to the allowlist as dropped. This is the only fix that is permanent; everything downstream is a scrubber for what the source forgot.
- 8Close the gap in the agent pipeline as the safety net: an
attributesortransformprocessor in the OpenTelemetry Collector for OTLP attributes, and astage.replaceorstage.label_dropin Alloy'sloki.processfor log lines. Restart the agent and confirm it came back - an OTTL statement that does not parse stops the collector starting, and a rollback to the previous config is a rollback to no redaction. - 9Verify the new pipeline against a deliberately bad payload, pushed through the agent rather than directly into the store. A test line pushed straight to the Loki push API exercises Loki, not the redaction rule, and a green result from it means nothing.
- 10Deal with what is already stored, per store, and expect three different answers. Loki: a delete request against the compactor, scoped by stream selector and time range. Tempo: nothing - the span attribute stays until the block ages out under retention. Prometheus: a leaked label value is a series, and series deletion needs the admin API that is disabled by default.
- 11Confirm the delete request was accepted and is progressing, and record its request id. It is applied after a cancellation period - 24 hours by default - which is both the safety net and the reason the store is not yet clean when the ticket is closed.
- 12Re-run the detection against the same field, on a fresh sample, and require it to find nothing new after the source fix. That is the difference between "the pipeline is fixed" and "the leak is closed".
- 13Write the post-incident record: the exposure window, time from detection to revocation, which stores still hold the value and until when, and the detector change that catches this pattern next time.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓The old credential FAILS a deliberate authentication attempt against the source system, and the source system's own audit log shows it revoked rather than merely replaced. This check must be run, and it must be capable of failing.
- ✓A payload containing the field, pushed through the agent, arrives at the store redacted. For logs:
logcli query --since=5mon the test stream shows the replacement text rather than the value. For spans: the attribute reads[REDACTED]or is absent when the trace is fetched back from Tempo. - ✓The dropped labels are genuinely absent from new streams:
logcli series --since=5mfor the affected job lists no label carrying the field name, in any of its spellings.stage.label_droptakes literal names, soemailbeing gone says nothing aboutuser_email. - ✓The agent came back up after the configuration change:
systemctl is-activeon the collector, and noinvalid configin its journal. A collector that failed to start is a pipeline running with no redaction at all, and it looks identical from the store side to one that is working. - ✓The collector reports it is processing records - the processor's accepted-records counter on the agent's own metrics endpoint is advancing. A processor that is defined but not wired into the
service.pipelineslist is a no-op with a perfectly valid configuration. - ✓The Loki delete request exists, is listed against the tenant, and its status shows it progressing. Record the request id; a delete request nobody can name later is a delete request nobody can prove was filed.
- ✓A fresh detection run over a new sample of the affected store finds no occurrences of the field NAME after the source fix. Searching for the name rather than the value keeps the check from re-spreading the secret.
- ✓The stores that cannot delete are documented with an end date: for Tempo, the date the affected blocks age out under
block_retention. "Cannot remove" is an acceptable answer only when it comes with the date it stops being true. - ✓The source-side regression test exists and fails when the field is reintroduced. Without it the same field returns at the next refactor, and the next audit finds it in a different store.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶There is no rollback for exposure. Once a value has been readable in a telemetry store it must be treated as read, and every step in this runbook is about shortening the window rather than undoing it.
- ↶Rotation is reversible only before revocation. If a consumer breaks on the new credential, move that consumer back while both are valid - but do not delay revocation for a consumer that is merely inconvenient. Accepting a short outage rather than extending a live credential exposure is the service owner's decision, and it is usually the outage.
- ↶POINT OF NO RETURN: the delete request past its cancellation window. Before it expires,
DELETE /loki/api/v1/deletewith the request id cancels it. After it, the compactor has applied it and the log lines are gone - including the ones the incident's own timeline needed. - ↶Do NOT reach for retention acceleration as a quick purge. Shortening
retention_periodto sweep out a leak deletes every tenant's logs for that window, not just the affected stream, and it destroys the evidence of the incident along with the leak. It is a legal-owner decision, not an operator convenience. - ↶An over-broad redaction rule is reversible and should be treated as a defect in its own right. A
delete_matching_keyspattern that also removes the attributes an investigation needs turns the fix into a permanent observability gap; revert the pattern, narrow it, and redeploy. - ↶A collector that will not start after the config change rolls back to the previous configuration, which has no redaction in it. Treat a failed collector start as a live leak continuing, not as a deployment nuisance, and fix forward under the incident rather than leaving the rollback in place.
- ↶Access restrictions are reversible and should be reversed deliberately, with a note of who restored access and when. An emergency permission change left in place for months is its own finding at the next audit.
6 · Escalation
When the runbook isn't enough, contact:
- · Escalate to the security on-call on confirmation, not after triage. This is an incident from the moment the finding is real, and the platform team is not the owner of the disclosure decision.
- · Escalate to legal within the notification clock for tier-0 and tier-1 findings. Statutory notification windows run from confirmation of the breach, not from completion of the engineering fix, and a late page is a late decision.
- · Escalate to the credential owner for rotation. Do not attempt to rotate a credential you do not own, and do not skip a consumer because its owner is unreachable - that is an escalation, not a decision.
- · Escalate to the service team that emits the field. The source fix is theirs; the agent-side scrubber is yours, and a runbook that ends at the scrubber leaves the leak live in every pipeline the scrubber does not cover.
- · Escalate to the data owner before any deletion or retention change. Deleting telemetry destroys evidence, and whether the value's removal outranks the record of the incident is not a platform decision.
- · Escalate if the exposure window cannot be bounded, if the value reached a store outside your control - a backup, a replica in another region, a downstream export - or if the affected store is shared with a tenant you have not told.
A detector has fired, or an engineer has noticed something in a panel, and a value that should never have been in telemetry is in telemetry. A bearer token in a log line. A card number in a span attribute. A customer email promoted to a Loki label.
The instinct is to make it disappear. Work out how to delete it, get it out of the store, close the ticket. That instinct produces the wrong ordering, and telemetry punishes the wrong ordering harder than a code repository does.
When to use this runbook
- A secret scanner or the periodic audit flags a value in Loki, Tempo or Prometheus.
- An engineer finds a credential, a card number or personal data in a panel, a log line or a trace.
- A schema change ships a new field and the classification review is retrospective.
- A tenant reports seeing another tenant’s data.
It does not apply when:
- The finding is a detector false positive. A documentation example, a test fixture, or a placeholder that matches the pattern. Confirm by field name and location first - the confirmation is cheap and the incident is not.
- The value is only in an application log file on the host and has not reached the pipeline. That is a host-level problem with a much smaller blast radius and a completely different remedy.
- The concern is a high-cardinality label with no sensitivity. That is a cardinality incident, not a leak. It has its own runbook and a different urgency.
- A leaked credential lives in a Git repository rather than in telemetry. Same rotation discipline, different hunt: the copies are in clones and CI caches rather than in chunks and blocks.
Blast radius
Two blast radii run on different clocks, and conflating them is the commonest mistake in this class of incident.
The credential’s blast radius is everything the value grants access to, for the whole interval from first emission to revocation. It is bounded by an action you control and can complete quickly.
The store’s blast radius is everyone who can read the affected tenant, for as long as the data exists there. That is bounded by retention, by the delete-request machinery if it is configured, and by backups that nobody has inventoried. It is often measured in weeks.
The remediation carries its own blast radius, which is why the ordering below puts evidence first. A delete request removes log lines the incident’s own timeline may need. A retention change removes them for every tenant in the window, not just the affected stream. An over-broad redaction rule creates a permanent observability gap that nobody notices until the next outage.
The clocks
| Phase | Clock | Owner | What it changes |
|---|---|---|---|
| Contain | 1 hour | Security on-call | Who can read it, and whether the credential still works |
| Notify | 4 hours | Legal | The disclosure decision, on a statutory window |
| Rotate | 24 hours | Engineering and security | Ends the exposure for a credential |
| Redact | 7 days | Engineering | Stops new values reaching the store |
| Document | 30 days | Incident commander | The detector that catches the next one |
The phases run in parallel where they can. Containment does not wait for legal, and legal does not wait for engineering.
Step 1: Confirm, without spreading the value
# Which streams carry the field name at all, and since when.
logcli series --since=24h '{job="checkout"}'
# Is the field a LABEL (in the index) or content (in the chunk)?
# The answer changes the cardinality picture and the remediation.
logcli labels --since=24h
# Count, do not print. The count is the finding; the value is the leak.
logcli query --since=24h --quiet \
'count_over_time({job="checkout"} |= "authToken" [24h])'Record four facts before moving: the store and tenant, the stream or service, the field name, and the earliest timestamp it appears. Those four are what every later phase is scoped by, and the fourth is one end of the exposure window.
Step 2: Contain
Containment is two actions with very different value. Restricting the audience limits who else can read it. Revoking the credential ends the exposure. Do the first while you organise the second; do not mistake the first for remediation.
# 1. Restrict read access to the affected tenant to the incident team.
# Then invalidate existing sessions: a permission change does not
# evict a session that is already open.
# 2. Preserve the affected chunks before anything destructive happens.
# The preservation target must not be the store you are about to modify.
rclone sync /loki/chunks/ preservation:leak-incident-2026-08-19/ \
--transfers=4 --checkers=4 --log-file=/var/log/preserve.logStep 3: Rotate, if the value is a credential
This is the step that ends the exposure, and it is the only step in this runbook whose completion is unambiguous.
The order inside it is fixed: enumerate every consumer, issue the replacement, distribute it and confirm each consumer works, then revoke. Rotating without the inventory turns a security incident into an outage; revoking before the inventory is confirmed does the same thing faster.
# After revocation, use the old credential deliberately and require failure.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $LEAKED_TOKEN" https://api.example.com/v1/user
# Expect 401. A 200 here means the exposure is still open.
# Then confirm at the provider's own audit log that the status is
# "revoked", with a timestamp - not "replaced".401Illustrative output
“Superseded” is not “revoked”. A token that was replaced but never invalidated is a live credential sitting in a log store, and a rotation that stopped at issuing a new value has closed nothing.
Record the revocation timestamp. It is the end of the exposure window, and it is the number the incident is measured by.
Step 4: Stop the emission - source first, agent second
The application is the primary defence and the agent is the safety net. Fixing only the agent leaves the field being emitted into every pipeline the agent does not cover, and the next audit finds it in a different store.
The source fix is the field removed from the call site and added to the allowlist as dropped, with a regression test that fails if it comes back.
Then the agent. For OTLP attributes, the collector’s attributes
processor is the simplest tool and the one least able to fail silently:
processors:
attributes/redact:
actions:
- key: authToken
action: update
value: "[REDACTED]"
service:
pipelines:
traces:
receivers: [otlp]
processors: [attributes/redact, batch]
exporters: [otlp/tempo]update rewrites a key only if it already exists, which is the behaviour
you want: a service that never emits the field does not acquire an
attribute reading [REDACTED]. Note the service.pipelines block - a
processor that is defined but never listed in a pipeline is a perfectly
valid no-op, and it is the most common reason a redaction rule “does not
work”.
For patterns rather than exact keys, the transform processor evaluates
OTTL statements; for a fail-closed allowlist, the redaction processor
names what to keep instead of what to remove. Both are stronger tools and
both are easier to get wrong under pressure.
For log lines, the equivalent is Alloy’s loki.process:
loki.process "redact" {
stage.replace {
expression = "Bearer\\s+([A-Za-z0-9._\\-/+=]+)"
replace = "[REDACTED]"
}
stage.label_drop {
values = ["authToken", "auth_token", "token"]
}
forward_to = [loki.write.default.receiver]
}Then verify through the agent, not around it:
# The collector came back, and its journal has no config error.
systemctl is-active otelcol
journalctl -u otelcol --since '5 min ago' | grep -i 'invalid config'
# The processor is actually seeing records.
curl -s http://otelcol:8888/metrics | grep otelcol_processor_accepted_spans
# Push a known-bad line through the AGENT and read it back from Loki.
echo 'auth header: Bearer eyJhbGciOi... test line' >> /var/log/application/test.log
logcli query --since=5m '{job="application"}' --output=rawThe last step is the one teams skip. Pushing a test line straight to
/loki/api/v1/push exercises Loki, not the redaction rule, and a green
result from it proves nothing about the pipeline.
Step 5: What is already stored
Three stores, three different answers. Write the answer down for each one, including the ones where the answer is “we cannot”.
| Store | Can the stored value be removed? | What you actually do |
|---|---|---|
| Loki | Yes, with preconditions | File a delete request scoped by stream selector and time range; the compactor applies it after the cancellation period |
| Tempo | No | Record the date the affected blocks age out under retention, and treat that date as the end of the exposure for that store |
| Prometheus | Only via the admin API | A leaked label value is series; series deletion needs an API that is off by default and is a separate, deliberate change |
| Grafana | Not applicable | Grafana stores no telemetry, but its query history holds any search you ran that contained the value |
# Scope it as narrowly as the finding allows. This deletes log lines.
curl -X POST -G 'http://loki:3100/loki/api/v1/delete' \
-H "X-Scope-OrgID: prod" \
--data-urlencode 'query={job="checkout"} |= "authToken"' \
--data-urlencode 'start=2026-08-17T00:00:00Z' \
--data-urlencode 'end=2026-08-19T09:00:00Z'
# 204 No Content on acceptance.
# List the requests for the tenant and record the id.
curl -s -G 'http://loki:3100/loki/api/v1/delete' -H "X-Scope-OrgID: prod"
# Cancel, while still inside the cancellation window:
REQ_ID=replace-with-the-id-from-the-listing
curl -X DELETE -G 'http://loki:3100/loki/api/v1/delete' \
-H "X-Scope-OrgID: prod" --data-urlencode "request_id=$REQ_ID"The cancellation window - 24 hours by default - is the reversibility that makes this step safe to take. It is also why the store is not clean when the request returns 204, and why “deleted” is not a claim to put in the ticket until the compactor has swept.
Step 6: Close the loop
Re-run the detection over a fresh sample, searching for the field name. A clean result proves the source fix landed; it says nothing about the historical data, and the two must be reported separately.
Then the record: first emission, detection, containment, revocation, source fix merged, delete request filed, and the date each store stops holding the value. Two intervals matter and they have different fixes. Time from first emission to detection is a detector problem. Time from detection to revocation is a rehearsal problem.
Common patterns
| Symptom | Likely cause | Resolution |
|---|---|---|
| Redaction merged, value still in new lines | Processor defined but not listed in service.pipelines | Add it to the pipeline; verify the accepted-records counter moves |
| Collector healthy, no redaction happening | Deploy rolled back to the previous config after an OTTL parse error | Check the journal for invalid config; fix forward |
email label gone, user_email still there | stage.label_drop takes literal names, not patterns | Name every spelling, or use a relabel rule with a regex |
| Test line arrives redacted, production does not | Test pushed directly to the Loki API, bypassing the agent | Re-test through the agent |
| Delete request returned 204, value still queryable | Cancellation window has not elapsed; compactor has not swept | Wait, then re-verify; do not file a second request |
| Delete request rejected | Compactor preconditions not met | Establish deletion_mode and retention_enabled before relying on deletion |
| Value gone from Loki, still in Tempo | Tempo has no delete path | Record the block-retention expiry date as the exposure end for that store |
| Old credential still works after “rotation” | Superseded rather than revoked | Revoke at the source; verify with a deliberate attempt |
| The same field reappears next quarter | Source never fixed; only the scrubber was | Fix the call site and add the regression test |
Escalation
Escalate when:
- The finding is confirmed. Immediately, to security. This is an incident, not a configuration ticket.
- The classification is tier 0 or tier 1 - legal has a statutory clock running from confirmation.
- Rotation needs a credential owner who is not you.
- Deletion or a retention change is on the table: destroying telemetry is the data owner’s decision.
- The exposure window cannot be bounded, or the value reached a backup, a replica, or a downstream export outside your control.
- A consumer cannot be updated before revocation.
References
- Loki: delete requests and the compactor
- Loki HTTP API
- Loki retention configuration
- Grafana Alloy: loki.process stages
- OpenTelemetry Collector: attributes processor
- OpenTelemetry Collector: transform processor (OTTL)
- OpenTelemetry Collector: redaction processor
- Tempo: retention and the compactor
- NIST SP 800-61: Computer Security Incident Handling Guide
- GDPR Article 33: notification of a personal data breach