From 2da6305626018e2870b3bb898d61870588f1826b Mon Sep 17 00:00:00 2001 From: Cyrene Date: Mon, 28 Sep 2026 19:13:51 +0800 Subject: [PATCH] docs: full operator manual in README (setup, spec, API, troubleshooting) Replaces the 3-line upstream stub with a manual that documents the platform as it actually runs. Every claim is derived from the live code and registry rather than from memory. Challenge spec: - 28-challenge tables (6 XVIII / 10 XVI / 12 XVII, 16 active) generated from teams/challenge_registry.json, with per-challenge org_port, chall/ssh offsets, and the real team-1 runtime ports read from state.json. - Port formula corrected to the real one: port = 30000 + idx*1000 + chall_offset. org_port is the native graveyard port and is NOT used for runtime allocation, so two challenges sharing an org_port (carbeat offset 1 vs anti-alchemy offset 30) never collide. - Per-challenge ssh_user documented: only the 6 native XVIII images provision ctfuser; all imported XVI/XVII images chpasswd root, so hardcoding ctfuser breaks 10 of the 16 active challenges. - Scoring: 100 per flag awarded to the ATTACKER (first solve only), +50 SLA bonus at most once per 5-minute window, runtime threshold documented as len(enabled_challenges()) rather than the hardcoded constant 6. Setup and operations: - Setup from clone: required /opt path, Docker, venv, panel credentials, both systemd units verbatim, team creation, verification step. - Full HTTP API split into public / admin / team, including why challenge toggle and bulk team delete are async jobs. - Troubleshooting and operational traps as declarative rules: the bare domain is the receiver and not the panel, EOL base images, UFW default-deny silently blackholing ports, the mandatory compose -p teamN project name, and why docker image prune -af destroys services-* images that are in use. - Image sizes measured from the host (189MB-903MB, ~8GB for 16 active) instead of the incorrect "~3GB per challenge" figure. - Topology section: PixiJS v8, on-demand rendering, and the parent-to-child drag hierarchy derived from the edge list. The flag example is redacted to a placeholder. No live credential, token, or flag is committed. README.md is the only file touched. --- README.md | 650 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 636 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ab32b0b..84b983c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,640 @@ -# Gemastik XVIII Cybersecurity Final Round - Attack Defense Repository +# Attack Defense Platform -## challenges +Platform attack-defense (serang–serang) untuk GEMASTIK Final Round, menyatukan +**28 challenge** dari tiga set (**GEMASTIK XVIII**, **XVI**, **XVII**) di bawah +satu panel admin, satu flag store, satu SLA checker, dan satu skorboard. -| challenges | author | category | -| ---------- | ----------- | -------- | -| blogpost | keii | web | -| cdn | keii | foren | -| phew | itoid | crypto | -| sheesh | itoid | crypto | -| carbeat | rui | pwn | -| warmup | hanz0 | warmup | +Multi-team: N tim, masing-masing memiliki copy semua challenge + flag sendiri, +dengan receiver terisolasi per tim, checker SLA otomatis, dan visualisasi +topologi serangan real-time. -## how-to-run +``` + Browser (admin/team) + | HTTP + WebSocket (proxy server-side) + v + Panel :18081 (FastAPI) ---- systemd: gemastik-panel + | + +--> Receiver global :18080 ---- systemd: gemastik-receiver + +--> Receiver tim N :31080+1000*(N-1) ---- systemd: gemastik-receiver-teamN + | (menjalankan checker SLA untuk tim N) + +--> N x _container_teamN ---- docker compose + | + +--> flags + leaderboard + points (teams/leaderboard.json, teams/points.json) +``` -```` -sudo python3 starter.py -```` +--- + +## Daftar Isi + +- [Arsitektur](#arsitektur) +- [Spesifikasi Challenge](#spesifikasi-challenge) +- [Spesifikasi Port](#spesifikasi-port) +- [Skor dan Penilaian](#skor-dan-penilaian) +- [Kebutuhan Sistem](#kebutuhan-sistem) +- [Setup](#setup) +- [Operasional Harian](#operasional-harian) +- [Topologi](#topologi) +- [HTTP API](#http-api) +- [Troubleshooting](#troubleshooting) +- [Jebakan Operasional](#jebakan-operasional) +- [Struktur Direktori](#struktur-direktori) + +--- + +## Arsitektur + +Tiga proses inti, semuanya dikelola systemd: + +| Proses | Port | Unit systemd | Peran | +|---|---|---|---| +| Panel admin | **18081** | `gemastik-panel` | Web UI admin + seluruh API | +| Receiver global | **18080** | `gemastik-receiver` | Flag store untuk mode single-node | +| Receiver tim N | **31080 + 1000×(N−1)** | `gemastik-receiver-teamN` | Checker SLA tim N | + +**Mengapa receiver per tim harus unit terpisah.** Kalau receiver dijalankan +sebagai child process dari panel, `systemctl restart gemastik-panel` akan +membunuh seluruh cgroup — termasuk semua receiver — dan SLA semua tim ikut +turun ke 0. Unit terpisah membuat restart panel tidak menyentuh receiver. +Generatornya: `panel/gen_receiver_services.py`. + +**Kredensial tidak pernah masuk browser.** Panel melakukan proxy ke receiver +secara server-side, sehingga password admin hanya ada di `panel/.env` pada host. + +**Sumber data tunggal.** `teams/challenge_registry.json` dibaca oleh panel, +generator compose, dan generator receiver. Menambah challenge = menambah satu +entri di registry, bukan menyunting tiga tempat. + +### Alur satu flag + +``` +tim penyerang submit flag + -> panel POST /api/flag/submit + -> baca flag tim target dari receiver + -> cocok? + ya -> catat di leaderboard + skor untuk PENYERANG + tidak -> tolak +``` + +Flag di-mint per (tim, challenge), bukan satu flag global. + +--- + +## Spesifikasi Challenge + +**28 challenge terdaftar, 16 aktif secara default.** + +Kolom `Port team 1` / `SSH team 1` diisi `-` untuk challenge nonaktif karena +port-nya baru dialokasikan saat challenge diaktifkan. + +### GEMASTIK XVIII — 6 challenge, 6 aktif + +User SSH: `ctfuser`. + +| # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status | +|---|---|---|---|---|---|---|---| +| 1 | `blogpost` | web | 10000 | 0/22 | 31000 | 31022 | aktif | +| 2 | `carbeat` | pwn | 11000 | 1/23 | 31001 | 31023 | aktif | +| 3 | `cdn` | web | 12000 | 2/24 | 31002 | 31024 | aktif | +| 4 | `phew` | crypto | 13000 | 3/25 | 31003 | 31025 | aktif | +| 5 | `sheesh` | crypto | 14000 | 4/26 | 31004 | 31026 | aktif | +| 6 | `warmup` | warmup | 15000 | 5/27 | 31005 | 31027 | aktif | + +### GEMASTIK XVI — 10 challenge, 3 aktif + +User SSH: `root`. + +| # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status | +|---|---|---|---|---|---|---|---| +| 7 | `art` | web | 10000 | 10/110 | 31010 | 31110 | aktif | +| 8 | `xl` | web | 11000 | 11/111 | 31011 | 31111 | aktif | +| 9 | `gemas-notes` | web | 12000 | 12/112 | - | - | nonaktif | +| 10 | `pasta` | web | 13000 | 13/113 | - | - | nonaktif | +| 11 | `burvesigner` | crypto | 14000 | 14/114 | - | - | nonaktif | +| 12 | `hirnfick` | pwn | 15000 | 15/115 | - | - | nonaktif | +| 13 | `gemas-fetcher` | web | 16000 | 16/116 | - | - | nonaktif | +| 14 | `s3` | web | 20000 | 20/120 | 31020 | 31120 | aktif | +| 15 | `crawlback` | web | 21000 | 21/121 | - | - | nonaktif | +| 16 | `back-to-basic` | warmup | 22000 | 22/122 | - | - | nonaktif | + +### GEMASTIK XVII — 12 challenge, 7 aktif + +User SSH: `root`. + +| # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status | +|---|---|---|---|---|---|---|---| +| 17 | `anti-alchemy` | web | 11000 | 30/130 | 31030 | 31130 | aktif | +| 18 | `asmr` | pwn | 15000 | 31/131 | - | - | nonaktif | +| 19 | `bit-canvas` | pwn | 20000 | 32/132 | 31032 | 31132 | aktif | +| 20 | `fjb` | web-pwn | 17000 | 33/133 | - | - | nonaktif | +| 21 | `gift-card` | crypto | 21000 | 34/134 | 31034 | 31134 | aktif | +| 22 | `gift-voucher` | crypto | 16000 | 35/135 | 31035 | 31135 | aktif | +| 23 | `gleam-drive` | web-crypto | 12000 | 36/136 | 31036 | 31136 | aktif | +| 24 | `go-green` | rev | 20000 | 37/137 | - | - | nonaktif | +| 25 | `kode-viewer` | web | 10000 | 38/138 | - | - | nonaktif | +| 26 | `more-less` | web | 22000 | 39/139 | 31039 | 31139 | aktif | +| 27 | `tempest-poc` | web | 14080 | 40/140 | - | - | nonaktif | +| 28 | `ticketer` | crypto | 14000 | 41/141 | 31041 | 31141 | aktif | + +Kategori: 13 web, 6 crypto, 4 pwn, 2 warmup, dan masing-masing satu +web-pwn, web-crypto, rev. `gleam-drive` dilayani lewat HTTPS (field `scheme` +di registry), 27 challenge lainnya HTTP. + +### Format flag + +``` +GEMASTIK18{TEAM__<12 hex>} + +contoh: GEMASTIK18{TEAM1_BLOGPOST_<12 hex acak>} +``` + +### User SSH per challenge + +Hanya 6 challenge native GEMASTIK XVIII yang membuat user `ctfuser`. Semua +challenge impor XVI/XVII menjalankan `echo root:${PASSWORD} | chpasswd` di +Dockerfile, sehingga login sebagai `ctfuser` ditolak walaupun password benar. + +Field `ssh_user` di `teams/challenge_registry.json` yang menentukan ini, dibaca +`panel/teams.py` saat `set_ssh_passwords()`. Men-hardcode `ctfuser` membuat 10 +dari 16 challenge gagal login padahal `state.json` terlihat benar. + +--- + +## Spesifikasi Port + +Setiap tim mendapat blok port sendiri, dengan basis 30000 dan langkah 1000: + +``` +port_challenge(tim i, challenge c) = 30000 + 1000 x i + chall_offset(c) +port_ssh(tim i, challenge c) = 30000 + 1000 x i + ssh_offset(c) +port_receiver(tim i) = 30000 + 1000 x i + 80 +``` + +`chall_offset` dan `ssh_offset` dibaca dari `teams/challenge_registry.json` +(`panel/teams.py`, `create_team()`). Field `org_port` di registry adalah port +native challenge di graveyard asalnya dan **tidak dipakai** untuk menghitung +port runtime. + +Enam challenge native GEMASTIK XVIII memakai offset challenge 0–5 dan SSH +22–27, sehingga untuk team 1 berada di 31000–31005 dan 31022–31027: + +| Tim | Port challenge | Port SSH | Receiver | +|---|---|---|---| +| 1 | 31000–31005 | 31022–31027 | 31080 | +| 2 | 32000–32005 | 32022–32027 | 32080 | +| 3 | 33000–33005 | 33022–33027 | 33080 | +| 4 | 34000–34005 | 34022–34027 | 34080 | + +Challenge impor memakai offset sendiri, jadi portnya tidak selalu berakhiran +`0000`–`0005`. Angka nyata team 1: `art` 31010/31110, `xl` 31011/31111, +`s3` 31020/31120, `anti-alchemy` 31030/31130, `bit-canvas` 31032/31132, +`gift-card` 31034/31134, `gift-voucher` 31035/31135, +`gleam-drive` 31036/31136, `more-less` 31039/31139, `ticketer` 31041/31141. + +Dua challenge dengan `org_port` sama tidak bentrok, karena yang dipakai adalah +`chall_offset`. `anti-alchemy` (XVII, offset 30) dan `carbeat` (XVIII, +offset 1) sama-sama punya `org_port` 11000, tetapi memakai port 31030 dan +31001. + +--- + +## Skor dan Penilaian + +```python +POINTS_PER_FLAG = 100 # ke tim PENYERANG, hanya solve pertama +SLA_BONUS_POINTS = 50 # bonus bila semua challenge aktif UP +SLA_BONUS_MIN_ALIVE = 6 # konstanta; threshold runtime = len(enabled_challenges()) +``` + +**Attack points.** Submit flag benar milik tim lain memberi +100 ke tim +penyerang. Duplikat (flag + penyerang + target sama) tidak dihitung dua kali. + +**SLA bonus.** Diberi bila semua challenge yang aktif UP, maksimal sekali per +jendela 5 menit. Threshold runtime bukan angka tetap 6 melainkan +`len(enabled_challenges())` — mengaktifkan challenge ke-17 membuat syaratnya +"semua 17 UP". + +**Badge.** Juara, runner-up, dan tempat ketiga dihitung dari total poin. + +--- + +## Kebutuhan Sistem + +Host reference: Ubuntu 24.04 (noble), x86_64, Docker + Compose v2, systemd. + +| Sumber daya | Minimum | Recommended | +|---|---|---| +| CPU | 2 vCPU | 4 vCPU | +| RAM | 8 GB | 16 GB | +| Disk | 60 GB | 100 GB+ | +| Docker | Compose v2 (`docker compose`) | — | + +Compose v1 (`docker-compose`) tidak didukung — seluruh generator memakai +`docker compose`. + +Disk adalah pembatas utama. Tiap challenge yang aktif menjadi satu image +`services-`; ukurannya bervariasi dari ~190 MB (`gift-card`) sampai ~900 MB +(`warmup`), dan pada host ini 16 image aktif menempati sekitar 8 GB. Membangun +banyak challenge sekaligus akan mengisi disk sebelum selesai — implementasi +terbaik adalah membangun challenge secara berurutan dan menjalankan +`docker builder prune -af` di antaranya. + +`phew` menjalankan generator kunci Paillier saat start (±12 detik) sehingga +butuh RAM ekstra dan checker-nya memakai `_CRYPTO_TIMEOUT`, bukan timeout prompt +bawaan 5 detik. + +Prasyarat jaringan: setiap compose template sudah memuat +`extra_hosts: host.docker.internal:host-gateway`, dan UFW host harus +mengizinkan port challenge (lihat [Jebakan Operasional](#jebakan-operasional)). + +Dependency checker ada di `receiver/requirements.txt`: fastapi, uvicorn, +pwntools, pyelftools, pycryptodome, fastecdsa, ecdsa, Pillow, pandas, openpyxl, +PyPDF2. + +--- + +## Setup + +### 1. Clone + +```bash +git clone attack-defense-platform +cd attack-defense-platform +``` + +Semua path di dalam kode memakai `/opt/gemastik18-final` sebagai `BASE`, jadi +letakkan repo di sana: + +```bash +sudo mkdir -p /opt +sudo mv attack-defense-platform /opt/gemastik18-final +cd /opt/gemastik18-final +``` + +### 2. Docker + +```bash +sudo bash node.sh +``` + +`node.sh` memasang Docker CE dari repo resmi lalu menjalankan `starter.py`. +Instalasi manual: + +```bash +sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ + docker-buildx-plugin docker-compose-plugin +``` + +### 3. Kredensial panel + +```bash +cat > panel/.env <<'EOF' +PANEL_ADMIN_USER=admin +PANEL_ADMIN_PASS=ganti-dengan-password-kuat +EOF +chmod 600 panel/.env +``` + +`panel/.env` sudah masuk `.gitignore` dan tidak pernah ter-commit. + +### 4. Python environment + +```bash +cd /opt/gemastik18-final/receiver +sudo python3 -m venv .venv +sudo .venv/bin/pip install -r requirements.txt +``` + +### 5. Unit systemd + +Panel (:18081): + +```ini +# /etc/systemd/system/gemastik-panel.service +[Unit] +Description=Gemastik A/D Panel (web UI for receiver) +After=gemastik-receiver.service network-online.target +Wants=gemastik-receiver.service + +[Service] +Type=simple +WorkingDirectory=/opt/gemastik18-final/panel +EnvironmentFile=-/opt/gemastik18-final/panel/.env +ExecStart=/opt/gemastik18-final/receiver/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 18081 +Restart=always +RestartSec=5 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target +``` + +Receiver global (:18080): + +```ini +# /etc/systemd/system/gemastik-receiver.service +[Unit] +Description=Gemastik18 Receiver Service (CTF flag/control API) +After=docker.service network-online.target +Wants=docker.service +Requires=docker.service + +[Service] +Type=simple +WorkingDirectory=/opt/gemastik18-final/receiver +ExecStart=/opt/gemastik18-final/receiver/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 18080 +Restart=always +RestartSec=5 +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now gemastik-receiver gemastik-panel +systemctl is-active gemastik-panel gemastik-receiver +``` + +### 6. Buat tim + +Lewat UI (**Teams** tab, admin login) atau API: + +```bash +curl -X POST http://127.0.0.1:18081/api/teams/set \ + -H 'Content-Type: application/json' \ + -b cookies.txt -c cookies.txt \ + -d '{"count":2,"labels":{"1":"Tim Satu","2":"Tim Dua"}}' +``` + +Endpoint ini idempoten (membuat yang hilang, mempertahankan yang ada) dan +otomatis menjalankan `sync_team_ufw()` untuk setiap tim baru — tanpa itu port +tim akan di-blackhole UFW. + +### 7. Verifikasi + +```bash +bash panel/verify_platform_health.py +``` + +--- + +## Operasional Harian + +| Aksi | Perintah | +|---|---| +| Lihat status semua service | `systemctl is-active gemastik-panel gemastik-receiver gemastik-receiver-team*` | +| Restart panel | `systemctl restart gemastik-panel` | +| Sinkronkan container tim dengan registry | `bash panel/apply_registry.sh` | +| Health check | `python3 panel/verify_platform_health.py` | +| SSH round-trip ke semua challenge | `python3 panel/verify_ssh_e2e.py` | +| Cek user SSH per challenge | `bash panel/audit_ssh_users.sh` | +| Reset penuh (tim, flag, kredensial) | `bash panel/reset_runtime.sh` | +| Health suite topologi | `bash panel/verify_topo_full.sh` | +| Beban host | `bash panel/watch_load.sh` | + +**Reverse proxy.** Domain challenge dan panel dilayani Traefik lewat file +dynamic di `/data/coolify/proxy/dynamic/attackdefense.yaml`. Pola service: + +```yaml +services: + gemastik-panel-service: + loadBalancer: + servers: + - url: "http://host.docker.internal:18081" +``` + +Cert TLS terbit otomatis lewat `certResolver: letsencrypt` selama DNS +terresolve dan port 80 terbuka. + +Domain yang dipakai di host ini: `panel.attackdefense.imrnes.team` (panel, :18081) +dan `attackdefense.imrnes.team` (receiver global, :18080), plus subdomain +per challenge aktif. Perhatikan domain bare menunjuk ke receiver, bukan panel — +`/login` di sana akan 404 dan terlihat seperti panel mati. + +--- + +## Topologi + +Tab **Topology** merender graf serangan antar tim dengan PixiJS v8 +(`panel/static/topo_pixi.js`, engine di `panel/static/vendor/pixi.mjs`). + +- Pan, zoom, dan drag berjalan lewat satu funnel `applyView()`. +- Drag node tim menyeret seluruh challenge-nya. Indeks parent→child dibangun + dari edge list — sumber yang sama untuk menggambar garis — sehingga hierarki + drag tidak mungkin berbeda dari gambar. +- Ticker Pixi didaftarkan tapi tidak dinyalakan: render berlangsung on demand + (hanya saat ada pulse serangan atau sedang drag), lalu berhenti saat sunyi. + Pada host tanpa GPU, repaint 60fps atas scene statis membuat halaman tidak + merespons (rAF turun ke 2 FPS, lag `setTimeout(0)` 1353 ms). +- Posisi drag kembali ke layout otomatis saat data di-refresh tiap 10 detik. + Untuk merender ulang objek, `.text` hanya di-set bila string benar-benar + berubah — setiap `Text` baru meng-upload texture GPU (~1,6 detik per siklus + bila di-rebuild terus-menerus). + +Suite tes: `bash panel/run_topo_tests.sh` +(`test_topo_pixels`, `test_topo_browser`, `test_topo_viewports`, +`test_topo_race`, `test_topo_drag`). + +--- + +## HTTP API + +Semua endpoint di `/api` kecuali yang ditandai publik. + +### Publik + +| Method | Path | Keterangan | +|---|---|---| +| GET | `/submit` | UI submit flag publik | +| POST | `/api/flag/submit` | Submit flag | +| GET | `/api/public/scoreboard` | Skorboard tanpa login | +| GET | `/api/public/teams` | Daftar tim tanpa login | + +### Admin (butuh login) + +| Method | Path | Keterangan | +|---|---|---| +| POST | `/api/login` | Login admin | +| POST | `/api/logout` | Logout | +| GET | `/api/challenges` | Daftar challenge + status | +| PATCH | `/api/challenges/{challenge}` | Toggle enable/disable (body `{"enabled":bool}`) | +| GET | `/api/challenges/jobs/{job_id}` | Progress job toggle | +| GET | `/api/status` | Status runtime | +| GET | `/api/topology` | Data graf topologi | +| GET | `/api/teams` | Daftar tim | +| POST | `/api/teams/set` | Buat N tim (idempoten) | +| PUT | `/api/teams/{idx}` | Ubah label/domain tim | +| DELETE | `/api/teams/{idx}` | Hapus satu tim (body `{"purge_scores":true}`) | +| POST | `/api/teams/bulk-delete` | Hapus beberapa tim (job) | +| GET | `/api/teams/bulk-delete/{job_id}` | Progress job hapus massal | +| POST | `/api/teams/{idx}/ufw` | Sinkronkan aturan UFW tim | +| POST | `/api/teams/start` | Start semua tim | +| POST | `/api/teams/stop` | Stop semua tim | +| POST | `/api/teams/{idx}/randomize` | Acak flag tim | +| GET | `/api/teams/{idx}/logs` | Log tim | +| GET | `/api/teams/{idx}/creds` | Kredensial tim | +| GET | `/api/credential/{challenge}` | Kredensial satu challenge | +| GET | `/api/targets` | Target serangan | +| GET | `/api/attacks` | Log serangan | +| GET | `/api/leaderboard` | Leaderboard | +| GET | `/api/scoreboard` | Skorboard internal | +| GET | `/api/history` | Riwayat | +| POST | `/api/restart/{challenge}` | Restart challenge | +| POST | `/api/rollback/{challenge}` | Rollback challenge | +| POST | `/api/activate/{challenge}` | Aktifkan challenge | +| POST | `/api/deactivate/{challenge}` | Nonaktifkan challenge | +| POST | `/api/reset/scores` | Reset skor | +| POST | `/api/reset/environment` | Reset environment (hapus tim + flag) | + +Toggle challenge jalan asinkron: build per tim bisa memakan waktu menit, jadi +kerjaan dijalankan di background thread dan klien melakukan polling ke +`/api/challenges/jobs/{job_id}`. Hapus tim massal juga berupa job karena satu +tim butuh sekitar 100 detik (`compose down` 16 service) — empat tim inline akan +menahan request sekitar 7 menit dan memicu timeout di semua proxy. + +### Tim (login tim) + +| Method | Path | Keterangan | +|---|---|---| +| GET | `/team/{idx}` | Portal tim | +| GET | `/team/{idx}/guide` | Panduan tim | +| POST | `/api/team/{idx}/login` | Login tim | +| POST | `/api/team/logout` | Logout tim | +| GET | `/api/team/{idx}/session` | Status sesi | +| GET | `/api/team/{idx}/own-challenges` | Challenge milik tim | +| GET | `/api/team/{idx}/targets` | Target untuk diserang | +| GET | `/api/team/{idx}/info` | Info tim | +| GET | `/api/team/{idx}/status` | Status challenge tim | +| GET | `/api/team/{idx}/activity` | Aktivitas tim | +| WS | `/api/team/{idx}/ssh/ws` | Terminal web ke container tim | + +--- + +## Troubleshooting + +**`/login` di domain attackdefense.imrnes.team mengembalikan 404.** +Domain bare diarahkan ke receiver global (:18080), bukan panel. Panel ada di +`panel.attackdefense.imrnes.team` (:18081). Dua router berbeda melayani kedua +subdomain di `attackdefense.yaml`. + +**SLA turun ke 0 padahal container hidup.** +Bisa jadi receiver-nya mati, atau sering: restart panel mematikan receiver +karena keduanya satu cgroup. Cek `systemctl is-active gemastik-receiver-team*`. + +**SSH ditolak padahal password di `state.json` benar.** +Cek `ssh_user` untuk challenge tersebut di registry. 10 dari 16 challenge +impor login sebagai `root`, bukan `ctfuser`. + +**Flag expired / tidak cocok.** +`reset_environment()` menghapus flag lama. Cek +`teams/team/receiver/flags/.txt`. + +**Waktu toggle sangat lama.** +Normal — satu toggle membangun image per tim. Pantau lewat +`/api/challenges/jobs/{job_id}`, bukan dengan kill prosesnya. + +**`pull access denied for services-`.** +Image belum ada sehingga compose mencoba build dengan context yang salah. +`compose_gen` hanya menukar `build` menjadi `image` bila image-nya benar-benar +ada di `docker images`. + +**Disk penuh (`/` 0 byte).** +Lihat [Jebakan Operasional](#jebakan-operasional). Yang benar: +`docker builder prune -af` dan `journalctl --vacuum-size=50M`. +`docker image prune -af` menghapus image `services-*` yang sedang dipakai. + +--- + +## Jebakan Operasional + +Yang sudah ketahuan dan sudah diperbaiki. Semua masih berlaku sebagai alasan +mengapa kode sekarang berbentuk seperti sekarang. + +**Base image EOL.** `debian:buster`, `ubuntu:20.04`, dan `node:14` gagal +`apt-get update` karena GPG kedaluwarsa atau mirror 404. Pakai bookworm/noble, +`node:20`. + +**UFW default deny.** Host ini punya UFW aktif default deny. Port baru harus +dibuka (`ufw allow /tcp`) atau traffic di-blackhole diam-diam — +termasuk dari container lewat docker bridge. `POST /api/teams/set` sudah +menjalankan `sync_team_ufw()` karena alasan ini. + +**Project name compose wajib.** Per-team compose harus dijalankan dengan +`-p teamN`. Tanpa itu `docker compose` memakai nama direktori induk (`services`) +untuk semua tim, sehingga container team2 tertimpa team1. + +**`docker image prune -af` menghapus image yang sedang dipakai.** Image +`services-*` menjadi dangling dan terhapus meski container masih jalan. +Container tetap hidup tetapi image hilang dan tidak bisa di-recreate. Untuk +membersihkan ruang: `docker builder prune -af` + +`journalctl --vacuum-size=100M` + hapus `/root/.cache`. + +**Container orphan.** Sweep dengan: + +```bash +docker ps --format '{{.Names}}' | grep -E '_container$' | grep -vE '_team[0-9]+$' +``` + +**Beban checker.** Host 2-CPU/8GB tidak boleh meng-probe 4 receiver +sekaligus (Flask sinkron + CPU bersama = SLA timeout palsu), dan tidak boleh +menjalankan banyak generator kunci Paillier/RSA bersamaan. Cek `uptime`, +`free -m`, `vmstat 1 3` sebelum menyalahkan checker. + +**`subprocess.run(['docker','exec',...])` tanpa timeout.** Container yang +jenuh memblokir selamanya dan menahan seluruh loop SLA. + +--- + +## Struktur Direktori + +``` +/opt/gemastik18-final/ +├── README.md +├── node.sh # installer Docker +├── starter.py # bootstrap single-node (upstream) +├── teams/ +│ ├── challenge_registry.json # sumber data tunggal 28 challenge +│ ├── points.json # skor + event +│ ├── leaderboard.json # solve +│ ├── attacks.json # log serangan (visualisasi topologi) +│ └── teamN/ +│ ├── state.json # port, flag, kredensial, label, domain +│ ├── services/docker-compose.yml # hasil generate per tim +│ └── receiver/ # receiver terisolasi tim N +├── services// # Dockerfile + compose template (28 challenge) +├── panel/ +│ ├── main.py # FastAPI: seluruh route +│ ├── teams.py # orkestrasi tim, port, flag, skor, SLA +│ ├── compose_gen.py # render compose per tim dari registry +│ ├── gen_receiver_services.py # generate unit systemd per tim +│ ├── gen_receiver_main.py # generate main.py receiver per tim +│ ├── apply_registry.sh # sinkronkan container dengan registry +│ ├── reset_runtime.sh # reset penuh +│ ├── verify_platform_health.py +│ ├── verify_ssh_e2e.py +│ ├── audit_ssh_users.sh +│ ├── run_topo_tests.sh +│ ├── verify_topo_full.sh +│ └── static/ +│ ├── index.html # dashboard admin +│ ├── team.html # portal tim +│ ├── topo_pixi.js # renderer topologi PixiJS v8 +│ └── vendor/pixi.mjs +└── receiver/ + ├── main.py # API receiver (flag store + checker) + ├── config.py + ├── requirements.txt + ├── challenges/ # checker: Blogpost, Phew, xvi/, xvii/ + ├── flags/ # flag default + └── .venv/ +``` + +--- + +## Credit + +Challenge berasal dari tiga repositori: +[gemastik18-final](https://github.com/rayhanhanaputra/gemastik18-final), +[gemastik-xvi-final](https://github.com/vidner/gemastik-xvi-final), +[gemastik-xvii-final](https://github.com/vidner/gemastik-xvii-final).