All lessons › Function C — Application Security with an AI SDLC › The AI SDLC — an Agentic AppSec Pipeline, Before and After Deploy
C2.6

Sandbox replication

C2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/C2.6.mp4
Risk

Dynamic testing is run against staging, so a destructive probe becomes an incident.

Control

Stage 11: replicate the application in an isolated, disposable runtime with no path to production.

The lab

Stand up an isolated replica, prove egress and credential isolation, and show what a destructive probe touches.

◉Use case relevance

Change the model and the harness at the same time and you have learned nothing about either. Separating those two effects is the whole job — and it is also how you read somebody else's published number without being misled by it.

At CyberTravels. appsec/replica.py. The interesting part is the refusal: for_target('cybertravels-staging') raises rather than quietly building something safe-looking. isolated_db() then points your application's storage at the replica — and finds that it does not move INVOICE_ROOT, which is computed at import from the source tree. That bug is in the file, named, because a replica has to be checked rather than assumed.

◴What this lesson is — Day 0, Day 1, Day 2

A disposable replica, and the refusal that makes it one — a stage that quietly downgrades to something safe-looking produces output nobody can interpret. Plus the isolation bug the stage finds in itself, because a replica has to be checked rather than assumed.

Day 0 — why

Dynamic testing run against staging turns a destructive probe into an incident.

Day 1 — how

Replicate the application in an isolated, disposable runtime with no path to production.

Day 2 — measure

Probes executed in the sandbox, and connections from it to anything production — where the second number must be zero.

Measure this

what to measuredenominator or target
probes executed in the sandboxcount
connections from the sandbox to anything productioncount; must be zero

▦The framework, and how it works

   two effects, one number
   +-------------+     +--------------+
   |   model     |  x  |   harness    |  = the result you published
   +-------------+     +--------------+

   change one at a time, on fixed seeds and a fixed corpus.

   then the three checks on anybody's benchmark
   class balance -> the floor . held-out key -> a result . matcher -> real

Phase 4 turns hypotheses into facts by running the application. That is only safe if the thing you run it against cannot hurt anyone.

Stage 11 — Sandbox replication. Deploy the application in an isolated, disposable runtime: its own container, its own synthetic data, no route to production, no real credentials.

The reason this is a stage rather than a footnote is that the obvious shortcut — point the dynamic tests at staging — converts every destructive probe into an incident. Staging usually shares an identity provider, a message bus, sometimes a database replica, and always someone's on-call rota.

Four isolation properties, and you need all four:

  • network — no egress except to the replica itself,
  • credentials — synthetic secrets, so a leak is worthless,
  • data — synthetic records, so an exfiltration test exfiltrates nothing,
  • lifetime — destroyed after the run, so state cannot leak between tests.
Where you are in the pipeline. `` [Ingestion & Mapping] ──> [Threat Modelling] ──> [Discovery] └─ stages 1-4 └─ stages 5-6 └─ stages 7-10 ──> [Dynamic Validation] ──> [Reporting] └─ stages 11-14 └─ stage 15 ``

2 · Stage 11 — model the replica and its isolation

3 · Where it breaks — the same probes against staging

4 · The control — the four isolation properties, checked

5 · The stage, as a skill

Before anything is executed against CyberTravels' environment, four checks decide whether it is a replica or staging with a different DNS name. The skill runs them — egress, credentials, data, and the destructive probes you would only run somewhere built to be destroyed.

▶Real time execution as skill

The skill — skills/appsec/exploit-replica-isolation-check/SKILL.md

name: exploit-replica-isolation-check
description: >-
  Check that the environment a finding is proved in is a replica rather than
  staging — on egress, credentials, data realism and destructive probes — before
  anything is executed against it. Use before dynamic testing, exploit
  validation, or letting an agent run a proof of concept.
allowed-tools: Read, Grep, Glob, Bash

Staging is production with a different DNS name

The reason exploitation is safe is the environment, and "staging" is not that environment: it holds real credentials, real-shaped customer records, and a route to things that matter. A replica is built to be destroyed, and the four checks that distinguish them take minutes.

When to use this

Before running any probe that could change state, before pointing an agent at a target, and every time somebody offers staging because the replica is not ready.

Procedure

1 — Check egress. The environment should reach its own internal hosts and nothing else. Test the three that matter specifically: a code host, the cloud metadata address, and any private range. A replica that can reach GitHub can exfiltrate.

2 — Check credentials. Every secret in the environment should be synthetic. Grep the environment, the mounted files and the database. One real key makes the whole environment production for the purposes of this decision.

3 — Check the data. Real-shaped is not the same as real. Sample records and confirm they are generated, not copied — a customer row with a real email address is a breach waiting for a log line.

4 — Run the destructive probes. Drop a table, delete a file, exhaust a quota. In a replica these are unremarkable. If you would not do them here, this is not the environment to prove an exploit in, and that is the finding.

5 — Record the verdict per check, not overall. "Isolated: no" sends people looking; "credentials: fail, egress: pass" tells them what to fix.

Example

Input — the fixture committed at the top of scripts/exploit_replica_isolation_check.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

ALLOW http://replica.local/reports                   replica-internal
ALLOW http://db.replica.local:5432/                  replica-internal
DENY  https://api.github.com/                        not on the replica allowlist
DENY  http://169.254.169.254/latest/meta-data/       private address outside the replica — blocked
DENY  http://10.0.3.14:9200/_search                  private address outside the replica — blocked
probe                       on staging    on replica
--------------------------------------------------------
drop a table                   REACHES     contained

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "environment": "str",
  "checks": {"egress": {"pass": false, "reachable": ["str"]},
             "credentials": {"pass": false, "real_found": ["str"]},
             "data": {"pass": false, "sample_real": ["str"]},
             "destructive": {"pass": false, "refused": ["str"]}},
  "verdict": "replica|not a replica",
  "safe_to_exploit": false
}

Failure modes

  • Accepting the name. "staging" and "replica" are labels; the four checks are the definition.
  • Testing general egress only. The metadata address is the one that turns a probe into a credential theft.
  • Skipping the destructive probes because they are destructive. That reluctance is the answer.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons C2.6

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick c2-6-sandbox-replication:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons C2.6type /c2-6-sandbox-replication
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons C2.6type /c2-6-sandbox-replication in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons C2.6type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons C2.6type $c2-6-sandbox-replication, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py C2.6

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at C2.6 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.6 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The replica permits only its own internal hosts and blocks GitHub, the metadata service and private addresses. Staging holds real credentials and a real-shaped customer record while the replica holds synthetic ones. The four isolation checks pass for the replica and fail for staging on credentials, data and lifetime, and destroying the replica clears its state.

✎Your turn

Check whether your dynamic testing currently runs against staging. If it does, list what staging shares with production — identity provider, message bus, data replica. Each shared component is a path from a test probe to a real incident.

Tools used

DockergVisorCiliumGLM-4.6Claude Haiku 4.5

Skill source: skills/appsec/exploit-replica-isolation-check

← PreviousC2.5 · Feasibility filtering, reachability and dead code