Reproducible pen-test / demo environment

This is the written-down answer to issue #849: a single command that stands up a running Mycorrhizal instance pre-populated with realistic, multi-user data — for an external penetration test, or for documentation screenshots — and that is cheap to regenerate every time rather than hand-built once and left to rot.

It runs the shipped all-in-one image (nginx + backend under supervisord, the same artifact users deploy), so a tester attacks what actually ships.

Throwaway data only. The generated database, its fixed JWT_SECRET_KEY, and the shared passwords are test fixtures, and registration is left open. Never point this compose file at a real database, and don’t expose the instance on an untrusted network — bind it to localhost or a private interface and reach it over SSH / VPN.

Bring it up

docker compose -f docker-compose.pentest.yml up -d --build --wait

Then read the seeded credentials out of the data volume:

docker compose -f docker-compose.pentest.yml exec mycorrhizal cat /app/data/credentials.json

The app is on http://localhost:7300. Log in as the user whose is_admin is true (the shared password is in the file, PentestPassword123! by default).

Wipe everything and start from a clean dataset:

docker compose -f docker-compose.pentest.yml down -v

Running it next to a real instance

On a host that already serves the real app on 7300, publish the pen-test stack on another port and tell it the origin testers will use. Put these in a .env file next to the compose file (or export them):

PENTEST_PORT=7400
PENTEST_BASE_URL=http://your-server.internal:7400

PENTEST_BASE_URL only matters for the browser SPA (it sets FRONTEND_URL, so CORS matches); curl / Burp / ZAP against the API don’t need it. The pen-test stack has its own container name (mycorrhizal-pentest) and its own pentest-* volumes, so it never touches the real deployment’s data.

Choosing how much data

The PENTEST_PROFILE environment variable selects the dataset shape:

Profile Users Contacts / user For
demo (default) 1 27 screenshots & docs — realistic display names, clean group labels, one of every data shape
smoke 2 162 a small cross-tenant target set — two isolated users to probe for IDOR
typical 1 918 a realistic long-lived personal CRM
large 3 ~15k a heavy multi-account instance
PENTEST_PROFILE=smoke docker compose -f docker-compose.pentest.yml up -d --build --wait

The user count, per-user volume and relationship-graph shape of each profile are catalogued in scale-profiles.md.

What’s in the data

The dataset is the canonical pathological fixture (TEST-02, issue #430) — testdata/canonical-fixture/manifest.json, described in its README. Every profile carries, per user:

  • ~27 contacts with full RFC 9553 cards: international names and scripts, multi-valued emails/phones/addresses, pronouns, personal info, keywords, localizations, single-name edge cases;
  • notes, activities, gifts, life events, households, circles, tags, preferences, external identities, attachments;
  • a relationship graph spanning normal, private and secret sensitivities (the credentials file lists which are present) — the cross-cutting rule a leak test targets;
  • one soft-deleted contact plus a contact that re-uses its vcard_uid, and duplicate-detection pairs (the Review duplicates surface is populated).

On top of the fixture loader, pentestseed does the one thing it deliberately does not: rewrites every seeded user into a working HTTP login (bcrypt-hashed shared password) and promotes one to admin.

How it stays current

pentestseed does not author its own data. It generates from the same drift-gated canonical manifest that every migration / import-export / perf suite consumes, via backend/internal/canonicalfixture and backend/internal/largedata. A new entity or field is added to the manifest as part of its own feature work (the manifest’s drift tests fail until it is), and this environment picks it up for free on the next run — there is no second dataset to maintain.

Running the seeder directly (local, with a Go toolchain)

The compose seed service just runs this in the Dockerfile’s Go build stage. From a checkout you can run it straight against a file:

cd backend && go run ./cmd/pentestseed seed --db /tmp/pentest.db --profile demo

Flags: --profile, --password, --user-prefix, --admin-index, --manifest, --manifest-out, --base-url (see backend/cmd/pentestseed/main.go). Point a dev server at the result with SQLITE_DB_PATH=/tmp/pentest.db.

Recurring engagement cadence

This environment exists to be re-attacked, not built once. The #860 engagement (2026-09, two credentialed agents) is the baseline; as of issue #946 a repeat #860-style engagement — or a scoped target where a full one is disproportionate — runs at least once per minor release, and never more than six months apart. Scope it to the surfaces added since the last engagement, and file findings as issues exactly as #860 did (#861–#877).

The cadence is a human obligation, but the environment is not left to memory. .github/workflows/pentest-harness.yml boots this compose file from the shipped all-in-one image on a weekly schedule (and on a PR that touches the harness), asserts the seeded admin’s generated credentials actually log in, then runs backend/cmd/bolacheck/main.go’s cross-account BOLA/IDOR sweep against the two-user smoke profile. TestPentestComposeIsExercisedOnSchedule (backend/cmd/pentestseed/compose_test.go) fails if no scheduled workflow references this compose file, so the harness cannot drift back to being exercised by nothing — the state issue #946 found it in.

   
Last engagement #860 — two-agent credentialed assessment, 2026-09 (findings #861–#877)
Cadence at least once per minor release, at most 6 months apart (#946)
Harness drift check .github/workflows/pentest-harness.yml, weekly (Mondays 04:00 UTC)

Relationship to the DAST job

.github/workflows/zap-dast.yml boots the same image and bootstraps a single throwaway user for an automated ZAP scan. This environment is the richer, manual counterpart: many users, many permissions, and a full spread of data shapes and sensitivities for hands-on testing.


Back to top

Mycorrhizal CRM - Self-hosted personal contact management

This site uses Just the Docs, a documentation theme for Jekyll.