Blog

Pentesting an LLM without leaking your own infrastructure to the LLM

Why an offensive LLM-endpoint agent ships with privacy-gateway hardening: pre-model prompt tokenization from terminal and UI, server-side report rehydration and a per-session override, measured on a real run where the model saw the target 0 times across 3.5 MB of traffic.

· 9 min read

There is a neat symmetry in using an AI agent to attack an AI endpoint. The agent's job is to make the target model leak what it should keep secret, its system prompt, its credentials, its hidden context. But the agent is itself driven by a reasoning model, and if that model sees your real infrastructure while it works, you have simply moved the leak. Release v1.4.0 shipped the offensive llm agent and, in the same release, hardened the privacy gateway that keeps your infrastructure hidden from the model doing the attacking. This article is about why those two things belong in the same release, and what the gateway does and does not protect.

The threat model, stated plainly

The hardest blocker for AI driven penetration testing has never been the reasoning. It is data exposure. The moment an autonomous agent sends a real IP, hostname, credential or internal path to a hosted model, sensitive facts about your infrastructure leave your perimeter and land in a third party's logs. For regulated, defence and sovereignty constrained teams that single fact rules the whole approach out, and the usual workaround, run a small local model, trades away most of the reasoning quality that made the approach worth having.

Darkmoon's answer is an anonymization gateway between the agent and the model. Every sensitive value is replaced by a deterministic placeholder before anything reaches the LLM: IP_PRIVATE_001, HOST_INTERNAL_001, URL_001, EMAIL_001. Deterministic is the load bearing word. The same real value always maps to the same placeholder within a run, so the model can still reason about relationships, this host talks to that host, this credential works there, without ever seeing the real data. The real values live in a local vault and are rehydrated locally, just in time, right before a tool actually runs, then masked back out of the result before the model reads it.

What v1.4.0 closed: the prompt itself

The gateway used to tokenize tool inputs and tool outputs, which covered almost everything the model touched. Almost. The very first thing the model reads in a campaign is the operator's launch prompt, and that prompt routinely carries the target: a URL, an IP, a scope. Until this release that first message could carry raw values straight to the model before any tool ran. Issue 40 closed that gap with pre model prompt tokenization: your prompt is now anonymized before it reaches the model, from both the terminal and the UI, through an opencode plugin that routes it to the persistent MCP privacy vault over a local socket. The same session id means the report still rehydrates the real values at the end.

You can see it in the LLM validation run. The launch prompt named an http:// target on loopback. What the model received was already tokenized:

# operator types a real target; the model receives placeholders
tokenize_prompt  ->  "TARGET: URL_001 PROGRAM=\"LLM inference endpoint\" SCOPE=URL_001"

# the campaign is initialized on placeholders too
init_campaign    ->  target_host: URL_001   target_ip: IP_PRIVATE_001

The offensive LLM run is where this gets tested hardest

An LLM pentest is a stress test for the gateway, because the whole point of the engagement is to move large volumes of adversarial text through the reasoning model. So we measured it. Across the full model facing traffic for the validation run, roughly three and a half megabytes of it, the target appeared as the placeholder URL_001 thirty five times and the real loopback address appeared zero times. The model reasoned about the target throughout, decided which attacks to run, and never once saw where it actually was.

ControlResult on the validation run
Real target address seen by the model0 times, across ~3.5 MB of model traffic
Target seen as the placeholder URL_00135 times, so the model could still reason about it
Report rehydrated on diskreal target restored 37 times, 0 residual placeholders
Exploited secrets restored to the reportcanary DMK-CANARY-7731 and the admin key, real values

The two directions matter equally. The target model's leaked secrets, the canary and the admin credential, are the findings, so they have to end up in the report in full. The client's own infrastructure, the address of the endpoint under test, must never reach Darkmoon's reasoning model. The gateway does both at once: it captured the exploited canary DMK-CANARY-7731 as evidence and wrote it into the finalized report, while the target address the operator supplied stayed a placeholder for the model and was only restored to the report file at write time.

Rehydration is server side, and it caught a real bug

Because the report is regenerated from the stored findings, and those findings hold placeholders, the finalized document is written by rehydrating placeholders back to real values on the way to disk. This is not a detail we take on faith. On disk after the run, the real target appeared throughout the report, the exploited secrets were restored to their real values, and there were zero residual placeholders left behind.

The LLM run also earned its keep by surfacing a rehydration bug that earlier engagements had not. The report writer had been rehydrating the target host into the report filename as well as its body. For an IP target that is harmless, because an IP is filename safe. For a URL target like http://host:port it is not: the colon and the slashes break the write path, and the finalize step failed to write the file at all. The fix was to slugify the rehydrated host for the filename while the report body keeps the real, rehydrated values. It is a small fix, but it is exactly the kind of thing that only shows up when you run the full path end to end on a URL target rather than an IP.

The command gateway, and the honest limits

Tokenisation alone is not enough, because an agent can always try to be clever. A command gateway inspects what the agent attempts to run and blocks exfiltration patterns, for example curling a secret to an external host, or encoding and leaking the placeholder mapping itself. v1.4.0 also decoupled the exfiltration checks from the structural safety checks in that gateway, and added a per session privacy override that degrades gracefully, tokenizing and continuing rather than refusing a command outright when a value cannot be classified.

We would rather state the limits than have you find them:

  • Deterministic placeholders leak structure by design. Cardinality and co occurrence remain visible to the model even when the values are not. For most infrastructure data that is an acceptable trade, but it is a real information channel and we treat it as one.
  • The command gateway is a policy layer, not a sandbox. It shrinks the exfiltration surface. It does not make a genuinely hostile model safe, which is why tools run in containers on top of it.
  • The validation target was a deliberately vulnerable teaching endpoint. The privacy measurements prove the gateway does not blind the agent and does not leak the target; they are not a claim about a hardened production system.

Where garak fits, and why it stays local

The same release adds garak, NVIDIA's open source LLM vulnerability scanner (Apache 2.0), to the toolbox, pinned to 0.16.0 for reproducible builds. It is worth noting how it fits the privacy story. garak is installed CPU only, because it scans a remote endpoint and needs no GPU of its own, and its install is non blocking, so a failed install never breaks the image and the agent falls back to its manual methodology. Like every other tool, when garak runs it runs locally through the same MCP tool layer, against the rehydrated real target, and its output is masked back to placeholders before any of it reaches the reasoning model. Adding a scanner did not add a new path by which your infrastructure reaches a third party.

What we validated, precisely

The privacy figures here are from a single end to end run of the llm agent against a deliberately vulnerable OpenAI compatible target on loopback, on the sealed build. The model facing traffic was captured through a logging proxy so we could count exactly what the model received. The report content was verified on disk after finalize. These numbers describe that run, not a general guarantee about every possible input; the structural limits above always apply.

FAQ

Does my target endpoint get sent to a hosted model when Darkmoon pentests it? No. The target address is tokenized to a deterministic placeholder before the reasoning model sees anything, including your launch prompt as of v1.4.0. On the validation run the real address reached the model zero times across the whole engagement, while the placeholder let the model still reason about the target.

If the model only sees placeholders, how does the report contain real values? The real values live in a local vault keyed to the session, and the report is rehydrated from them server side at write time. On the validation run the report was written with the real target and the real exploited secrets and left zero residual placeholders behind.

What is pre model prompt tokenization? It is the v1.4.0 fix, issue 40, that anonymizes the operator's launch prompt before it reaches the model, from both the terminal and the UI. Previously only tool inputs and outputs were tokenized, so the first prompt could carry raw values to the model; now it cannot.

Is this the same privacy model you use for a normal pentest? Yes. The gateway is the same one described in how to run an AI pentest without sending your data to the LLM, and it applies whether the target is a web app, an Active Directory domain or, as here, an LLM endpoint. Running the whole platform on your own hardware is covered in the local self hosted AI pentester.

The full offensive side of this release is in the eight OWASP LLM Top 10 findings and the agent methodology. Darkmoon is our open source project (GPL-3.0): github.com/ASCIT31/Dark-Moon, docs.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.