Post-CTF Infrastructure Analysis

Event: THEM?!CTF 2026

Duration: ~48 hours


1. Infrastructure Overview

VMs

VMRole
CTFd VMCTFd platform, MySQL, Redis, Caddy, behind cf so no public ip
Runner 1Docker Swarm leader, frps/frpc, registry, static challenges
Runner 2Docker Swarm worker, additional instancer

CTFd VM Stack (mainctfd-1)

Stock infra from CTFd opensource a bit modified(some hashing funcs n all, mentioned at the end)

Containerwhat for?
Elastic AgentSIEM/monitoring
Cloudflare TunnelPublic access without exposed ports

Runner 1 Stack (ctf-instance)

ContainerPurpose
themctf-runner-frps-1FRP server for routing player traffic to challenge containers
themctf-runner-frpc-1FRP client for registering challenge proxies
themctf-runner-registry-1Private Docker image registry
themctf-runner-chall-manager-1IAC challenge manager
themctf-runner-chall-manager-janitor-1Cleanup expired IAC instances

Runner 2 Stack (instancer2)

ContainerPurpose
Docker Swarm workerReceives whale-scheduled challenge instances

Networking

Players
  │
  ├── ctf.themctf.com (Cloudflare Tunnel → mainVM)
  │
  └── runner1(frps HTTP — web challenges)
  Other chals for players were mostly single instanced 

WireGuard VPN 
  mainctfd-1 ←→ ctf-instance ←→ instancer2 

CTFd Plugins

There was other shit as well not important.

PluginPurpose
ctfd-whalePer-team Docker instance spawning
ctfd-chall-managerIAC-based on-demand infra

2. Architecture Diagram

┌────────────────────────────────────────────────────────────────────┐
│                        THEM?!CTF 2026                              │
│                                                                    │
│  Players ──► Cloudflare ──► Caddy ──► CTFd (mainctfd-1)            │
│                                         │                          │
│                                    WireGuard VPN                   │
│                                    10.8.0.0/24                     │
│                                         │                          │
│              ┌──────────────────────────┴──────────────────┐       │
│              │                                             │       │
│         ctf-instance                    instancer2         │       │
│         Runner 1 — Swarm Leader            Runner 2 — Swarm Worker │
│              │                                             │       │
│         ┌────┴────┐                               ┌────────┘       │
│         frps      Registry                    Whale instances      │
│         frpc                                  (random placement)   │
│         chall-mgr                                                  │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

3. Incidents & Issues

INC-001: frpc Admin & Registry Accessible from Containers

Occurrence: This shit was detected during live CTF

Root Cause: The iptable rules for DOCKER wasnt configured properly, I tried fixing but it was casuing a pain so i left it open~

Impact: Challenge containers could potentially accessfrpc admin API and pull/push images from the registry.( in theory but pushing actual img to reg was not possible from any chal)

Fix: NAH didnt fix shit, do i look like the builder from COC always fixin stuff

INC-002: Rollback 2hrs.

  • what caused it?
    • you guessed it someone pushed untested code in prod and somehow also wiped the then current state of CTFd config and the flags werent gettin verified.

FIX: unfortunately a rollback

  • The instancers broke following this incident and werent up for almost 2 hours. why you may ask? something was constantly overwriting config of WHALE plugin so the host proxies were failing. L3AF came in clutch and helped me fix it. basically since this was on wg, one of the local deployments of one user was overwriting it constantly with default config, idk bout further details(i havent done no diggin on this yet).

INC-003: Recurring Kernel Soft Lockups & Unplanned Reboots

Based on ELK log analysis

Summary of Events

Time (UTC)HostEvent
May 30 @ 12:58instancer2Soft lockups on all CPUs — runc, elastic-otel-co, gunicorn, event_engine
May 30 @ 12:59instancer2systemd-journald.service: Watchdog timeout → reboot
May 30 @ 12:49–12:51ctf-instanceSoft lockups — elastic-otel-co, runc, migration threads
May 30 @ 22:07instancer2rcu_preempt soft lockup
May 30 @ 22:14–22:15instancer2Mass soft lockups — socat stuck 245s, migration stuck 273s, elastic-otel-co stuck 115s
May 30 @ 22:15instancer2systemd-journald.service: Watchdog timeout → reboot
May 30 @ 22:15ctf-instanceNMI watchdog: Perf NMI watchdog permanently disabled
May 31 @ 12:48–12:52instancer2Soft lockups — docker, runc, elastic-otel-co, khugepaged
May 31 @ 12:52instancer2systemd-journald.service: Watchdog timeout → reboot
May 31 @ 12:49–12:51ctf-instanceSoft lockups — elastic-otel-co, runc
May 31 @ 20:48–20:51ctf-instanceSoft lockups — gunicorn, runc, khugepaged, elastic-endpoin
May 31 @ 20:51instancer2Mass soft lockups — gunicorn, socat, dockerd, runc, khugepaged
May 31 @ 20:51instancer2systemd-journald.service: Watchdog timeout → reboot
May 31 @ 23:25BothSimultaneous soft lockups on all CPUs of both hosts
May 31 @ 23:25instancer2systemd-journald.service: Watchdog timeout → reboot
May 31 @ 23:25ctf-instancesystemd-journald.service: Watchdog timeout (limit 3min)!

Root Cause Analysis

Primary cause: Elastic Agent (elastic-otel-co, elastic-endpoin) CPU starvation

The kernel soft lockup messages consistently show elastic-otel-co and elastic-endpoin processes stuck for 21–115 seconds across multiple CPUs simultaneously.
This might be due to too much data ingestion not really sure. There were several ghost containers and all of its data was bein ingested so might be(not really sure)

Elastic Agent was consuming excessive CPU, starving the kernel scheduler. When CPU migration threads (migration/N) themselves got stuck, the kernel could no longer schedule work causing a kinda domino where journald's watchdog timer couldnt be serviced, triggering the reboot.

Contributing factors observed in logs:

  • gunicorn workers stuck 33–82 seconds (whale challenge containers under load)
  • runc stuck 28–48 seconds (container spawning/destroying under load)
  • khugepaged stuck 23–45 seconds (transparent huge page management under memory pressure)
  • socat stuck 245 seconds on instancer2 (challenge1 static container)
  • dockerd stuck 22–36 seconds
  • kworker threads stuck (kernel work queue backlog)

The simultaneous crash at May 31 23:25 affected both runners and well the instancers never came online again. yea im bein fr they js loop crashed idk how, since this was the end of the CTF i didnt care huh.

Also yes, they both crashed on 2 seperate ocassions and "idk how" basically means i dont know how or either there was too much workload to be like investigatin why shit happened.

Remediation

  1. Increase journald watchdog timeout to 10 minute.
  2. Killing ghost containers.

INC-004: Points Spike

  • what caused it?
    • you guessed it right again someone pushed untested code again in prod.

FIX: Manually updating DB

  • so after freezing scoreboard all the chal points in private scoreboard went crazy and every solve was worth like 1000 points but we fixed it pretty fas. No real user side trouble.

Another minor incident was that the FRPC was constantly gettin fucked up, so we had to kill ghost containers manually and smtimes configs were overwritten so we had to fix that everytime.

4. Security Hardening

CTFd VM

Control
SSH key-only auth on private network only
X11 forwarding disabled
UFW firewall
No public ports
Elastic Agent SIEM
Unattended upgrades

Runner 1

Control
SSH key-only auth
Docker TLS on 2376
UFW firewall
DOCKER-USER iptables
raw table blocks
Container isolation
Whale containers: non-root
Resource limits
Presisting IPtable rules

Runner 2

Control
SSH key-only auth
UFW firewall
Docker icc disabled
DOCKER-USER isolation
docker-restrict.service
Infra unreachable from containers

ELK Stack

Elk stack was sestup on all 3 vms with proper data ingestion. Rules were in place too for alerts and was integrated with discord for overlooking. Thank God no security incident took place.

5. Performance Observations

CTFd VM Resource Usage (Peak)

ContainerCPURAMNotes
CTFd112%1.88GB / 13GBHigh due to frpc retry storm
MySQL36%4.7GB / 11GBConnection leak 100+ idle connections
other containers abd vms were pretty normal throughout

6. Tech Stack

For the final question? TECH STACK

Cloud & Infrastructure

Container Orchestration & Security

Networking

Platform & Backend

Security Operations

Languages & Tools


So yea guys thats it for this time ig. If you ask me what i found the hardest? prolly debugging in prod. this is smthg i'd like to not do again(especially with so many tickets being created and constant discord notifs.)

some of you might be wondering did i slop this too? nah. The log part was summarised by slop but i verified it manually and removed the non-important ones.