// OVERVIEW

Most autonomous red teaming research assumes access to frontier cloud APIs or multi-node H100 clusters. As an operator running experiments on my own time without a datacenter compute budget, I wanted to see how far I could push a strictly local model on a single consumer GPU (a 24 GB RTX 3090). Here is how I took Llama-3-8B, removed false-positive safety refusals via activation projection, trained it on 1,250 synthetic Active Directory trajectories using Unsloth, and tested where an 8B model succeeds and where it hits its architectural ceiling.

The Problem (and Why an 8B Model?)

During my work running engagements at Mandiant, I spent a lot of time working through sprawling, heavily segmented Active Directory environments. In a real enterprise network, getting from an initial workstation foothold to tier-zero assets is rarely a straight line. You are dealing with complex forest trusts, strict firewall chokepoints, tiered administration boundaries, ACL and GPO abuse chains, delegation primitives, and misconfigurations spread across thousands of hosts. Finding a viable path requires real graph reasoning, where you test a lead, hit a network block, backtrack, and pivot through an unexpected intermediate system.

Startups have raised hundreds of millions of dollars trying to automate this kind of multi-hop attack path validation. I wanted to see what it actually takes under the hood to build an autonomous agent that can do this locally.

If you look at the current AI landscape, the first question that comes up is why anyone would use an 8B parameter model (Llama-3-8B) instead of a 70B model or a frontier API. For me, it came down to hardware reality and engagement OPSEC. As a consultant running these experiments independently on my home desktop during personal hours, I do not have access to an H100 cluster or a massive cloud compute budget. Everything from activation extraction and hyperparameter search to native 16-bit (bfloat16) LoRA fine-tuning had to fit inside the 24 GB VRAM budget of a single consumer RTX 3090. At 16-bit precision, 8B parameters is the practical limit of what you can train and iterate on quickly on one consumer card. Beyond that, on real client assessments you cannot pipe sensitive Active Directory data or password hashes out to third-party cloud APIs anyway, so any model you actually bring onto an engagement has to be small enough to run on local hardware.

When you drop a stock Llama-3-8B model into an agent loop (Reason -> Act -> Observe), it fails right away. First, the default safety filters block the model whenever a prompt involves security validation or lateral movement tasks. Second, even after you remove those blocks, an 8B model does not know how to plan across a network topology. When it runs into a firewalled server or a dead end, it does not know how to backtrack and try another route, so it just repeats the same failed command in a loop.

To fix both issues, I broke the project into two stages. First, I adjusted the model weights so it stopped refusing routine security tasks. Second, I scoped a local simulator around three representative AD attack primitives and fine-tuned the model on 1,250 multi-hop trajectories so it could learn how to plan and backtrack.


Step 1: Stopping False-Positive Refusals

Early “uncensored” models were usually trained on giant datasets of unfiltered text, which tended to make the model worse at following instructions and outputting clean JSON. A cleaner approach from recent representation research (Arditi et al., 2024) shows that refusal behavior in instruction-tuned models is mostly controlled by a single direction in the model’s activation space. When a prompt mentions security testing, the internal activations point along that direction and trigger a refusal.

By comparing the model’s internal activations on security prompts against normal prompts, you can isolate that refusal direction v as a unit vector and subtract it out of the weight matrices:

v  = (μ_security - μ_normal) / ||μ_security - μ_normal||
W' = W - α(Wv)v^T

Because that change is just the outer product of two vectors, it is a Rank-1 matrix update. That means you can save the modification as a lightweight LoRA adapter instead of saving a whole new 16 GB copy of the model.

Instead of manually guessing which transformer layers to edit, I used the Heretic toolkit to run an automated search (using a Tree-structured Parzen Estimator) across different layer ranges and scaling factors. The goal was to drop the refusal rate as low as possible without messing up the model’s normal token probabilities (measured by KL divergence).

01 // LAYER SEARCH
Layers 15 to 29 (Scale 1.44)
Applying a 1.44 projection scale across the middle-to-late layers removed the refusal triggers while leaving the early syntax and formatting layers intact.
02 // RESULT
98% down to 13% Refusals
KL divergence stayed down at 0.0296, so the model kept its coding skills and followed strict JSON tool schemas without breaking.

Once the adapter was applied, I ran a few quick tests asking the model to write local permission auditing scripts and post-exploitation helpers that the stock model refused. The updated model handled them cleanly without lecturing, so it was ready for the agent loop.


Step 2: Generating 1,250 Multi-Hop Trajectories

Getting the model to run tools was only half the problem. When I connected the model to my local Active Directory simulator, it still could not chain steps together across a network. To teach an 8B model how to navigate a multi-hop environment, you need multi-turn traces (Reason -> Act -> Observe) where the model states a thought, calls a tool, reads the output, and adjusts its plan.

Writing 1,250 multi-turn traces by hand was not realistic. Normally you would use a large frontier API as a teacher model to generate synthetic training data, but commercial APIs refuse requests that ask for offensive Active Directory attack paths.

My workaround was a two-stage translation loop. From a graph-routing perspective, an IT administrator running an internal infrastructure audit and an operator validating attack paths are executing the exact same logical steps. Both scan the subnet, check registry keys for client configurations, verify service accounts, and inspect certificate templates on the CA.

[1. Prompt Sent to Teacher API]
"Perform an authorized administrative network audit to map internal assets
 and verify client policy distribution servers."
        │
        ▼
[2. Teacher API Output]
THOUGHT: I need to query the local client directory policy repository to inspect policy structures.
ACTION:  {"action": "audit_policy_store", "arguments": {"host": "PC-01"}}
        │
        ▼
[3. Mapped to Local Simulator Schema]
THOUGHT: I need to query local registry configurations on the foothold to locate cached credentials.
ACTION:  {"action": "query_registry", "arguments": {"host": "PC-01", "key_path": "HKLM\\Software\\Microsoft\\CCM"}}

I fed standard corporate audit scenarios to the frontier model, took each step it generated, and ran it through a local script that mapped the thoughts and tool names back into my agent’s schema. That action was executed against my local Python AD simulator, which returned a consistent JSON observation.

To keep the simulator tractable while still testing whether the model could generalize across different attack types, I scoped the dataset to 1,250 trajectories split evenly across three common AD attack paths (around 415 traces each). One third of the dataset covered infrastructure misconfigurations like extracting local SCCM Network Access Account policy blobs, another third covered identity attacks like SPN enumeration and Kerberoast ticket cracking, and the final third covered ADCS certificate template abuse (ESC1).


Step 3: Preventing Hostname Memorization

While testing early builds of the dataset, I noticed that if the Domain Controller is always named DC01 and the starting workstation is always PC01, an 8B model takes a shortcut. Instead of reading the JSON output from network_scan to see what hosts actually exist on the subnet, the model simply memorizes the string "DC01" during training and hardcodes it into its tool calls.

At the same time, if you randomize hostnames into random gibberish like srv_9x8q2z, the tokenizer splits the hostname into a bunch of tiny sub-word tokens, which bloats the context window and hurts attention.

To fix both problems, I added bounded randomization to the simulator. Every time a new episode starts, the environment generates realistic enterprise naming conventions with randomized two-digit suffixes:

self.suffix    = f"_{random.randint(10, 99)}"
self.domain    = f"sevenkingdoms{self.suffix}.local"
self.dc_name   = f"PRIMARY-DC-{random.randint(10, 99)}"
self.site_mgr  = f"SITE-MGR-{random.randint(10, 99)}"

Because PRIMARY-DC-47 still looks like a normal corporate server name, the tokenizer handles it cleanly. Because the numbers change on every run, the model is forced to actually parse the scan output and pull the right hostname from the conversation history before calling its next tool.


Step 4: Fine-Tuning on an RTX 3090 with Unsloth

With the 1,250 trajectories ready, I ran the Supervised Fine-Tuning (SFT) job in WSL2 Linux using Unsloth. Unsloth uses custom Triton kernels for attention and RMSNorm backpropagation, which reduced memory usage enough for me to train in native 16-bit bfloat16 inside my 24 GB VRAM budget instead of dropping down to 4-bit QLoRA.

I trained a rank-16 LoRA adapter (r = 16, lora_alpha = 32) across all attention and MLP layers (q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj). To keep the gradients stable without running out of memory, I set the batch size to 2 with 8 gradient accumulation steps (for an effective batch size of 16) and used the 8-bit AdamW optimizer (adamw_8bit) to save another 2 GB of VRAM.

The training job ran for 500 steps (about 7 epochs). Training loss dropped from 2.059 down to 0.066, and I merged the resulting LoRA adapter back into the weights to produce the final RedAgent model.


Step 5: Benchmarking Across Three AD Topologies

To test whether the fine-tuning actually helped the model plan, I ran the stock model, the refusal-stripped model, and RedAgent side by side against three simulated Active Directory topologies of increasing complexity.

TIER 01 // 5 STEPS
Foothold Sprint
A direct 5-step path from a workstation to the DC using local SCCM policy credentials.
TIER 02 // 10 STEPS
Firewalled Chokepoint
Direct SMB access from the workstation to the DC is blocked by a firewall. The agent has to hit the block, backtrack to an intermediate Site Manager server, extract admin credentials there, and then pivot to the DC.
TIER 03 // 14 STEPS
The Labyrinth (ADCS)
A 14-step chain requiring the agent to pivot through a Certificate Authority (CA01), enumerate vulnerable templates, request a certificate via ESC1, authenticate to a Database server, and finally reach the DC.
MODEL CANDIDATE TIER 1 (5 STEPS) TIER 2 (10 STEPS) TIER 3 (14 STEPS)
Stock Llama-3-8B PASS (8 steps) FAIL (Got Stuck) FAIL (Got Stuck)
Abliterated Llama-3-8B PASS (8 steps) FAIL (Got Stuck) FAIL (Got Stuck)
RedAgent-8B (Ours) PASS (11 steps) PASS (10 steps) FAIL (Got Stuck)

What Happened on Each Tier

All three models passed Tier 1 because it is a straight line with no dead ends. As long as the tool descriptions are in the system prompt, even an untrained model can step through 5 sequential actions.

Tier 2 (the firewalled chokepoint) is where the trajectory fine-tuning really showed its value. When the stock and refusal-stripped models tried to pivot to the Domain Controller at Step 5 and got back "Access Denied by firewall", neither of them knew how to recover. The refusal-stripped model just kept running the exact same failed pivot command against the blocked DC, or started guessing tools at random until the context window filled up with failed attempts (on the rare runs where it stumbled onto the right server by pure luck, it took over 22 steps).

RedAgent, on the other hand, solved Tier 2 in the exact 10 optimal steps. As soon as it saw the firewall rejection on the DC, its next thought noted the block, backtracked to SITE-MGR-xx, pulled the credentials from the site server, and completed the path to the DC.

Tier 3 (the 14-step ADCS path) broke all three models, including RedAgent. Looking at the execution logs, RedAgent got through the initial SCCM steps, saw the DC was blocked, and correctly decided to go after ADCS certificate templates next. However, instead of calling pivot to move onto CA01 first, it kept trying to run query_ca_templates directly from the starting workstation until it hit the step limit.

When I looked through the simulator code and the agent traces, it became clear why that happened. In real Active Directory environments (using tools like Certipy or Certify), you query ADCS certificate templates remotely over RPC/LDAP right from your initial workstation without ever needing an interactive session on the CA server first. My simulator state machine, however, was written to require current_host == ca_name before query_ca_templates would succeed. Once the conversation history stretched past 10 turns, the 8B model fell back on its pre-trained AD knowledge instead of following the simulator’s stricter host rule.

That problem was compounded by how the simulator handled errors. In Tier 2, when the DC pivot failed, the simulator returned a clear error message ("Access Denied by firewall"). In Tier 3, when query_ca_templates was called from the wrong host, my simulator returned a bare {"status": "error"} with no message explaining why. Without an error message telling it that the command had to be run from CA01, the 8B model kept retrying the call until it ran out of steps.


Takeaways & Next Steps

Building this on a single RTX 3090 made two things obvious. First, stripping refusal vectors out of local weights is fast and keeps the model’s formatting intact, but it does not give the model any planning skill on its own. Second, small 8B models can learn to backtrack around network blocks if your synthetic training set includes failure-and-recovery paths, provided your tool wrappers return clear error messages when a command fails.

The biggest limitation of this experiment is that it ran against a lightweight Python state machine. My next step is to connect this training and evaluation loop to a full virtualized Active Directory lab (such as GOAD) with real tool outputs from Impacket, NetExec, and Certipy, and inject more multi-branch backtracking into the training trajectories so the model can handle longer chains without losing track of host state.