ai newsMo., 19. Okt.
EilmeldungDeepSeek Search & Fetch: Low‑Cost Real‑Time Retrieval for LLM Apps

Your Leaked Prompt Secret Has a Rotation Order — Compute It Before You Click Rotate

It is 02:14. A CI job fails with a 401, a developer pastes the last sixty lines of the job log into a model chat to ask why, and the log happens to contain the runner registration token leaked by an env dump. That is the leak. Everything after it is process. At 02:40 the on-call engineer revokes th

dev.toMontag 09:51 · 19.10.8 Min. Lesezeit
Bild: dev.to

It is 02:14. A CI job fails with a 401, a developer pastes the last sixty lines of the job log into a model chat to ask why, and the log happens to contain the runner registration token leaked by an env dump. That is the leak. Everything after it is process.

At 02:40 the on-call engineer revokes the runner token. Correct instinct, wrong scope. At 09:00 a runner registers from a host nobody recognises — not because revocation failed, but because the CI personal access token that mints runner tokens was never touched. The attacker never needed the leaked value. They needed the parent. (Reconstructed tabletop sequence, not a single reported incident.)

So here is the question I keep asking platform teams: if a secret lands in a prompt at 02:14, what does your runbook do at 02:41 — and in what order?

Most runbooks I have read answer "rotate everything" and stop there. Order is the part that decides whether containment takes four minutes or four hours of re-leaking.

A prompt is a log line with an unknown retention window. Once you accept that, the interesting question stops being "did we leak a secret?" and becomes "how many copies exist now, and which of them can mint more?"

For a hosted workspace — including a free server option — the boundary map looks like this:

Notice the last row. The minting capability usually lives on the other side of the boundary from the leak, which is exactly why rotating the leaked leaf feels like it worked.

I keep the inventory as a CSV edge list, because it diffs cleanly in a PR and survives a wiki migration. Edge means "the left side can mint or replace the right side."

# minter,mintee ci_github_pat,repo_deploy_key ci_github_pat,ci_runner_reg_token ci_runner_reg_token,runner_ephemeral_token Enter fullscreen mode Exit fullscreen mode Three edges are enough to catch the treadmill. Add one line every time you add a minting path: token-to-token, token-to-key, key-to-certificate, org-admin-to-anything.

This is the whole artifact. Standard library only, no dependencies — I ran it against the graph above and got the output shown below.

#!/usr/bin/env python3 """ rotate_order.py - containment and restore order for a leaked credential. Usage: python3 rotate_order.py edges.csv ci_runner_reg_token """ import csv import sys from collections import defaultdict def load ( path ): children, parents = defaultdict ( list ), defaultdict ( list ) nodes = set () with open ( path, newline = '' ) as f: rows = ( r for r in csv. reader ( f ) if r and not r [ 0 ]. startswith ( ' # ' )) for row in rows: parent, child = row [ 0 ]. strip (), row [ 1 ]. strip () children [ parent ]. append ( child ) parents [ child ]. append ( parent ) nodes. update (( parent, child )) return nodes, children, parents def reach ( start, graph ): seen, stack = set (), [ start ] while stack: node = stack. pop () for nxt in graph. get ( node, []): if nxt not in seen: seen. add ( nxt ) stack. append ( nxt ) return seen def topo ( subset, children ): indeg = { n: 0 for n in subset } for n in subset: for c in children. get ( n, []): if c in subset: indeg [ c ] += 1 ready = sorted ( n for n, d in indeg. items () if d == 0 ) order = [] while ready: n = ready. pop ( 0 ) order. append ( n ) for c in sorted ( children. get ( n, [])): if c in subset: indeg [ c ] -= 1 if indeg [ c ] == 0: ready. append ( c ) ready. sort () if len ( order )!= len ( subset ): stuck = ', '. join ( sorted ( subset - set ( order ))) raise SystemExit ( ' cycle among: ' + stuck + ' - revoke the whole cycle by hand ' ) return order def main (): path, leaked = sys. argv [ 1 ], sys. argv [ 2 ] nodes, children, parents = load ( path ) if leaked not in nodes: raise SystemExit ( leaked + ' is not in the graph ' ) down = reach ( leaked, children ) # what the leak can mint up = reach ( leaked, parents ) # what can re-mint the leak scope = up | down | { leaked } contain = topo ( scope, children ) print ( ' guard: ', ', '. join ( sorted ( up )) or ' none ' ) print ( ' contain: ', ' -> '. join ( contain )) print ( ' restore: ', ' -> '. join ( reversed ( contain ))) if __name__ == ' __main__ ': main () Enter fullscreen mode Exit fullscreen mode Running it:

$ python3 rotate_order.py edges.csv ci_runner_reg_token guard: ci_github_pat contain: ci_github_pat -> ci_runner_reg_token -> runner_ephemeral_token restore: runner_ephemeral_token -> ci_runner_reg_token -> ci_github_pat Enter fullscreen mode Exit fullscreen mode Read that output carefully, because it says something counterintuitive. The first credential to revoke is the one that was not leaked. Kill the minter, then the leaf, then the leaf's leaf. Restore in the opposite direction so dependent jobs come back before the parent starts minting replacements.

If you rotate leaves first, you get the treadmill: revoke, re-mint, revoke, re-mint, and a very long night.

The script hard-fails on cycles, and cycles are real — an org-admin token with permission to issue tokens can, in some configurations, mint a sibling of itself. When that happens, the honest answer is: you cannot compute order, so revoke the entire cycle manually, accept the outage, and then remove the minting loop so the next incident is computable. A graph that lies is worse than no graph.

"Don't send secrets to a model" is advice, not a control. The control is a redactor that preserves enough shape for the model to help you.

# redact.py import re SHAPES = [ ( ' aws_access_key_id ', re. compile ( r ' \b(?:AKIA|ASIA)[0-9A-Z]{16}\b ' )), ( ' github_token ', re. compile ( r ' \bgh[pousr]_[A-Za-z0-9]{36,255}\b ' )), ( ' private_key ', re. compile ( r ' -----BEGIN [A-Z ]*PRIVATE KEY----- ' )), ( ' bearer ', re. compile ( r ' (?i)\bauthorization:\s*bearer\s+[A-Za-z0-9._\-]{20,} ' )), ] def classify ( text ): return sorted ({ name for name, rx in SHAPES if rx. search ( text )}) def redact ( text ): for name, rx in SHAPES: text = rx. sub ( ' [REDACTED: ' + name + ' ] ', text ) return text Enter fullscreen mode Exit fullscreen mode The negative fixtures matter more than the positive ones. A redactor that eats git SHAs destroys debugging value and gets disabled within a week.

# test_redact.py import pytest from redact import classify, redact POSITIVE = [ ' AKIA ' + ' Q ' * 16, ' ghp_ ' + ' a1B2 ' * 9, ' -----BEGIN OPENSSH PRIVATE KEY----- ', ' Authorization: Bearer abcdefghijklmnopqrstuvwx1234 ', ] NEGATIVE = [ ' 9f2c1e7a4b8d3c5e6f708192a3b4c5d6e7f80912 ', # 40-hex git SHA ' a3b1f2c4-9d8e-4f10-8a2b-1c2d3e4f5a6b ', # UUID ' sha256:6b3f9c1d2e4a5b6c7d8e9f0a1b2c3d4e ', # digest label ] @pytest.mark.parametrize ( ' sample ', POSITIVE ) def test_positive_fixtures_are_caught ( sample ): assert classify ( sample ), sample assert ' REDACTED ' in redact ( sample ) @pytest.mark.parametrize ( ' sample ', NEGATIVE ) def test_negative_fixtures_stay_readable ( sample ): assert classify ( sample ) == [], sample Enter fullscreen mode Exit fullscreen mode One tradeoff to state out loud: keeping a last4 for correlation is defensible for a 40-character high-entropy token and indefensible for low-entropy strings like internal hostnames. Decide per shape, not per policy paragraph.

The step everyone skips. These are command templates — I have not executed them for this article, and the expected strings vary by provider version and plan.

No evidence line, no closure. "I rotated it" is a claim; a 401 is a record.

Not every leak deserves an incident. Here is the table I actually use.

The middle two rows are where teams burn goodwill: rotating everything on every signal trains people to ignore the alert.

Two availability claims are relevant here, and I am treating both as operator-supplied: MonkeyCode offers free model access and a free server option. Check the current offer terms yourself, since quotas and terms change faster than articles do.

Neither of those is a security control. They do change your threat model in one specific way: a free hosted workspace sits outside your network boundary, so your egress rules, disk encryption, and log pipeline no longer apply to the copy running there. That leads to three rules I would apply before pasting anything into it:

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The useful part for me is the drill, not the pricing. Free access means a small team can rehearse containment end to end before it is real.

The honest summary: containment is a graph problem wearing a runbook costume, and the leaked secret is almost never the first thing you should revoke.

So which invariant do you put in CI: "no secret shape leaves the runner without a redaction record," or "every mint-capable credential has a documented parent"? I would start with the second — because the first one only fails once, and the second one fails every time somebody clicks rotate in the wrong order. If you want to rehearse the drill without provisioning anything, the free tier is a reasonable place to start.

Quelle: dev.to (dev.to). Original öffnen