Safe Penetration Testing Labs · beginner · ~10 min

Setting up a local-only lab

- 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

Overview

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.

Why it matters

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.

Core concepts

1. Isolation options

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.

2. "No network" vs "private network"

  • --network none = no network at all (not even to other containers). Safest; use when a single program is enough.
  • A private bridge = an isolated virtual network shared only by your lab containers, so an app can reach its database but nothing can reach the internet.
  Internet / home LAN
        X   (no route)
        |
  +----------------------------+
  |   private lab bridge       |
  |   app  <-->  database      |   <-- talk to each other only
  +----------------------------+

3. Verify, don't assume

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.

4. Snapshots and cleanup

  • A snapshot (VM) or a fresh --rm container lets you reset to clean state after you break something.
  • Cleanup means removing containers, images, and volumes you created so nothing lingers.

Knowledge check:

  1. Why is --network none safer than a private bridge, and when would you still want the bridge?
  2. How would you prove your container can't reach the internet?
  3. What's the difference between "reset the lab" and "clean up the lab"?

Syntax notes

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.

Lesson

What a local lab is

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.

Common lab setups

A lab can take several shapes. Pick the one that fits your needs:

  • Docker containers on your laptop with no host networking. Use --network none (no network at all) or a private bridge (an isolated virtual network shared only by your containers).
  • A virtual machine with two network interfaces (NICs). One NIC connects to an isolated lab network. The other is used only for management.
  • Dedicated air-gapped hardware for advanced practice. Air-gapped means the machine has no connection to any outside network.

How the C-platform sandboxes your code

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.

Code examples

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.

Line by line

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.

Common mistakes

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.

Debugging tips

  • App can't reach the DB? Confirm both are on the same lab-net; --internal blocks the internet, not container-to-container.
  • External lookup unexpectedly succeeds? You're probably on the default bridge — recreate with --network none or --internal and re-verify.
  • "network not found"? Create it (docker network create --internal lab-net) before running containers on it.
  • Container exits immediately? Check 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?

Memory safety

Security & safety — keep the lab a lab.

  • The whole point is containment: verify isolation every time before you run anything offensive, not just the first time.
  • Use test data and placeholder credentials only — never copy production data or real secrets into a lab.
  • Keep the host patched and don't bridge the lab to your real network "just for a minute."
  • Log what you run inside the lab for your own learning notes, but never store real secrets; tear down and delete lab data when finished.

Isolation + verification + cleanup is a small routine that makes everything downstream safe.

Real-world uses

  • Security training & CTFs ship as isolated Docker/VM environments for exactly this reason.
  • Intentionally-vulnerable apps (used for learning) are meant to run only on an isolated local network, never exposed.
  • Malware analysis is done in isolated, snapshot-able VMs so a sample can't spread.
  • CI security tests run scanners against services on a private, internal-only network.

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.

Practice tasks

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.

Summary

  • A local-only lab runs on hardware you own with no path to the internet or your LAN, so mistakes can't escape.
  • Isolate with --network none (nothing) or an --internal private bridge (containers talk to each other, not out); VMs use host-only networking.
  • Verify isolation every time — try an external lookup and confirm it fails; an unverified lab isn't safe.
  • Use snapshots / --rm to reset, and clean up (containers, network, volumes) when done.
  • Never put real secrets or real data in a lab — placeholders and test data only.

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.

Practice with these exercises