This file is an opinionated, practical playbook for building working CyLab Security Academy challenges with cmgr. It is based on:
- Building with CMGR - Quick Start Guide
- common-errors/duplicate-network.md
- Every tutorial/challenge under example-problems/
Use this as a hands-on checklist. If you follow it top-to-bottom, you should be able to stand up a working challenge quickly and avoid common cmgr pitfalls.
If you are new to cmgr, do only this first:
- Pick the closest template from
example-problems/and copy it into yourCMGR_DIRtree. - Edit
problem.mdmetadata (Namespace,ID,Category,Templatable,MaxUsers) and challenge text. - Ensure your Docker build writes
/challenge/metadata.json. - If players need files, package them into
/challenge/artifacts.tar.gzand link withurl_for(...). - If you expose a service port, include
EXPOSE <port>and# PUBLISH <port> AS <name>, then reference it inproblem.md. - Build once with Docker (faster errors), then test with cmgr.
- Confirm wrong flag fails, correct flag passes, and intended solve path works.
Fast command sequence:
cp -r example-problems/sanity-static-flag "$CMGR_DIR/my-first-challenge"
cd "$CMGR_DIR/my-first-challenge"
# Quick sanity build (cmgr supplies FLAG automatically; Docker does not)
docker build . --build-arg FLAG='academy{deadbeef}'
# Register and run in cmgr
cmgr update
cmgr list
cmgr playtest academy/examples/my-first-challenge
# Optional solver validation (expects solver/solve.py writing ./flag)
cmgr test academy/examples/my-first-challenge
If cmgr update does not discover your challenge, it is almost always outside
CMGR_DIR or has invalid problem.md metadata.
If playtest errors with duplicate cmgr networks, stop stale containers, prune
unused Docker networks, then run cmgr update again.
- cmgr is a Docker wrapper with challenge-specific metadata, templating, and orchestration behavior.
- Containers are still normal Docker containers.
- cmgr discovers challenges from a single configured challenge tree
(
CMGR_DIR). If your challenge is outside that tree, cmgr will not find it. - You reference challenges by namespace and ID (for example,
academy/examples/sanity-download), not by filesystem path.
- Install Docker and cmgr.
- Configure these env vars (commonly in shell profile):
CMGR_DBCMGR_DIRCMGR_ARTIFACTS_DIR
- Keep all challenge folders under
CMGR_DIR.
Quick health checks:
cmgr updateshould discover your challenge.cmgr listshould shownamespace/id.
If discovery fails, the challenge is usually in the wrong directory tree.
Do this before writing anything: choose the challenge type and start from the nearest example.
- Static downloadable artifact:
example-problems/sanity-static-flag - Dynamic downloadable artifact (templated flag injection):
example-problems/forensics-grep - Complex forensics artifact with anti-cheese considerations:
example-problems/forensics-disk - Web service challenge:
example-problems/web-css - Single-container netcat/socat service:
example-problems/reversing-python - Multi-container service with builder/runtime separation:
example-problems/general-ssh - Interactive black-box service pattern:
example-problems/perceptron-gate
Every cmgr challenge must have:
Dockerfileproblem.md/challenge/metadata.jsongenerated during image build
Most challenges should also have:
/challenge/artifacts.tar.gzif users need downloadable filessolver/solve.py(required in CyLab Security Academy authoring workflow)solver/requirements.txtif solver uses non-stdlib packages
Two high-frequency wiring rules from topics/:
- Downloadable artifact requires both:
- Add file to
/challenge/artifacts.tar.gz. - Link it in
problem.mdwithurl_for(...).
- Add file to
- Extra instance data requires both:
- Add key/value to
/challenge/metadata.json. - Reference it in
problem.mdwithlookup("key").
- Add key/value to
The first metadata block should include at least:
Namespace: academy/...ID: ...Type: customCategory: ...Points: 1(initially)Templatable: yes|noMaxUsers: 0|1
Operational guidance:
- Put artifact download links in Description (Details may be hidden until launch for on-demand challenges).
- Use Details for instance-specific connection info.
- Include Solution Overview and hints.
- Keep Challenge Options resource limits minimal but sufficient.
Useful template functions used in examples:
{{url_for("file", "link text")}}for artifacts{{link_as('/', 'text')}}for web links (single published port){{link_as('/', 'text', 'port_name')}}for named published ports{{server}},{{port}}for default network endpoint{{server("name")}},{{port("name")}}for named endpoints{{lookup("key")}}for values stored inmetadata.json
Authoring note:
- Ensure Markdown renders correctly in
problem.md(headings, lists, code blocks, and template snippets).
- Create
/challengewith restrictive permissions. - Copy files.
- Package artifacts to
/challenge/artifacts.tar.gz. - Write
/challenge/metadata.jsonwith accepted flag.
- Add
ARG FLAGin the challenge stage. - Derive your custom flag string from cmgr-provided
FLAGrandomness. - Inject flag into artifact at build time.
- Save the final accepted flag in
/challenge/metadata.json.
Why this matters: templated flags are critical for anti-cheating and instance regeneration.
EXPOSE <port>in Dockerfile.- Add cmgr publish tag exactly as an uppercase comment:
# PUBLISH <port> AS <name>
- Reference that endpoint in
problem.mdvialink_as.
- Install service dependencies (for example
socat,python3). - Provide
start.shthat launches listener:socat tcp-listen:<port>,reuseaddr,fork SYSTEM:"..."
EXPOSEand optionally# PUBLISHport.- Ensure service exits promptly on success, and does not hang forever waiting for input.
- Use a
builderstage to generate secret material and metadata. - Use a separate runtime stage for user interaction.
- Copy only what runtime needs via
COPY --from=builder .... - Use
# LAUNCH <stage_name>to select which stage stays running.
This separation reduces accidental metadata exposure on exploitable services.
- cmgr supplies build args like
FLAG(and oftenSEED) automatically. - If you run
docker buildmanually, you must pass required args yourself:docker build . --build-arg FLAG='academy{deadbeef}'- Add
--build-arg SEED='1234'if your build logic needs seed.
Practical recommendation from examples:
- Validate
FLAGandSEEDexplicitly in Python setup scripts. - Fail fast with clear errors if missing or malformed.
Preferred build loop:
- Build and debug with Docker first (better low-level error output).
- Then run with cmgr.
Typical sequence:
cmgr updatecmgr listcmgr playtest <namespace/id>cmgr test <namespace/id>(or your team standard test command)
For templating verification, build multiple seeds and compare generated flags
using cmgr system dump tooling (as shown in forensics-grep walkthrough).
- Place solver at
solver/solve.py. - Solver must write recovered answer to
./flag. - If solver uses extra packages (for example
pwntools), declare them insolver/requirements.txt. - Solver container receives challenge artifacts in its working directory.
- Solver can reach challenge containers by Docker stage name as DNS hostnames.
Important nuance from topics/cmgr-solver.md:
cmgr testoften prints nothing on success.metadata.jsonin solver context is for instance data (for example SSH password); do not assume it contains the flag.- A solver may use an implementation shortcut; it only proves solvability, not player-friendliness. Keep separate human playtesting.
- Problem not found after
cmgr update: challenge likely not underCMGR_DIRtree. - Playtest target is
namespace/id, not folder path.
PUBLISH ... AS ...is a cmgr comment tag, not Docker syntax.- Keep it commented and uppercase (
PUBLISH+AS). - If you name a published port, use the same name in
problem.mdtemplates.
- No
/challenge/metadata.json=> challenge is invalid. - If
problem.mdreferences downloadable files, ensure they are included in/challenge/artifacts.tar.gz.
Symptom:
- cmgr errors that
network with name cmgr-<n> already exists.
Cause:
- Docker restored old networks/containers while cmgr state differs.
Fix (careful if you run other Docker projects):
- Stop stale containers.
- Prune stale networks.
- Run
cmgr updateagain.
- Verify paths in
RUNcommands against actualWORKDIRandCOPYtargets. - Classic failure from examples: executing setup script from a path where it was never copied.
- Interactive services should not block forever.
- Exit after flag output.
- Set minimal challenge options in
problem.mdand increase only if required. - Handle EOF safely in interactive programs (for example
scanf/stdin loops). - Prefer not writing user-interaction temp files to disk; if used, clean them up.
- Test whether raw artifacts leak flags via
strings/grep. - If needed, obfuscate or encode flag placement to preserve intended learning objective.
- Do not rely on outbound Internet access at runtime.
- Vendor external JS/CSS/assets into challenge source.
- Do not depend on third-party APIs/services remaining online.
- Check for unintended directory traversal reads to flag files.
- Avoid red-herring “correct-looking” flags that create support incidents.
- If challenge output must display non-standard format, clearly document the
accepted wrapped flag format in
problem.md.
- Avoid guess-based solves; difficulty should come from skill application.
- Prefer open-source tools and workflows that are feasible in webshell-style environments.
- Keep client-side compute requirements reasonable for constrained runners.
- If challenge has mutable state, mark and deploy as on-demand (
MaxUsers: 1). - For high-compute or brute-force-prone tasks, add rate limiting / controls.
- Challenge builds cleanly in Docker with explicit build args.
- Challenge is discovered by
cmgr updatefromCMGR_DIRtree. cmgr playtest <namespace/id>launches successfully.- Incorrect flag is rejected.
- Correct flag is accepted.
- Intended path to solve works from player-visible materials.
problem.mdmetadata, details, hints, and Solution Overview are complete.- Port tags (
PUBLISH) and problem templates (link_as,port,server) are aligned. - Solver script works and writes recovered flag to
./flag. - If templated, verify multiple generated instances produce different flags and remain solvable.
- Artifact links in Description/Details match files included in
artifacts.tar.gz. - Any instance-specific values shown to players come from keys present in
metadata.json. - Web challenges function without outbound Internet and with locally bundled assets.
- Interactive services exit cleanly after success and on inactivity/EOF.
When creating a new challenge quickly:
- Copy the closest archetype folder from
example-problems/. - Rename folder and update
problem.md(Namespace,ID, text). - Replace challenge logic/artifacts.
- Ensure Dockerfile generates
/challenge/metadata.json. - If web/service challenge, ensure
EXPOSE+ correct# PUBLISHtag and matchingproblem.mdreferences. - Run Docker build manually with
--build-arg FLAG=...first. - Move/copy challenge into
CMGR_DIRtree. - Run
cmgr update, thencmgr playtest namespace/id. - Validate wrong/correct flag handling.
- Finalize a solver script and clean problem wording.
If you follow this recipe, you avoid nearly all first-time cmgr failures seen in the training examples.