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.
Attack Defense Platform
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.
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.
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 <challenge>_container_teamN ---- docker compose
|
+--> flags + leaderboard + points (teams/leaderboard.json, teams/points.json)
Daftar Isi
- Arsitektur
- Spesifikasi Challenge
- Spesifikasi Port
- Skor dan Penilaian
- Kebutuhan Sistem
- Setup
- Operasional Harian
- Topologi
- HTTP API
- Troubleshooting
- Jebakan Operasional
- 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<idx>_<CHALLENGE>_<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
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-<name>; 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).
Dependency checker ada di receiver/requirements.txt: fastapi, uvicorn,
pwntools, pyelftools, pycryptodome, fastecdsa, ecdsa, Pillow, pandas, openpyxl,
PyPDF2.
Setup
1. Clone
git clone <repo-url> attack-defense-platform
cd attack-defense-platform
Semua path di dalam kode memakai /opt/gemastik18-final sebagai BASE, jadi
letakkan repo di sana:
sudo mkdir -p /opt
sudo mv attack-defense-platform /opt/gemastik18-final
cd /opt/gemastik18-final
2. Docker
sudo bash node.sh
node.sh memasang Docker CE dari repo resmi lalu menjalankan starter.py.
Instalasi manual:
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
3. Kredensial panel
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
cd /opt/gemastik18-final/receiver
sudo python3 -m venv .venv
sudo .venv/bin/pip install -r requirements.txt
5. Unit systemd
Panel (:18081):
# /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):
# /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
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:
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 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:
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,
.texthanya di-set bila string benar-benar berubah — setiapTextbaru 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<N>/receiver/flags/<challenge>.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-<name>.
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. 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 <port>/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:
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/<challenge>/ # 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, gemastik-xvi-final, gemastik-xvii-final.