Safe Penetration Testing Labs · beginner · ~10 min
- Explain what a **local-only lab** is and why it's the only safe place to practice offensive techniques - Build an isolated environment with **Docker** (`--network none` / a private bridge) or a **VM** - **Verify** that your lab genuinely cannot reach the outside world - Use **snapshots/resets** so you can return to a clean state - Clean up a lab completely when you're done
A local-only lab is a self-contained practice environment that runs entirely on hardware you own and cannot reach the outside world. It is the safe place the previous two lessons pointed at: authorization plus an RoE tells you what you may test, and the lab guarantees that even a mistake stays on your own machine.
This lesson builds on Rules of engagement: the RoE you drafted named loopback-only targets — here you actually stand them up. The core safety property is network isolation: if the lab has no path off your machine, you physically cannot attack anyone else by accident.
You'll set one up, and — just as importantly — prove it's isolated, because an unverified assumption is not a safety control.
Offensive techniques are only safe to practice where a mistake can't escape. A scan aimed at the wrong address, a fuzzer that finds a real service, a payload that "works" against a live host — all become non-issues when the lab has no network path out.
Isolation also makes practice repeatable: with snapshots you can break things freely, then reset to a known-good state in seconds. That combination — safe to break, easy to reset — is what lets you learn fast without risk.
| Setup | How it isolates | Good for |
|---|---|---|
Docker, --network none |
Container has only loopback; no route off the box | Toy programs, single services |
| Docker private bridge | Containers talk to each other, not the internet | Multi-service labs (app + db) |
| VM with host-only networking | VM reaches host/other VMs, not the internet | Full-OS practice, snapshots |
| Air-gapped machine | No networking hardware in use at all | Maximum caution |
The rule across all of them: no path to the internet or your home/work LAN.
--network none = no network at all (not even to other containers). Safest; use when a single program is enough. Internet / home LAN
X (no route)
|
+----------------------------+
| private lab bridge |
| app <--> database | <-- talk to each other only
+----------------------------+
Isolation is a claim until you test it. From inside the lab, try to reach an external host and confirm it fails. A lab you didn't verify is a lab you don't trust.
--rm container lets you reset to clean state after you break something.Knowledge check:
--network none safer than a private bridge, and when would you still want the bridge?The key Docker flags for isolation and cleanup:
--network none # no network interfaces except loopback (max isolation)
--network lab-net # attach to a user-defined bridge you created (private)
--rm # delete the container automatically when it exits
docker network create --internal lab-net # a bridge with NO gateway to outside
--internal on a user-defined network is important: it creates a bridge with no route to the host's external network, so containers on it can reach each other but not the internet.
A local lab is a self-contained practice environment that runs entirely on hardware you own. It is the only safe place to practice offensive security techniques. Because nothing in the lab can reach the outside world, you cannot accidentally attack systems that are not yours.
A lab can take several shapes. Pick the one that fits your needs:
--network none (no network at all) or a private bridge (an isolated virtual network shared only by your containers).The C-platform exercises run each submission inside a Docker container with --network none by default.
This means your code cannot reach anything except its own loopback fixtures (local test data served on the container's own loopback interface). It has no path to the wider network.
Stand up an isolated two-service lab (app + database) on a private, internet-less network, then verify isolation and tear it down:
# 1. Create a private network with NO gateway to the outside (--internal).
docker network create --internal lab-net
# 2. Run a test database on it (test data only; placeholder password).
docker run -d --name lab-db --network lab-net -e POSTGRES_PASSWORD=<lab-only-placeholder> postgres:16
# 3. Run the app container on the same private network.
docker run -d --name lab-app --network lab-net myapp:dev
# 4. VERIFY isolation from inside the app: reaching the internet must FAIL.
docker exec lab-app sh -c 'getent hosts example.com || echo "isolated: no DNS/route"'
# 5. Clean up everything when done.
docker rm -f lab-app lab-db
docker network rm lab-net
What it does. Step 1 makes a bridge with --internal, so nothing on it can route to the internet. Steps 2–3 put the app and its database on that private bridge — they can talk to each other but not out. Step 4 is the safety check: the lookup fails, printing isolated: no DNS/route. Step 5 removes the containers and network so nothing is left running.
Expected result. The verification line prints the "isolated" message (the external lookup fails), and after cleanup docker ps shows none of the lab containers.
| Command | What happens | Why it matters |
|---|---|---|
network create --internal lab-net |
Makes a bridge with no external gateway | The isolation boundary |
run -d --name lab-db --network lab-net … |
DB joins the private net only | Reachable by the app, not the internet |
-e POSTGRES_PASSWORD=<placeholder> |
Sets a lab-only password | Never a real secret in examples |
run -d --name lab-app --network lab-net … |
App joins the same net | App↔DB works; app↔internet doesn't |
exec lab-app … getent hosts example.com |
Tries an external lookup | Proves isolation by failing |
rm -f … / network rm lab-net |
Removes containers + network | Full cleanup, nothing lingers |
The shape is always: isolate → run → verify → clean up.
Mistake 1 — assuming isolation without checking.
Wrong: "I used a bridge, so it's fine."
Why wrong: a default Docker bridge does reach the internet; only --network none or --internal blocks it.
Fix: use --internal (or none) and verify with an external lookup that fails.
Mistake 2 — real secrets in the lab.
Wrong: pasting a real API key or password into a container's env.
Why wrong: secrets leak into images, shell history, and logs.
Fix: use obvious placeholders (<lab-only-placeholder>); never real credentials.
Mistake 3 — leaving the lab running.
Wrong: forgetting containers/volumes after you're done.
Why wrong: stale services consume resources and can drift out of isolation on the next reboot/reconfig.
Fix: docker rm -f the containers and remove the network/volumes; verify with docker ps -a.
Recognize it: if you never ran a command that proves isolation, assume you don't have it yet.
lab-net; --internal blocks the internet, not container-to-container.--network none or --internal and re-verify.docker network create --internal lab-net) before running containers on it.docker logs <name>; the isolation is fine, the app config isn't.Questions to ask: Is there any interface that routes off this box? Did my verification step actually fail to reach the internet? Is anything from a previous lab still running?
Security & safety — keep the lab a lab.
Isolation + verification + cleanup is a small routine that makes everything downstream safe.
Professional habits: default to the strongest isolation the task allows; verify before you trust; snapshot before risky steps; clean up after. Advanced habits: script lab setup/teardown so it's reproducible; keep lab and production tooling clearly separated to avoid pointing a test at the wrong place.
Beginner 1 — No-network container. Objective: run a --network none container and, from inside, show that an external lookup fails. Describe the command and its output. Concepts: isolation, verification.
Beginner 2 — Placeholder audit. Objective: given a docker run line containing -e API_KEY=sk-live-abc123, rewrite it to use a safe placeholder and explain why the original is dangerous even in a lab. Concepts: secrets hygiene.
Intermediate 1 — Private two-service lab. Objective: create an --internal network, run two containers on it, and demonstrate that they can reach each other but not the internet (show both the success and the failure). Concepts: private bridge, verification.
Intermediate 2 — Clean teardown. Objective: write the exact commands to remove all containers, the network, and any volumes from your lab, then a command that proves nothing is left. Concepts: cleanup, docker ps -a.
Challenge — Reproducible lab script. Objective: write a small shell script that (1) creates an internal network, (2) starts an app + database on it, (3) runs an isolation-verification step that exits non-zero if the container can reach the internet, and (4) provides a teardown function. Requirements: placeholders only, no real hosts, safe to run repeatedly. Concepts: everything in this lesson; this is the environment the rest of the security track runs in.
--network none (nothing) or an --internal private bridge (containers talk to each other, not out); VMs use host-only networking.--rm to reset, and clean up (containers, network, volumes) when done.Next: with a verified, isolated lab in place, you're ready for the hands-on defensive labs — starting with reading and parsing untrusted input safely.