Remote build / deploy / troubleshooting manual. Condensed agent guide: AGENTS.md;
authoritative challenge list: CHALLENGE.md. Per-challenge detail lives in each
<challenge>/README.md.
| Item | Value |
|---|---|
| Remote build host | r3kapig@ops.ctf2026.r3kapig.com (hostname r3ctf-ops) |
| Architecture | x86_64, 4 cores, 15Gi RAM, 80G disk, Docker 29.6 + buildx |
| Remote build dir | ~/r3ctf-build/<challenge>/ |
| Registry | registry.ctf2026.r3kapig.com/r3ctf_2026_6a511700/<challenge>:latest |
| Registry push | Push directly from the ops host, no docker login needed (IP allowlist) |
| Git repo | https://github.com/r3kapig/r3ctf-2026.git, branch master |
| Git auth | gh auth setup-git (github.com uses the gh token, non-interactive) |
| SSH | key-based (BatchMode=yes works) |
The remote host has no
rsync— ship files withtar | ssh tar(seeAGENTS.md).
| Image | Purpose |
|---|---|
registry.ctf2026.r3kapig.com/r3ctf_2026_6a511700/sleepy:latest |
Helper image for exposing multiple ports on a pod / service (port forwarding / multi-port listening). Source: docker.io/reverier/sleepy:latest. |
Use the two commands in AGENTS.md ("Key commands"): clean-tar the context over ssh
into ~/r3ctf-build/<name>/, then docker build + docker push on the ops host.
- If the Dockerfile isn't at the context root (e.g. r3map / polys use
deploy/Dockerfile):docker build -f deploy/Dockerfile -t <reg>/<name>:latest <context>. - Warnings like
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr...'are harmless (GNU tar dropping macOS xattrs). What actually breaks builds is._*AppleDouble files — the tar command already excludes them; after shipping you can confirm withfind ~/r3ctf-build/<name> -name '._*' | wc -l(expect 0). See §4.1.
ssh r3kapig@ops.ctf2026.r3kapig.com \
'docker buildx imagetools inspect registry.ctf2026.r3kapig.com/r3ctf_2026_6a511700/<name>:latest'- The build host has 15Gi RAM — do not run multiple heavy builds in parallel (this has OOM-rebooted the machine before).
- Heavy builds (SEAL / Nix / PHP source compiles) run serially; for SEAL, change
cmake --build build -jto-j4to cap parallelism. - Lightweight challenges (Python / small C++) can build 2–3 in parallel.
- Understand the challenge: category, flag injection method, whether it needs KVM / special privileges, ports.
- Lay out the directory: per
AGENTS.md, place it at repo root as<challenge>/withREADME.md+infra.sh, container files indeploy/, player handout inattachment/. - Check the flag: sweep the whole dir with
grep -rEn 'flag\{|r3ctf\{|R3CTF\{'and confirm flag placement matches expectations — dynamic challenges use$FLAGinjection (image bakes only a placeholder); static flags ship with the attachment / README. - Remote build + push: see §2.
- Register in
CHALLENGE.md: name / image / CPU / memory / special needs. - Commit:
git add <files> && git commit -m "..." && git push origin master.
if [ -n "$FLAG" ]; then
INSERT_FLAG="$FLAG"
export FLAG=no_FLAG
FLAG=no_FLAG
else
INSERT_FLAG="flag{TEST_Dynamic_FLAG}"
fi
# then write INSERT_FLAG to /flag or the DB or argv, and start the service- Symptom:
docker buildfails with e.g.._message.cpp: error: 'Mac' does not name a type. - Cause: macOS quarantine / provenance xattrs, packed by bsdtar, get restored by
GNU tar on Linux as real
._<file>files, whichcmake file(GLOB ...)then compiles as sources. - Fix:
- Locally
xattr -cr <dir>to clear xattrs; - Ship with
COPYFILE_DISABLE=1 tar --exclude='._*' ...; - Add a
.dockerignoreto the challenge:**/.git/**/.DS_Store/**/._*; - If the remote dir is already polluted:
find <dir> -name '._*' -delete.
- Locally
- Symptom: SSH suddenly
banner exchange timeout; after recoveryuptimeshows 1 minute (the machine rebooted). - Cause: three concurrent builds — SEAL (unbounded
-j), p1groxy, netshare — exhausted the 15Gi RAM. - Fix: run heavy builds serially, cap SEAL at
-j4; the machine has been upgraded, but still don't abuse concurrency.
- Background: early on, HEuristic's
docker-compose.ymlhardcoded a flag and P1gROXY's Dockerfile didprintf > /flag.txt, baking the flag into the image — every team got the same flag / the image carried the flag. - Rule: dynamic challenges always inject
$FLAGat runtime (entrypoint writes/flag.txtthen scrubs the env); the image bakes only a placeholder. Static flags live in the repo with the attachment / README.
- whisper does not push images; it runs only on the KVM host:
cd deploy/deploy && ./run.sh <public-ip> [N](N = concurrent device cap). Currently deployed onvm.ctf2026.r3kapig.com(8 victim devices). - Building victim images: pulling the debian base from docker.io gets reset by the
CloudFront CDN in mainland China. Pull the base via the daocloud mirror and tag it
as the official name:
docker pull docker.m.daocloud.io/library/debian:bookworm-slim && docker tag docker.m.daocloud.io/library/debian:bookworm-slim debian:bookworm-slim(same for nginx). - Its 481M
whisper-local-stack.7zis hosted on a cloud drive (see.gitignore+ the placeholder.txt).
- r3map's compose uses
FLAG: "${FLAG:?FLAG is required}", so a bare localdocker compose configfails. This is expected (FLAG unset), not a config error; verify withFLAG=test docker compose config.
Hide the judge and give each team a pod as the sole entry point:
player ──► auth pod ──X-Admin-Token + team_id──► judge (internal)
player ──────────────────────────────────────────► backend (public, in APK)
- judge changes (done):
team_flags.py: per-team flags are file-backed by/data/team_flags.json(every read/write hits the file, atomicos.replace, no in-memory cache).POST /admin/flags(admin auth): the auth pod pushes the flag here./lease/release/status: admin auth +team_idfrom body/query (team tokens +teams.jsonremoved; the pool indexes directly byteam_id).pool._do_assign: requires a pushed flag (team_flags.get(team_id)), otherwise the lease is refused (flag_stego.make_flagremoved — the judge no longer produces flags).worker._flag_accepted: compares directly against the pushed flag (flag-sharing / stegano validation is left to the platform checker).
- auth pod (
whisper/auth-pod/):- Environment:
TEAM_ID/WHISPER_JUDGE_URL(internal) /WHISPER_BACKEND_URL(public) /WHISPER_ADMIN_TOKEN/ optionalFLAG(defaults to theR3CTF{TEST_FLGA}placeholder). - On startup it pushes the flag; at runtime it proxies
lease / release / status / download/whisper.apkand serves a player dashboard (/).
- Environment:
- Deployment: judge + backend + victim pool are started with
deploy/deploy/run.sh(judge not exposed); the platform (ret.sh / k8s-on-demand) starts one auth-pod per team and gives players the pod URL. - The backend must be publicly exposed (APK connects directly); the judge must be internal (pods only).
Details: whisper/README.md and whisper/auth-pod/README.md.
reference/creating-ctf-docker/SKILL.md— source of the conventions this repo uses (flag injection, xinetd/socat/direct-listen selection, per-category skeletons). Gitignored.reference/r3ctf-2025/— 40 real 2025 challenges for local reference (gitignored).CHALLENGE.md— authoritative list of all images / resources / special deployment needs.