Module 7 · 6 min read
Secrets and shared infrastructure: prompt caches, logs and system prompts
Why a system prompt is not a vault, and how a cost optimisation on a shared inference path becomes a cross-tenant disclosure.
Two habits put credentials into places they cannot be retrieved from. The first is putting a key in the system prompt with an instruction that the model must never reveal it. The instruction and the secret occupy the same channel that an attacker can influence, so the control is being enforced by the thing under attack, and extraction of system prompt content under sustained adversarial pressure should be treated as a matter of time rather than a possibility. Base64-encoding the secret first is not secrecy: it is an encoding anyone can reverse, and it survives casual extraction no better than plaintext because the extracted string is just as usable.
The sound pattern is that secrets never enter model context at all. Keep API keys server-side and have the platform attach them to outbound tool calls at execution time, outside the context window, and scope credentials per tool invocation so that a leaked value has minimal reach and a short life, as covered in the tool design module. The model asks for an action; the platform decides whether the action is authorised and supplies the credential. The model does not need to know a credential exists.
Secret handling: sound or unsound?
Select a card to turn it over.
The same reasoning extends past the prompt. Secrets leak into tracing spans that capture full request payloads, into error messages that include the failing request, into evaluation datasets curated from production traffic, and into the transcripts that get pasted into bug reports. Redact at capture in each of those paths, and test the redaction, because a redaction rule written against last quarter's payload shape fails silently when a field is renamed.
Caching: a shared optimisation on a shared path
A multi-tenant inference gateway caches responses keyed on a hash of the normalised prompt, to cut cost and latency. Tenants sometimes embed customer records in their prompts. The hazard is direct: if tenant identity is not part of the cache key, then a prompt from tenant B that normalises to the same value as one from tenant A returns A's cached response verbatim. Whether B can produce such a prompt depends on how guessable the prompt is, and prompts built from templates over modest parameter spaces are very guessable. There is a second, quieter leak: response timing distinguishes a cache hit from a miss, so an attacker can confirm whether a particular prompt has been submitted before, which reveals what other tenants have asked even when the response is never returned.
The fix is to partition caches per tenant, and more precisely to include in the cache key every input that could change who is entitled to the response: tenant, principal where results are user-specific, the entitlements or document set used to build the context, the model version and the system prompt version. Semantic caches that return an answer for a near-match rather than an exact match deserve particular care, because they will happily serve one tenant's answer for another tenant's similar-but-not-identical question. Encryption of the cache store is the tempting non-answer: the leak occurs through legitimate reads on a correctly functioning system, not through theft of the disk. Cache staleness and hash collisions are real engineering concerns but neither is the cross-tenant security issue.
# Unsafe: two tenants that build the same prompt share an entry
key = sha256(normalise(prompt))
# Safe: every input that affects entitlement is bound into the key
key = sha256(b"|".join([
tenant_id, # hard partition
principal_id, # if the response is user-specific
entitlement_set_version, # documents the caller may see
model_version, # pinned identifier, not a floating alias
system_prompt_version,
normalise(prompt),
]))
# And bound what caching can do at all:
# - never cache responses derived from retrieval over restricted documents
# - set a short TTL so a revoked entitlement stops mattering quickly
# - add jitter or constant-time lookup where hit timing is itself sensitiveCheck yourself
A gateway adds the tenant identifier to its cache key, closing the cross-tenant hole. Within one tenant, different users have different document entitlements and the assistant answers from retrieved documents. What risk remains?
Tenant partitioning does not distinguish users inside the tenant. If the cached answer was built from documents that only some users may see, another user in the same tenant can receive it by submitting a matching prompt. The key must also bind the principal or the entitlement set used to build the context, and answers derived from restricted documents are often better left uncached.
Try it first
Your cache is correctly partitioned and never returns another tenant's content. An attacker still learns something from it. What, and how?
Timing. A cache hit answers far faster than a miss, so an attacker who can submit candidate prompts can distinguish the two and confirm which prompts have been seen before. Where prompts are templated over a small parameter space, that is enough to learn what other users or tenants have asked about, for example whether a particular customer identifier has been queried, without ever receiving their content. Mitigations are to add jitter or a constant-time response path where hit timing is itself sensitive, to scope the cache narrowly, and to keep the time-to-live short.