Blog

Multi-agent AI penetration testing without giving the model a shell

How multi-agent AI reshapes autonomous penetration testing: a controlled MCP tool layer, credential-gated dispatch of specialist agents, and a bounded executor.

· 8 min read

The first question any security architect asks about autonomous penetration testing is not about coverage or accuracy. It is about control. If an agent is going to run real offensive operations on your network, what exactly is it allowed to do, and what stops a language model from turning a misread instruction into a destructive command? The honest fear behind the whole category is a simple mental image: a large language model with a root shell, improvising against production.

Darkmoon is built to make that image inaccurate. The model reasons, plans, and decides, but it never holds a terminal. Every action passes through a controlled tool layer built on the Model Context Protocol, an orchestrator dispatches 50 specialist agents rather than one omnipotent operator, and the operations that matter are credential gated and dispatched manually rather than triggered by inference. This piece walks through that architecture and, just as importantly, is precise about what it does and does not guarantee.

The fear: an LLM with a root shell

The naive way to build an AI pentester is to give a capable model a shell and a system prompt that says "find vulnerabilities". It demos well and it is genuinely dangerous. A model that can type arbitrary commands into a live environment inherits every failure mode of the model itself: misinterpretation, prompt injection from a target that talks back, and the plain fact that generation is non deterministic. The same prompt can dispatch a benign scan on one run and something far less benign on the next.

The OWASP Top 10 for LLM Applications names this directly under excessive agency, and the NIST AI Risk Management Framework frames it as the gap between an autonomous system's capability and the controls placed around it. The mitigation is not a smarter prompt. It is architecture: the model should never be the thing that executes, and the surface it can reach should be defined by code that does not itself reason.

The design instead: a controlled MCP tool layer

In Darkmoon the model does not run commands. It selects tools exposed through a controlled MCP layer. The Model Context Protocol is an open standard from Anthropic that lets a model call a defined set of tools with typed inputs, rather than emitting free text that some wrapper then executes. The distinction is the whole point. A shell accepts anything. An MCP toolbox accepts only the operations it was built to expose, in the shapes it declares.

Under that layer sits an MCP backed toolbox, internally called fastcmp, that wraps the real offensive tooling: the Docker image ships 50 specialist agents and more than 50 offensive tools, including Nuclei, NetExec, BloodHound, sqlmap, Impacket, Naabu, httpx and ffuf. The agent asks the toolbox to run a defined operation. The toolbox is what actually invokes the binary, on your infrastructure, under constraints the model does not control. We are careful to scope the claim precisely: operations go through a controlled MCP layer rather than a raw shell handed to the model. That is an architectural boundary you can read in the code, not a marketing absolute.

An orchestrator that plans and dispatches 50 specialist agents

Above the tool layer, an orchestrator does the reasoning. It reads the signal from a target, plans an approach, and dispatches specialist agents that each own a narrow domain. There is an active-directory agent that drives BloodHound, the Impacket suite, Kerberoasting and DCSync, and a separate pki-adcs agent for AD CS escalation paths. There are agents for Kubernetes, container registries, cloud providers, Terraform state, CI/CD systems, IoT firmware and more. No single agent is a generalist with keys to everything.

This is not just tidiness. A narrow agent has a narrow tool surface, a narrow objective, and a finding format it must satisfy. When the orchestrator sees several independent signals it dispatches several agents in parallel: our Terraform tfstate write-up shows the terraform, aws and ansible agents fanning out at once against the same environment. The same pattern drives our autonomous cloud penetration testing work across AWS, Azure and GCP.

Signal at the targetAgents dispatchedRuns only with
Reachable web applicationweb, api specialistsIn-scope target URL
Windows domain reachableactive-directory, pki-adcsDomain credentials you supply
Cloud API endpointaws / azure / gcp specialistProvider credentials you supply
Terraform state discoveredterraform, aws, ansibleAccess to the state backend

Credential-gated, manual-only dispatch

The most sensitive agents are not launched by inference. Dispatching the Active Directory, cloud or Kubernetes specialists requires credentials that you provide, and the dispatch is manual by design. This is a deliberate reversal of the convenient default, where the orchestrator would guess that a domain is present and fire the domain agent on its own.

We chose manual, credential gated dispatch after living through the failure mode of the alternative. Inference based dispatch means a model's reading of ambiguous output decides whether a powerful, credentialed agent runs. That is exactly where non determinism becomes a liability: the same environment can trigger a heavy agent on one run and not the next, and a target that returns misleading responses can nudge the model toward launching operations no one authorized. Gating dispatch on real credentials plus an explicit action removes the model from that decision entirely. The agent runs because you handed it access and told it to, not because it inferred it should.

Authorization and ethical testing

Every technique described here is offensive. Run Darkmoon only against systems you own or are explicitly authorized in writing to test, and keep the engagement inside its agreed scope. The credential gate is a safety control, not authorization. Providing credentials to an agent does not grant you permission to test the underlying system, and unauthorized testing is unlawful in most jurisdictions regardless of the tooling used.

Bounded execution: the executor refuses commands that never end

Underneath the toolbox is the executor, and it enforces a rule that a raw shell never would: commands must be bounded. An operation that would run without a defined end is refused rather than launched. In practice this means the executor rejects the class of commands that hang forever, tail indefinitely, or wait on input that will never come, all of which are ordinary ways for an autonomous run to stall or to sit open on a target far longer than intended.

A bounded executor changes the safety story in a concrete way. A misjudged step fails closed and the run continues, instead of an agent holding an open, unbounded process against your environment while the orchestrator waits. It is a small guarantee stated plainly, and it is the kind of constraint that is only possible because execution lives in code below the model rather than in the model's own output.

orchestrator -> dispatch(active-directory)   # manual, credential-gated
  active-directory -> fastcmp.run("GetUserSPNs.py ...")   # MCP tool call, not a shell
    executor: bounded command accepted, runs on your host
    executor: unbounded / no-timeout command -> refused
  result masked back to placeholders -> returned to the model

The privacy and command gateways as the last line

Controlling execution is one half of the problem. The other half is what the model gets to see and what it is allowed to send outward. Two gateways handle that, and both are covered in depth in how to run an AI pentest without sending your data to the LLM.

The Privacy Gateway tokenizes sensitive values before anything reaches the model. Real IPs, hostnames, emails and credentials are replaced with deterministic placeholders such as IP_PRIVATE_001, HOST_INTERNAL_001 and EMAIL_001. The model only ever sees those placeholders, never your real IPs, hosts or credentials. Real values live in a local vault and are rehydrated locally, just in time, at the moment a tool actually runs, then masked back out of every result before the model reads it. Determinism lets the agent still reason about relationships, which host talks to which, whether a credential repeats, without ever seeing the underlying data. We state the limit as plainly as the feature: deterministic placeholders leak structure, cardinality and co occurrence by design. That is a real information channel, and we treat it as one.

The command gateway inspects what an agent tries to run and blocks exfiltration patterns, for example pushing a placeholder mapping to an external host. It is a policy layer, not a sandbox. It shrinks the exfiltration surface but does not make a genuinely hostile model safe, which is why tools also run in containers on top of it. Because the whole loop can run on a local, self hosted model, the reasoning never has to leave your infrastructure at all.

What this architecture does and does not guarantee

The findings that come out the other end are held to the same standard as the controls that produce them. Every agent carries an adversarial status qualification step. A finding is only marked EXPLOITED when impact ran end to end, CONFIRMED when it is backed by the exact request or payload plus the raw response and extracted data, and UNCONFIRMED when there is a real lead but the impact was not demonstrated, which caps its severity low. Agents challenge their own claims and demote bare 200 responses, echoed payloads and by design public secrets. You can see that discipline applied to a real engagement in our autonomous attacker versus OpenNHP report.

What we do not claim

We do not claim there is no way to run a raw shell anywhere in the system. The specific, verifiable claim is narrower and more useful: the model's offensive operations go through a controlled MCP tool layer, the executor refuses unbounded commands, sensitive agents are credential gated and dispatched manually, and the model only ever sees deterministic placeholders. Those are boundaries in code. They reduce the blast radius of a misbehaving or manipulated model. They are not a proof that a determined, hostile model with sufficient access could never cause harm, and the command gateway in particular is a policy layer rather than a sandbox.

Remediation

If you are evaluating any AI pentesting tool, the architecture questions below separate a controlled system from a model with a shell:

  • Ask whether the model executes commands directly or selects tools through a typed layer such as MCP. Free text execution is the thing to avoid.
  • Require that the most powerful, credentialed operations are gated on credentials you supply and dispatched explicitly, never launched on the model's own inference.
  • Confirm the executor enforces bounded commands, so a misjudged step fails closed instead of hanging open against your environment.
  • Check what the model actually receives. If real IPs, hosts and credentials reach a hosted model, your infrastructure detail is in a third party's logs. Deterministic placeholders plus local rehydration keep it on your side.
  • Insist on proof per finding. A status that separates EXPLOITED from CONFIRMED from UNCONFIRMED is worth more than a long list of unverified leads.

FAQ

Does the AI model get a shell on my network? No. The model selects tools through a controlled MCP layer, and an MCP backed toolbox runs the actual binaries under an executor that constrains what can run. We scope this precisely: the model's operations go through that controlled layer rather than a raw shell handed to the model.

What is MCP and why does it matter for security tooling? The Model Context Protocol is an open standard from Anthropic that lets a model call a defined set of tools with typed inputs instead of emitting free text that some wrapper executes. For offensive tooling that difference is the safety boundary: the toolbox exposes only the operations it was built to expose, in the shapes it declares.

Why is dispatch credential-gated instead of automatic? Because inference based dispatch lets a model's reading of ambiguous output decide whether a powerful, credentialed agent runs. That is non deterministic and manipulable by a target that talks back. Gating on credentials you supply plus an explicit action takes the model out of that decision.

How does a bounded executor prevent runaway commands? The executor refuses commands that have no defined end, the class of operations that hang, tail forever or wait on input that never arrives. A misjudged step fails closed and the run continues, rather than leaving an open, unbounded process against your environment.

What does the architecture not protect against? It reduces the blast radius of a misbehaving model, it does not eliminate it. Deterministic placeholders leak structure and cardinality by design, the command gateway is a policy layer rather than a sandbox, and none of this substitutes for scoping and written authorization before a test.

What this proves about autonomous pentesting

Autonomous penetration testing does not require handing a model the keys. The useful version of the idea keeps the reasoning in the model and keeps execution, dispatch and data exposure in code that does not reason: a controlled MCP tool layer, credential gated manual dispatch of 50 specialist agents, a bounded executor, and privacy and command gateways as the last line. Each of those is a boundary you can read, and each narrows what a bad run can do. If you want to see how the same discipline shapes the wider field, our field guide to open source AI pentest tools puts the options side by side.

Darkmoon is GPL-3.0 and self hosted: source, 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.